Repository navigation
feat(game): authoritative multiplayer game runtime for v5 - #280
Open
patrickleet wants to merge 258 commits into
Open
patrickleet wants to merge 258 commits into
patrickleet wants to merge 258 commits into
Conversation
Introduce the distributed_game workspace member: a Zone trait plus a synchronous ZoneHost runtime with lifecycle hooks (create, auth, join, leave, observe, dispose), capacity, per-zone simulation-time timers, fixed timestep with catch-up cap, patch cadence independent of the tick rate, and Hot/Idle/Dormant activity tiers with observable transitions. Inputs are buffered per player (sequence dedupe, bounded, sanitize hook, next/latest/drain policies) and the last processed sequence is acked in every Delta. Clocks are injectable (system/manual) and each zone gets a seeded xoshiro256** RNG derived from world seed and zone id; a test proves identical state hashes for identical seed and input log. Facts are emitted without blocking the tick into a bounded queue; a FactFeed worker commits them through a FactSink with retries and backoff and settlements return on later ticks. The cell feature adds CellFactSink, which dispatches facts into an AggregateCell via dispatch_idempotent with a server-built Session whose principal comes only from the verified ticket. FactStamp helpers stamp zone/tick/content version into EventRecord metadata. The binary protocol codecs, entity schema with quantization and the interest-managed delta replicator used by the host are included here; the transport and protocol spec follow. Refs [[tasks/gr-p2-zone-runtime]]
Document the v1 binary wire protocol in PROTOCOL.md (little-endian,
LEB128 varints, one message per WebSocket frame) with worked byte
examples for Hello, Input, Welcome (including the entity schema) and
Delta, bit-exact f64 quantization rules for quant_f32 and angle fields,
and HS256 ticket rules. Golden fixtures (primitives, quantization,
messages) are generated into tests/fixtures for the JS client.
Replication is per client against last-sent state with a uniform
spatial grid, view radius with leave hysteresis, global entities,
observer subscriptions and a byte budget driven by priority
accumulation so starved entities are eventually sent.
The net feature adds GameServer: an axum WebSocket server hosting many
zones (one task per zone, routed by /zones/{zone_id}), binary frames
only, HMAC-SHA256 ticket admission with single-use nonces where the
principal comes only from the verified ticket, per-connection input
rate limiting, reconnect tokens within the grace window, dev latency
and jitter simulation, and graceful shutdown. An end-to-end test drives
it with tokio-tungstenite.
Refs [[tasks/gr-p3-replication-transport]]
New `@hops-ops/distributed/game` subpath: a byte-exact implementation of distributed_game PROTOCOL.md v1 (varints, schema from Welcome, quantization identical to Rust), GameConnection (handshake, reconnect token with grace fallback to a fresh app ticket, Ping/Pong clock sync, typed events, injectable socket and scheduler), EntityReplica with interpolation buffers, Predictor with ease/snap/replace reconciliation and a multi-zone GameClient that follows Handoff without blanking the rendered world. Tests assert every Rust golden fixture byte-for-byte and cover replica interpolation, divergence correction, reconnect and handoff over a fake socket. Refs [[tasks/gr-p4-client-sdk]]
Lag compensation: a bounded per-zone ring buffer of entity hit volumes (sphere / vertical capsule + numeric fields) filled by the new Zone::record_rewind hook after every tick. Ctx::rewind() exposes exact and interpolated lookups and segment/ray/point hit tests evaluated at the shooter's view time (client_tick minus a game-reported interpolation delay), clamped to HostConfig::rewind.max_rewind. Record/replay: Recorder wraps a ZoneHost and logs every stimulus with its clock reading plus per-tick state hashes and fact settlements with their delivery tick, into a compact DGR1 file. replay() re-runs it on a ManualClock with a scripted fact queue and reports the first divergence with the suspect stimuli. Zone::state_hash lets games cover unreplicated state (default: hash the replicated WorldView). AdvanceReport additively gains dropped_ticks, tick_durations, entities, per-frame DeltaStats, settlements and tick hashes. No wire change. [[tasks/gr-p8-netcode-ops]]
New dependency-free `metrics` feature (implied by `net`): an instance-scoped GameMetrics registry rendering Prometheus text for tick duration, ticks/dropped ticks, tier, players, observers, entities, inputs by outcome (incl. transport rate limiting), Delta bytes per frame and facts pending/settled. The zone label is bounded: only the first ServerConfig::metrics_zone_labels live zones are labelled, the rest fold into zone="_other", and stopped zones free their slot. GameServer serves it at GET /metrics. The core distributed::metrics registry stays untouched: it is a fixed, process-global framework set and distributed_game does not depend on distributed outside the `cell` feature. NetSim gains loss with WebSocket-correct semantics (a retransmission delay with head-of-line blocking, never a dropped or reordered frame) and NetSim::from_env for demos. [[tasks/gr-p8-netcode-ops]]
New `bots` feature: a WebSocket bot library built on the reference ClientMirror. Bots mint signed tickets, join a zone, send inputs at a fixed rate from a pluggable Behaviour (random walk, circle), ping for RTT and count Deltas and bytes; run_bots returns a Summary with joined/failed counts, deltas per bot, bytes/s and RTT quantiles. The game-bots binary wraps it with flags/env for demos. A test runs 50 bots against an in-process server and scrapes /metrics. [[tasks/gr-p8-netcode-ops]]
README gains a netcode/operations section (rewind, record/replay, metrics cardinality guard, bots, NetSim loss semantics and env vars). PROTOCOL.md clarifies the meaning of Input.client_tick for lag compensation; the encoding is unchanged. [[tasks/gr-p8-netcode-ops]]
IndexedDB persistence now advertises supportsOfflineCommandOutbox and offers commandOutbox(): a command-transport decorator that writes the exact prepared request before sending, keeps live callers waiting through outages, drains in enqueue order on reconnect or after a reload reusing the same command ids (server receipts dedupe resends) and surfaces rejections to the live caller and via onSettled. Durability is opt-in per command and fails closed for sensitive or unlisted commands; entries replay only under the same cache scope. Confirmed-only state persistence is unchanged. Also exports the ./game subpath in the pack-smoke contract and documents the game client and outbox in the package README. Refs [[tasks/gr-p4-client-sdk]]
P5 world structure: ZoneDirectory (StaticDirectory, HashRing with virtual nodes) with Redirect on misrouted joins; fenced zone ownership (LocalOwnership, FencedLeaseOwnership over distributed::lease) renewed by every activation, lost claim stops the zone; dormant zones hibernate into a fenced ZoneStateStore (memory, native SQLite) and restore on the next activation; facts queued at dispose are still committed; signed single-use transfer tickets with carried state, reserve/confirm/release at the issuing host and return-to-source on refusal; zone hierarchy with tier-up ChildEvents routed across hosts. P7 freeze barrier: two-phase WorldCoordinator over local servers and admin-HTTP remote hosts (freeze + ack with timeout, apply, commit or abort to the old version), Zone::freeze_ready, inputs dropped and counted while frozen, version stamps, StalePolicy::SafeRoom gates with Kick(RELOAD) and rolling/hard modes. Wire (additive): 0x89 Redirect, Kick code 7 RELOAD; golden fixtures. Refs [[tasks/gr-p5-world-structure]] [[tasks/gr-p7-freeze-barrier]]
PROTOCOL.md: Redirect message, Kick RELOAD, transfer tickets and the client handoff/return procedure, content-version policies. README: world features, hibernation fact policy, saga sketch for durable player relocation, admin routes. Refs [[tasks/gr-p5-world-structure]] [[tasks/gr-p7-freeze-barrier]]
A tiny `net` WebSocket zone server example that the JS suite spawns via cargo when DISTRIBUTED_GAME_E2E=1. The test mints HS256 tickets per PROTOCOL.md §8, joins, predicts and reconciles against real acks, checks clock sync, resumes after an abrupt drop with the reconnect token and opens an observer connection to a neighbouring zone. Refs [[tasks/gr-p4-client-sdk]]
Tracks the additive protocol v1 changes from the world/barrier phase: Redirect (0x89) resends the same Hello to the owning host (max 3 hops), Kick RELOAD and handshake VERSION_MISMATCH enter reload-required and rejoin with the new content version (token within grace, else fresh ticket), Resume.version is remembered as the reload target, and a refused or silent handoff target sends the same Hello back to the source zone before falling back to a fresh ticket. Interpolation delay and view tick are exposed to encodeInput and the replica for lag compensation. Golden tests include the kick_reload and redirect fixtures copied from the world branch. Refs [[tasks/gr-p4-client-sdk]]
Combines world structure/barrier with netcode ops: ServerConfig and
Inner carry both metrics and world fields; the router serves /metrics
alongside the /admin routes; run_zone records metrics in the reworked
loop and forgets a zone's series only when no newer activation runs;
frozen input drops get their own metrics outcome ("frozen"); the replay
Recorder covers join_transfer, reconnect_with_version,
set_content_version, request/cancel_freeze, reload_stale_players,
deliver_child_event and restore (DGR1 tags 9-16, stale_policy in the
config), with handoffs replayed against a stub issuer.
Refs [[tasks/gr-p5-world-structure]] [[tasks/gr-p7-freeze-barrier]]
[[tasks/gr-p8-netcode-ops]]
# Conflicts: # distributed_game/Cargo.toml
The temporary copy of the Redirect and Kick RELOAD fixtures is no longer needed after merging the world branch. [[tasks/gr-p4-client-sdk]]
Dev network simulation was fixed at ServerConfig time, so demos could not offer a live latency/jitter/loss control. GameServer now holds the simulation in a watch channel that every connection reads per message. Switching it off while messages are still delayed keeps them ordered. Refs [[tasks/gr-p10-demos]]
HUDs and bandwidth graphs need Delta bytes/s, which the connection did not surface. Count bytes and messages in both directions (Delta bytes separately) across every socket of the connection. Refs [[tasks/gr-p10-demos]]
Apps that host a lobby, health or dev endpoint next to the zones had to re-implement serve() to merge routes, losing the wait for zone tasks on shutdown. serve_with merges an extra Router and keeps the graceful shutdown (kick, dispose, join zone tasks). serve() delegates to it. Refs [[tasks/gr-p10-demos]]
A symmetric quant_f32 range has 2^bits - 1 intervals, so 0 is not on the grid: a server that canonicalizes its state each tick (to make client prediction exact) stores a resting velocity as half a step and entities creep. quant_centered(range, bits) shifts max by one step so 0 encodes exactly at q = 2^(bits-1). No wire change; PROTOCOL.md notes it. Refs [[tasks/gr-p10-demos]]
examples/game hosts the distributed_game demos: a `game-demos` binary (own cargo workspace) with one subcommand per demo, a Vite + Babylon.js client using the local SDK, and run.sh for one-command dev and smoke. Shared scaffolding: common::Demo trait, spawn/run, dev-only /dev/ticket (server-generated guest principals, signed guest tokens) and /dev/netsim (live NetSim), JSON events; client hud, input, scene, dev-api and events modules. Arena: 30 Hz zone with capsules, dash, pillars, 30 interpolated drones and a 14 m interest radius. Movement is shared by Rust and TypeScript and bit-exact (fixture replayed by a node test); smoke tests run 20 bots in-process and a headless SDK client against the real binary. Refs [[tasks/gr-p10-demos]]
Zones of one server shared a single HostConfig, so a hierarchy could not run a slow province clock next to hot neighborhoods. The hook adjusts each activation's HostConfig; the world content version is applied afterwards and cannot be overridden. Refs [[tasks/gr-p10-demos]]
GameClient built every new connection (handoff target, observer, rejoin) from options.contentVersion, so after a Kick(RELOAD) the next handoff presented the stale version and was refused. The client now tracks the version its authority presented after a reload and exposes setContentVersion() for clients that patch content live. Refs [[tasks/gr-p10-demos]]
Targets race on rails and circles; shooters fire hitscan rays from fixed stations. The zone resolves each shot with the rewind history at the shooter's exact view time (client_tick plus the delay to the rendered instant), clamped to 500 ms, or at the present when compensation is off. The client adapts its interpolation delay to the measured latency and compares predicted with confirmed hits. Refs [[tasks/gr-p10-demos]]
Two multi-host demos on a shared cluster module (--host-id, --peer, admin secret derived from the ticket secret, epoch-based fenced claims, SQLite snapshots, guest names shared with peers): - grid: procedural 32 m cells as zones on a hash ring, handoff across borders and hosts, hibernation of critters and markers, world map dev route; Rust/TS parity fixture for terrain, props and movement. - ripple: realm > city > borough > hood with per-tier tick rates, bosses whose defeats cascade upward as tier-up events, every hop on a different host. Refs [[tasks/gr-p10-demos]]
common::spawn_with / run_with take a builder returning the GameServer plus demo-specific dev routes, so a demo can share state (a durable store, a leaderboard) between its zones and its routes. spawn/run delegate to them unchanged; random_hex is re-exported. Refs [[tasks/gr-p10-demos]]
Mob Pit: 30 Hz melee zone with 12 telegraphing mobs. Kills emit non-blocking facts committed into per-player Crawler aggregates in native SQLite cells (cell-sqlite) via CellFactSink; XP is decided by the cell from the mob tier, the principal comes from the verified ticket. A dev "slow storage" knob delays commits; the tick stays flat. The ticket secret persists in the data dir for crash-recovery runs. Leaderboard: kills/XP boards (in-memory or Redis) fed fire-and-forget through BackgroundLeaderboard with deterministic occurrence ids, so live updates, ledger replays and rebuilds from cell event history never double count. Top 10 is pushed as a zone event. Smoke tests cover exactly-once commits, slow storage, kill -9 restart on the same data dir with replay of a pre-crash fact key, and boards matching the cells. Refs [[tasks/gr-p10-demos]]
Mob Pit page (Babylon): mobs with HP bars and filling telegraph rings, blade swing and cone, kill popups that turn into "+XP saved" when the fact settles, XP bar from the cell, pending-facts vs tick-interval graph and a slow-storage toggle. Leaderboard page (DOM) observes the pit zone for pushed top-10 snapshots, shows your rank and neighbours and triggers a rebuild from cell events. Headless Node tests drive both against the real binary, including a restart on the same data. Refs [[tasks/gr-p10-demos]]
Register both demos in run.sh (MOBPIT_DATA_DIR defaults to examples/game/.data/mobpit, ignored) and explain the fact flow, slow storage, the kill -9 exactly-once story and the leaderboard design. Refs [[tasks/gr-p10-demos]]
The return path re-presents the transfer ticket to the source zone. When the refusing target ran on the same host, its nonce replay cache had already recorded the nonce, so the return failed with Replayed and the player fell back to a fresh join. The issuer's transfer registry keeps the return single use, so it now gets its own replay key. Refs [[tasks/gr-p10-demos]]
mobpit-recovery wakes N rooms with one bot each, waits for (or runs) the kill, probes every room with real joins and reports when each answered (p50/p90/max, per 10 s), then each room's ticks over a hold. Bots record when their Welcome came (joined_after). Rooms report a wake trace in /info: when the object was built, its first call, and the awaited storage spans of its first wake and join. [[celld-fleet-recovery-hops]]
`AggregateCell::dispatch_idempotent_batch` runs commands in order, each exactly as `dispatch_idempotent` (own ledger entry, identity, session), under one command guard and one alarm reschedule instead of one per command. A rejected command leaves the earlier ones committed and the later ones run; the first retryable result (in progress, 408, 429, 5xx) ends the batch; one result per command attempted. The `__batch` wire moves to `distributed_game::cell::run_fact_batch` (host-testable); `serve_cell` parses, calls it and drains the outbox once. An entry that does not parse is answered in its place. Each entry is still its own transaction: a handler runs between its reservation and its commit, outside the synchronous cell SQL transaction, and must see the entries before it committed. Tests: a batch runs in order and replays as a whole; a rejected entry leaves the others committed; a replayed `__batch` body answers only replays and credits once. [[zone-fact-batches]]
Mob Pit's crawler and presence cells used their own router, which took `__batch` for a command name and answered 400. The object host only fell back to one-by-one on 404, so it read that 400 as the first fact's permanent refusal: a batch of kills for one crawler dropped its first kill. The cells now use `serve_cell`, and the object host sends the facts one by one whenever the batch request as a whole gets a 4xx other than 408/429, so each fact gets its own answer. [[zone-fact-batches]]
Plain integer counts that do not move with machine load, so A/B runs of
one build compare exactly: WorkCounters (records encoded per patch,
creates/updates sent, updates held by rates, Delta bytes, fact commands,
cell and __batch requests, outbox deletes, world reads and pushes) in
GET /zones/{zone}/info as `work`, with the switches in effect; watched
cells answer GET /{kind}/{shard}/__watchers with WatcherStats.
New A/B switches: ZONE_UPDATE_RATES=0, ZONE_WORLD_PUSH=0. The zone-side
reconcile schedule moves into world::watch::WorldSync, with a fake-clock
test that a reconcile repairs missed pushes within one period.
[[framework-work-counters]] [[zone-world-views-pushed]]
[[replication-encode-once-lod]] [[zone-fact-batches]]
Each step prints the rooms' and watched cells' work counters normalised per room patch, room-second or room-minute; --ab VAR=VAL with --pairs N alternates fresh-celld runs (A,B then B,A) and prints medians per side and of the per-pair change. Skill and README "Checking your work". [[framework-work-counters]] [[mobpit-zone-load-test]]
[[zone-fact-batches]]
Fact rates follow the fighting, which moves with machine load; the ratios are what a fact-batch A/B compares. [[framework-work-counters]] [[zone-fact-batches]]
Hello offered only version 1 and decoders reject trailing bytes, so Welcome could not grow and a new client message (Focus) made an old celld zone close the connection: servers had to deploy first. Servers (native net, celld zone object) now answer v1 or v2 in the client's version (ZoneHost::speaking) and refuse others with VERSION_MISMATCH "protocol ..." before the ticket is touched. Welcome v2 appends runtime features (bit 0 FOCUS) and the zone's world origin (Zone::origin). The JS SDK offers 2, resends the same Hello once with 1 to a v1-only server (remembered per URL, not surfaced as an error) and sends Focus only when FOCUS is announced. Golden fixtures cover v1 and v2; PROTOCOL.md gains a negotiation section. Game surfaces keep protocolVersion 1 (payload encodings are unchanged). [[zone-protocol-v2-negotiation]]
Each game converted the player's position into every neighbour's coordinates and called setFocus, and Mob Pit overrode on_observer_focus only to use its view radius. Zones now declare their world origin and player view radius once (Zone::origin, Zone::view_radius); the default on_observer_focus gives a focused observer that radius. The JS GameClient takes the player's position with one call (setPosition(x, y, zoneId?), local to a zone or world) and focuses every observed zone with an origin itself, also after its Welcome; setFocus stays for unplaced zones. Mob Pit declares both and drops its override; its client and the SvelteKit loop drop their coordinate math. [[zone-placement-auto-focus]]
[[zone-protocol-v2-negotiation]] [[zone-placement-auto-focus]]
celld holds a room's frames until the writes issued before them are durable. A wake wrote the zone id, the status and the activity fact before the first player's Welcome (and the join wrote the status again), so under a recovery burst Welcomes waited on the bucket. Now the wake only reads; the zone id is written once per zone, the status only when it changed and in the same batch as the facts, after the Welcome. Own celld node on the Hops RustFS, 150 fresh rooms: wake joins 89 -> 150 of 150 in the ramp; after a SIGKILL p50 38 -> 16 s, p90 44 -> 31 s; 2709 -> 2157 txns shipped in recovery. [[celld-fleet-recovery-hops]]
Probes and the staying bot reuse the room's principal (a reconnecting player, not new Crawler and Presence cells per attempt), send the game's content version, log why a wake bot was lost and count rooms that lose their player again; t0 is when the socket dropped. [[celld-fleet-recovery-hops]]
…lcome [[celld-fleet-recovery-hops]]
[[framework-work-counters]] [[zone-world-views-pushed]] # Conflicts: # distributed_game/src/celld_zone/object.rs
The watched world views (f93b24a) added the `hood` and `borough` zone messages; the generated client was not regenerated, so CI's `game-client --check` failed in the game demos smoke job. [[zone-world-views-pushed]]
Hood, borough and city are separate watched cells, so a waking room's views arrive independently. The test waited for the hood tier only and then asserted the borough count and city banner at once (failed in CI and 2 of 3 local runs). Wait for each, and retry a polled tiers read that fails while celld is busy. [[zone-world-views-pushed]]
Same hood/borough messages as e487715; the SvelteKit copy of the generated client was stale, so `npm run check:games` failed in CI. [[zone-world-views-pushed]]
One tick of walking is exactly the 0.25 m bound (5 m/s over a frame of up to 50 ms); crossing into the next room's coordinates adds f32 rounding, so CI saw 0.251 m (also on #291). Bound 0.26 m: a real doorway jump is a whole room or a visible snap.
A cell pushed each view once and logged only transport errors; a busy zone's 5xx/429 went unnoticed, so it caught up only at its next reconcile (45 ± 15 s). That is too late for a content rollout's freeze (the content headless test timed out waiting for both rooms frozen in CI). Retry after 0.25, 1 and 3 s on transport errors, 5xx and 429; a zone answers every parsed push with 204, so other 4xx are not retried. [[zone-world-views-pushed]]
The base removed the orphaned e2e-ui gitops tree and unused scripts (2597265); this branch's edits to SECRETS.md, envs/local/ui.yaml and dual-worktree-suite.sh go with them. The live Login V2 changes (test-users template and values, helm-contract-test) merge cleanly; the contract test passes.
Zones declare cylinders and boxes once in on_create (ctx.set_colliders). ctx.raycast / raycast_at / raycast_where / raycast_shot return the closest RayHit (entity or wall) with distance, point and surface normal. Rays carry max_dist, a layer mask and an ignored id (the shooter). Colliders::push_out keeps server-moved circles out of walls. Mob Pit's arrows and mob push-out use it; blocked_at and push_out_of_pillars are gone. [[zone-static-colliders]]
feat(game): static colliders and one raycast for entities and walls
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes distributed v5 an authoritative multiplayer game backend: a new
distributed_gamecrate (zone runtime, binary replication, world structure), coordination primitives and native durable cells in core, a JS game SDK under@hops-ops/distributed/game, and seven playable demos inexamples/game.Core (
distributed)cell-sqlite): SQLite per cell with the same atomic events + snapshots + ledger + outbox contract as the Workers store (BEGIN IMMEDIATE); stream history reads.deliver_attimers, leases with fencing tokens (docs/coordination-primitives.md).BackgroundLeaderboardbehind its own feature.distributed_gameZonetrait +ZoneHost, fixed timestep with catch-up cap, Hot/Idle/Dormant tiers, lifecycle hooks (incl.on_rejoin,on_handoff_join), sequenced input buffer with acks and credits, injectable clock + seeded RNG.FactSink/CellFactSink, idempotent fact keys with a boot epoch, bounded concurrent commits per principal lane. The tick never waits on storage.PROTOCOL.md: LEB128, field-mask deltas, quantization), spatial-grid interest with global entities, per-client byte budgets, axum WebSocket server, HS256 tickets / pluggableTicketVerifier,Redirect,Kick(RELOAD), NetSim.ZoneDirectory/HashRing, local and fenced-lease ownership, hibernation (SqliteZoneStateStore), transfer-ticket handoff across hosts,DottedTopologytier-up events.WorldCoordinatorfreeze → apply (async) → resume, inputs dropped while frozen,StalePolicy::SafeRoomgate zones, abort onNotAcked.GameMetrics+/metrics, DGR2 record/replay, load bots (game-bots).JS SDK (
js/src/game)GameClient/GameConnection, entity replica with interpolation, prediction + reconciliation (ease/snap/replace), adaptive interpolation delay,InputPacer, multi-zone observers, handoff following (incl. return path), offline command outbox.Demos (
examples/game, Rust server + Vite/Babylon client,./run.sh <demo>)Arena · Mob Pit · Leaderboard · Shooting Gallery · The Freeze · Endless Grid · Boss Ripple. Each was play-tested by hand; fixes from that review are the last commits on the branch:
Test plan
cargo testindistributed_game— all passing (incl. new rejoin/reconnect sequence tests)node --test tests/game-*.test.mjs) — 30 pass, 1 skipped (opt-in e2e); full JS suite 445 passing at the last full runexamples/game/run.sh smoke— every demo's Rust bots + headless JS tests (last full run before the final review fixes; Freeze and Grid headless re-run after them)Also included: authoring improvements (merged from #281, #282, #283)
#[derive(Replicated | GameInput | GameEvent | GameRecord | FactCommand)](cratedistributed_game_macros).ctx.emit; the principal is still bound from the ticket, and identity or reward field names are refused.GameSurfacemanifest, plusdistributed game-manifestanddistributed game-client, which generate typed TS.run.sh smokechecks that the generated code is fresh.#[predict]step is compiled bydistributed game-wasmand run with a binary ABI.#[game_pure]functions,GameSurface::constantand record codecs.distributed_gameand the macro tests pass; JS game tests 49 pass, 1 skipped;examples/game/run.sh smokeOK (client 25/25).distributed_climigration_inventory…fails;grid_smokeintermittently hits SQLite "database is locked".Folded in (2026-10-24)
The stacked PRs on top of this one were merged down into it, so this is
the single game PR into
v5:replication, protocol v2 with placed auto focus, one-override fact
batches, work counters, celld crash-recovery harness)
#292's head (45c922a) passed all 34 CI checks, including the game demos
smoke job, before the fold.