Progress / Phase 2
Phase 2: Two Players, One World
Two players on different machines connect to the server at play.lorehallow.com and see each other move smoothly around one shared 30×20 map, on a connection layer that already survives slow and hostile clients and a bot swarm that measures it.
Phase 239 / 66 · 59%
This phase is still ahead.
2.1 Protocol v1
8 of 8 done- Handshake:
ClientHello(protocol version, client version, display name) andServerHello(player id, tick rate, server tick, spawn state); mismatched protocol versions get a clear rejection. -
PlayerInput(notInput, a Godot class name): sequence number, client tick and 8-way movement intent. Never a position. -
Snapshot: server tick, the last input sequence processed for the receiving player, and entity states (id, position, facing, animation state). -
EntityJoined,EntityLeft,PingandPong. -
Disconnect: a reason code (protocol mismatch, server full, too slow, kicked, shutting down, timed out, bad message) and readable text, sent before the server closes a connection. - Framing, limits and a byte budget per message (one
Envelopeper binary WebSocket message, maximum sizes per direction) documented indocs/design/protocol.md. - Round-trip golden tests for every new message.
- Hardened decoding: size checked before decoding, unknown payloads rejected, and fuzz targets for envelope decoding with a committed seed corpus, run by
make fuzz.
2.2 Shared map format
4 of 5 done- Decision Map source of truth: Tiled JSON (
.tmj) incontent/maps/(recommended), read by the server for collision and spawn points and imported by the client for rendering. - Server map loader for tile layers, the collision layer and spawn points, tested against a small fixture map.
- Client map import that builds TileMapLayers from the processed tilesets.
- Map validator in
make check: tilesets exist, collision layer present, at least one spawn point. -
make syncalso mirrorscontent/intoclient/assets/content/.
2.3 Connection lifecycle and backpressure
7 of 7 done- Decision WebSocket library (coder/websocket recommended); endpoint
/wswith origin checks, read limits and compression off. - Connection lifecycle as an explicit state machine (handshaking, in world, closing, closed): handshake timeout, player id assignment, spawn selection, and one cleanup path for every exit (close, error, timeout, eviction, shutdown).
- One reader and one writer goroutine per connection: the reader decodes, validates and forwards to the zone’s bounded inbox; the writer drains a send queue bounded by message count and bytes.
- Slow-client eviction: a full send queue closes the connection with
Disconnect(“too slow”), and the tick loop never waits on a socket (tested with a stalled reader). - Heartbeats: the server pings every connection, a read deadline drops silent ones, and round-trip time is recorded per connection.
- Limits: maximum connections (beyond it,
Disconnect“server full”), pending handshakes per IP, message size and inbound message rate, each with a test that proves the limit holds. - Graceful shutdown sends
Disconnect(“shutting down”) to everyone, drains, and leaves no goroutines behind (leak test).
2.4 Server simulation
9 of 9 done- One goroutine owns the zone: a fixed 20 Hz tick on a monotonic clock, measuring tick duration and warning on overruns, with no locks around world state.
- Per-connection input queues drained each tick; inputs applied in sequence order; stale and duplicate inputs dropped.
- Movement: fixed speed in tiles per second, axis-separated collision against the collision layer.
- Validation: input floods, impossible speeds and wall-walking have no effect; repeat offenders are disconnected and logged.
- Snapshot broadcast every tick to every connected player, each entity’s state encoded once per tick and shared by every recipient (interest management comes in Phase 3).
- Decision Snapshots carry only the entities that changed, and a player with nothing new gets no Snapshot but a keepalive each second (TCP is reliable, so no acknowledged baselines), measured before and after with
make loadtest. - Disconnects remove the entity for everyone within one second (
EntityLefton close, heartbeat timeout for silent drops). - Deterministic tests of the simulation with a fake clock: movement, collision, validation, join and leave.
- Benchmarks with
ReportAllocsfor the tick, snapshot encoding and input decoding, and an allocation test that failsmake checkif the steady-state simulation step allocates.
2.5 Bots and load generation
6 of 6 done-
server/cmd/lorehallow-bot: a headless Go client that connects, handshakes and wanders;-nruns many from one process cheaply, and it reports its own CPU and memory. - Cheat mode for the bot: input flooding, forged sequence numbers, oversized messages, unknown message types and a handshake that never finishes.
- Integration test proving cheat mode never moves a bot faster than allowed or through walls.
- Integration test: server plus two bots; each sees the other move, and sees the leave within one second of a disconnect.
-
make loadtest SCENARIO=<name>runs a scaling.md scenario against a fresh server and writes a draft result todocs/perf/;idleandspreadwork. - Baseline on
dev-m5: Case 1 Tier 1 (100 idle connections) and 100 bots inspread, recorded indocs/perf/with memory per connection, goroutines, tick p99 and bytes per player.
2.6 Client networking
0 of 8 done- Connect screen: server choice (default
wss://play.lorehallow.com/ws, plus local), display name, connect button and clear error states, including eachDisconnectreason. -
Netautoload: WebSocketPeer connection, envelope encode and decode, reconnect with jittered exponential backoff, signals per message type. - Input sampled at a fixed rate and sent as
Inputmessages with sequence numbers. - Local player drawn at the server’s authoritative position (prediction comes in Phase 3).
- Remote players drawn about 100 ms behind the latest snapshot, interpolating between buffered snapshots, with capped extrapolation for short gaps.
- Idle and walk animations per direction, driven by movement state.
- Debug overlay (F3): RTT, snapshot rate, interpolation buffer depth, server tick and entity count.
- Dev network simulation: configurable delay, jitter and drops, on the server and in the client.
2.7 Art Batch 03: player character and starter tiles
0 of 3 done- Draw Batch 03 by hand (manifest and sources in
art/src/): the player character sheet (32×48, four directions, idle 2 frames, walk 4 frames) and the extra tiles the starter map needs (trees, rocks, path edges, water edges and a few props). - Run intake, check frame alignment, iterate until accepted, and
make sync. - Character SpriteFrames and animation setup in the client; placeholder art fully replaced.
2.8 Starter map
0 of 2 done- Hand-build a 30×20 map from Batch 01 and 03 tiles, with paths, obstacles worth testing collision against, and at least four spawn points.
- Camera follows the local player with pixel-snapped movement and stops at map edges.
2.9 Deploy the game server
4 of 6 done- systemd unit
deploy/server/lorehallow-server.service: dedicated user, restart on failure, sandboxing options,LimitNOFILEraised for many connections, env file at/etc/lorehallow/server.env. - Caddy site for
play.lorehallow.comproxying/wsWebSocket upgrades to the server on localhost. -
make deploy-server: cross-compile for the host, upload, switch, restart, check/healthz, and roll back automatically on failure. - Public
GET /api/status(online count, uptime, version, protocol version) atlorehallow.com/api/status, cached briefly. - Website /status page and a landing-page indicator showing server state and player count (progressive enhancement).
- Logs in journald, and
docs/ops/runbook.mdstarted with start, stop, logs, deploy and rollback commands.
2.10 Playtest builds
0 of 2 done-
make exportbuilds Windows, macOS and Linux clients headless with the pinned export templates. - Publish the v0.2.0 builds as GitHub and Codeberg releases and list them on /downloads as “Prototype”.
2.11 Walkthrough
0 of 3 done-
docs/walkthroughs/phase-02-two-players.md: the tick loop, the snapshot format, interpolation, the connection lifecycle and backpressure, authoritative validation and the deploy, with pointers into the code. - Devlog draft for v0.2.0 with a short clip of two players moving.
- Release notes in
docs/releases/v0.2.0.mdand tagv0.2.0.
Acceptance criteria
1 of 7 done- Owner Playtest: two clients on different machines connect to the server at play.lorehallow.com over
wss://. - Each player sees the other moving smoothly with no teleporting or jitter under normal latency, and also with 100 ms simulated latency and 20 ms jitter.
- Closing a client removes that player for everyone within one second.
- A modified client sending forged or flooded input cannot move faster than allowed (integration test).
- A stalled client is evicted without raising anyone else’s tick time, and the server shuts down with no goroutines left behind (integration tests).
- Case 1 Tier 1 in scaling.md (100 idle connections) is met on
dev-m5, with the result indocs/perf/. - The walkthrough explains the tick loop, the snapshot format, interpolation and backpressure.
