Files
CosmicClash/Game
Josh Creek 7a1668c902 fix(multiplayer): second adversarial review - Esc, stranded clients, clock
A second adversarial review (this one able to RUN things, unlike the
first) reproduced five defects. Fixing the critical and high ones.

CRITICAL - Esc no longer left a networked match, and a client whose
server vanished was stranded forever. Two independent bugs composing:
_unhandled_input (added for spectator target cycling) overrode
GameMode._unhandled_input and returned early for every non-spectator
without ever calling super(), silently killing ui_cancel -> main menu;
and NetworkedMatch never connected NetworkManager.disconnected_from_
server the way lobby.gd does. Measured: a client whose host exited
emitted 7,235 engine errors in ~18s and only left because a test timer
fired. Now 1 benign teardown error, and it returns to the main menu.

HIGH - the match clock lost up to 3 seconds of regulation per goal.
_on_goal_registered extended end_tick by the celebration only
(resume_tick - goal_tick) and never by the 180-tick kickoff countdown
that follows it, while _update_clock derived remaining time from the
current tick regardless of _clock_running - so regulation drained during
every stoppage. Measured 660 PLAYING ticks for a 14s match against 840
expected: exactly one WARMUP lost. The HUD also opened at 0:17 for a 14s
match because the initial arm folded WARMUP into end_tick.

Replaced the per-goal arithmetic with bank-and-rebase: entering any
non-live state banks the remaining ticks, leaving it rebases end_tick
off the banked value. That covers celebration and countdown together and
cannot drift, since nothing has to predict how long a stoppage will be.
clock_state and match_bootstrap now carry remaining_ticks, which is
authoritative whenever the clock is stopped. Verified with the
reviewer's own metric: 840 PLAYING ticks for a 14s match, exactly.

MEDIUM - clients never froze at FULL_TIME/RESULTS. The freeze handling
sat inside `if multiplayer.is_server()`, so a local player flew around
for the whole 8s results screen while every other peer saw their ship
parked.

Not fixed, and now demonstrated rather than merely suspected:

- The 30s slot reservation is keyed on display NAME, so a stranger can
  take a departed player's ship and the real player is then locked out
  (reproduced). Worse than first thought: MatchNet.local_player_name
  defaults to "Player" and uniqueness is never enforced, so collisions
  are the common case, not an attack setup. Needs a real identity token;
  §6.2 step 1 reserves auth_ticket for Phase 7.
- §6.3's "late joiner takes the slot at the next kickoff" is
  unimplemented - _is_spectator is assigned once and never revisited -
  while the server logs that it happened.
- Replay log still ignores store_* return values, never records
  malformed/rejected inputs, and close() has no caller.
- --role=host-disconnect grades the reconnecting client on ~1s of life
  before the host quits, and never asserts the client owns _my_slot.

Regression: 87 unit tests; free-flight LAN; transition gate 0.00%; goal
cycle; spectator; disconnect and reconnect; full match to RESULTS.
2026-08-21 12:17:06 +01:00
..