← Server architecture · all tasks
Stage 3 — Godot client with prediction and reconciliation; two clients
server-architecture/007 · created 2026-08-02 · updated 2026-08-04 · blocked by server-architecture/006 · delivers #6
What to do
Add the client side: a Godot client that connects to stage 2’s world server, predicts its own movement locally for responsiveness, and reconciles against the server’s authoritative state as it arrives — the netcode decision overview.md calls out as “not deferred… retrofitting prediction onto a netcode layer that didn’t have it from the start is close to a rewrite.”
- A Godot client project using the
EntityNoderendering half of the split stage 2’s server already established forEntitydata — same codebase family, client-specific concerns only (rendering, local input, prediction). - Client-side prediction: on local input, move immediately and locally, without waiting for the server round-trip.
- Server reconciliation: when the server’s authoritative position for this client’s own entity arrives (per stage 2’s protocol), reconcile — correct the local predicted position if it’s diverged, per the standard prediction/reconciliation pattern this decision is named after.
- Two clients, simultaneously connected to the same world instance, each seeing the other move — this is the concrete proof this stage exists to deliver, not just “one client works.”
- Portal routing: a player crosses a portal from world A to world B — membership enforced at the destination, the destination world spawned/hydrated on demand if not already running, and the player’s entity state transferred through the director with no loss or duplication. See
overview.md’s “Not yet decided: the portal-transfer mechanism itself” note — the actual transfer mechanism (candidate: Postgres-as-transfer-medium, snapshotting out of world A and hydrating into world B) and its related snapshot-boundary question are open design questions, not settled ones; confirm an approach with Kris before locking in the implementation, don’t just build the candidate silently. - Automated tests where feasible (reconciliation logic, message handling) plus a headless two-client test-client script that connects two instances of the test client (not two humans clicking) and asserts each sees the other’s position update correctly — consistent with every other stage’s “no listening port to click on” constraint. Extend this (or add a companion script) to cover the portal-routing case: a client crosses a portal and ends up correctly placed in, and only in, the destination world.
- Handoff note:
land-of-lor/pocs/multiplayer-poc/handoff/stage-3.md, including explicitly what a human should look at when actually running two real Godot client windows on the droplet/locally (this is the one stage where an actual visual check by Kris is genuinely the most meaningful verification, even though the automated headless version has to exist too). - Web export, deployed to the droplet — this stage’s close-out, and how Kris human-verifies it. Godot’s HTML5/web export target for the same client from step 1, built and served from
lor-game-server-01(deliverable #4/#5), connected to a live world. Verification is two browser tabs open to that URL, each seeing the other move — the same bar as the headless two-client test, just witnessed directly instead of read from a script’s assertions. This isdeliverable #6(deliverables/006-play-in-your-browser.md); it does not flip todelivereduntil this link is live and that two-tab check has actually been done. Known prerequisite, not yet resolved: there’s no deploy key set up for getting build output onto the droplet yet (see deliverable #5’s server notes) — sort that out as part of this step, don’t assume it exists. Known constraint, not a blocker: the droplet currently serves plain HTTP with no domain/TLS, so this ships asws://from anhttp://page — that’s fine for the PoC, but flag in the handoff note that a later move to a real domain + TLS turns this intowss://, a client connection-code change, not just a hosting one.
Definition of done
Automated/headless-scripted verification passes and demonstrates two clients observing each other’s movement, plus a portal crossing between two world instances with membership enforced, on-demand destination hydration, and lossless entity-state transfer; the handoff note exists. Additionally, per the delivers: [6] gate (see epics/README.md’s “Promised deliverables” section): this task cannot move to done until deliverable #6 is delivered with its live link in place — the web export running on the droplet and the two-browser-tab check, not just the headless proof. The review→done gate this extends already required Kris’s sign-off; this makes explicit that his sign-off on deliverable #6 specifically is part of it. Set state: review, not done, once the automated/headless portion lands — done still waits on the web-export deploy and Kris’s check.
Where the work lands
land-of-lor/pocs/multiplayer-poc/.
Docs to read first
server-architecture/overview.md’s “Netcode” section (prediction/reconciliation, the reason it’s day-one not deferred). Stage 2’s handoff note for the protocol/movement contract this stage’s client talks to.
Status (2026-08-03)
State: review. All of steps 1-8 are built and automated-verified; deliverable #6 is delivered (live link in place, verified externally per the gate in “Definition of done” above) — so the delivers: [6] gate is satisfied. state: review (not done) regardless, per this epic’s build-plan gating: only Kris moves a stage to done, same rule every prior stage followed.
Built, across three sessions (server protocol, client, deploy):
- Portal transfer (“Postgres as the transfer medium, via the director” — closed 2026-08-03, see
overview.md’s “Persistence” section) andseq-based move reconciliation protocol (intent_ack/intent_rejected), director + world-server. - A real Godot client (
world-server/client/) reusingmovement.gd/grid.gd/entity.gd/protocol.gdbyte-for-byte: client-side prediction against the sharedMovement.try_move(), server reconciliation onintent_ack, interpolated rendering of remote entities,EntityNode(ColorRect-based, per Kris’s own “we see each other’s balls moving” bar), full connect/join/hello flow with remembered identity, and portal traversal (direct handoff + a proven 2-second crash-safety fallback — see handoff note’s Deviation 3 for why the fallback, not the direct handoff, is the actually-reliable path in this Godot build). - Automated headless verification: 28 vitest + 12 + 15 CLI-driven checks (portal transfer incl. real
SIGKILLcrash-safety, mutual visibility, reconciliation-correction proof, illegal-intent refusal, portal crossing incl. crash-safety) + 41 Godot pure-logic checks — all independently re-run and confirmed by the orchestrating session, not just trusted from the building agents’ own reports. - Web export (Godot 4.7.1 HTML5,
world-server/client/web-build/, committed to the repo) deployed tolor-game-server-01: Node 22 installed, director built and running aslor-director.service(replacing a placeholder stub), the obsolete placeholderlor-world@1.servicestopped/disabled (the director spawns world processes itself, on demand — no per-world systemd unit), Caddy serving the web export at/play/*, firewall opened for direct game-channel connections (32768:60999/tcp, the ephemeral rangeallocateFreePort()actually uses). - Live, external verification from the container:
director/cli-test-client/verify-live-droplet.ts(npm run verify-live-droplet) proves, over the public internet against the real running droplet (not a local stand-in), two accounts joining the same live-spawned world, both connecting directly to its realgamePort, mutual movement visibility both directions, and a live illegal-intent refusal — 9/9 passed. The web client URL and its.wasm/.pckassets independently confirmed to return HTTP 200 with correct sizes.
Known gap, flagged plainly, not silently worked around: a GitHub deploy key for the droplet (deliverable #5’s outstanding follow-up) could not be registered via API this session — the container’s token lacks the required “Administration” repo permission (403). A read-only keypair was generated on the droplet (public key in deliverables/006-play-in-your-browser.md) and needs a human with GitHub admin access to add it. Code was still gotten onto the droplet, via the same git bundle transfer method deliverable #5 originally used — not blocking, just not the cleaner git pull workflow deliverable #5 described as the goal.
What’s still a human check, not provable from this container: actually opening http://209.38.29.22/play/index.html?director=ws://209.38.29.22/director/ in two browser tabs and watching the canvas render — no display/browser exists in this container. The live-verification script proves the underlying protocol traffic works end-to-end; the visual confirmation is Kris’s own remaining step, same as it would be for the desktop two-client check.
Full detail: land-of-lor/pocs/multiplayer-poc/handoff/stage-3.md (and handoff/stage-3-server-side.md for the protocol-only first half).
Verified and closed (2026-08-04)
Kris performed the human verification bar directly: the web client at https://lor-server.cocreations.com.au/play/index.html?director=wss://lor-server.cocreations.com.au/director/ — two browser tabs, each seeing the other’s player move in the live commons world. (His words on seeing it work are not printable on a family-friendly board, but they were an emphatic yes.) Getting the browser path to this point required the same-day HTTPS wiring and the public-commons-world work recorded in deliverable #6’s 2026-08-04 update. The delivers: [6] gate was already satisfied; with the visual check done, this stage is done.
View source in repository · also available as raw markdown.