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

@ -32,7 +32,10 @@ func _validate_latency_smoothing() -> void:
await process_frame
avatar.set_process(false)
avatar.set_physics_process(false)
_validate_compact_input_encoding()
_validate_compact_snapshot_encoding()
_validate_compact_animation_encoding()
_validate_animation_action_ordering(avatar)
_validate_transit_estimation()
_validate_remote_snapshot_smoothing(avatar)
_validate_reliable_jump_intent(avatar)
@ -41,6 +44,26 @@ func _validate_latency_smoothing() -> void:
await process_frame
func _validate_compact_input_encoding() -> void:
var input: Dictionary = _movement_input(
12,
true,
true,
true,
&"strike",
4,
true,
)
var encoded: Array = NetworkSession._encode_movement_input(input)
assert(encoded.size() == NetworkSession.MOVEMENT_INPUT_FIELD_COUNT)
assert(var_to_bytes(encoded).size() < var_to_bytes(input).size())
assert(NetworkSession._decode_movement_input(encoded) == input)
assert(NetworkSession._decode_movement_input(encoded.slice(0, 3)).is_empty())
var unknown_flags: Array = encoded.duplicate()
unknown_flags[3] = 1 << 12
assert(NetworkSession._decode_movement_input(unknown_flags).is_empty())
func _validate_compact_snapshot_encoding() -> void:
var moving_snapshot: Dictionary = _network_snapshot(
Vector3.ZERO,
@ -54,31 +77,133 @@ func _validate_compact_snapshot_encoding() -> void:
NetworkSession._encode_movement_snapshot(moving_snapshot)
)
assert(var_to_bytes(encoded_snapshots).size() < 1200)
var expected_snapshot: Dictionary = moving_snapshot.duplicate(true)
expected_snapshot.erase("animation_state")
assert(
NetworkSession._decode_movement_snapshot(encoded_snapshots[0])
== expected_snapshot
)
assert(
NetworkSession._decode_movement_snapshot(
encoded_snapshots[0]
) == moving_snapshot
encoded_snapshots[0].slice(0, 4)
).is_empty()
)
var paused_snapshot: Dictionary = moving_snapshot.duplicate(true)
paused_snapshot["animation_state"] = (
var invalid_snapshot_flags: Array = encoded_snapshots[0].duplicate()
invalid_snapshot_flags[5] = 1 << 12
assert(
NetworkSession._decode_movement_snapshot(
invalid_snapshot_flags
).is_empty()
)
func _validate_compact_animation_encoding() -> void:
var paused_state := NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
true,
&"strike",
7,
0.5,
true,
)
var encoded: Array = NetworkSession._encode_movement_animation(
2,
paused_state,
)
assert(encoded.size() == NetworkSession.MOVEMENT_ANIMATION_FIELD_COUNT)
var decoded: Dictionary = NetworkSession._decode_movement_animation(encoded)
assert(int(decoded.get("peer_id", 0)) == 2)
assert(Dictionary(decoded.get("state", {})) == paused_state)
var advanced_state: Dictionary = paused_state.duplicate(true)
advanced_state["action"]["elapsed"] = 0.75
assert(
NetworkSession._movement_animation_signature(advanced_state)
== NetworkSession._movement_animation_signature(paused_state)
)
var action: Dictionary = paused_state["action"]
var encoded_action: Array = (
NetworkSession._encode_movement_animation_action(action, true)
)
assert(
encoded_action.size()
== NetworkSession.MOVEMENT_ANIMATION_ACTION_FIELD_COUNT
)
var decoded_action: Dictionary = (
NetworkSession._decode_movement_animation_action(encoded_action)
)
assert(Dictionary(decoded_action.get("action", {})) == action)
assert(bool(decoded_action.get("sitting", false)))
assert(
NetworkSession._decode_movement_animation_action(
encoded_action.slice(0, 2)
).is_empty()
)
func _validate_animation_action_ordering(avatar: Player) -> void:
var draw := NetworkPlayerAnimationProtocol.make_action_state(
&"draw", 5, 0.2
)
avatar.apply_authoritative_network_animation_action(draw)
assert(StringName(str(avatar.get("_animation_action_id"))) == &"draw")
var cleared := NetworkPlayerAnimationProtocol.make_action_state(
&"", 6, 0.0
)
avatar.apply_authoritative_network_animation_action(cleared)
assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
avatar.apply_authoritative_network_animation_action(draw)
assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
avatar.apply_authoritative_network_animation_action(
NetworkPlayerAnimationProtocol.make_action_state(&"strike", 6, 0.0)
)
assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
var strike := NetworkPlayerAnimationProtocol.make_action_state(
&"strike", 7, 0.2
)
avatar.apply_authoritative_network_animation_action(strike)
avatar.apply_authoritative_network_animation_action(
NetworkPlayerAnimationProtocol.make_action_state(
&"strike", 8, 0.4, true
)
)
avatar.apply_authoritative_network_animation_action(strike)
assert(bool(avatar.get("_animation_action_paused")))
avatar.apply_authoritative_network_animation_action(
NetworkPlayerAnimationProtocol.make_action_state(&"", 9, 0.0)
)
avatar.configure_network_remote(false)
avatar.apply_network_animation_state(
NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
true,
&"strike",
7,
0.5,
9,
0.4,
true,
)
)
var paused_encoded: Array = NetworkSession._encode_movement_snapshot(
paused_snapshot
avatar.apply_network_animation_state(
NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING,
true,
&"draw",
8,
0.1,
)
)
assert(paused_encoded.size() == NetworkSession.MOVEMENT_SNAPSHOT_FIELD_COUNT)
var paused_decoded: Dictionary = (
NetworkSession._decode_movement_snapshot(paused_encoded)
assert(
StringName(str(avatar.get("_network_target_animation_action_id")))
== &"strike"
)
avatar.apply_network_animation_state(
NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
true,
&"",
10,
)
)
assert(not paused_decoded.is_empty())
assert(bool(paused_decoded["animation_state"]["action"]["paused"]))
func _validate_transit_estimation() -> void:
@ -158,14 +283,16 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
avatar.set_local_control(true)
avatar.global_position = Vector3(0.8, 0.0, 0.0)
var replay_input: Dictionary = _movement_input(2, false)
replay_input["axis"] = [1.0, 0.0]
var pending_inputs: Array[Dictionary] = [replay_input]
avatar.apply_local_prediction_correction(
moving_snapshot,
10,
pending_inputs,
1.0 / 30.0,
0.1,
)
assert(avatar.global_position.x < 0.8)
assert(avatar.global_position.x > 0.7)
assert(avatar.global_position.x > 0.0)
func _validate_reliable_jump_intent(avatar: Player) -> void:
@ -178,9 +305,8 @@ func _validate_reliable_jump_intent(avatar: Player) -> void:
assert(int(avatar.get("_local_network_jump_intent_sequence")) == 20)
avatar.apply_local_prediction_correction(
_network_snapshot(avatar.global_position, Vector3.ZERO, 20),
21,
[],
1.0 / 30.0,
0.0,
)
assert(not bool(avatar.capture_network_input(22)["jump"]))
@ -209,12 +335,24 @@ func _validate_reliable_jump_intent(avatar: Player) -> void:
func _validate_stale_input_expiry(avatar: Player) -> void:
avatar.configure_network_remote(true)
avatar.apply_authoritative_network_input(_movement_input(40, true))
var high_latency_timeout := (
Player.resolve_network_input_stale_timeout_seconds(400)
)
assert(high_latency_timeout > Player.NETWORK_INPUT_STALE_TIMEOUT_SECONDS)
avatar.apply_authoritative_network_input(
_movement_input(40, true),
high_latency_timeout,
)
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
avatar.call(
"_update_network_input_freshness",
Player.NETWORK_INPUT_STALE_TIMEOUT_SECONDS + 0.01,
)
assert(not bool(avatar.get("_network_input_stale")))
avatar.call(
"_update_network_input_freshness",
high_latency_timeout,
)
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
assert(not bool(avatar.get("_network_sprint")))
assert(bool(avatar.get("_network_input_stale")))
@ -234,6 +372,7 @@ func _network_snapshot(
"position": [position.x, position.y, position.z],
"velocity": [velocity.x, velocity.y, velocity.z],
"visual_yaw": 0.0,
"grounded": true,
"animation_state": NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING,
true,
@ -247,8 +386,15 @@ func _run_host() -> void:
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(session.set_host_world(
WorldLayout.STARTER_ISLAND,
NetworkProtocol.DEFAULT_WORLD_SEED,
))
assert(session.start_private_host(TEST_PORT))
assert(save_manager.initialize_new_game())
assert(save_manager.initialize_new_game(
NetworkProtocol.DEFAULT_WORLD_SEED,
WorldLayout.STARTER_ISLAND,
))
main.call("_enter_gameplay")
for _frame: int in 4:
await physics_frame