NEO-22: suppress stream snap-back while locomoting

Stream HTTP responses can lag integrated move_and_slide by roughly one RTT,
so apply_as_snap was pulling the capsule backward each tick while moving.
Skip small XZ corrections when wish or horizontal speed indicates locomotion,
with a cap so large desync still snaps. move_rejected sets a one-shot flag so
resync after move-stream 400 still applies the authoritative pose.
pull/42/head
VinPropane 2026-04-17 19:29:56 -04:00
parent 878b1f4aee
commit 39a41ac62c
1 changed files with 22 additions and 4 deletions

View File

@ -30,11 +30,19 @@ const STREAM_IDLE_VEL_HZ_SQ: float = 0.0004
const AUTH_SNAP_MIN_HORIZONTAL: float = 0.08
const AUTH_SNAP_MIN_VERTICAL: float = 0.04
## While moving, ignore stream snaps smaller than this (XZ m): responses are often one RTT behind
## integrated `move_and_slide`, so snapping causes rubber-band / bounciness.
const AUTH_SNAP_LOCOMOTION_SKIP_MAX_DH: float = 0.55
## Horizontal speed² above this (m²/s²) counts as “moving” for snap suppression (~0.35 m/s).
const AUTH_SNAP_LOCOMOTION_VEL_HZ_SQ: float = 0.12
## Bump on each rejection so older one-shot timers do not clear a newer message.
var _move_reject_msg_token: int = 0
var _stream_physics_counter: int = 0
## After the first `apply_as_snap` authority update, micro-snaps may be skipped (boot always applies).
var _allow_authority_soft_snap_skip: bool = false
## Next `apply_as_snap` must not be suppressed (e.g. `move-stream` 400 → `sync_from_server`).
var _authority_force_snap_next: bool = false
## [InputMap] [code]dev_toggle_occluder_obstacle[/code] (default **Ctrl+Shift+K**):
## Restored when the obstacle is shown again.
@ -125,10 +133,19 @@ func _on_target_chosen(world: Vector3) -> void:
func _on_authoritative_position(world: Vector3, apply_as_snap: bool) -> void:
if apply_as_snap:
if _allow_authority_soft_snap_skip:
var p: Vector3 = _player.global_position
var dh: float = Vector2(world.x - p.x, world.z - p.z).length()
var dy: float = absf(world.y - p.y)
var force: bool = _authority_force_snap_next
_authority_force_snap_next = false
var p: Vector3 = _player.global_position
var dh: float = Vector2(world.x - p.x, world.z - p.z).length()
var dy: float = absf(world.y - p.y)
if _allow_authority_soft_snap_skip and not force:
var wish_len2: float = _player._locomotion_wish_world_xz.length_squared()
var vel_hz_sq: float = Vector2(_player.velocity.x, _player.velocity.z).length_squared()
var locomoting: bool = (
wish_len2 > 1e-10 or vel_hz_sq > AUTH_SNAP_LOCOMOTION_VEL_HZ_SQ
)
if locomoting and dh < AUTH_SNAP_LOCOMOTION_SKIP_MAX_DH:
return
if dh < AUTH_SNAP_MIN_HORIZONTAL and dy < AUTH_SNAP_MIN_VERTICAL:
return
_allow_authority_soft_snap_skip = true
@ -138,6 +155,7 @@ func _on_authoritative_position(world: Vector3, apply_as_snap: bool) -> void:
func _on_move_rejected(reason_code: String) -> void:
_authority_force_snap_next = true
# Rejected POST never reached VERIFY_GET, so no new nav target was applied; do not clear an
# in-flight path from a prior successful move.
push_warning("Move rejected: %s" % reason_code)