← Server architecture · all tasks
Stage 2 — Godot headless world server with grid movement
server-architecture/006 · created 2026-08-02 · updated 2026-08-03
What to do
Add the actual Godot headless world-server process — the thing stage 1’s director spawns/hydrates/snapshots/exits, per overview.md’s “Persistence” lifecycle.
- A Godot 4 headless project (in
land-of-lor/pocs/multiplayer-poc/, alongside stage 1’s director) implementing grid-based entity movement server-side, following theEntity/EntityNodesplitoverview.md’s “Runtime” section names — server runs the pure-dataEntitylogic, no rendering. - Grid storage uses the required fix from
overview.md’s “Pathfinding and sensing” section from the start — a flatPackedInt32Arrayindexed by(layer * h + y) * w + x, statically typed hot-loop variables — notlor-elementals’ string-keyedDictionarypattern. This stage is exactly where that correction has to actually land in code, not just in spec. - Server-authoritative movement: the world server accepts intent messages (over the stage-1 protocol, extended with movement messages) and computes position itself; nothing about position is ever trusted from a client message.
- Hydration/snapshot hooks that actually plug into stage 1’s director-driven lifecycle (spawn → hydrate from Postgres → run → snapshot every 30s + on shutdown → exit).
- Automated tests: movement logic, grid indexing correctness, hydrate/snapshot round-trip of actual entity state (not just the director’s world-record scaffolding from stage 1).
- A headless test-client script (extends stage 1’s CLI client) that connects, sends movement intents, and asserts the server’s resulting authoritative position matches expectations — no manual clicking required to verify this stage.
- Handoff note:
land-of-lor/pocs/multiplayer-poc/handoff/stage-2.md.
Definition of done
Automated tests pass, the headless test-client script demonstrates server-authoritative grid movement end-to-end, the handoff note exists. Set state: review, not done — same rule as stage 1.
Where the work lands
land-of-lor/pocs/multiplayer-poc/.
Docs to read first
server-architecture/overview.md’s “Runtime” and “Pathfinding and sensing” sections. lor-elementals/scripts/entities/entity.gd, entity_node.gd, and grid_manager.gd — the pattern to reuse (Entity/EntityNode split) and the pattern to specifically not repeat (the string-keyed grid Dictionary). Stage 1’s handoff note (handoff/stage-1.md) for the protocol/schema this stage builds on.
Status (2026-08-03)
State: review, not done — awaiting Kris’s verification per the epic’s build-plan gating; do not start Stage 3 (007) until he moves this to done. No delivers: gate applies to this task (unlike 007/009) — Kris’s sign-off alone unblocks Stage 3.
Built: land-of-lor/pocs/multiplayer-poc/world-server/ — a headless Godot 4.7.1-stable project (toolchain already pinned/provisioned at /workspace/lor/godot-setup-for-pocs/, no new provisioning needed) implementing the real world-server process that replaces Stage 1’s in-memory stub. Server-side WorldEntity (pure RefCounted, no Node) and WorldGrid (flat PackedInt32Array, (layer*h+y)*w+x indexing, statically typed — not grid_manager.gd’s string-keyed Dictionary). Two WebSocket channels: a control channel (world↔director, reusing the director’s existing WS port, extending Stage 1’s protocol with world_register/world_snapshot/world_shutdown) and a game channel (game client↔world process directly, via WebSocketMultiplayerPeer at the low-level packet-peer API). Server-authoritative movement at the decided 20Hz sim / 10Hz broadcast rates; illegal intents (walls, out-of-bounds) and unrecognised message types (standing in for direct position assertion) are refused/rejected, tested explicitly. All three required persistence mitigations from the 2026-08-03 “snapshots flow via the director” decision are implemented and tested against real OS processes: idempotent snapshots keyed by world id + seq, buffer-last-unacked + retry-on-reconnect, and shutdown blocking on a persistence ack. director/src/worldInstance.ts rewritten to actually spawn/hydrate/register/snapshot/shut down the real Godot process (env-overridable GODOT_BIN/world-server path), with protocol.ts/server.ts/worlds.ts extended accordingly.
Verification, independently re-run and confirmed by this session (not just the building agent’s own report): npm test — 23/23 passed (17 from Stage 1 + 6 new). npm run test-client — 9/9 passed (Stage 1 unmodified, no regression). npm run test-client-2 — 17/17 passed, the actual Stage 2 proof: server-computed movement, wall/out-of-bounds/unknown-type rejection, and — the hardest property — killing the Godot world process directly (SIGKILL, not the director) mid-play, rejoining, and confirming a fresh OS process (different pid) respawns and hydrates from the last persisted snapshot. Godot pure-logic test runner (test_runner.gd) — 31/31 passed standalone, no networking.
Full detail, including deviations from the original design note (SIGTERM doesn’t reach headless GDScript; saveWorldState now merges instead of replaces; WebSocketMultiplayerPeer’s leading system packet; preload()-based cross-file references instead of bare class_name): land-of-lor/pocs/multiplayer-poc/handoff/stage-2.md.
Assumptions
- Game clients connect directly to the spawned world process’s own port (
gamePort), not through the director, for gameplay traffic — the director’sjoin_worldresponse now includesgamePortand only resolves once the process has registered, so the port is always live. This wasn’t explicitly specified in the task body, but follows directly fromoverview.md’s “Runtime” rationale for choosing WebSocket (client and server run the same codebase, talking directly) and from the epic’s own Stage 3 framing (“two clients connected simultaneously to the same world instance”). - Control channel reuses the director’s existing WS server/port rather than a second listener, per the persistence decision’s “existing world↔director channel” wording — a spawned world process connects to it as a client alongside game clients, distinguished only by message type, not by a separate port or connection role.
- A per-spawn random
spawnToken(not tied to accounts/auth) is the trust mechanism between a specific spawned process and the director’s in-memory record of it — sufficient for this PoC stage; not a security mechanism intended to survive into a production identity story.
Closed 2026-08-03 — state: done
Kris directed Stage 3 to proceed, in direct conversation with his assistant (2026-08-03), rather than by personally re-running the suites himself. Recorded honestly: this sign-off rests on the Stage 2 closing session’s own independent re-verification logged above (23/23 vitest, 9/9 Stage 1 client, 17/17 Stage 2 client, 31/31 Godot logic tests), not on a fresh personal run by Kris. This unblocks server-architecture/007 (Stage 3).
View source in repository · also available as raw markdown.