diff --git a/client/README.md b/client/README.md index 7738c28..db6f804 100644 --- a/client/README.md +++ b/client/README.md @@ -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. diff --git a/client/scripts/position_authority_client.gd b/client/scripts/position_authority_client.gd index dd1c9b3..5dd1cb2 100644 --- a/client/scripts/position_authority_client.gd +++ b/client/scripts/position_authority_client.gd @@ -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() diff --git a/client/test/position_authority_client_test.gd b/client/test/position_authority_client_test.gd index 5591b90..5896b0e 100644 --- a/client/test/position_authority_client_test.gd +++ b/client/test/position_authority_client_test.gd @@ -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")