Committed together: both streams landed in libs/game/script, so splitting
them would produce two commits that don't compile.
M6 — packaging and sharing
- libs/zip_file gains a writer (store + deflate); real `unzip -t` validates
our archives in an interop test. Packing is deterministic (fixed
timestamps, sorted entries), so a package can be addressed by its own
sha256 — which is what makes the registry's digest check mean anything
- libs/game/pkg: .arcade format (game.splash + manifest.toml + assets),
total manifest parsing (attacker bytes always yield a Manifest or an
error, never a panic; non-finite numbers refused rather than defaulted),
registry client that verifies sha256 INSIDE download so tampered bytes
never reach the extractor
- Hardened extraction: absolute paths, drive letters (C:x is absolute on
Windows), UNC, backslashes, .., NUL/control chars, symlink members (via
mode bits), duplicate names (the ambiguity IS the attack), declared-size
caps checked before decompressing plus a post-decompress check, entry/
total/archive caps, and a post-join re-check that the resolved parent is
still inside the destination — which catches a pre-existing symlink the
name test cannot see. 4000-round mutation fuzz with a canary file beside
the destination; a 320 MB deflate bomb under 1 MB on the wire is refused
- Capability stripping rebinds fs/run/net to FRESH EMPTY OBJECTS rather
than shadowing known verbs, so there is no hole the day someone adds one.
Applied before the game handle is registered. Vacuity guard: an unstripped
isolate genuinely reads a file, so the sandbox tests can't pass for
unrelated reasons. Browser-installed games load Trust::Downloaded
M7 — pretty pass
- GameSun adopts draw::SceneSun (axis-converted: SceneSun is map-space
y-south/z-up, games are y-up). Shaders compute hemisphere ambient +
direct instead of each hardcoding its own split; defaults collapse the
new formula to the old constants exactly, so unifying did not restyle
existing games. write_into is the single write path — "one sun" is
compiler-enforced
- Projected shadow geometry: the caster's silhouette along the sun, fitted
in the sun's own (u,v) frame, so it stretches as the sun swings. Nearest
N casters get projection, the rest blobs; one instance in the existing
alpha batch, no extra pass. 0.6us for 24 casters
- Two pre-existing shadow bugs found via capture: the pipeline blends
premultiplied, so unpremultiplied dark RGB ADDED light instead of
removing it; and shadows were fogged, mixing them toward the bright
horizon so a distant shadow came out lighter than the ground it darkened
- Particles are structurally isolated from the sim: GameWorld has no
particle field and step_world has no particle code — the renderer owns
simulation and its own RNG. particles_never_advance_the_world_rng
interleaves particle verbs with real rand() draws over 32 rounds and
asserts both the RNG state and the drawn stream are identical
- game.sfx_at with listener-relative gain/pan and a near-field ease so a
sound at your feet doesn't flip channels; 2D verbs unchanged
- apps/arcade/BUDGETS.md: measured particle/sim costs, Quest columns marked
as estimates (the real particle limit is fill rate, not CPU)
Tape probe BYTE_IDENTICAL. Not done: arcade has no audio backend, so
positional sound is implemented and tested but not audible there yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
libs/game/coedit (zero deps, so merge logic is testable without a socket or
a VM). Not a CRDT: transactions are host-serialized into an append-only
generation history, and a conflict is answered by handing the author the new
base so THEY re-derive their intent.
- Transaction carries the author's whole intended file, not a patch: the
diff against its declared base is derived host-side, so a stale or
malformed patch can never be applied — and an AI writes whole files anyway
- diff3 over lines with LCS anchors. Merge::Conflict deliberately carries NO
merged text: a half-merged game file that still parses is worse than an
honest rejection. Conflict -> Rebase{new base + per-generation summary of
what landed underneath}. An edit already present in the tip is refused as
NoChange rather than appended as an empty generation
- Leases are advisory as designed: a test asserts a submit SUCCEEDS while
another author holds the lease. They shape who chooses to edit; they never
gate the log. TTL expiry means a crashed author cannot lock a region
- Wire: coedit is reliable-channel ONLY — a test signs a valid submission,
sends it by datagram, and asserts it is ignored, so no datagram can
rewrite the game. Every response is addressed, never broadcast
- Arcade bridge routes in exactly one place; local agent and remote authors
share the queue and the rules, and a test asserts the local agent gets
rebased identically when it loses a race. Remote players map to
AuthorId(player+1) so a client holding player id 0 cannot impersonate the
host's agent
- 200-round deterministic fuzz: 4 authors submit against deliberately stale
bases, rebase, resubmit — asserting linear append-only numbering, no
generation claiming a base from the future, and that replaying accepted
diffs from generation 0 reproduces the head exactly
Two real bugs found building it: validate used `?` on the base lookup, so an
unknown base returned "no refusal" instead of UnknownBase; and
MAX_COEDIT_SOURCE (512 KiB) exceeded MAX_FRAME_BYTES (256 KiB), so the host
would have accepted a source it could never hand back inside a Rebase,
stranding the next author on an answer that never arrives. Both now 192 KiB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
game_view.rs 4525 -> 1878. Deleted in one marker-verified cut: the 84-arm
game_dispatch chain, all arg/option/value helpers, spawn_entity/spawn_terrain
/spawn_car/spawn_plane/spawn_character/spawn_block_body/set_brain, the
GAME_API table, suggest_verb/edit_distance, and the duplicated CallbackTable.
register_game_handle now binds game_script's verb table; unknown verbs keep
the same hard-fail plus did-you-mean. Kept: run_tick and everything parity
depends on — tape input, gamepad poll, camera mailbox, perf channels, agent
RPC, save/log flush.
Rollback deliberately still gamemaker's hand-written WorldSnapshot, not
GameWorld::clone(). Clone is strictly safer and is what Arcade uses, but
eval_body is the parity-critical path and the snapshot is what the tape was
established against — swapping it deserves its own tape run, not a rider on
a 2700-line deletion.
Three pre-existing divergences fixed in game_script (sfx/beep/jingle predated
the 31-verb port and had drifted): beep's `to` defaulted to 0.0 instead of
freq, so every beep swept to silence; beep never read its wave option at all
(allowed but unused, and AudioRequest::Beep had no wave field); gain and
jingle ms were off. Audio drains host-side in run_tick and after a successful
eval so a startup jingle isn't held a frame; a failed eval discards its queue.
Regression the migration exposed: block verbs spawn through the shared box
path, which re-validated keys against the BOX allow-list, so game.car(...)
logged 8 bogus "unknown option" warnings per eval straight into the channel
the agent reads. Added spawn_entity_unchecked for the block path; a real typo
on game.box still warns. Racing fixture: 8 warnings -> 0.
Tape probe BYTE_IDENTICAL (re-run after the audio and warn changes too).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage as presentation, never simulation. Stage{mode, origin, yaw, scale} is
applied as the scene draw list's view_transform uniform — one value per
frame, so sky/terrain/cubes/skinned characters all move together, cached
static slabs never invalidate (they hold stage-independent world transforms)
and per-instance cost is zero. The view matrix cannot carry this: in XR the
platform overwrites camera_view with the runtime's eye matrices every frame
(openxr_opengl.rs:20-25), discarding anything the app wrote.
- MrDiorama: world scaled onto an anchored slab, sky/fog/terrain-horizon
suppressed (the room IS the environment), shadow-catcher quad so it looks
planted. VrFullScale: 1:1, environment intact. Flat: unchanged
- stage_invariants.rs proves the design point: two worlds run 120 ticks, one
switching flat->MR->VR->MR(new anchor)->flat mid-run, hashes bit-identical
at every switch, with an assert_ne against a fresh world so the equality
isn't vacuous. The stage has no API by which it could reach the world
- Stereo needed NO work, and rendering twice would have been wrong: XR uses
single-pass GL_OVR_multiview2, so draw_pass.camera_view compiles to an
indexed [VIEW_ID] lookup and the GPU rasterizes both eyes from one
encoding. The existing draw path is already stereo-correct
- XR input maps controllers/pinch onto the same per-player InputState the
net layer sends — an XR player is just another player to the sim
- Quest APK packages (arm64-v8a, passthrough/handtracking/anchor/colocation
manifest bits) with no cargo_makepad changes
- Settings panel: provider/model pickers, masked key entry, pair button
showing LAN URL + confirm code, active-tier status line
- Authoring inbox: keyless clients' Intent::Authoring queued for the host's
agent, bounded and refusing with a reason — every entry eventually costs a
paid call and it is filled by peers who hold the lobby key but are not
trusted terminals
Pre-existing startup crash fixed: run_tick held a RefMut across
self.world.borrow() (`let _ = w` drops the reborrow, not the RefMut it came
from), aborting the demo path. Reproduced at HEAD with changes stashed.
Not wired, needs a device: passthrough is requested via
StageMode::wants_passthrough() but not handed to xr_passthrough (arcade has
no XR root widget yet); env-depth occlusion has a hook and no consumer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the 31 verbs game_script lacked — part, move_part, beam, attach,
detach, speed_mult, raycast, overlap_sphere, ground_peak, held, pressed,
axis, player_input, the cam_* readers and set_cam_* writers, save, load,
tone/tone_set/tone_stop, format, api, reset — verbatim, including the
details that bite: part's defaults and half-floor, move_part's only-given-
keys rule plus its leaves-the-static-slab redraw, attach's vec3-or-options
overload (vec3 parses first; the options path defaults to (0,1,0), not the
previous offset) with velocity zeroed, every documented clamp, raycast's
terrain-reports-as-minus-one convention, and save's strings-before-numeric
ordering (the numeric cast NaNs strings).
Audio stays host-installed: AudioRequest gained Tone/ToneSet/ToneStop/
StopAllTones with a ToneWave mirroring synth::Wave's parse fallbacks. tone()
must return an id synchronously, which a drained queue cannot do, so ids are
minted on Ctx and the host maps them to its own voices — script only ever
holds an opaque handle, so this is observationally identical. save/load
needed no hook at all: save_data lives on GameWorld and flushing was always
the host's job.
the_verb_surface_matches_gamemakers pins the count at 102. (Gamemaker's 98
match arms are 102 names — four are || aliases.) This unblocks migrating
gamemaker off its duplicate binding layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
from_tcp_stream wrapped the socket in a BufReader that was dropped on
return, so body bytes read ahead into it vanished and handle_post blocked
forever on bytes that no longer existed — one wedged thread per request
that sent headers and body in the same TCP segment. Browsers split the two,
which is why nothing noticed. The function now owns its buffer, reads to
\r\n\r\n, and returns the remainder alongside the headers for handle_post
to consume first. The websocket upgrade path had the identical exposure (a
frame pipelined with the upgrade was silently dropped) and consumes the
same prefix now; EOF mid-head returns instead of spinning to the 4096-line
guard. New tests cover headers+body in ONE write (the case that hung, with
the connection held open afterwards so a regression blocks rather than
passing on EOF), the split case, and a plain GET.
game_net: Intent::Authoring{text} + MAX_AUTHORING_TEXT so a keyless client
in a hosted room can route a creation request to the host's agent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>