Skip to content

feat(game): authoritative multiplayer game runtime for v5 - #280

Open
patrickleet wants to merge 258 commits into
v5from
feat/v5-game-runtime
Open

patrickleet wants to merge 258 commits into
v5from
feat/v5-game-runtime

Conversation

@patrickleet

@patrickleet patrickleet commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Makes distributed v5 an authoritative multiplayer game backend: a new distributed_game crate (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 in examples/game.

Core (distributed)

  • Native durable cells (cell-sqlite): SQLite per cell with the same atomic events + snapshots + ledger + outbox contract as the Workers store (BEGIN IMMEDIATE); stream history reads.
  • Coordination primitives: sagas/process managers with compensation, durable deliver_at timers, leases with fencing tokens (docs/coordination-primitives.md).
  • Redis: sorted-set leaderboard read model + publisher; BackgroundLeaderboard behind its own feature.

distributed_game

  • Zone runtime: Zone trait + 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.
  • Non-blocking durable facts: FactSink/CellFactSink, idempotent fact keys with a boot epoch, bounded concurrent commits per principal lane. The tick never waits on storage.
  • Replication/transport: binary protocol (PROTOCOL.md: LEB128, field-mask deltas, quantization), spatial-grid interest with global entities, per-client byte budgets, axum WebSocket server, HS256 tickets / pluggable TicketVerifier, Redirect, Kick(RELOAD), NetSim.
  • World structure: ZoneDirectory/HashRing, local and fenced-lease ownership, hibernation (SqliteZoneStateStore), transfer-ticket handoff across hosts, DottedTopology tier-up events.
  • World version barrier: WorldCoordinator freeze → apply (async) → resume, inputs dropped while frozen, StalePolicy::SafeRoom gate zones, abort on NotAcked.
  • Netcode/ops: rewind lag compensation, 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:

  • fresh-ticket rejoin and token reconnect restart the server-side input sequence (players were stuck after a reload or a content update);
  • the SDK holds the predicted entity through a zone handoff (no flash back at borders);
  • demo camera, gamepad and Freeze admin-secret fixes.

Test plan

  • cargo test in distributed_game — all passing (incl. new rejoin/reconnect sequence tests)
  • JS game SDK tests (node --test tests/game-*.test.mjs) — 30 pass, 1 skipped (opt-in e2e); full JS suite 445 passing at the last full run
  • examples/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)
  • Manual play-through of all seven demos
  • Postgres lease/saga tests — not run here (Docker unavailable)
  • Redis leaderboard against a real Redis — verified only against fakeredis

Also included: authoring improvements (merged from #281, #282, #283)

  • feat(game): authoring derives and generated typed clients #281, derives and generated clients:
    • #[derive(Replicated | GameInput | GameEvent | GameRecord | FactCommand)] (crate distributed_game_macros).
    • Typed facts via ctx.emit; the principal is still bound from the ticket, and identity or reward field names are refused.
    • The GameSurface manifest, plus distributed game-manifest and distributed game-client, which generate typed TS.
    • All 7 demos converted; run.sh smoke checks that the generated code is fresh.
  • feat(game): client prediction from Rust compiled to WASM #282, prediction in WASM:
    • A #[predict] step is compiled by distributed game-wasm and run with a binary ABI.
    • The browser predicts with the server's own Rust code, byte-identical over 16,788 recorded frames.
    • The TS movement twins are removed.
  • feat(game): share world content and constants from Rust #283, shared content:
    • #[game_pure] functions, GameSurface::constant and record codecs.
    • Grid world generation now comes from Rust via WASM, about 6× faster than the old TS.
    • Freeze packs are a binary record; hand-copied constants are generated.
  • Verified on the final head (17ee2d7): distributed_game and the macro tests pass; JS game tests 49 pass, 1 skipped; examples/game/run.sh smoke OK (client 25/25).
  • Known, not from this work: distributed_cli migration_inventory… fails; grid_smoke intermittently 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:

#292's head (45c922a) passed all 34 CI checks, including the game demos
smoke job, before the fold.

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]]
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]]
[[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]]
feat(game): world-scale game services (watched views, fact batches, replication)
feat(game): Mob Pit through the framework gateway at /zones
fix(game): keep celld rooms smooth on a long-lived state
fix(game): stop celld dev stalling Mob Pit rooms
feat(game)!: Mob Pit world, fold demos in
feat(game)!: zones as celld objects, Mob Pit room grid
feat(game): game-services framework gaps and modules
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.
fix(e2e-ui): keep instance Login V2 off by default per lane
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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant