Commit graph

11 commits

Author SHA1 Message Date
Admin
6623408a57 Bind the library and composition into script — and tell the model it exists
8 new verbs (table now 115): find_model (DISTINCT ids, not ranked duplicates),
find_palette (matched set from one pack), model, kits, cast, road_network,
town, dungeon. game.find was ALREADY TAKEN by entity-by-tag lookup — the
duplicate-name test caught the clash before it shipped, and find_model/
find_palette now match the agent TOOL names, so the model's knowledge
transfers between the tool it calls and the verb it writes.

Verbs run synchronously (search and layout are pure CPU); only GLB load and
draw need a host, so placements queue through the same mechanism as audio and
particles — which also means a scene composes headlessly with no renderer
attached. Tiles carry their own collider from the kit pitch, so scripted props
are as solid as hand-placed ones.

THE MOST IMPORTANT EDIT WAS A DELETION. splashgame.md said "Everything is
procedural... No image, model, or audio files" — the doc was actively telling
the model it had no models, which is why generated games were bare primitives
while 4,442 models sat unused. Replaced with an instruction to reach for the
library before game.box, three rules (never place result #1 five times; one art
pack per region; generate layouts rather than hand-placing) and a wrong-vs-
right example. A test asserts that claim cannot come back.

Two bugs found by probing the REAL library rather than reasoning:
- town() would have placed ZERO buildings, silently: it selects
  TileRole::Building, but every role-less model mapped to Prop — and
  city-kit-suburban is 40 whole buildings with no parsed roles. A role-less
  model is genuinely ambiguous (a building on a lot, or a cone at a kerb), so
  kit_from_index now takes a KitUse hint. Against the real library: 104
  buildings, 136 road tiles, 0 adjacency errors
- the index folds crossroads and T-junctions into one `junction` role, but a
  4-way cell needs four open edges; a T standing in for a crossroad leaves a
  road stub pointing at nothing. Disambiguated by name

village.splash is the scenery counterpart to racing.splash: a town, a wood of
four different conifers, a dungeon, a playable character — and not one model id
written by hand.

NOT BOUND, and why: game.tree/rock/blob and game.scatter generate MESHES, and
set_models takes an asset id, not geometry — there is no mesh-upload path for
generated meshes yet, so binding them would have meant faking it. Additive once
a generated-mesh queue exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 12:24:10 +02:00
Admin
294314fb73 render: Kenney's 4,442 static models actually draw — the demo is a place now
THE BLOCKER WAS UPSTREAM OF THE RENDERER: zero of the 4,442 models had a
texture on disk. download_assets.sh extracted only .glb and deleted the zip,
reasoning "GLB is self-contained" — false for Kenney, where every material
points at an external Textures/colormap.png shared across the pack. PNGs are
extracted alongside now, and the resume check REQUIRES a texture, because a
pack of GLBs with no atlas renders white, which is worse than missing.
(The atlases are tiny: 212 textures, 42 KB.)

Static path (model.rs) reuses skin.rs's container/JSON/accessor code rather
than growing a second parser. A static mesh is a skinned one minus joints,
plus one difference: a prop never animates, so each node's world transform is
BAKED into its vertices at load and the model becomes one buffer. Dropping
that bake is exactly how a prop silently renders at the origin, so there is a
test for it. All 4,442 models parse: 1.31M triangles total, 294 average —
comfortably Quest-sized.

Kenney ships TWO conventions, and the second only turned up by looking at a
failure: most packs UV-map into colormap.png, but nature-kit and friends carry
no texture at all and colour each primitive with a material baseColorFactor.
Rather than branch, that factor is baked into the packed vertex's colour lane
and multiplied in the shader (albedo * v_tint) — atlas models carry white,
untextured models get a white 1x1. One shader, both conventions. A model that
DECLARES an atlas but cannot find it stays a hard error; that case really is
broken.

Batching sorts instances by model so equal geometry+texture land adjacent and
accumulate into one draw item: the demo runs 36 instances in 5 draw items,
9,887 triangles. Copies of a prop are free; cost is per distinct model.

