← Server architecture · all tasks

Stage 2 — Godot headless world server with grid movement

done priority 2 estimate L

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.

  1. 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 the Entity/EntityNode split overview.md’s “Runtime” section names — server runs the pure-data Entity logic, no rendering.
  2. Grid storage uses the required fix from overview.md’s “Pathfinding and sensing” section from the start — a flat PackedInt32Array indexed by (layer * h + y) * w + x, statically typed hot-loop variables — not lor-elementals’ string-keyed Dictionary pattern. This stage is exactly where that correction has to actually land in code, not just in spec.
  3. 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.
  4. Hydration/snapshot hooks that actually plug into stage 1’s director-driven lifecycle (spawn → hydrate from Postgres → run → snapshot every 30s + on shutdown → exit).
  5. 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).
  6. 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.
  7. 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

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.