fix: harden recovery flow and clean diagnostics
This commit is contained in:
parent
0158b0215f
commit
ff78710303
64 changed files with 1163 additions and 418 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue