- libs/game/script (new): the table-driven game.* binding layer game.md
called for — 71 verbs in a HashMap built once per isolate, replacing
gamemaker's 84-arm linear chain. spawn_entity/spawn_terrain ported
verbatim so fixture terrain matches bit-for-bit; generation-tagged
callback slots; streaming eval + hot reload. Rollback snapshot is
GameWorld::clone() — M1a made the world Clone, and a clone cannot forget
a field, which is the exact bug class that put next_id in M0r's fix list.
(Gamemaker still runs its own copy; migrating it is a follow-up.)
- Capability tiers: Voice (VAD + Whisper + local judge) -> VoiceUnfiltered
(push-to-talk, every utterance costs a call) -> Chatbox (typing is the
gate). Text box in every tier, mic only above Chatbox. The chain sits
behind the local-llm feature so Quest/mobile never link a backend they
don't have
- Librarian: the local model is an optional override (None = no opinion),
never a gate — a flaky or absent model can only sharpen a decision.
Beneath it, deterministic word-overlap matching for load-by-description,
restart, and manifest-clamped knob writes. Creative requests deliberately
do NOT match an existing game (tested). Locally-answerable utterances are
dropped before they reach the cloud
- /pair: self-contained page (no external URLs, asserted), 4-digit confirm
code so a room of headsets can't take the wrong key, 0600 config-dir
storage documented as NOT a keystore — Android/iOS must move to the
platform one before shipping. Key never enters a log, package, or error
- Racing fixture evals through the new dispatch (28 entities) and renders;
hot-reload rollback verified live — a bad verb reports with a suggestion
and keeps the last good world
Found (platform, unfixed — out of this task's scope):
HttpServerHeaders::from_tcp_stream buffers past the headers into a
BufReader, so a body arriving in the same TCP segment is swallowed and
handle_post blocks forever on a body that's already gone. Browsers split
the two, which is why nothing noticed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A room of devices now plays one game: host simulates, clients send input
and render replicated truth.
- libs/game/sim/player.rs: Players roster on GameWorld, slot 0 is always
this device. Player 0's input stays in the world's original held/pressed/
pad/cam_yaw fields and is mirrored into the roster, which is what keeps
single-player numerics bit-identical (tape gate confirms). Ids are never
reused, so a stale reference resolves to None, never to somebody else
- The camera-movement knot resolved: world.player_move(p) rotates that
player's axes by THEIR cam_yaw, carried in their input packet. Player 0's
branch is the original expression character-for-character (the f32 cos
widened to f64 kept deliberately — tidying it would move the numbers)
- libs/game/session/replication.rs: Shared = pos/vel/size/kind/tag;
Derived = facing/anim/scale/glow/blob shadows, recomputed client-side and
costing zero wire bytes; Local = camera/audio/effects. Statics never enter
the per-tick stream
- Protocol (additive, version unchanged): EntityDesc + Descriptors message
splits rare reliable construction data from volatile unreliable state —
without it a joiner sees poses for entities it cannot build. EntityDesc
carries pos because statics never appear in the state stream (the
late-joiner test caught ground arriving at the origin)
- Script: game.players/player_name/player_entity/player_input/bot/on_join/
on_leave; blocks gained owner: PlayerId so a car reads its own driver
- Arcade: ARCADE_HOST=1 / ARCADE_JOIN=<addr>; clients skip world
construction and don't simulate
- Racing wire volume, 6 players x 60Hz x 200 entities: 2400 pps,
20.9 Mbit/s up (audit projected 74 Mbit for the XR stack). Asserted in
racing_scenario_wire_volume_fits_a_living_room
Two more not-a-playable-default bugs, same class as M1b's rng-at-zero:
Entity::default() leaves gravity_scale 0 (weightless wheels) and ground
without friction gives no traction — only the DSL path filled these in.
255 tests green; tape probe BYTE_IDENTICAL; xr/arcade/gamemaker build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The thesis proof: examples/gamemaker/resources/fixtures/racing.splash is a
complete playable racing game in 72 lines (12-corner oval from a waypoint
loop, 4 cars, gates, standings, restart) with no physics, no AI and no lap
bookkeeping in script. Blocks run engine-side at 60Hz: Blocks::pre_step
(intent -> motion) before step_world, post_step after; Blocks is Clone and
snapshots beside GameWorld so a failed eval rolls both back together.
- game.car (4 suspension raycasts on a box3d rigid chassis), game.character
(drives the existing mover sweep + owns idle/walk/run blending),
game.plane; game.drive/autodrive/speed
- Brains: game.wander/chase/patrol/caught — the fixture's hand-rolled AI,
absorbed engine-side
- Race kit: spawnpoint, checkpoint, place, race, standings, lap/rank/
finished, score/score_of (Shared-tier data, ready for replication)
raycast_vehicle audit (defects documented in car.rs, still live in xr):
libm sin/cos in steering (unreplicable), a wrong side-impulse denominator
(iaj.dot(iaj) where it should be (I^-1 aJ).aJ), and an unguarded division.
Kept the structure, replaced Bullet's friction solver with an arcade force
model: suspension acts at the contact point, grip and drive through the
centre of mass, steering as yaw torque — no lateral force can generate
roll, so it is stable by construction rather than by roll_influence fudge.
Engine bug fixed at the source: GameWorld::new() left rng at 0 and
xorshift64* is a fixed point at zero, so rand() returned 0 forever for any
world built through the sim API. reset_content seeds on every eval, which
hid it from gamemaker entirely; found by a wander brain that never left home.
Tape probe byte-identical; racing fixture evals clean and drives (AI follow
the line, gates bank in order).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Harvest-and-rebuild from xr/src/net after the adversarial audit (verdict:
transport promote-with-fixes, authority model rewrite).
- Authority: explicit Host/Client roles. Client->host Join/Input/Intent/
Leave/Ping; host->client Welcome/StateBatch/Event/Bye/Pong. No per-object
authority field and no takeover messages — authority theft (audit H-11)
is unrepresentable, not merely blocked
- Auth: self-contained SHA-256/HMAC; every datagram and frame is
magic|version|sender|payload|mac, verified BEFORE any peer state is read
or written. Closes seq-window poisoning, spoofed kick, address hijack
- Endpoints are pumped, not threaded: nothing blocks, so the connect-flood
stall (H-5) cannot occur and a full session runs deterministically in one
test process. The host only ever accepts, never initiates
- Harvested: LZ4 frame codec (check-before-allocate), partial-tail drain,
budgeted read/write loops, MTU batching, peer/config shapes. Frame cap
4MiB -> 256KiB now that XR alignment payloads are gone
- Per-entity sequencing (a stale datagram drops only its stale members),
rejoin seq reset, player cap, peer timeout, snapshot-based mid-join
- Measured 6 clients x 60Hz x 200 entities: 2880 pps, 3.13 MB/s (~25 Mbit
up) — Quest WiFi viable without delta encoding yet
- Hostile suite: one test per audit attack + 3000-mutation fuzz that must
never panic or wedge the host. 24/24 green
- micro_serde: String::de_bin no longer panics on invalid UTF-8, checked
offset arithmetic, Vec::de_bin rejects counts the buffer cannot back and
never sizes allocation from the wire (11/11)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- libs/game/sim/dynamics.rs: box3d world inside GameWorld, body mirror
RECONCILED against the sorted entity list per tick (merge-walk), so
retain/rollback/reset are correct by construction. Statics + kinematics
mirrored (kinematics via target transforms so resting rigids inherit
platform velocity); smooth terrain as a box3d heightfield
- New BodyKind::Rigid + body:"rigid" with density/friction/restitution;
game.push = mass-scaled impulse; set_pos/set_vel detected via bit-exact
pose caches (no new dispatch arms). Sphere rigids roll on real spheres
- Entity.orient quat read back per tick; renderer builds quat instance
transforms for rigids (no shader change). Step order: mover sweep
verbatim -> reconcile -> world_step(dt, 4) -> readback; rigid-free
worlds skip the solver
- GameWorld stays Clone via box3d snapshot round-trip (bit-identical
continuation proven by test)
- Determinism: double-run equality + golden hash 0xa8a2baf71e4a564f
(aarch64, debug and release). Perf: 0.038 ms/tick for 100 movers +
50 rigids + 65x65 terrain (budget 2 ms)
- Tape probe BYTE_IDENTICAL; box3d crate untouched; arcade demo gains a
kicked crate stack (captures verified: settles upright, then topples
with rotated resting poses)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- libs/game/render/skin.rs: GLB container + owned-JSON parser, dense
accessors, multi-primitive skinned meshes, skin + inverse binds, full
node-hierarchy palette, T/R/S clips (linear/step), nlerp blending.
Hermetic tests via an in-code 2-joint GLB; real-asset test skips with a
hint when the download hasn't run
- DrawGameSkinned shader (PbrVertex + albedo texture, terrain-style
lighting); skinned batch draws between opaque and alpha passes.
CPU skinning for now (sub-ms at 3716 verts): the uniform_buffer GPU path
has zero in-tree runtime consumers — SkinnedModel::palette is the seam
for the GPU swap
- apps/arcade: download_assets.sh (KayKit pinned commit + sha256,
idempotent, dir gitignored, CC0 license note in-repo); Knight patrols
the demo world with idle/walk blending, yaw follows path; captures
verified (pose advances, depth-occludes correctly)
- renderer binary_search sites use the shared sorted-id helper from M0r
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Entity lookup O(log n): shared entity_index_sorted helper, single
push_entity spawn path with ascending-id assert + per-tick sorted
debug_assert — the sorted-Vec invariant is now enforced, not hoped
- WorldSnapshot captures next_id: failed-eval rollback can no longer mint
colliding ids under surviving entities (review defect fixed)
- game.log buffered (1s/16KiB/eval-boundary flush), agent RPC poll gated
on one .agent dir-mtime stat — no per-tick file I/O left
- Camera rig authoritative on GameWorld (orbit/chase state); widget keeps
only device-input accumulation; mailbox drain order unchanged
- Per-tick cumulative script budget: with_instruction_limit reports
consumption (incl. trap-wipe subtlety), gamemaker tick holds ONE 500k
pool across on_tick + timers + touch events
- Tape probe verified BYTE-IDENTICAL vs pre-cleanup reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- libs/game/math: cross-arch bit-deterministic f32 kernel (f64-internal
fdlibm-style polynomial kernels, exactly-rounded IEEE ops only, zero
platform-libm transcendentals). Parity golden hashes recorded on aarch64;
equal hashes on every other target IS the determinism test
- libs/game/sim: the gamemaker world moved verbatim (entity/terrain/world/
queries/step — float expression order preserved so tapes replay
bit-identically). No Cx, no draw types, no ScriptObjectRef: script
callbacks are CallbackSlots resolved through a generation-tagged
host-side CallbackTable in game_view.rs; sim reset no longer touches audio
- examples/gamemaker: game_view.rs 5545 -> 4640 lines (script boundary,
rendering, input devices, host I/O remain); per-tick args/input objects
built via unchecked pushes + release_transient (flat-heap tick path)
- apps/arcade: Makepad Arcade app shell + workspace membership
- Verified: headless sandbox3d fixture (90 entities) boots end-to-end;
gamemaker/sim/math/script-test suites green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>