test(multiplayer): grade §6.4's reconnect from the returning player's side

The disconnect scenario only ever asserted the server's bookkeeping, and
the client's half was failing every run. run_disconnect_host_check ticked
60 physics frames past the reclaim and then shut the server down, so the
reconnecting client - whose wiring check waits a 2.0s settle before it
looks at anything - had its peer torn out from under it and reported
"current_scene is not NetworkedMatch after 2.0s". The host printed PASS
throughout, and the host was the side anyone read.

The hold is now a real window (8s), and the host also asserts that the
reconnected player's input reaches the server and moves the ship the
server owns - every other assertion there is slot bookkeeping that would
hold identically for a client whose input pipeline came back dead. Both
position and connection state are sampled while the peer is still
connected: the client leaves on its own schedule, and an end-of-hold
sample reported still_connected=false for a good run.

New --role=client-reconnect asserts the returning player is not a
spectator, owns a slot with its own peer_id, has a real ship, rejoined a
live match with the clock already known (§6.2 step 2's bootstrap), and
can still drive. That set is chosen because a stale _last_match_config
once made a reconnecting player a spectator, and that bug was visible in
this scenario's own logs while it reported PASS.

Verified 3/3 both sides. Control: rejoining while the slot is still
occupied fails on is_player=false - and since the first control run
reported it as the generic "lost its ship mid-drive", the spectator case
is now diagnosed before the drive rather than after.
This commit is contained in:
Josh Creek
2026-08-21 16:25:25 +01:00
parent 866efa0d9b
commit 5714829c13
3 changed files with 158 additions and 9 deletions
+5 -1
View File
@@ -1032,9 +1032,13 @@ Fixed by not policing a backlog the server caused: `MatchSim._physics_process` w
Abuse detection is unweakened and this was checked rather than argued: all three abuse roles still disconnect, and **no flood induced a server stall in any run**, so the grace cannot be farmed by flooding. An attacker who *can* induce server stalls to earn budget already has a strictly worse capability than sending extra input packets.
**§6.4's reconnect was only ever graded from the server's side, and the client's side was failing the whole time.** `run_disconnect_host_check` ticked 60 physics frames (1.0s) past the reclaim and then shut the server down — so the reconnecting client, whose wiring check waits a 2.0s settle before it looks at anything, had its peer torn out from under it every single run and reported `current_scene is not NetworkedMatch after 2.0s`. The host printed PASS throughout, and the host was the side anyone read. The hold is now a real window (default 8s), and the host additionally asserts that the reconnected player's input reaches the server and moves the ship the server owns — every other assertion there is slot bookkeeping that would hold identically for a client whose input pipeline came back dead, which is the exact failure the reservation exists to prevent. Both the position and the connection state are sampled *while the peer is still connected*, not once at the end of the hold: the client leaves on its own schedule, and an end-of-hold sample reported `still_connected=false` for a perfectly good run — the same mis-timed sampling a Phase 3 review caught in the CI gate.
New `--role=client-reconnect` grades the returning player: not a spectator, owns a slot whose `peer_id` is its own, has a real ship, rejoined a live match with the clock already known (`_end_tick >= 0` — §6.2 step 2's bootstrap, since a player who must wait for the next goal to learn the score has not really rejoined), and its input still moves its ship. That set is chosen because a stale `_last_match_config` once made a reconnecting player a spectator, and *that bug was visible in this scenario's own logs while it reported PASS*. Verified 3/3 both sides, with a control that rejoins while the slot is still occupied and correctly fails on `is_player=false`. The first version of that control failed with the generic "lost its ship mid-drive", so the spectator case is now reported before the drive rather than after.
`tools/replay_dump.gd` reads a log back — record counts by kind, plus how much of the input sequence stream actually reached the server once redundancy is counted. It is committed rather than left in a scratch directory because it is what turned "the server dropped some input" into the numbers above, and a log nobody can read is half a feature.
**New/changed test surface:** `--exercise-match-state` (both roles; host forces a goal, client validates the whole observed sequence and the wire byte), `--role=host-disconnect` for the 5.6/5.7 three-process scenario, `--match-length=<s>` to reach `FULL_TIME` in a short run, `--replay-log=<path>`, `--fill-bots`/`--no-fill-bots`, `--max-spectators=<n>`. The ball-contact scenario now **steers at the ball with closed-loop real input** instead of a hand-tuned fixed-heading burst, which 5.3 broke by adding `KICKOFF_YAW_JITTER` (0 contacts in 3/3 runs); it thrusts while turning rather than hovering to aim, which took it from 2/3 to 5/5.
**New/changed test surface:** `--exercise-match-state` (both roles; host forces a goal, client validates the whole observed sequence and the wire byte), `--role=host-disconnect` for the 5.6/5.7 three-process scenario (paired with `--role=client-reconnect`, which grades the returning player), `--match-length=<s>` to reach `FULL_TIME` in a short run, `--replay-log=<path>`, `--fill-bots`/`--no-fill-bots`, `--max-spectators=<n>`. The ball-contact scenario now **steers at the ball with closed-loop real input** instead of a hand-tuned fixed-heading burst, which 5.3 broke by adding `KICKOFF_YAW_JITTER` (0 contacts in 3/3 runs); it thrusts while turning rather than hovering to aim, which took it from 2/3 to 5/5.
**Phase gate:** a full 3v3 start-to-finish including a mid-match disconnect and a late joiner. **Not yet run** — every scenario above was verified at 1v1 (plus a two-bot CI match). The 3v3 gate needs a real multi-client session and is the outstanding item for this phase, alongside Phase 4's own un-run human playtest.