The demo picks props BY DESCRIPTION through the asset index (find("pine
tree")), not by hardcoded paths, so it exercises the same path a generated
game takes — and it walks the ranked hits taking the first that loads, so a
pack with a missing atlas yields to the next candidate instead of leaving a
hole. Pillar ring and cone removed; they read as a test harness.

Honest read of the captures: before, coloured cylinders and spheres on a slab
— unmistakably a tech demo. After, a woodland treeline at mixed scale and
species, a suburban house with windows and a teal roof, wooden fences,
textured crates, correctly lit and shadowed. Still imperfect: "boulder"
resolves to nature-kit/cliff_blockCave_rock, a cave-mouth block that reads as
a small building scattered about — a SEARCH-QUALITY gap for the alias owner,
not a render bug.

Washed-out look diagnosed (not fixed, out of scope): it is FOG, not the bake
or the textures. SkyConfig::default()'s density mixes every surface toward the
pale horizon (0.75,0.87,0.96) over a 34-unit camera distance — the far
treeline desaturates toward sky colour while near crates keep their brown. Fix
is either a lower default density or making fog colour follow the sun's
horizon tint so it reads as haze rather than a grey wash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 10:16:07 +02:00
Admin
3cef1eb290 Arcade binary -40%: make TTS optional, and lazy when present
libs/converse gains a default-on `tts` feature so gamemaker and route are
unaffected; Arcade takes default-features = false and re-enables it through
its existing `voice` feature. With tts off, SpeechOutput still exists and its
worker drains the queue silently, so no Arcade source changes were needed —
which also avoided colliding with concurrent edits to those files. No
capability is lost: speech is fully reachable under --features voice.

  Arcade before            25.11 MB
  Arcade after (default)   15.01 MB   (-10.10 MB, -40%)
  Arcade --features voice  26.18 MB
  hello_world              13.46 MB

Isolated by A/B in a throwaway worktree before making the real change. Of the
10.1 MB, 2.78 MB is us_lexicon.bin embedded via include_bytes!; __const alone
was 11.2 MB, larger than __text.

Also: Kokoro construction is now lazy, deferring ~327 MB until first
utterance. That already closed the RSS gap this pass was chasing — Arcade's
max RSS measures 190 MB against hello_world's 222 MB on the same method, i.e.
BELOW the baseline app, so the premised memory problem no longer exists and
was not invented into one. The lean-isolate prelude was likewise dropped after
instrumentation showed Arcade allocates no script isolate on the demo path at
all; it should be re-measured against a loaded script game before anyone
spends effort there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 09:22:03 +02:00
Admin
3387e06f28 Arcade: chat panel, mic, and the audio backend it never had
Arcade had no chat UI and no cx.audio_output at all, so M7's positional
audio was silent there and there was no way to talk to the AI.

- synth.rs is a port of gamemaker's 24-voice synth (a shared libs/game/audio
  crate was out of this task's scope; deduplicating the two copies is a
  mechanical follow-up). Gamemaker's audio path is untouched, so no tape
  run was needed. One behavioural addition: STEREO — gamemaker's synth
  writes the same sample to every channel, so a positional sound had
  nowhere to go. Voices now carry pan, and centre keeps full volume in both
  channels rather than equal-power, so every existing 2D sound is exactly
  as loud as before
- audio.rs drains the AudioRequest queue and resolves SfxAt against a
  listener built from THIS device's camera — Local tier by construction,
  nothing reaches the wire. A sound past its range queues no voice at all,
  so a busy world doesn't burn its 24 slots on things nobody can hear. The
  demo world clanks when crates land, so the positional path is audible out
  of the box rather than merely implemented
- chat.rs: PortalList with User/Assistant/System bubbles — engine trouble
  gets its own colour because the player didn't say it and the AI didn't
  either. main.rs is now a Splitter: chat + input + status left, game right
- Voice degrades per tier: a `voice` feature gates the mic (local-llm
  implies it — a judge needs a mic to judge), the text box exists in every
  tier. Caught in headless boot: naming ptt_use_escape in script logged an
  [E] every startup when the feature is off, because VoiceWave is a stub
  View there; push-to-talk moved to feature-gated Rust
- authoring.rs submits the agent's edit to the intent log as a transaction
  against the generation the turn started from, then writes the merged head
  back to disk — otherwise the next mtime poll would re-propose the agent's
  stale text as new. Eval errors return via CoeditResponse::EvalError and
  land in chat as System messages naming the last version that worked.
  Tests assert a typed request reaches the log, and that the LOCAL agent
  gets rebased when it loses a race to a remote one — no privileged path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 08:28:54 +02:00
Admin
bdbc946012 Arcade M6+M7: packaging/sharing with sandboxed installs, and the pretty pass
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>
2026-08-03 03:27:09 +02:00
Admin
c042c06eba Arcade M5: multi-Claude co-editing — intent log, semantic rebase, soft leases
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>
2026-08-03 02:45:31 +02:00
Admin
cec4f5b380 Arcade M4: voice + AI tiers, game library, /pair key flow, and libs/game/script
- 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>
2026-08-03 02:05:50 +02:00
Admin
a8427cda75 Arcade M2b: multiplayer — players in the sim, tiered replication, host/join
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>
2026-08-03 01:21:44 +02:00
Admin
c13c867547 Arcade M1b: libs/game/blocks — car/character/plane, brains, race kit; racing game in 72 lines
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>
2026-08-03 00:54:22 +02:00
Admin
7bb0c3be34 Arcade M0 stage B: rendering extracted to libs/game/render + arcade live viewport
- libs/game/render: the 5 game draw shaders (script_mod registration),
  shape geometry + winding test, terrain mesh, static-slab instancing,
  draw_scene pass (sky/terrain/opaque/alpha) as GameRenderer over GameDraws
  (draw structs stay #[live] on the host widget so script theming works),
  CameraRig + scene_state per-view (multi-view ready), HUD + billboard
  label drawing. game_view.rs 4640 -> 3599 lines
- apps/arcade: first engine-only viewport (arcade_view.rs) — GameWorld
  built via the sim API with no script VM, 60Hz tick, orbit camera,
  offscreen pass composite; ARCADE_CAPTURE=<png> GPU-capture test hook
- Verified: sandbox3d fixture evals clean headless; arcade demo frame
  GPU-captured and visually checked; suites green. Sim untouched — stage-A
  tape byte-parity (probe.txt identical vs pre-refactor binary) stands

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 22:56:22 +02:00
Admin
f2228a4654 Arcade M0 stage A: gamemaker sim extracted to libs/game/sim + deterministic math + apps/arcade shell
- 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>
2026-08-02 22:28:06 +02:00