feat: expand progression and multiplayer systems

Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
This commit is contained in:
Alexander Sellite 2026-08-23 20:48:38 -04:00
parent 1db1a5b754
commit 3b84bfe3a0
97 changed files with 7869 additions and 982 deletions

View file

@ -79,8 +79,6 @@ func _ready() -> void:
_validate_biome_catalog()
_configure_static_water()
_build_shoreline_reference()
if not generate_world(initial_seed):
push_error("The initial generated world could not be built.")
func generate_world(seed: int) -> bool:
@ -100,6 +98,10 @@ func get_generation_seed() -> int:
return _current_seed
func is_world_generated() -> bool:
return is_instance_valid(_generator.get_generated_chunks_root())
func get_playable_half_extents() -> Vector2:
var size := Vector2(
float(_generator.grid_size.x),

View file

@ -25,6 +25,7 @@ const MAX_PACKED_SOLVER_VARIANTS := 62
@export var generation_seed := 13001
@export var generate_on_ready := true
@export var build_collision := true
@export_range(1, 16, 1) var collision_batch_size := 4
@export var show_chunk_labels := false
@export var force_center_chunk_id: StringName = &"chunk_0000"
@export var required_chunk_ids := PackedStringArray()
@ -3928,9 +3929,102 @@ func _build_solution_root() -> Node3D:
if not _add_stacked_elevated_chunks(solution_root):
solution_root.free()
return null
if build_collision and collision_batch_size > 1:
_batch_generated_terrain_collision(solution_root)
return solution_root
func _batch_generated_terrain_collision(solution_root: Node3D) -> void:
var shapes_by_batch: Dictionary[Vector2i, Array] = {}
var host_by_batch: Dictionary[Vector2i, Node3D] = {}
var bodies_to_remove: Array[StaticBody3D] = []
for chunk_node: Node in solution_root.get_children():
var chunk_root := chunk_node as Node3D
if chunk_root == null:
continue
var coordinate: Vector2i = chunk_root.get_meta(
&"terrain_chunk_coordinate",
Vector2i.ZERO,
)
var batch_coordinate := Vector2i(
floori(float(coordinate.x) / float(collision_batch_size)),
floori(float(coordinate.y) / float(collision_batch_size)),
)
if not host_by_batch.has(batch_coordinate):
host_by_batch[batch_coordinate] = chunk_root
var batch_shapes: Array = shapes_by_batch.get(
batch_coordinate,
[],
)
for value: Node in chunk_root.find_children(
"TerrainShape",
"CollisionShape3D",
true,
false,
):
var collision_shape := value as CollisionShape3D
if collision_shape == null:
continue
var collision_body := collision_shape.get_parent() as StaticBody3D
if collision_shape.shape == null or collision_body == null:
continue
var shape_to_solution := _transform_to_ancestor(
collision_shape,
solution_root,
)
batch_shapes.append({
"shape": collision_shape.shape,
"transform": shape_to_solution,
})
if collision_body not in bodies_to_remove:
bodies_to_remove.append(collision_body)
shapes_by_batch[batch_coordinate] = batch_shapes
while not bodies_to_remove.is_empty():
var collision_body: StaticBody3D = bodies_to_remove.pop_back()
collision_body.free()
for batch_coordinate: Vector2i in shapes_by_batch:
var batch_shapes: Array = shapes_by_batch[batch_coordinate]
if batch_shapes.is_empty():
continue
var host_root: Node3D = host_by_batch.get(batch_coordinate)
if host_root == null:
continue
var body := StaticBody3D.new()
body.name = "TerrainCollisionBatch_%d_%d" % [
batch_coordinate.x,
batch_coordinate.y,
]
body.collision_layer = 1
body.collision_mask = 0
body.set_meta(&"terrain_collision_batch", batch_coordinate)
host_root.add_child(body)
var solution_to_body: Transform3D = host_root.transform.affine_inverse()
for shape_index: int in batch_shapes.size():
var record: Dictionary = batch_shapes[shape_index]
var collision := CollisionShape3D.new()
collision.name = "TerrainShape_%d" % shape_index
collision.shape = record.get("shape") as Shape3D
collision.transform = (
solution_to_body
* (record.get("transform", Transform3D.IDENTITY) as Transform3D)
)
body.add_child(collision)
static func _transform_to_ancestor(
node: Node3D,
ancestor: Node3D,
) -> Transform3D:
var result := Transform3D.IDENTITY
var current: Node3D = node
while current != ancestor:
result = current.transform * result
current = current.get_parent() as Node3D
if current == null:
return Transform3D.IDENTITY
return result
func _add_stacked_elevated_chunks(solution_root: Node3D) -> bool:
for index: int in _stacked_elevated_placements.size():
var record := _stacked_elevated_placements[index]

View file

@ -40,6 +40,10 @@ func _ready() -> void:
return
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
# Generated worlds can contain many independent water volumes. Keep an
# empty trigger completely asleep instead of polling an empty array on every
# physics tick.
set_physics_process(false)
func _physics_process(_delta: float) -> void:
@ -57,6 +61,8 @@ func _physics_process(_delta: float) -> void:
if entry_height <= active_surface_height - entry_depth_threshold:
_triggered_players[player_key] = true
recovery_requested.emit(player, active_surface_height)
if _tracked_players.is_empty():
set_physics_process(false)
func get_surface_height() -> float:
@ -80,6 +86,7 @@ func _on_body_entered(body: Node3D) -> void:
if player == null or player in _tracked_players:
return
_tracked_players.append(player)
set_physics_process(true)
func _on_body_exited(body: Node3D) -> void:
@ -88,3 +95,5 @@ func _on_body_exited(body: Node3D) -> void:
return
_tracked_players.erase(player)
_triggered_players.erase(StringName(str(player.get_instance_id())))
if _tracked_players.is_empty():
set_physics_process(false)

View file

@ -30,6 +30,7 @@ const WORLD_BOUNDARY_SHORELINE_CLEARANCE := 18.0
var _world_layout: StringName = WorldLayoutType.GENERATED
var _world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED
var _light_performance_profile: bool = false
var _dedicated_simulation: bool = false
func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
@ -74,6 +75,62 @@ func set_light_performance_profile(enabled: bool) -> void:
_active_region.set_light_performance_profile(enabled)
func set_dedicated_simulation(enabled: bool) -> void:
_dedicated_simulation = enabled
if _active_region != null:
_set_region_presentation_enabled(_active_region, not enabled)
_world_environment.process_mode = (
Node.PROCESS_MODE_DISABLED if enabled else Node.PROCESS_MODE_INHERIT
)
_sun.visible = not enabled
_sun.process_mode = (
Node.PROCESS_MODE_DISABLED if enabled else Node.PROCESS_MODE_INHERIT
)
func _set_region_presentation_enabled(
region: WorldRegion,
enabled: bool,
) -> void:
for class_name_value: String in [
"WaterSurfaceMotion",
"LocalStormCloudLayer",
"WorldCharacterDisplay",
]:
for value: Node in region.find_children(
"*", class_name_value, true, false
):
value.process_mode = (
Node.PROCESS_MODE_INHERIT
if enabled
else Node.PROCESS_MODE_DISABLED
)
for value: Node in region.find_children("*", "GeometryInstance3D", true, false):
(value as GeometryInstance3D).visible = enabled
for value: Node in region.find_children("*", "GPUParticles3D", true, false):
var particles := value as GPUParticles3D
particles.emitting = enabled
particles.process_mode = (
Node.PROCESS_MODE_INHERIT if enabled else Node.PROCESS_MODE_DISABLED
)
for value: Node in region.find_children("*", "AnimationPlayer", true, false):
var animation_player := value as AnimationPlayer
animation_player.active = enabled
animation_player.process_mode = (
Node.PROCESS_MODE_INHERIT if enabled else Node.PROCESS_MODE_DISABLED
)
for class_name_value: String in ["AudioStreamPlayer", "AudioStreamPlayer3D"]:
for value: Node in region.find_children(
"*", class_name_value, true, false
):
value.call("stop")
value.process_mode = (
Node.PROCESS_MODE_INHERIT
if enabled
else Node.PROCESS_MODE_DISABLED
)
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
return (
_active_region.get_fishable_water_regions()
@ -129,6 +186,17 @@ func get_generation_seed() -> int:
return _world_seed
func is_world_ready() -> bool:
if _active_region == null:
return false
if _world_layout != WorldLayoutType.GENERATED:
return true
return (
_active_region.has_method(&"is_world_generated")
and bool(_active_region.call(&"is_world_generated"))
)
func get_diggable_area_triangles(
area_id: StringName,
) -> Array[PackedVector3Array]:
@ -182,6 +250,8 @@ func _replace_active_region(layout: StringName, seed: int) -> bool:
_active_region = replacement
_regions_root.add_child(_active_region)
_active_region.set_light_performance_profile(_light_performance_profile)
if _dedicated_simulation:
_set_region_presentation_enabled(_active_region, false)
return true