diff --git a/docs/architecture/networking.md b/docs/architecture/networking.md index ca523a5..c313ad8 100644 --- a/docs/architecture/networking.md +++ b/docs/architecture/networking.md @@ -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

--world + │ --status-file + │ ◀─────────────────────────── { 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