NEO-22: stop applying move-stream HTTP body as client teleport

Successful move-stream responses echoed server position roughly one RTT behind
integrated move_and_slide, so apply_as_snap still fired whenever dh exceeded
the locomotion skip threshold. Treat stream 200 as ack-only: do not emit
authoritative_position_received. Boot GET and click-move verify unchanged.
Update README + GdUnit expectation.
pull/42/head
VinPropane 2026-04-17 19:34:45 -04:00
parent 39a41ac62c
commit 6cc2ac60d4
3 changed files with 10 additions and 7 deletions

View File

@ -53,7 +53,7 @@ Elevated walkables use **distinct albedo tints** in `scenes/main.tscn` so screen
## Authoritative movement (NEO-7, NEO-11, NEO-22)
With the game server running ([`server/README.md`](../server/README.md)), **WASD** (input actions **`move_*`** in `project.godot`) drives **camera-relative XZ** wish direction on the **`Player`**, and **`main.gd`** periodically **`POST`**s **`/game/players/{id}/move-stream`** with the **latest** body-centre sample while you are moving (no stream while standing still, so idle anchor does not fight HTTP). **`PositionAuthorityClient`** coalesces to **one target per request** so a slow round-trip cannot apply a stale multi-step chain after the capsule has moved. The server validates each leg with the same rules as a single **`MoveCommand`**, applies **all targets in order** (reject = **no** partial apply), and returns **`PositionStateResponse`**; the client **snaps** when the correction exceeds a small threshold (boot snap always applies once). **`POST /game/players/{id}/move`** remains for **debug** click-to-move when running a **debug** Godot build (`ground_pick.gd` only listens in **`OS.is_debug_build()`**): it still does **POST** + **`GET`** verify and sets a **nav walk goal** on the player.
With the game server running ([`server/README.md`](../server/README.md)), **WASD** (input actions **`move_*`** in `project.godot`) drives **camera-relative XZ** wish direction on the **`Player`**, and **`main.gd`** periodically **`POST`**s **`/game/players/{id}/move-stream`** with the **latest** body-centre sample while you are moving (no stream while standing still, so idle anchor does not fight HTTP). **`PositionAuthorityClient`** coalesces to **one target per request** so a slow round-trip cannot apply a stale multi-step chain after the capsule has moved. The server validates each leg with the same rules as a single **`MoveCommand`**, applies **all targets in order** (reject = **no** partial apply), and returns **`PositionStateResponse`**. A successful **`move-stream`** response is treated as an **ack only** (no client teleport): the echoed position typically trails **`move_and_slide`** by roughly one RTT, so snapping it caused rubber-banding. **Boot** `GET` and **debug** click-move **verify** `GET` still emit **`authoritative_position_received`** so **`main.gd`** can **`snap_to_server`** once or set a **nav walk goal**. **`POST /game/players/{id}/move`** remains for **debug** click-to-move when running a **debug** Godot build (`ground_pick.gd` only listens in **`OS.is_debug_build()`**): it still does **POST** + **`GET`** verify and sets a **nav walk goal** on the player.
**Boot** `sync_from_server()` **snaps** once so spawn matches the server.

View File

@ -3,7 +3,8 @@ extends Node
## NS-16: POST MoveCommand then GET PositionState; emits authoritative world position for the
## avatar. NS-19: failed validation (HTTP 400 + rejection body) emits `move_rejected`.
## NS-23: second signal arg — `true` = boot snap, `false` = post-move (nav goal, no teleport).
## NEO-22: `submit_stream_targets` POSTs `move-stream` (batched small moves); no VERIFY GET.
## NEO-22: `submit_stream_targets` POSTs `move-stream` (batched small moves); no VERIFY GET. A 200
## response does not emit `authoritative_position_received` (ack-only; avoids RTT snap-back).
## (No `class_name` so headless/CI can load the project before `.godot` import exists.)
signal authoritative_position_received(world: Vector3, apply_as_snap: bool)
@ -207,7 +208,11 @@ func _on_request_completed(
_stream_in_flight_count = 0
_busy = false
if response_code == 200:
_emit_position_from_response(text, true)
# Do not emit `authoritative_position_received` here. The body echoes server position
# after applying the batch, which typically lags the client's integrated capsule by
# roughly one RTT — snapping caused visible rubber-banding during WASD. Boot GET and
# click-move VERIFY still drive snaps / nav goals.
pass
_try_flush_pending()

View File

@ -113,7 +113,7 @@ func test_second_sync_while_busy_is_ignored() -> void:
assert_that(mock.request_count).is_equal(1)
func test_move_stream_post_200_emits_snap_from_body() -> void:
func test_move_stream_post_200_does_not_emit_position() -> void:
var mock := MockHttpTransport.new()
mock.enqueue(
HTTPRequest.RESULT_SUCCESS,
@ -126,6 +126,4 @@ func test_move_stream_post_200_emits_snap_from_body() -> void:
var c := _make_client(mock)
monitor_signals(c)
c.submit_stream_targets([Vector3(-4.9, 0.9, -5.0)])
await assert_signal(c).is_emitted(
"authoritative_position_received", Vector3(-4.9, 0.9, -5.0), true
)
await assert_signal(c).wait_until(400).is_not_emitted("authoritative_position_received")