fix: harden recovery flow and clean diagnostics

This commit is contained in:
Alexander Sellite 2026-09-01 07:44:01 -04:00
parent 0158b0215f
commit ff78710303
64 changed files with 1163 additions and 418 deletions

View file

@ -34,6 +34,7 @@ func _validate_latency_smoothing() -> void:
avatar.set_physics_process(false)
_validate_compact_input_encoding()
_validate_compact_snapshot_encoding()
_validate_authoritative_teleport_snapshot_guard()
_validate_compact_animation_encoding()
_validate_animation_action_ordering(avatar)
_validate_transit_estimation()
@ -72,8 +73,12 @@ func _validate_compact_snapshot_encoding() -> void:
Vector3(4.5, 0.0, 0.0),
1,
)
assert(NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE == 8)
assert(NetworkSession.OWNER_SNAPSHOT_DIVISOR == 3)
var movement_snapshot_batch_size: int = (
NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE
)
var owner_snapshot_divisor: int = NetworkSession.OWNER_SNAPSHOT_DIVISOR
assert(movement_snapshot_batch_size == 8)
assert(owner_snapshot_divisor == 3)
var encoded_snapshots: Array = []
for _peer: int in NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE:
encoded_snapshots.append(
@ -100,6 +105,50 @@ func _validate_compact_snapshot_encoding() -> void:
)
func _validate_authoritative_teleport_snapshot_guard() -> void:
var session := NetworkSession.new()
var destination := Vector3(24.0, 0.0, -8.0)
session.set("_local_authoritative_teleport_guard_active", true)
session.set("_local_authoritative_teleport_guard_position", destination)
session.set("_local_authoritative_teleport_minimum_ack", 48)
# A channel-two audit from before the reliable relocation must not move the
# local player back to its old position.
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(Vector3.ZERO, Vector3.ZERO, 48),
)))
assert(bool(session.get("_local_authoritative_teleport_guard_active")))
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination, Vector3.ZERO, 47),
)))
# A matching audit from the new location clears the barrier. A higher
# acknowledgement at the old position is not sufficient: movement input and
# the reliable teleport travelled on different channels, so it can still be
# a held packet from before recovery.
assert(not bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination + Vector3.RIGHT, Vector3.ZERO, 48),
)))
assert(not bool(session.get("_local_authoritative_teleport_guard_active")))
session.set("_local_authoritative_teleport_guard_active", true)
session.set("_local_authoritative_teleport_guard_position", destination)
session.set("_local_authoritative_teleport_minimum_ack", 48)
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(Vector3.ZERO, Vector3.ZERO, 49),
)))
assert(bool(session.get("_local_authoritative_teleport_guard_active")))
assert(not bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination, Vector3.ZERO, 49),
)))
assert(not bool(session.get("_local_authoritative_teleport_guard_active")))
session.free()
func _validate_compact_animation_encoding() -> void:
var paused_state := NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
@ -315,16 +364,16 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
# Reconciliation only applies after both simulations report a grounded body.
# Give the unit avatar a real floor so this exercises the production path.
var floor := StaticBody3D.new()
floor.collision_layer = 1
floor.collision_mask = 0
floor.position = Vector3(0.0, -0.1, 0.0)
var floor_body := StaticBody3D.new()
floor_body.collision_layer = 1
floor_body.collision_mask = 0
floor_body.position = Vector3(0.0, -0.1, 0.0)
var floor_shape := CollisionShape3D.new()
var floor_box := BoxShape3D.new()
floor_box.size = Vector3(100.0, 0.2, 100.0)
floor_shape.shape = floor_box
floor.add_child(floor_shape)
root.add_child(floor)
floor_body.add_child(floor_shape)
root.add_child(floor_body)
await physics_frame
avatar.set_local_control(true)
avatar.global_position = Vector3(0.8, 0.0, 0.0)
@ -434,6 +483,24 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
int(avatar.get("_local_prediction_hard_corrections")) == 1
)
# Recovery owns all local state until its reliable teleport arrives. A
# delayed audit must neither pull the body back nor acknowledge an old jump.
avatar.global_position = Vector3(10.0, 0.0, 0.0)
avatar.call("_queue_local_network_jump_intent")
avatar.capture_network_input(20)
avatar.set_water_recovery_active(true)
avatar.apply_local_prediction_correction(
drift_snapshot,
20,
1.0 / 30.0,
0.0,
0.1,
)
assert(avatar.global_position == Vector3(10.0, 0.0, 0.0))
assert(bool(avatar.get("_local_network_jump_intent_pending")))
avatar.set_water_recovery_active(false)
avatar.call("_clear_local_network_jump_intent")
# Soft reconciliation must respect the same terrain collision as ordinary
# player movement instead of phasing the capsule through a solid obstacle.
var blocker := StaticBody3D.new()
@ -470,7 +537,7 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
== blocked_camera_position
)
blocker.queue_free()
floor.queue_free()
floor_body.queue_free()
await physics_frame
@ -567,6 +634,40 @@ func _validate_stale_input_expiry(avatar: Player) -> void:
assert(not bool(avatar.get("_network_input_stale")))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
# The reliable recovery request carries the last client-created sequence.
# A host uses it as a hard watermark, so every delayed input from before the
# request is discarded even if it arrives after the teleport RPC.
avatar.neutralize_authoritative_input_for_relocation(44)
assert(int(avatar.get("_last_network_input_sequence")) == 44)
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
assert(not bool(avatar.get("_network_sprint")))
assert(bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(41, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
avatar.apply_authoritative_network_input(_movement_input(42, false, false))
assert(int(avatar.get("_last_network_input_sequence")) == 44)
# A non-neutral post-watermark packet cannot restart motion. The first
# neutral packet past the watermark releases the barrier; the following
# input is the first one allowed to move the recovered avatar.
avatar.apply_authoritative_network_input(_movement_input(45, false, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
assert(bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(46, false, false))
assert(not bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(47, false, true))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
# Recovery itself also consumes incoming movement rather than remembering a
# held axis which could resume on the exact frame recovery presentation ends.
avatar.set_water_recovery_active(true)
avatar.apply_authoritative_network_input(_movement_input(48, false, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
avatar.set_water_recovery_active(false)
avatar.apply_authoritative_network_input(_movement_input(42, false, false))
avatar.apply_authoritative_network_input(_movement_input(49, false, true))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
func _network_snapshot(
position: Vector3,