PC3 Phase 2: orchestration under checkpoint control + networking doc

- pm/checkpoint_ops.py (framework): playtest backend now launches with
  GAME_SERVERS_ENABLED + GODOT_BIN + GAME_PROJECT_DIR + GAME_SERVER_HOST so
  playtesters actually share a world.
- docs/architecture/networking.md 7a/7b: the orchestration + client-play
  design as built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
2026-09-05 22:08:02 +00:00
parent dba865ae4b
commit 50c6a31c25
+56
View File
@@ -237,6 +237,62 @@ snapshot_state:
---
## 7a. Server Orchestration (implemented, PC3 Phase 2)
**One dedicated server process per world**, spawned on demand.
```
client backend (single worker) host
│ POST /api/worlds/{id}/join (bearer JWT) │
│ ───────────────────────────▶ GameServerSupervisor │
│ ensure_server(world_id): │
│ if none running for world ─── spawn:
│ godot --headless
│ res://scenes/server.tscn --
│ --port <p> --world <id>
│ --status-file <path>
│ ◀─────────────────────────── { host, port, world_id } │
│ │
│ ENet connect host:port ───────────────────────────────────▶ dedicated server
```
- **Supervisor** (`backend/app/services/game_servers.py`): in-process singleton,
so the backend MUST run single-worker. Keys servers by `world_id`. Allocates
ports from `GAME_SERVER_PORT_MIN..MAX`. A background reaper kills a world's
server once its **status file** (`{players, tick, updated}`, rewritten by the
server ~1×/s) reports `players == 0` for `GAME_SERVER_IDLE_TIMEOUT_SECONDS`.
No crash-restart — the next join respawns.
- **Enable flag:** `GAME_SERVERS_ENABLED` (default off). The checkpoint/playtest
launcher (`pm/checkpoint_ops.py`) turns it on, passes `GODOT_BIN`,
`GAME_PROJECT_DIR`, and `GAME_SERVER_HOST` (the LAN IP baked into the `.exe`).
A backend with it off returns **503** from `/join`, and the client falls back
to local single-player.
- **Per-world content:** all worlds currently load the same test level (M2/M7
work). `world_id` still scopes a distinct process + session.
### 7b. Client play path
`world_list.gd` → `POST /join` → `Session.begin(world_id, host, port)` →
load `game.tscn`. `game.gd` sees `Session.active` and connects a
`NetworkClient`:
- **Local player** keeps predicting with its own collision-aware
`CharacterBody3D` (`player.gd`) — that *is* the prediction. Its input frame
(`player.net_input`) is forwarded to the server every physics tick.
- **Correction backstop:** if the local position drifts from the server's by
more than `CORRECTION_THRESHOLD` (1.5 m — generous; catches teleport / a wall
walked through / real desync, not prediction noise) it snaps.
- **Remote players:** rendered from snapshots, position lerped. *Appearance sync
is a follow-up* — remotes currently show as labelled capsules; the server
does not yet replicate avatar loadouts.
- **Disconnect** → status message → back to the world list.
The `NetworkClient`'s own flat-plane prediction + input-replay reconciliation
(§5) is exercised by `smoke_test_network_client.gd`; the collision-aware game
path above uses the lighter `send_input()` + drift-snap instead.
---
## 8. Security & Anti-Cheat
- **No client-authoritative gameplay values.** Clients only send *inputs* and