diff --git a/.gitignore b/.gitignore index ad13aaa04..8d012a217 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,31 @@ ggml-*.bin /kokoro_voices/ __pycache__/ tesla_credentials.json + +# Studio per-checkout runtime state: terminal scrollback, AI chat logs, window +# layout. Committed once by mistake (fab00bf27 "Studio term histories") — it is +# local scratch, it churns every session, and it had grown to ~30 MB of chat +# JSON and terminal buffers carrying developer email addresses and home paths. +.makepad/ + +# Working notes, plans and handoff docs. These are scratch for whoever is +# mid-task — they go stale the moment the work lands, and a repo root full of +# them buries the three docs that are actually reference material. Kept on +# disk, out of the repo. The reference docs (README, splash.md, splashgame.md) +# and the Studio runbook (AGENTS.md) stay tracked. +/aigame*.md +/blur.md +/bridge.md +/datasources.md +/game.md +/glass.md +/gps.md +/handoff.md +/isolate.md +/layers.md +/map.md +/map2.md +/profiler.md +/route.md +/shiny.md +/status.md diff --git a/Cargo.toml b/Cargo.toml index eff1c3ddd..6d14b3718 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,20 @@ workspace.members = [ - "widgets/dll", - # === apps === + # === app === "apps/route", + # === arcade (game.md) === + "apps/arcade", + "libs/game/math", + "libs/game/render", + "libs/game/sim", + "libs/game/blocks", + "libs/game/gen", + "libs/game/audio", + "libs/game/script", + "libs/game/net", + "libs/game/coedit", + "libs/game/pkg", + "libs/game/session", + "libs/game/assets", # === examples === "examples/hotload_ui", "examples/teamtalk", @@ -40,46 +53,19 @@ workspace.members = [ "examples/cad", "examples/box3d", "examples/hello_world", - "libs/box3d", - "libs/gif", - "libs/gltf", - "libs/splat", - "libs/latex_math", - "libs/mbtile_reader", - "libs/map_nav", - "libs/geodata", - "libs/makepad_ai", - "libs/tesla", - "libs/converse", - "libs/pdf_parse", - "libs/regex", - "libs/svg", - "libs/apple_sys", - "libs/mlx", - "libs/mlx/cli", - "libs/shared_bytes", - "libs/tsdf", - "platform/network", - "libs/filesystem_watcher", - "libs/live_reload_core", - "platform/script/test", - "platform/script/std", - "platform/studio", - "studio/hub", - "libs/makepad_test", - "libs/makepad_test/macros", - "studio/desktop", - "tools/cargo_makepad", - "tools/tui_test", - "tools/profiler", - "tools/map_tiles", - "libs/mb3d", - "libs/openexr", - "libs/git", - "libs/fast_inflate", - "libs/lz4", + # === xr app === "xr", - "libs/cef", + # === studio === + "studio/hub", + "studio/desktop", + # === necessary tools === + "tools/cargo_makepad", + "tools/map_tiles", + "tools/map_bake", + "tools/remote", + "tools/arcade_eval", + # === tests === + "platform/script/test", ] workspace.exclude = [ diff --git a/README.md b/README.md index f17960fd4..ce322c8d4 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ - Discord: https://discord.gg/adqBRq7Ece - Rik Arends: https://twitter.com/rikarends -- Eddy Bruel: - -- Sebastian Michailidis: https://bsky.app/profile/okpokpokp.bsky.social -Makepad is an AI-accelerated application development environment for Rust. It combines a high-performance UI runtime, a live-editable design language, and a fast iteration loop so you can build native and web apps with a tight feedback cycle. +Makepad is an AI-accelerated application and game development environment for Rust. It combines a high-performance UI runtime, a live-editable design language, and a fast iteration loop so you can build native and web apps with a tight feedback cycle. + +It also has a large set of AI backends integrated for embedding llms or generative AI models inside applications or run them easily on local hardware This repository contains the core engine, widgets, tools, and examples. @@ -17,6 +17,7 @@ This repository contains the core engine, widgets, tools, and examples. - A Rust-first framework with a scriptable UI DSL. - A studio app for running, inspecting, and iterating on examples and projects. - An AI-accelerated workflow: structure and tooling aimed at making code generation, refactoring, and iteration faster and safer. +- Simple forward 3D renderer for making games on Quest and all other supported platforms ## Features @@ -52,7 +53,7 @@ Linux build/runtime dependencies are listed in `./tools/linux_deps.sh`: Use the apt-get command below, or run the script on Ubuntu/WSL2: ```bash -sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential pkg-config clang ca-certificates libssl-dev libx11-dev libxcursor-dev libxkbcommon-dev libxrandr-dev libxi-dev libxinerama-dev libasound2-dev libpulse-dev libwayland-dev wayland-protocols libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglx-dev libdrm-dev libgbm-dev libgl1-mesa-dri mesa-vulkan-drivers mesa-utils mesa-utils-extra x11-apps +sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential pkg-config clang ca-certificates libssl-dev libx11-dev libxcursor-dev libxkbcommon-dev libxrandr-dev libxi-dev libxinerama-dev libasound2-dev libpulse-dev libwayland-dev wayland-protocols libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglx-dev libdrm-dev libgbm-dev libgl1-mesa-dri mesa-vulkan-drivers mesa-utils mesa-utils-extra x11-apps gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-gl gstreamer1.0-alsa gstreamer1.0-pipewire libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 ``` ## Build And Run Makepad Studio diff --git a/aigame.md b/aigame.md deleted file mode 100644 index d6335cb67..000000000 --- a/aigame.md +++ /dev/null @@ -1,308 +0,0 @@ -# aigame — moving the AI Game Maker from Godot to Makepad - -Plan for replacing the Godot backend of the kids' Game Maker (`examples/godot`) with a -small, AI-generatable game engine running **inside makepad itself**, built from three -things we already have: the **splash script VM** (hot-reloadable, per-isolate), the -**box3d** physics engine, and the **xr** 3D scene framework. Written 2026-07-09. - ---- - -## 1. What we have today (the Godot version) - -**Where games go:** `~/games//` (override with `GAMEMAKER_HOME`), one Godot 4.7 -project per game. The app owns `tools/` (agent harness) and `CLAUDE.md`; the kid's game -is `project.godot` + `scenes/*.tscn` + `scripts/*.gd`. Per-game app state (chat log, -Claude session id, model choice) lives in `/.gamemaker/`. - -**The loop:** kid holds F1 and talks (Whisper) or types → transcript goes to Claude Code -(via `makepad_ai::ClaudeCodeAgent`, tool policy locked to Read/Glob/Grep/Edit/Write + -`tools/gd` only) → Claude edits GDScript/tscn files → the app keeps the kid on the -last-good running game and relaunches it only when the turn completes. - -**What the AI actually builds** (from the real `~/games/my-game`, ~2600 lines generated): - -- Everything is **procedural colored boxes** — no image/model/sound assets at all - (enforced by the system prompt). 2D: `StaticBody2D`+`ColorRect` ground segments, - platforms, moving platforms, walls, a goal flag. 3D: cube ground/stairs/towers/trees, - a gold goal block, box-people. -- **Character controllers**: `CharacterBody2D/3D` + gravity + jump + `move_and_slide`, - respawn on fall, mount/dismount vehicles. -- **Behaviors**: per-frame `_physics_process(delta)` steering (villagers chase, soldiers - patrol), group queries (`get_nodes_in_group("vehicle")`), distance checks, spawn/despawn, - timers (`create_timer`), win/lose conditions. -- **HUD**: `Label`s toggled visible ("You win!", "Caught!"). -- **Input** through named actions only (`ui_left/ui_right/ui_accept`) so keyboard, input - tapes, and gamepad (AgentEye binds the A button) all work through one vocabulary. - -This is the complete capability envelope a kids' game engine needs. It is small. - -## 2. How the agent remote-operates the game and looks at pictures - -Three mechanisms, all file-based (Claude only has `tools/gd` as a shell surface): - -| Verb | Mechanism | What the AI gets | -|---|---|---| -| `gd peek` | Drops `.agent/peek_request`; the **AgentEye autoload** in the *live* game polls (250ms), grabs 4 viewport screenshots over ~1.2s via `get_viewport().get_texture().get_image()`, writes player pos/vel/`is_on_floor()` | `.agent/sheet.png` + `state.txt` — sees what the kid sees, zero interruption | -| `gd shot [frames] [tape]` | Second Godot instance boots `tools/harness.tscn`; harness loads the target scene, **replays a JSON input tape by frame number** (`{"f":30,"press":"ui_accept"}` → `Input.action_press`), prints `[probe]` pos/vel/floor lines every 15 frames; recorded with `--write-movie --fixed-fps 60` (deterministic: same tape ⇒ same frames). Since 2026-07-09 it launches via `open -g` + an unfocusable offscreen window so it never steals the kid's focus | contact sheet + numeric probe log — "the jump feels floaty" becomes a number | -| `gd errors` | Greps the run log for `SCRIPT ERROR` / `ERROR:` | error text | - -**The pictures:** `tools/sheet.py` tiles N evenly-spaced frames into one labelled contact -sheet (`.agent/sheet.png`) so the agent reads *one* image showing motion over time instead -of 120 frames. - -**Weaknesses inherent to the Godot backend** (what the migration removes): - -1. **Separate process.** Applying `.tscn` changes needs a full game restart (state loss, - focus management, pid babysitting, zombie processes). We just spent a day making this - tolerable; in-process it disappears entirely. -2. **Capture needs a real (hidden) window** — Godot's `--headless` crashes with - `--write-movie`. Makepad has a true headless CPU renderer. -3. **Opaque runtime.** The only introspection is print statements the harness happened to - include; errors come from log-grepping. -4. **Two unfamiliar languages** (GDScript + tscn) and a giant API surface the model can - misuse. A curated DSL of ~25 constructs is easier to prompt and to verify. - -## 3. Building blocks in makepad (surveyed 2026-07-09) - -### 3.1 Splash script VM — the hosting/hot-reload story is already built - -- `widgets/src/splash.rs`: the `Splash` widget evaluates DSL **strings** in a dedicated - isolate VM (`cx.alloc_splash_vm_with_network`), with instruction limits - (`with_instruction_limit(200_000, …)`), and re-evaluates **incrementally** via - `eval_with_append_source` parser checkpoints — this is how aichat streams a growing - `runsplash` block into a live widget. Per-isolate `let`/`fn` state persists across events. -- The DSL is a real language: `let`/`fn`, templates, `for`, `if/else`, closures, - `on_click`-style handlers, struct arrays, `promise()/.await()`, HTTP. `splash.md` is the - authoring manual the AI already follows; `examples/splash_preview/` is an offline - generate-and-verify corpus harness (drives the `claude` CLI, evals every generated app, - flags empty widget trees). -- **Gaps for games:** no script-facing frame tick or timers, no keyboard events routed - into isolates, and eval errors are *not* fed back (a broken block renders blank; errors - only reach stderr via `ScriptVm::drain_errors`). All three are core workstreams below. - -### 3.2 box3d — the physics core (`libs/box3d`, pure Rust port of Erin Catto's Box3D) - -- Shapes: sphere, capsule, convex hull (`make_box_hull`), triangle mesh, heightfield, - compound. Bodies: static/kinematic/dynamic. Full joint set. Sensors with begin/end - touch **events**, contact/hit events, ray/shape casts, explosions, wind. -- A **kinematic character controller** (`mover.rs`, `world_collide_mover`, - `world_cast_mover`) — exactly what the player/NPC vocabulary needs. -- **Bit-exact deterministic** across architectures and worker counts, faster than Rapier - on 8/9 benchmark scenes, with a built-in **snapshot + record/replay** substrate - (hash-exact). This upgrades the whole verify story: same tape ⇒ same *simulation*, not - just same frames. -- Consumer API is flat free functions (`create_world`, `create_body`, - `create_hull_shape`, `world_step`, `body_get_transform`). **Rust-only today — zero - script bindings.** That binding layer is the single biggest work item. -- `examples/box3d/src/main.rs` already shows the renderer we need: instanced lit - boxes/spheres (`DrawPhysMesh` script-shader, per-instance color+transform), an - `XrCamera` orbit, `NextFrame`-driven stepping. ~600 lines, self-contained. - -### 3.3 makepad-xr — the 3D scene layer (`xr/`, crate `makepad-xr`) - -- `XrNode` scene graph (pos/rot/scale, physics body kind, children), object library - (`Cube`, `IcoSphere`, `Gltf`, `FractalTree`, splats), behaviors in Rust (`Tank`, `Car`, - `Shooter`), 2D-UI-on-a-plane (`XrView` — a free HUD system), PBR-ish shading. -- **Worlds are authored in the script DSL and hot-reload** (`XrNode` handles - `apply.is_reload()`; `on_render` closures rebuild geometry live) — proof that - "AI edits script → live 3D scene updates" already works in this codebase. -- Desktop fallback: orbit camera + **gamepad** gameplay (`cx.game_input_states()`). - Quest: hands, passthrough, depth-scanned colliders, multiplayer (`xr/net`). -- Caveats: physics is **Rapier3D** (not box3d), **no audio**, **no keyboard gameplay - input**, and behaviors are compiled Rust (only the scene layer is scriptable). - -### 3.4 Remote-operate substrate we already proved this session - -- **Headless renderer**: `MAKEPAD=headless` builds render real frames to PNG on CPU with - JIT-compiled shaders (`--draws=N`, `MAKEPAD_HEADLESS_OUT_DIR`). We used it today to - pixel-verify a widget fix. This replaces the hidden-window capture instance outright. -- **makepad_test** (`libs/makepad_test`): selector-driven UI automation (click/fill/ - wait_text) against a headless app instance, with failure screenshots — the skeleton of - an input-tape runner. -- **Studio hub bridge**: `WidgetTreeDump` / `Click` / `Screenshot` RPC into a running - app — the skeleton of `peek` without file polling. - -## 4. The aigame engine — design - -**Principle: keep the exact product shape** (voice → Claude edits files → kid keeps -playing until the AI is happy → instant apply), swap the engine underneath. The Game -Maker app shell (chat, Whisper, TTS, sessions, model picker, relaunch policy) is reused -as-is; only "relaunch Godot" becomes "hot-swap the game script". - -### 4.1 Architecture - -``` -~/games//game.splash ← the file(s) Claude edits (Read/Edit/Write, same as now) - │ (file watch) -GameMaker app ─── GameHost widget ← owns a splash isolate + a box3d World + fixed 60Hz tick - │ │ - │ ├─ shadow-eval on change: new isolate, eval, 1 smoke tick - │ │ ├─ clean → swap in (kid sees change in <1s, mid-play) - │ │ └─ errors → keep last-good running, errors go to the agent - │ ├─ renderer: instanced boxes/spheres/capsules (from examples/box3d) - │ ├─ HUD: plain makepad widgets overlaid (or XrView in 3D) - │ └─ ActionMap: keyboard + gamepad + tape → named actions - └─ agent harness (tools/ag): test / peek / errors — see §5 -``` - -- **One process.** The game is a widget in the Game Maker window (optionally poppable - into its own window later). No pids, no focus stealing, no restart. -- **"It works" state, upgraded:** today the kid keeps the old *process*; here the - last-good *isolate + world* keep running while the new source shadow-evals. A turn that - ends broken never even flickers the kid's game — and the AI gets the error text - immediately instead of a blank screen (fixes the Splash blank-on-error gap). -- **Fixed timestep** (1/60, 4 substeps, like the box3d example) for determinism; render - interpolation optional later. - -### 4.2 The script surface the AI writes (curated, not raw bindings) - -Bind a small **game vocabulary** into the isolate rather than exposing raw box3d — a -~25-construct API is easier to prompt, to sandbox, and to keep stable. Sketch (syntax -illustrative, follows splash rules): - -```splash -// game.splash — everything the AI edits lives here -let SPEED = 240.0 -let JUMP = 520.0 - -fn build_world() { - game.gravity(vec3(0, -30, 0)) - game.box{pos: vec3(0, -1, 0) size: vec3(120, 2, 8) color: #x3a8f4a} // ground - for i in 0..6 { - game.box{pos: vec3(10 + i * 8, i * 2, 0) size: vec3(4, 1, 4) color: #x8a6a3a} - } - game.box{pos: vec3(58, 13, 0) size: vec3(2, 2, 2) color: #xf5c13a tag: "goal" sensor: true} -} - -player := game.mover{pos: vec3(0, 2, 0) size: vec3(1, 2, 1) color: #x4466aa lock_z: true} - -npc := game.mover{pos: vec3(30, 2, 0) size: vec3(1, 2, 1) color: #xaa4444 lock_z: true - on_tick: |dt| { self.walk_towards(player.pos(), 3.0) } -} - -player.on_tick: |dt| { - self.walk(input.axis("left", "right") * SPEED * dt) - if input.pressed("jump") && self.on_floor() { self.jump(JUMP) } - if self.pos().y < -20 { self.teleport(vec3(0, 2, 0)) } -} - -game.on_touch: |a, b| { - if a.tag() == "goal" || b.tag() == "goal" { ui.hud_win.set_visible(true) } -} - -game.camera.follow(player, side_2d: true) -``` - -Vocabulary checklist, derived 1:1 from what the Godot corpus actually used: - -| Corpus need (Godot) | aigame construct | Backed by | -|---|---|---| -| ColorRect/box world building | `game.box/sphere/capsule{...}` (static/dynamic/kinematic, color, tag, sensor) | box3d shapes + instanced `DrawPhysMesh` | -| CharacterBody + move_and_slide | `game.mover{...}` + `walk/jump/on_floor/teleport` | box3d `mover.rs` character controller | -| `_physics_process(delta)` | `on_tick: \|dt\| {...}` per entity + `game.on_tick` | fixed-step pump into isolate | -| groups / `get_nodes_in_group` | `tag:` + `game.find("tag")`, `e.distance_to(x)` | engine-side registry | -| Area2D / goal triggers | `sensor: true` + `game.on_touch` | box3d sensor events | -| moving platforms | kinematic body + `on_tick` setting velocity | box3d kinematic | -| vehicles/mount | `attach/detach` (weld joint or parent) | box3d joints | -| HUD labels | plain widgets over the viewport (`ui.hud_win`…) | makepad widgets (free) | -| respawn / timers | `teleport`, `game.after(secs, \|\| {...})` | engine timer wheel | -| input actions | `input.pressed("jump")`, `input.axis(..)` | ActionMap (§4.3) | -| 2D platformer | `lock_z: true` + `side_2d` camera | box3d motion locks / parallel joint — **verify which; fallback: post-step plane clamp** | -| win/lose sounds | `game.beep{...}` synth SFX | `cx.audio_output` (already used for TTS) | - -### 4.3 Input: one ActionMap for keyboard, gamepad, and tapes - -Mirror the Godot design that made tapes+controllers free: script code only ever sees -named actions (`left/right/jump/up`). The engine maps arrow keys/WASD + gamepad -(`cx.game_input_states()`, as xr's Tank does) + **tape events** onto the same names. -Makepad has full keyboard events (`Event::KeyDown`/`KeyCode`) — xr just never wired them; -we wire them in the GameHost, not in xr. - -### 4.4 Renderer choice - -**Phase 1: lift `examples/box3d`'s renderer** (instanced lit unit-cube/sphere + -`XrCamera`, ~200 lines) into the GameHost. It draws exactly the corpus art style. -**Later: converge with xr** — adopt `XrNode`/`XrView` for scene+HUD and port xr's physics -from Rapier to box3d (justified independently: box3d is faster on 8/9 scenes and -deterministic; one physics engine in the tree instead of two). That convergence buys -Quest/hands/multiplayer for the *same game scripts* — a kid's game playable in VR — but -it is explicitly not on the critical path. - -## 5. The agent harness on makepad (remote-operate, tier by tier) - -Same three verbs, better substrate. `tools/ag` (or a `gd`-compatible shim so the prompt -barely changes): - -| Verb | Godot today | aigame | -|---|---|---| -| `ag test [frames] [tape]` | hidden Godot instance, --write-movie, probe prints | **headless run of the same GameHost** (`MAKEPAD=headless`): eval `game.splash`, feed the tape into the ActionMap, `world_step` N frames, render PNGs on CPU, emit probe lines (engine reads pos/vel/on_floor directly — no print statements needed). box3d determinism ⇒ bit-exact repeatability, stronger than Godot's fixed-fps movie | -| `ag peek` | file-RPC into live game (AgentEye), viewport screenshots | in-process: the app screenshots its own game pass texture + dumps entity state on request (file trigger kept for CLI compat, or a local socket). Kid keeps playing, same as now | -| `ag errors` | grep run logs | **drain the script VM error queue** — precise parse/runtime errors with line numbers, returned as text. Also auto-attached to the turn when a shadow-eval fails, so the AI often self-corrects *without* running anything | -| pictures | `sheet.py` contact sheet | keep `sheet.py` verbatim (it's engine-agnostic: dir of PNGs → one labelled sheet) | -| tapes | JSON `{"f":N,"press":"ui_accept"}` | same format, actions renamed; probe list = tags | - -Bonus unlocked by box3d: `ag test --record` / snapshot scrubbing — the AI can capture a -deterministic recording once and re-probe it at different frames without re-running. - -## 6. Changes to the Game Maker app (small) - -- `play_game()`/`relaunch_if_pending()` → `GameHost::reload(path)` (shadow-eval + swap). - The turn-completion policy from 2026-07-09 stays: kid keeps last-good until the AI is - happy — it just gets cheaper (no process restart, sub-second apply). -- System prompt + per-game `CLAUDE.md`: rewritten against the aigame DSL; ship a new - **`aigame-dsl.md`** authoring guide (the `splash.md` equivalent for games: the - vocabulary table, tick/input/tape rules, the `#x` hex rule, worked platformer example). -- Template: `~/games//game.splash` starter + `tools/ag` + tapes. `refresh_harness` - (added today) already re-stamps tools on project switch. -- Permission policy shrinks: Claude gets `Edit/Write(./**)` + `Bash(./tools/ag:*)` only. - -## 7. Phases and milestones - -**Phase 0 — proof of loop (the risky part, do first)** -Script-bind the minimum box3d surface (world/step, box+sphere bodies, transforms) into a -splash isolate; GameHost widget with fixed tick, keyboard ActionMap, `on_tick` dispatch -into script closures; lift the box3d example renderer. -*Milestone:* a ~100-line `game.splash` platformer (locked z) runs at 60fps and hot-reloads -on file save without dropping world state of the running instance until swap. -*Verify here:* per-tick script-call overhead with ~20 entities × 60Hz under instruction -limits; box3d 2D locking mechanism; headless JIT compiles `DrawPhysMesh` (we fixed the -scalar-cast JIT bug today — same risk class). - -**Phase 1 — the corpus vocabulary** -Mover controller, sensors → `on_touch`, tags/queries, spawn/despawn, timers, camera -follow (side-2D + third-person), HUD overlay widgets, gamepad, synth SFX (`game.beep`). -*Milestone:* hand-port `~/games/my-game` (both the 2D level and the 3D chase sandbox) to -`game.splash` — the real generated corpus is the acceptance test for vocabulary -completeness. - -**Phase 2 — the agent loop** -Headless `ag test` (tape → frames → probe → sheet), `ag peek`, VM-error round-trip + -shadow-eval, Game Maker integration, new prompt + `aigame-dsl.md`, template swap. -*Milestone:* end-to-end kid session: "make him jump higher" → edit → self-test headless → -turn completes → game hot-swaps mid-play; zero focus steal; broken edits never reach the -kid and come back to the AI as line-numbered errors. -*Regression harness:* splash_preview-style batch corpus — a set of recorded kid requests -run through the real `claude` CLI against `aigame-dsl.md`, each result eval-checked and -tape-smoke-tested headlessly. - -**Phase 3 — convergence and reach** -Port `makepad-xr` physics Rapier→box3d; host aigame scenes on `XrNode`/`XrView`; the same -`game.splash` then runs on Quest (hands/passthrough) and inherits xr multiplayer. -Optional: box3d record/replay scrubbing in the harness; state-preserving hot reload via -world snapshots. - -## 8. Open questions / decision log - -1. **Curated `game.*` API vs raw box3d bindings** — recommended: curated (smaller prompt, - stable across engine refactors, sandboxable). Raw bindings can come later for power use. -2. **2D story** — one engine (3D + locked axis + orthographic-ish side camera), not a - second 2D engine. Needs the motion-lock verification in Phase 0. -3. **Where behaviors live** — corpus says script-side `on_tick` closures suffice (steering - is ~10 lines); compiled-Rust behaviors (xr's Tank pattern) stay an escape hatch for - things script is too slow for. -4. **Splash-in-chat vs GameHost** — games do NOT run as `runsplash` chat blocks; the - GameHost is a dedicated widget with its own isolate, tick, and input focus. Chat - blocks stay for the AI showing UI snippets. -5. **Error feedback for aichat generally** — the VM-error round-trip built for aigame - (drain_errors → agent) should be upstreamed to the `Splash` widget too; blank-on-error - hurts every runsplash use case. diff --git a/aigame_parity_gap.md b/aigame_parity_gap.md deleted file mode 100644 index 89693876c..000000000 --- a/aigame_parity_gap.md +++ /dev/null @@ -1,52 +0,0 @@ -# Full-parity gap: current ~/games/my-game vs the aigame engine - -Goal (rik, 2026-07-09 late): run the little Godot game — CURRENT state — fully in -splash/gamemaker. Evidence: 21:54 `gd shot` sheet + a full close-read of all 17 actor -scripts. The game now spawns **~44 creatures + 2 vehicles**: Giant DogDay guardian -(23-box model, 1.9x scale, intercept-charges nightmares), 3 headcrabs (leap + latch to -the player's head, 0.5x speed debuff, shake off by jumping), the Prototype -(weeping-angel: only moves unwatched, LOS dot 0.55 vs camera forward), Baba Chops ram -(27 boxes, fire eyes with proximity-ramped emission), Nightmare Huggy (arm-reach lerp), -4 nightmare critters, 10 farm animals (per-kind synth calls w/ pitch), Huggy pack, -Kissy-as-bodyguard, grapple hand (yank player to walls / haul creatures, stretched -cable), 64-chunk welded smooth terrain (256x256 cells, 0.5 terracing, caves, alpha -water at y=3.5), ProceduralSky, shadowed sun, crosshair + hint + 4 colored flash HUD. - -**Key architectural relief:** Godot NEVER rotates the physics body — only the visual -`Model` child yaws (atan2 facing + turn-rate clamp). Our unrotated-AABB physics is the -same design; the gap is visual/animation, not physics. - -Axis trap for the re-port: main3d.gd heightfield is x-major (`i*(CELLS+1)+j`), our -game.terrain is z-major row-major — transpose or the goal lands on the wrong peak. -(The Godot-side "_heights@62" parse error is a stale Godot cache: identifier doesn't -exist on disk; file is internally consistent.) - -## Engine work — Fork A "the look" (game_view render/world) - -| # | Item | Spec from the game | -|---|---|---| -| A1 | Per-entity model yaw | auto-face velocity w/ turn-rate clamp (default movers), `game.face(id, yaw)` / `turn_rate` override; physics AABB unchanged | -| A2 | Owner-local parts | part offsets rotate with owner yaw; fronts at −z convention | -| A3 | Part animation | `game.move_part(part, {pos, rot_x, rot_z…})` engine-lerped (t≈dt*9, Godot's constant); `game.scale(id, v)` model scale (CatNap curl 1/0.6/1, DogDay 1.9x); headcrab spin | -| A4 | Emission | glow parts (eyes 3–4, runtime ramp 1.5→5, bolt/beacon); `glow:` spawn opt + `game.glow(id, e)` | -| A5 | Sky + ambient + fog | gradient top (0.32,0.58,0.9) → horizon (0.75,0.87,0.96); distance fade | -| A6 | Blob shadows | dark ground quad per mover at height-lookup y (real shadow maps out of scope) | -| A7 | Smooth terrain | triangulated heightfield mesh, flat per-tri normals, per-vertex height colors (SAND≤3.6/GRASS≤13/DIRT≤17.5/STONE≤21/SNOW), height-lookup collision; translucent water sheet (0.25,0.55,0.85,0.6) | - -## Engine work — Fork B "systems" - -| # | Item | Spec | -|---|---|---| -| B1 | Ride attach + debuff | `attach(..., {mode:"ride", spin})` per-frame head-follow; `game.speed_mult(id, f)`; shake-off = script (vel.y > 5 or timeout → detach) | -| B2 | Grapple support | `game.beam(a, b, {size,color})` stretched transient box (cable); haul = attach + detach-with-pop (exists); yank = set_vel (exists) | -| B3 | Camera | third-person low rig: pivot height 1.6, boom 10, pitch clamp [−1.2, 0.25], occlusion pull-in vs terrain/boxes (`max(1, hit−0.5)`), ignore tagged scenery; optional mouse-look | -| B4 | HUD slots | crosshair toggle, hint line (top-left), 4 independent colored center banners: `game.text(slot, msg, {color, size})`; billboard labels: multiple per entity, color/size/height opts | -| B5 | Synth voices | bark, moo, clank, whip (+ per-call pitch already works) | - -## Script-side patterns (no engine work — fixture implements with existing APIs) -group-AI intercept (tags + find + dual distance gates), LOS gate (cam_yaw dot), duck -damage dispatch (tag → handler table in script), stun/stagger contract, animal AI. - -## Sequence -Fork A → Fork B (same files, sequential) → Fork C re-port of the CURRENT game as -fixtures/sandbox3d.splash v2 (acceptance test) → GPU visual check by rik. diff --git a/aigame_port_findings.md b/aigame_port_findings.md deleted file mode 100644 index 0aa6bc8d8..000000000 --- a/aigame_port_findings.md +++ /dev/null @@ -1,179 +0,0 @@ -# aigame port findings — my-game 3D sandbox → game.splash - -Result of test-porting the AI-generated Godot game (`~/games/my-game`, the active -3D sandbox: ~2100 lines across main3d + 9 actor scripts) onto the gamemaker -engine as one `game.splash`. Fixture: -`examples/gamemaker/resources/fixtures/sandbox3d.splash` (~370 lines — 5.7× -denser than the GDScript, mostly because the engine owns spawning/physics/SFX). -Written 2026-07-09. Companion checklist: `aigame_port_inventory.md`. - -## Verification - -- Whole world evaluates clean first-load: **1036 entities** (961 terrain columns, - water sheets, 14 trees, goal + beacon, 19-creature cast, 2 trucks), zero eval - or tick errors at the 500k/tick instruction limit. -- Tape run (`ag test`, 200 frames): spawn→fall→land at exactly ground+half; - camera-relative walk at exactly SPEED 6.0; jump arc peaks +2.25 = JUMP²/2G to - the decimal; wander AI steers the cast; respawn works. Probe numbers, not hopes. -- Mount/drive/dismount verified end-to-end *by accident*: the first fixture spawn - was 2.3 units from a truck and the player silently auto-mounted at tick 1 — - seat height, top speed, and jump-to-dismount all matched the Godot behavior - before I even tried to test them. (Fixed by matching the original's player - transform (-8, 11, 8).) -- NOT verified visually (headless pane compositing still pending, task #9): - colors, camera feel, HUD text rendering. Not exercised behaviorally: nightmare - wake/pounce and the win touch (code-verified only; both use the same verbs the - probes exercised). - -## Engine bugs the port flushed out (both fixed) - -1. **Sensors were solid.** `step_world` filtered collision candidates by body - kind only — `sensor: true` boxes (goals, my water sheets, the beacon) blocked - movement, contradicting the documented contract. One-line filter fix. The - starter game never noticed because its goal floats at jump apex. -2. **(Earlier, same session) optional positional args trapped** — probing - `args[1]` on a 1-arg call failed the whole eval. Every optional arg would - have hit this. - -A port whose only failures were engine bugs, not script bugs, is a good sign -for the DSL's authorability. - -## The six predicted cleanups — verdict from actually porting - -| # | Prediction (aigame_port_inventory.md) | Verdict | -|---|---|---| -| 1 | First-class SFX bank + synth | **Confirmed hard.** The corpus had invented `squeak` and `roar` beyond the bank we shipped — the bank grew twice in one day. Named-bank + beep/jingle is the right shape; expect the bank to keep growing from corpus usage. | -| 2 | Tags + broadcast/duck-dispatch | **Half-confirmed.** `find`/`tag`/`distance` + script-side state objects covered everything the port needed — cross-actor "messages" (enrage/zap/heal) became plain field writes on shared script objects, which is *better* than Godot's group+has_method bus. A native broadcast API is NOT needed while one script owns all actors. It becomes needed only if games ever split across isolates. **Deferred, deliberately.** | -| 3 | Script raycast | **Dodged, honestly.** Ledge-probe AI was replaced by heightmap lookup (`h_at`) because terrain heights are script data anyway. Fine for this game; a shooter that needs line-of-sight will force `game.raycast`. Keep on the roadmap, don't build speculatively. | -| 4 | Camera-relative helper | **Confirmed, split in two.** The missing primitive wasn't a movement helper — it was `game.cam_yaw()` (the orbit camera's yaw was engine-private). With yaw readable, two lines of cos/sin in script do the rest; documented as a pattern instead of an API. | -| 5 | `attach/detach` (seats/carrying) | **Confirmed emphatically.** Replaced ~100 lines of per-actor teleport-following + collision toggling in GDScript with 2 calls. Vehicles, passengers, and the dismount-pop all fell out. | -| 6 | Mover platform carry | Already in the engine (kinematic floor_id carry); the port didn't stress it (no moving platforms in the 3D sandbox — they're in the 2D game). | - -## New findings (not predicted) - -1. **No RNG in the script language** — the single most-used Godot facility - (randf/randi everywhere in wander AI) simply didn't exist. Added - `game.rand()`/`game.rand_range(a,b)`, xorshift **seeded per eval**: wander AI - now replays identically under input tapes, which Godot's `randomize()` - corpus could never do. Determinism became a feature of the port. -2. **No noise either** — the terrain wants value noise. A 12-line script - `hash/smooth/noise2` worked fine at 31×31. At the original's 256×256 it - wouldn't (65k columns ≈ 3M+ instructions, and 65k entities would swamp both - the O(statics×movers) physics and the instanced renderer). The Godot game - itself had to weld chunks — scale is an *engine* concern in any engine. - → Roadmap: `game.terrain({cells, cell, heights})` native heightfield - (box3d has heightfield shapes waiting) + greedy column merging. -3. **`shoot` was a hardcoded gap** — the ActionMap knew jump but nothing else; - the corpus registers custom actions at runtime (mouse+F+gamepad X). Added F → - `shoot`/`shoot_pressed`. Real fix on the roadmap: `game.action("dash", "KeyQ")` - runtime action registration, like the corpus does in Godot. -4. **Projectiles work but are clunky**: a bolt = gravity-0 mover + set_vel + - script-side life/hit bookkeeping in a `retain` closure. Fine at 5 bolts; - a bullet-hell would want `game.spawn_projectile({vel, life, on_hit})` with - engine-side lifetime. Mover-vs-mover hits also aren't reported by `on_touch` - (sensor×mover only) — the port used distance checks; real overlap events for - movers is a small, worthwhile addition. -5. **One HUD line is enough** — the original's 5 labels + colors + cancel-token - flashes collapsed into `game.text` + a 6-line script `flash()` with a - generation counter. Colored/positioned HUD text can wait. -6. **Multi-part models are the visible fidelity loss.** Every creature is one - colored box; the originals have legs/shirt/head/eyes/smile built from 6–12 - cubes, billboard nametags, emissive eyes. The single biggest visual upgrade - per line of API: `game.part(id, {offset, size, color})` decorative child - boxes (no physics), plus `game.label(id, text)` nametags. This is also what - made the Godot corpus *charming* — worth prioritizing over new mechanics. -7. **Script ergonomics held up.** Struct-arrays of actor state + one shared - steering function expressed 9 GDScript classes in ~120 lines. Field mutation - through array elements, closures capturing top-level `let`s, and `retain` - with side effects all just worked. The `0.0 - x` workaround for (possibly - fine) unary minus should be tested and, if broken, fixed — it's the ugliest - thing in the fixture. - -## Fidelity ledger (what the port drops vs the original) - -- **Terrain at 1/8 resolution** (31×31×4u columns vs 256×256×0.625u welded - chunks): terraces are chunkier, no caves (cave-carving needs the fine grid to - read as tunnels), no smooth triangulated slopes, no vertex-color blending. -- **Retired cast stays retired** (Huggy, Robot, soldiers — the original has them - commented out too), so Kissy's enrage/defend/flee arcs are dormant here as - there; her follow behavior is live. -- Single-box creatures, no nametags, no arm-reach animation, no emissive glow, - no procedural sky (fixed background), water is an opaque thin sheet (alpha - untested in the cube renderer), no camera-blocking ray (camera can clip - through hills), no mouse-capture (orbit-drag instead), mini-variants are - recolored critters. -- Behavior approximations: injured-Kissy heals into a follower (original swaps - to a full Kissy with her own arc); nightmare glow-eyes become a body-color - swap; critter squeak probabilities eyeballed. - -## Bottom line - -The port took one authoring pass, found two engine bugs and five API gaps, and -every gap closed with a small primitive rather than a framework. The engine's -current vocabulary + script-side state objects genuinely cover the corpus's -*behavioral* range; the visible gap is decoration (parts/labels), and the -structural ceiling is terrain scale — both have clear, small next steps. - -## v2 re-port (2026-07-09 late) — the full current game - -`fixtures/sandbox3d.splash` v2 ports the game's CURRENT state (~3600 GDScript lines, -44 creatures + 2 trucks) onto the upgraded engine: verified headless — eval clean at -90 entities, tape run writes test_done/captures/probes, plaza floor exactly 7.9 and -walk exactly 6.0 (Godot ground truth), and two identical runs produce byte-identical -probes. The tape also exercises the full truck mount/drive/dismount chain. - -### Fidelity ledger - -**Exact:** every spawn-table name/position/size/color/speed; all behavior constants — -Giant DogDay guardian (follow 34/stand 9.5, charge 8.4 to an intercept point, bonk 5 → -stagger+knockback+bark, golden beam flash), Kissy bodyguard (gates 18/34, intercept, -shove 2.2), headcrabs (chase 26, leap at 6 [up 8 fwd 7, cd 1.8], latch 1.5 → ride-attach -(0,1.95,0) spin 2 + speed_mult 0.5, shake-off on vel.y>5 or 4s, self-stun 2.5), -Prototype weeping-angel (LOS dot 0.55 in 34, creep 70, eye glow 1.5/4.5), Baba Chops -(charge 14 @7.2, ram 2.4, fire-eye glow 2→5 ramp), Nightmare Huggy (hunt 90, arm-reach -via move_part inside 30), nightmare critters (creep 30, nip 1.3 bounce), CatNap -(sleep-curl scale 1/0.6/1, wake 9, catch 1.9), farm animals (shy 4.5/flee 4.4, calls -4–14s at Godot's per-kind pitches), trucks/passengers, terraced terrain recipe -(12.5+noise, snap 0.5, plaza 26/14/7, water 3.5, height-color bands), goal on the true -peak + glowing beacon, third-person camera (1.6/10/−0.35) + crosshair + hint, zap -dispatch on all threat kinds, HUD flash tokens, win latch, full synth bank. - -**Approximated:** terrain colors are single-color auto-shade (Godot has height bands -sand/grass/dirt/stone/snow — a 257² script colors array would blow the eval budget; -engine follow-up: band colors as terrain options); -models simplified but silhouette-faithful (DogDay 21 parts incl. the 8-box sunbeam -collar vs 23; Baba 15 vs 27 — horns straight not curled); animal voices = moo/squeak -at Godot's pitch tables (no dedicated oink/baa/cluck recipes); only Nightmare Huggy's -arms animate (pack arms static); zap stun uniform 4.5s (Godot 4–5.5 per kind). - -**Dropped:** cave tunnels only. (x-major→z-major heightfield transpose handled; -goal verified on the true peak via `game.ground_peak()`.) - -### Engine gaps: found by the port, then closed engine-side (same day) - -1. **CLOSED — eval budget vs terrain scale.** `game.terrain` now runs its noise - engine-side (`freq`/`offset`/`step`/`min`/`max`/`plaza`, cells up to 384), plus - `game.ground_y(x, z)` and `game.ground_peak()`. The fixture builds the full - Godot-scale 257×257 world (256 cells @0.625u) with zero script instruction cost, - and all spawn/tree/goal heights come from ground queries. -2. **CLOSED — second input action.** `grab` exists (keyboard G, gamepad B, tape - action) with `grab`/`grab_pressed` snapshot fields. The grapple hand is ported: - 32 u/s flight to range 16 on a `game.beam` cable, terrain hit → player yank - (22 u/s + 6.5 up), creature hit → ride-attach haul, held 1.2s, set down at the - player's feet with a pop. Approximations (memo): solid-hit detection is - terrain-height only (tree/box bodies don't catch the cable — needs a script - raycast/box-probe verb); the hand vanishes at max range instead of retracting. -3. **CLOSED — label outlines.** Engine draws a 4-copy dark outline behind every - billboard label; nothing needed in script. -4. **OPEN — height-carving.** Caves need holes/overhangs in the heightfield (or - rock-slab CSG); the smooth mesh is single-valued. -5. **OPEN (new, minor) — terrain band colors.** Height-band coloring - (sand/grass/dirt/stone/snow) as engine-side terrain options, since script-built - color arrays don't scale past ~100² vertices. -6. **OPEN (new, minor) — solid-probe verb.** A `game.raycast`/box-probe would let - the grapple (and ledge-AI patterns from the corpus) hit boxes, not just terrain. - -Verification after the engine-noise/grapple update: eval clean at 90 entities, -zero JIT failures, last_error empty, tape (walk/jump/shoot/grab) → test_done + -captures + probes with plaza floor exactly 7.9 and walk exactly 6.0, two runs -byte-identical. diff --git a/aigame_port_inventory.md b/aigame_port_inventory.md deleted file mode 100644 index 9030a6d47..000000000 --- a/aigame_port_inventory.md +++ /dev/null @@ -1,70 +0,0 @@ -# my-game → splash port inventory (generated 2026-07-09) - -Exhaustive Godot API surface of ~/games/my-game (the AI-generated corpus), used as the -acceptance checklist for the aigame engine port. Two parallel games: 2D side-scroller -(main.tscn) and the ACTIVE 3D sandbox (main3d.tscn, run/main_scene). 13 actors share one -behavior pattern set; 2D/3D scripts are near-mirrors. - -## First-class engine APIs required (by measured usage weight) - -- **Position as currency**: `global_position` read/write ×122 (write = teleport: respawn, - mount-follow, carry). `distance_to` ×14, `length` ×23, `normalized` ×16. -- **Kinematic character motion**: `move_and_slide` ×14 (ALL actors), `is_on_floor()` ×33, - hand-rolled gravity (`velocity.y += G*dt`, engine gravity unused), `move_toward` ×9 - (friction), `is_on_wall` ×6 (hop trigger), per-actor jump/fall constants, respawn on - fall (y beyond limit → teleport to recorded `_spawn`). -- **World building**: StaticBody2D/3D + box colliders for ALL terrain; 3D visuals 100% - MeshInstance3D+BoxMesh+StandardMaterial3D.albedo_color (8 sites); 2D visuals 100% - ColorRect. Counts: 2D = 6 ground + 19 platforms + 7 movers + walls/flag/hills; 3D = - ground 3 + stairs 7 + towers 8 + trees 12 + goal + 10 creatures + 2 vehicles. -- **Groups as event bus** (no signal bus!): groups player/vehicle/soldier/kissy/huggy/game; - `get_nodes_in_group` iterated per-frame for nearest-target; - broadcast = scan group + `has_method()` duck-typing ×14 (`huggy_caught`, `enrage`, - `capture`, `rescue`, `zap`, `send_home`, `cam_yaw`…). Engine needs: tags + find/iterate - + dynamic method dispatch on entities. -- **Scene tree**: `add_child` ×53 (all procedural), `is_instance_valid` ×14 (cached-ref - guards), `set_script` ×6 (behavior attach to bare bodies), `queue_free` ×3 (bolt). -- **Input**: `get_axis(left,right)` ×6, `is_action_just_pressed` ×7; actions used: - shoot×8, ui_up/right/left/accept×4 each, ui_down×2, ui_cancel×1. RUNTIME InputMap - action creation (shoot = mouse-left + F key + gamepad X). Mouse-capture relative-motion - camera (captured/visible modes, pitch clamp, Esc release). -- **Camera**: 2D = child cam, position smoothing (speed 6), level-bounds limits. - 3D = CamYaw→CamPitch→Camera3D boom (z=13), mouse yaw/pitch, camera-relative locomotion - via `Basis(UP, yaw) * wish` — shared by player AND vehicle steering (`driver.cam_yaw()`). -- **Random/AI math**: randf_range ×16, randf ×7, randi/pick_random ×5, lerp ×8, atan2 - (model facing), sin/cos ping-pong (movers). -- **Audio — procedural synth, zero files** (sfx.gd autoload the AI wrote itself): - 8-voice AudioStreamPlayer pool, 22050Hz s16 WAV buffers generated by `_sweep` - (square pitch glide), `_zap` (falling tone+noise), `_notes` (arpeggio). Bank: jump, - shoot, zap, grab, angry, calm, rescue, shove, board, win. API `Sfx.play(name, pitch)`. - → aigame should ship this as first-class (`game.sfx("jump")` + beep/jingle synth). - -## Structural patterns to support - -- Mount/dismount: nearest group("vehicle") within REACH → `mount(player)` → disable - collider (deferred) + stop actor physics + per-frame teleport to seat; up to 3 - passenger seats; eject with pop velocity + cooldown. -- Win conditions polled in `_process` (goal distance / x threshold) + `_won` latch + HUD. -- HUD: Label text/visible + font/outline theme overrides; transient banners via - `await create_timer(s)` with generation-token cancel (the ONLY async in the game — no - tweens/AnimationPlayer anywhere). -- Ledge AI: RayCast repositioned per frame + force_raycast_update; ±90° sidestep along - edges (no navmesh). Needs script-facing raycast. -- Moving platforms (2D): AnimatableBody2D sync_to_physics, cosine ping-pong, carries - riders via floor tracking. -- Collision layers: 1=world, 2=creatures, 4=vehicle, 8=player; creatures/player mask - world-only (pass through each other); bolt masks world+creatures. -- Label3D billboard nametags (no_depth_test); emission material on bolt projectile; - ProceduralSky + one shadowed DirectionalLight3D. - -## Confirmed absent (do NOT build): tweens, particles, AnimationPlayer, navmesh, -custom shaders, paths, physics joints (in-game), signals (only Area3D.body_entered ×1). - -## Porting risk list (ranked) -1. Procedural audio synth (engine-side now, task #6) -2. Mouse-capture camera + camera-relative movement basis -3. Ledge raycast AI (script raycast API) -4. Moving platforms carrying riders (mover ground-velocity inheritance) -5. Runtime input action registration (ActionMap covers it) -6. Deferred collision toggling during mount (engine needs disable-collision-safe-point) -7. Jolt move_and_slide floor-snap feel parity (box3d mover tuning) diff --git a/apps/arcade/BUDGETS.md b/apps/arcade/BUDGETS.md new file mode 100644 index 000000000..795c72bc9 --- /dev/null +++ b/apps/arcade/BUDGETS.md @@ -0,0 +1,210 @@ +# Arcade budgets + +What a generated game can spend and still hold frame time. This feeds Fable's +system prompt so generated games stay inside the envelope by construction +rather than being profiled after the fact. + +**Status: measured on an M-class Mac (release), extrapolated conservatively +for Quest. The Quest column is an ESTIMATE until it is run on device.** + +## Simulation + +| thing | measured | notes | +|---|---|---| +| 100 movers + 50 rigid bodies + 65×65 terrain | **0.038 ms/tick** | M1a, release. The 60 Hz budget is 16.6 ms, so the sim is ~0.2 % of it | +| entity lookup | O(log n) | binary search over the sorted-id Vec (M0r) | +| script per tick | ≤ 2 ms | one cumulative 500k-instruction pool shared by on_tick + timers + touch events | + +The sim is nowhere near the limiting factor. Draw calls and CPU skinning are. + +## Vertex and instance bandwidth + +Quest is bandwidth-bound before it is ALU-bound, so this is the number that +matters most on device. + +| stream | before | after | note | +|---|---|---|---| +| cube instance | 44 floats / **176 B** | 32 floats / **128 B** | **−27 %**, measured from the compiled shader (`RenderStats::instance_floats`), not counted by hand | +| skinned character vertex | 16 floats / 64 B | 6 floats / **24 B** | **−62 %**, and this one is re-uploaded *every frame* | +| shadow mesh vertex | 16 floats / 64 B | 6 floats / **24 B** | **−62 %** | +| terrain vertex | 16 floats / 64 B | unchanged | uploaded once per terrain revision, so the win is small; see below | + +The Knight is the prize: 3716 verts × 64 B = **238 KB every frame** (skinning +is CPU-side, so the whole buffer is re-uploaded) → **89 KB**. On a +bandwidth-bound tiler that is the single biggest saving available. + +The instance saving came from moving `sun_color`, `sun_sky`, `sun_ground` and +`fog_color` **off the instance stream into shader uniforms**. They are +identical for every instance in a batch, so as instance fields they were 12 +floats of pure duplication per cube. `fog_density` stays per-instance because +shadows switch it off individually. At the demo's instance counts that is +48 B × every cube in the frame, every frame. + +### How the packing works + +**Vertex attributes in this engine are f32-only** — there is no u8/u16/i16 +attribute type. Compression therefore means bit-packing into f32 lanes and +unpacking in the shader, which the engine already supports: `unpack2f16` and +`unpack4u8` are builtins on every backend (Metal/GLSL/HLSL/WGSL), and +`geom.VectorVertexPacked` is the house precedent. + +`geom.GameMeshVertex` (`draw/geometry_gen.rs`) is the shared packed layout: + +| field | packing | floats | +|---|---|---| +| position | 3 × f32, kept exact | 3 | +| normal | octahedral, 2 × f16 in one lane | 1 | +| uv | 2 × f16 in one lane | 1 | +| colour | 4 × unorm8 in one lane | 1 | +| **total** | | **6 floats / 24 B** | + +Two gotchas worth keeping: the pod struct must use **flat `f32` fields, not +`Vec3f`** — std140 pads a vec3 to 16 bytes and the Rust repr(C) size then +fails the POD size assertion. And in the shader language `let` bindings are +immutable and helper fns cannot be forward-referenced, so the octahedral +decode uses a branchless `step(0,v)*2-1` for its sign rather than reassignment +or a shared helper (the `sign()` builtin returns 0 at 0, which would collapse +the fold on an axis-aligned normal). + +Terrain still uses PbrVertex: it uploads once per terrain revision rather than +per frame, so the saving does not justify re-verifying gamemaker's 257×257 +fixture. It is a mechanical follow-up if wanted. + +## Rendering + +One draw call per shape per pass, plus one per skinned character. Particles and +shadows join the existing alpha batch, so **neither adds a draw call**. + +| thing | desktop | Quest (est.) | why | +|---|---|---|---| +| entities | 2000 | 600 | instance packing is cheap; fill rate is not | +| skinned characters | 8 | 2–3 | CPU skinning: ~3.7k verts each, re-uploaded every frame | +| projected shadows (`shadow_budget`) | 24 | 8 | 0.6 µs for 24, ~2 µs for 64–128 — the CPU cost is trivial, the fill cost of large ground quads is not | +| particles (`ParticleSystem::cap`) | 2000 | 500 | see below | + +### Particle cost (measured, per frame, step + instance build) + +| cap | live | step | instances | total | +|---|---|---|---|---| +| 500 | 500 | 0.8 µs | 0.7 µs | **1.5 µs** | +| 1000 | 1000 | 3.1 µs | 2.1 µs | **5.2 µs** | +| 2000 | 2000 | 3.1 µs | 2.8 µs | **5.9 µs** | +| 4000 | 4000 | 11.5 µs | 14.5 µs | **26 µs** | + +CPU cost stays negligible even at 4000. The real limit is **overdraw**: every +particle is an alpha-blended quad, and a Quest fills pixels far more slowly +than it runs this loop. Hence the 500 cap there — it is a fill-rate budget, not +a CPU one. + +### CPU light bake (bake.rs), measured release + +| stage | cost | when it runs | +|---|---|---| +| AO (per static, 5 face samples × 8 rays) | **15 µs** | world edits only — sun-independent | +| sun visibility (1 ray per static + per probe) | **34 µs** | world edits **and** whenever the sun swings past 0.03 rad | +| probe lattice sky term | **61 µs** | world edits only | + +Measured on the demo world (12 statics, 13 occluders, 605 probes). Debug +builds are ~50× slower (5.7 ms for the probe pass) — measure in release. + +The split matters: AO is the expensive half and does not depend on the sun, so +a day/night cycle only pays the 34 µs sun pass. A ray that starts above the +heightfield's highest point and travels upward skips the terrain march +entirely, which is what keeps the probe pass in microseconds. + +Bake output costs **zero** bandwidth and zero GPU: it is folded into the +instance colours the renderer was already sending. + +### Shadow tiers + +Casters are ranked by camera distance. The nearest `shadow_budget` get a +projected silhouette; everything else gets a blob. Both cost one instance, so +the budget buys fidelity rather than draw calls. Rigid bodies and anything +person-sized (≥ 0.5 units tall) count as heroes; smaller movers always get +blobs. + +## Setting the budgets + +```rust +renderer.set_shadow_budget(8); // standalone XR +particles.set_cap(500); // standalone XR +``` + +Lowering these on one device is safe: particles and shadows are tier-3 Local +(game.md), so two devices in the same room may draw different numbers of them +and the simulation cannot diverge — particles never touch the world RNG, which +`particles_never_advance_the_world_rng` asserts. + +## Rules of thumb for generated games + +- A racing game with 4 cars, a track of ~200 static pieces and dust particles + sits at a few percent of frame budget on desktop. +- Prefer one emitter attached to a moving entity over per-frame bursts: an + emitter costs one request, bursts cost one per call. +- Characters are the expensive thing. Two or three on Quest, not eight. +- Terrain above ~129 cells starts to matter for eval time, not draw time (the + isolate's wall-clock budget is 64 ms and it is a hard bail, not a yield). + +## step_world weight (measured 2026-08-03, release, aarch64) + +Measured with `cargo run -p makepad-game-sim --release --example weigh`, which +wraps the global allocator so the byte counts include everything the tick +touches, not just what the harness allocates. + +The tick used to clone two things per tick purely to dodge a borrow: the whole +`Terrain` (its `heights: Vec` **and** `colors: Vec`) and every +static/kinematic `Entity` (208 bytes each). Terrain dominated — a 257² field is +1.3 MB/tick, i.e. **79 MB/s of memcpy at 60 Hz on a world with seven entities +in it**. Splitting the struct borrow removes the terrain copy entirely; the +statics snapshot has to stay a copy (movers must sweep against *last* tick's +kinematic poses — that ordering is load-bearing) but now copies a 48-byte +`Solid` view instead of the full entity. + +| scene | ms/tick before → after | B/tick before → after | +|---|---|---| +| demo (arcade) | 0.002 → 0.003 | 15,140 → 4,796 (−68%) | +| demo + terrain 65 | 0.003 → 0.004 | 99,640 → 4,796 (−95%) | +| racing-ish (129 terrain) | 0.007 → 0.002 (−71%) | 362,316 → 8,576 (−98%) | +| terrain 129 only | 0.005 → 0.000 | 335,804 → 896 (−99.7%) | +| terrain 257 only | 0.019 → 0.001 (−95%) | 1,323,964 → 896 (−99.93%) | +| large (500 static) | 0.063 → 0.056 (−11%) | 591,386 → 82,382 (−86%) | +| stress (2000 static) | 0.583 → 0.457 (−22%) | 2,353,936 → 327,812 (−86%) | + +Allocations/tick fell from 6–14 to 3–11; the residual is the statics snapshot, +the box3d reconcile and touch collection. + +**Leak check**: `--soak` runs 10 simulated minutes (36,000 ticks) of a busy +world with projectiles spawning and expiring throughout. RSS is flat at 3.8 MB +from warmup to the end (+0.4% drift, 242 entities alive) — the tick path does +not leak. + +**Result-neutrality** is gated by `libs/game/sim/tests/mover_golden.rs`: the +golden world-state hash is byte-identical before and after this optimisation +(verified by reverting the source and re-running), and it covers the terrain, +sweep, platform-carry, attach, projectile-lifetime and auto-face paths that +`rigid_dynamics.rs` doesn't reach. + +## Memory and binary, whole app (measured 2026-08-03) + +| | value | +|---|---| +| sim core only, all 7 scenes incl. 2000-static stress | 13.4 MB RSS | +| sim soak, busy world, steady state | 3.8 MB RSS | +| `hello_world` (baseline makepad + widgets + headless) | 195 MB RSS, 13.5 MB binary | +| `makepad-arcade` (headless) | 948 MB RSS, 25.1 MB binary | + +The engine core is genuinely small; the weight is above it. Two findings worth +acting on, both outside the sim/render/script crates: + +1. **~750 MB of Arcade's RSS is not the sim** (13 MB) and not the framebuffer + (unchanged when the headless size changes). It needs a profiler pass to + attribute properly — candidates are the script isolates (each one + re-evaluates the *entire* widgets DSL, `widget_async.rs:317`, and a game + isolate needs none of those prototypes), the glyph/texture atlases, and the + offscreen pass chain. +2. **The voice stack links unconditionally.** `makepad-converse` is a plain + dependency of `apps/arcade`, not feature-gated, so Kokoro TTS is compiled in + and initialised (`tts: backend Kokoro` appears in every boot log) even at the + `chatbox` tier where it can never be used. `voice`/`local-llm` gate the + *models*, not the crate. Gating this is the obvious binary-size win for a + Quest build; the binary carries whisper/kokoro/silero symbols today. diff --git a/apps/arcade/Cargo.toml b/apps/arcade/Cargo.toml new file mode 100644 index 000000000..6f7bf8067 --- /dev/null +++ b/apps/arcade/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "makepad-arcade" +version = "0.1.0" +edition = "2021" +description = "Makepad Arcade — networked AI game sandbox (see game.md)" +license = "MIT OR Apache-2.0" + +[lib] +name = "makepad_arcade" +path = "src/lib.rs" + +[[bin]] +name = "makepad-arcade" +path = "src/main.rs" + +[dependencies] +makepad-widgets = { path = "../../widgets", version = "2.0.0" } +makepad-game-math = { path = "../../libs/game/math" } +makepad-game-sim = { path = "../../libs/game/sim" } +makepad-game-session = { path = "../../libs/game/session" } +makepad-game-blocks = { path = "../../libs/game/blocks" } +makepad-game-render = { path = "../../libs/game/render" } +makepad-game-assets = { path = "../../libs/game/assets" } +makepad-game-gen = { path = "../../libs/game/gen" } +makepad-game-script = { path = "../../libs/game/script" } +makepad-game-net = { path = "../../libs/game/net" } +makepad-game-coedit = { path = "../../libs/game/coedit" } +makepad-game-pkg = { path = "../../libs/game/pkg" } +makepad-ai = { path = "../../libs/makepad_ai" } +# default-features off drops speech synthesis (~10 MB of binary, ~327 MB +# resident). Re-enabled by `voice` below: a device that can capture speech is +# the one that has any use for producing it. +makepad-converse = { path = "../../libs/converse", default-features = false } + +[features] +# Mic capture + Whisper, via the widgets VoiceWave. Separate from `local-llm` +# because transcription and the local judge are different models: a device can +# have one without the other. +voice = ["makepad-widgets/voice", "makepad-converse/tts"] +# Local model compute (Whisper/Silero/Qwen). Off by default so Quest/mobile +# builds — which have no backend — never try to link it. Implies `voice`: +# the judge only has something to judge once there is a mic. +local-llm = ["makepad-converse/local-llm", "voice"] + +[dev-dependencies] +makepad-test = { path = "../../libs/makepad_test", version = "0.1.0" } diff --git a/apps/arcade/README.md b/apps/arcade/README.md new file mode 100644 index 000000000..db998b1f9 --- /dev/null +++ b/apps/arcade/README.md @@ -0,0 +1,108 @@ +# Makepad Arcade + +Networked AI game sandbox (plan: repo-root game.md). Run with: + + cargo run -p makepad-arcade + +## Stock assets + +Arcade ships with a searchable library of CC0 models and sounds. The binaries +are **not** vendored in git — fetch them once: + + ./apps/arcade/download_assets.sh # core packs, ~75 MB + ./apps/arcade/download_assets.sh --packs=all # everything, ~185 MB + ./apps/arcade/download_assets.sh --list # show packs, download nothing + +The default fetches a **core** set (~1900 models) so a fresh clone isn't forced +to pull the lot; `--packs=all` gets the full Kenney 3D catalogue (**4669 models +across 47 packs**), and `--packs=nature-kit,car-kit` picks specific ones. Add +the 556 sounds and the nine rigged KayKit characters and the full library is +~5000 searchable assets — of which **36 are rigged and animated**. + +Everything is gitignored and pinned — starter kits to exact GitHub commits, the +rest to content-hashed kenney.nl URLs — and every file is sha256-verified, so a +moved or tampered upstream fails loudly instead of silently changing the +library. Downloads are sequential with a delay: this is someone else's +bandwidth. + +### Mirroring + +`resources/MIRROR.toml` lists every pack with its canonical URL, sha256, size +and model count. Because these assets are CC0, anyone may re-host them, and +that file is what makes a mirror reproducible and verifiable. Point at one with: + + ARCADE_ASSET_MIRROR=https://your.host/assets ./apps/arcade/download_assets.sh + ./apps/arcade/download_assets.sh --mirror=https://your.host/assets + +A mirror is expected to serve `/.zip`. **The sha256 from +MIRROR.toml is verified identically whichever host served the bytes** — a +mirror is never trusted more than upstream; the hash is the authority. + +Everything degrades gracefully without them: the demo runs with primitive +shapes, and tests that need real assets skip with a hint. + +**Audio is Ogg Vorbis.** Every Kenney audio pack ships `.ogg` only — no WAV +variant exists upstream — and this tree has no vorbis decoder. The sounds are +therefore indexed and searchable by the AI but **not yet playable**; adding a +decoder (or running `download_assets.sh --transcode`, which converts to WAV +when ffmpeg is installed) is what closes that gap. + +### Finding assets + +The library is queried by *description*, never by filename, via +`makepad-game-assets`. The agent gets a `find_model` tool and a one-paragraph +summary — it searches, it never receives the catalogue (5000 entries would not +fit in a prompt, and the summary stays ~480 characters however large the +library grows). Ids look like `kenney/racing/vehicle-truck-yellow` and are +stable across re-downloads, because generated game code writes them. + +Findability is built in three layers so it scales past 4000 models: +per-pack theme curation (~55 rows, giving every model its setting), filename +token parsing (free, and Kenney's names are systematic), and a hand-curated +query-time synonym table (~240 rows) that applies to the whole catalogue at +once. Item-level curation is spent only on the few hundred most-requested +things. At the full 4,999-entry catalogue: index build ~120 ms, a search +~0.2 ms, ~2.1 MB of heap (release). + +Query-side stemming means an inflected request still reaches a base-form +alias ("smashing" → the `smash` alias), and when two entries tie on score the +kind the query implies wins — an unqualified noun like "spaceship" is an +object request, while "metal clang" or "win music" wants something audible. + +## Credits + +Every asset here is CC0 (public domain). Attribution isn't required by the +licence — we credit anyway, because these libraries exist because someone chose +to give them away. + +- **Kenney** — — 47 model packs totalling ~4,670 + models (nature, city, castle, space, food, furniture, vehicles, dungeons, + characters and more), the five starter kits, and all seven sound packs + (impact, interface, sci-fi, music jingles, UI, RPG and digital audio). Thank + you for the extraordinary breadth of free, consistent, genuinely usable game + assets — this library is most of what Arcade can build with. +- **KayKit / Kay Lousberg** — — nine rigged and + animated characters: five adventurers (Knight, Barbarian, Mage, Rogue, + Rogue_Hooded) and four skeletons (Warrior, Mage, Rogue, Minion). All nine + share one 41-joint skeleton, so an animation authored for any of them plays + on every one — the adventurers ship 76 clips and the skeletons those same 76 + plus 19 undead extras. Thank you for giving away rigs and animation sets, + which are the expensive part. + +### Rigged characters + +`find_cast` lists the animated casts. A cast is a set of characters sharing one +skeleton, so any state works on any member and a part can be recast without +touching animation code: + +| rig | members | clips | notable states | +|-----|---------|-------|----------------| +| 41 joints (KayKit) | 9 | 76–95 | block, dodge, hurt, dance, sleep, carry | +| 7 joints (Kenney mini) | 22 | 25–32 | walk, run, jump, attack, sit, wave | +| 6 joints (Kenney platformer) | 5 | 25 | walk, run, jump, attack | + +Pick members from ONE cast for a scene, and use different members so a crowd +isn't clones — the same rule that applies to props. + +Machine-readable attribution lives in `resources/CREDITS.toml` so a published +game package can carry credit with it. diff --git a/apps/arcade/build.rs b/apps/arcade/build.rs new file mode 100644 index 000000000..520bbbac5 --- /dev/null +++ b/apps/arcade/build.rs @@ -0,0 +1,13 @@ +fn main() { + // Mirror platform/build.rs: MAKEPAD=headless builds get cfg(headless), so + // the view can stub the gamepad path (the headless platform backend has no + // game-input implementation). Without this arcade cannot build headless, + // and the render-to-PNG test path goes with it. + println!("cargo:rustc-check-cfg=cfg(headless)"); + println!("cargo:rerun-if-env-changed=MAKEPAD"); + if let Ok(configs) = std::env::var("MAKEPAD") { + if configs.split(['+', ',']).any(|c| c == "headless") { + println!("cargo:rustc-cfg=headless"); + } + } +} diff --git a/apps/arcade/download_assets.sh b/apps/arcade/download_assets.sh new file mode 100755 index 000000000..c40b941a9 --- /dev/null +++ b/apps/arcade/download_assets.sh @@ -0,0 +1,448 @@ +#!/usr/bin/env bash +# Fetch the CC0 model packs Makepad Arcade uses for its stock asset library. +# Nothing here is vendored in git (see resources/*/.gitignore) — run this once +# after checkout. Every file is pinned to an exact upstream commit and verified +# by sha256, so a moved or tampered upstream fails loudly instead of silently +# changing the library. +# +# ./download_assets.sh fetch everything (idempotent) +# ./download_assets.sh --list show packs, counts and licences, download nothing +# +# ---------------------------------------------------------------------------- +# CREDITS — all assets below are CC0 (public domain). Attribution is not +# required by the licence; we credit anyway, because these libraries exist +# because someone chose to give them away. +# +# Kenney — https://kenney.nl/assets +# Vehicles, city, arena, platformer and FPS kits. Thank you @KenneyNL. +# +# KayKit / Kay Lousberg — https://kaylousberg.itch.io/ +# Rigged + animated characters: 5 adventurers (Knight, Barbarian, Mage, +# Rogue, Rogue_Hooded) and 4 skeletons (Warrior, Mage, Rogue, Minion). +# All nine share one 41-joint rig. CC0 verified in each pack's LICENSE.txt +# at the pinned commit. Thank you Kay. +# +# NOTE ON AUDIO FORMAT: every Kenney audio pack ships Ogg Vorbis only (no WAV +# variant exists upstream — checked all seven packs). This tree has no vorbis +# decoder, so the sounds are downloaded and indexed (searchable by the AI) but +# not yet playable; a decoder, or an opt-in local transcode, is the missing +# piece. `--transcode` below does the latter when ffmpeg is installed. +# ---------------------------------------------------------------------------- +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resources" +MODELS="$ROOT/models/kenney" +CHARS="$ROOT/characters" +AUDIO="$ROOT/audio/kenney" +MANIFEST="$ROOT/MIRROR.toml" + +# --------------------------------------------------------------------------- +# 3D model packs (the full Kenney 3D catalogue) are described by +# resources/MIRROR.toml rather than inlined here: 50 packs is too many to keep +# readable in shell, and that file is also what makes an independent mirror +# reproducible. +# +# Source selection: +# ARCADE_ASSET_MIRROR=https://our.host/path or --mirror= +# A mirror is expected to serve /.zip. Whichever host serves the +# bytes, the sha256 from MIRROR.toml is verified identically — a mirror is +# never trusted more than upstream. +# +# Selection: +# (default) the "core" packs — ~1900 models, ~60 MB +# --packs=all every usable pack — 4669 models, ~166 MB +# --packs=a,b,c named packs +# --------------------------------------------------------------------------- +MIRROR="${ARCADE_ASSET_MIRROR:-}" +PACKSEL="core" +fetched=0 +cached=0 + + +# Kenney audio packs, pinned by content-hashed download URL. +# Each entry: ||| +KENNEY_AUDIO=( + "impact-sounds|87b4ddecda-1677589768|029d734af1582474edf3a694d1b0cebc97c1c152f2f39fa34d4c2bafc5de77f8|130" + "interface-sounds|fa43c1dd4d-1677589452|f2193d072726d6758a5f7871b2dcc54dcce0d5c35c6f0a62f92549b327c81232|100" + "sci-fi-sounds|6b296f9ecf-1677589334|119340f351a5098ad814f78719438c0da355a9ce8a4c8a3af6a8d48aa3d49e04|73" + "music-jingles|f37e530b9e-1677590399|b729ba57959bd58793d2c5cafa348aaf2655d354f3da35ec4729e03ec77197b8|86" + "ui-audio|490d233f68-1677590494|946fc23a63d535d693eb31b2eabb80c8c28d6351e2186b344ceb71b2cb1d5eb6|52" + "rpg-audio|8e99002d76-1677590336|6dbeaf8544da958d8f2adcb4a4a4b76c1ade34a05f8ab9edccd327da7375f38b|52" + "digital-audio|216eac4753-1677590265|24e6ce28b76a6d8c89cff4d331e0965ff5c3de8a73c612028e9d363cc64e4f06|63" +) + +# Kenney starter kits, pinned. Each entry: +KENNEY_PACKS=( + "arena|Starter-Kit-Basic-Scene|a6927e66ff8dd8e173660ce4825abe773c65f683|sample/Mini Arena/Models/GLB format" + "city|Starter-Kit-City-Builder|4535092b740b378b700efd9df9e27a631815b84a|models" + "fps|Starter-Kit-FPS|185fd2326d74a5cf858cffc616f87cf9696f9cc0|models" + "platformer|Starter-Kit-3D-Platformer|3fa8a04b1c01ab23db43123d4ce814a34c3fc7f0|models" + "racing|Starter-Kit-Racing|f5241ebdf00c25bc951bf4fdb7950bb1b78b4bcc|models" +) + +# KayKit character packs, pinned. CC0 verified in each repo's LICENSE.txt at +# the pinned commit ("License: (Creative Commons Zero, CC0)"). +# +# ALL NINE CHARACTERS SHARE ONE RIG — 41 joints, identical names in identical +# order, verified by hashing the joint-name list of every file (both packs +# produce the same digest). So one animation-driving code path serves the whole +# cast, and a clip authored against any of them plays on all of them. +# Adventurers ship 76 clips; skeletons ship those same 76 plus 19 undead extras +# (awaken, resurrect, spawn, taunt) — a strict superset, no clip is lost. +# +# The GLBs EMBED their texture (image/png in a bufferView), so unlike the Kenney +# packs there is no external URI to resolve and no path to get wrong. The +# sidecar *_texture.png files are fetched anyway because the skin loader +# deliberately ignores materials ("the caller binds its own texture"), so the +# app supplies the atlas itself. +KAYKIT_ADV_COMMIT="672074b73ba276876a19e8816ecdc5241817ab47" +KAYKIT_ADV_BASE="https://raw.githubusercontent.com/KayKit-Game-Assets/KayKit-Character-Pack-Adventures-1.0/$KAYKIT_ADV_COMMIT" +KAYKIT_ADV_DIR="addons/kaykit_character_pack_adventures/Characters/gltf" +KAYKIT_SKEL_COMMIT="15b62b9bad122f72926c10fb14d622c73819fa54" +KAYKIT_SKEL_BASE="https://raw.githubusercontent.com/KayKit-Game-Assets/KayKit-Character-Pack-Skeletons-1.0/$KAYKIT_SKEL_COMMIT" +KAYKIT_SKEL_DIR="addons/kaykit_character_pack_skeletons/Characters/gltf" + +# |||| +# Rogue_Hooded has no atlas of its own upstream — it re-skins rogue_texture. +KAYKIT_CHARS=( + "knight|adv|Knight.glb|60428e3abc09ba83e595d256e3af8c5c976b46cdae599f0802fc82b4a3445168|knight_texture.png" + "barbarian|adv|Barbarian.glb|cefc311a0e10c7858b6141f5ada7e33268727564fb8ac1347aab97d000669cc6|barbarian_texture.png" + "mage|adv|Mage.glb|cf898585da33fab50c724d31605fb931eb2912e6d2280092141e98ca81ad507d|mage_texture.png" + "rogue|adv|Rogue.glb|e825437cd4d2ee9c1960b517a74a69101e33eb409ae7fa8cedc7134a998fbb7d|rogue_texture.png" + "rogue_hooded|adv|Rogue_Hooded.glb|93e6e25213009952276d9cf34f5d96a243767334c66f280db0433ddfabb91545|rogue_texture.png" + "skeleton_warrior|skel|Skeleton_Warrior.glb|178b6fda810b814c250d8a2010c24dfd9b458b9006dd323353e620b7ff118bbe|skeleton_texture.png" + "skeleton_mage|skel|Skeleton_Mage.glb|e05b0f5cfa395271c9f75fd07c0a0613c56f401ece4ab644e080001a79971075|skeleton_texture.png" + "skeleton_rogue|skel|Skeleton_Rogue.glb|4003f2b77891bb56f7e0de7d555abcb497aebbcaee35b614211f16275f9ccae3|skeleton_texture.png" + "skeleton_minion|skel|Skeleton_Minion.glb|6ffc003f895bed0b074791e0e490846210a2e2f8fc7da300aba53cc185f95968|skeleton_texture.png" +) + +# ||| +KAYKIT_TEXTURES=( + "knight_texture.png|adv|knight_texture.png|5d250ccc5da020e6126bfa3839f83bd9a465a951ed223e4d13c08b1925e154d4" + "barbarian_texture.png|adv|barbarian_texture.png|7329b2ff9709e8d54c886d5ef49c08bd42b12be3bbb2facb1488a6b48b1e8a80" + "mage_texture.png|adv|mage_texture.png|ea49f094b960402635fe51db9f1864960c97271b17f2e3554e5aca1b2bbba144" + "rogue_texture.png|adv|rogue_texture.png|a4032e877c3b91939f5cdbb630349c1998fdbc3211bbd587c111125500fe4cc5" + "skeleton_texture.png|skel|skeleton_texture.png|15741a25c53e04fa9bf3beac3bc0de442359404b1ff9be863b892cb551ad3657" +) + +kaykit_base() { case "$1" in adv) echo "$KAYKIT_ADV_BASE/$KAYKIT_ADV_DIR" ;; skel) echo "$KAYKIT_SKEL_BASE/$KAYKIT_SKEL_DIR" ;; esac; } + +# Per-pack file manifest: " " lines. +pack_files() { + case "$1" in + arena) + echo "banner.glb 96e1cf7924fcc871be0e7e182c9b918de381b9783ca9e96922f8db62af384056" + echo "block.glb 656e07f0a3e7d0620892aca6fc40683b4266ff6b9672ecf89f3553e5ed85a1fb" + echo "border-corner.glb 120ce330ca50fe50e51bf693eadb957abaca972c42d573a07d8e8e404560bc3c" + echo "border-straight.glb 76ef69e8858f3d17c652d92033a553db58296135a5810d1bd5e54f47b5aa40c4" + echo "bricks.glb fd1d2c033c7db8b7352ac1becdab447d09cabcc11c73adf9bb6790f9680eaa86" + echo "character-soldier.glb b0410b4a34068e56ac1af3a2571f4497f84f1e481cced895b618d1fe8136529c" + echo "column-damaged.glb 442a6856e7e2d042c9e3af155dabf6d394f7ba91b2e271ee6392da22555a072c" + echo "column.glb 5c815b5fd43d613a21afddaaa90dc2f90b80e31f03dbdde98921cb7424e836c0" + echo "floor-detail.glb 804224caf3008fcdbd57d7ccc2c4734320c58ca880895699307670d2443e324b" + echo "floor.glb 15512c5ca6e9d3eb60efd237bbbf5b8010fdea63e0d8e6623f72b59de20279ab" + echo "stairs-corner-inner.glb 2e232cecda1d633acc1b78707ee50089fd40a167293c173536d61ef064697a5c" + echo "stairs-corner.glb af8eb87dc8826e184f585ebccf0592c909da7370559cd840b218593f4b1c8a38" + echo "stairs.glb 54a3f12c720f9cb83027b5429e330d7c50578f70771fd1bf9a2e521fe131ca6f" + echo "statue.glb 5ba8ecba963f6b3ec974809e2bf0b615da7e2701cd0efc6a3c0e946efb147397" + echo "tree.glb f8632c9758a78f60aa4ca68bf0e5ac972770167a5608e179293a12552d8fc2ef" + echo "trophy.glb 253f6d5f321ab20f8f505dfe35b7404c98d2d492728e6df5f69dd29e936b8be5" + echo "wall-corner.glb 4dc0e770106a122885f41be0bb4024466a7f58bc29a13f07b6600c4babbeb777" + echo "wall-gate.glb f70a86344f202b3a8057e3a6ee9cd632ba51a5c16795cfde34cd10e906f14697" + echo "wall.glb b05ab8645136cc26e761ae2189f660a585dca88c11721771bca3fb4c1f7a807f" + echo "weapon-rack.glb bb95da3746098e36f3d87a1e1036ca70ead7fe95860191871e02f7c58ff1efe5" + echo "weapon-spear.glb 9634e12abce01a07d40d5156454bfa1123eba72af6449fccf577980e331adbeb" + echo "weapon-sword.glb e4d2219d954148dd8b04fccc7602726c34eafad58c0dc5a45268155fcf6cc8c4" + ;; + city) + echo "building-garage.glb 7373b558fc9b1e27e60b60ca1ce53000af7e132d7304eeb6d49b47528a3b6806" + echo "building-small-a.glb 22ce989013bd16b1732e81798e343cf85f947e92b6c93825b18131feded48e07" + echo "building-small-b.glb 0bc8459045975158524753d6b648ff77e5f91fe8c033bc4886930b6d55543a2b" + echo "building-small-c.glb 3ea0f46fbed4e0acba7fda365cd968b3f23a6e02b9fee79dd54c99c42ffadf2f" + echo "building-small-d.glb 0ab476fbd7956cea52e590a8e68e351ce2f6fc04df158aa3f5a76b7744bf37a0" + echo "grass-trees-tall.glb d23e3b722453236bf6f4f8922b7bf64f81a7f3f804848a89c4b100ccbf13abef" + echo "grass-trees.glb cb06d03cc1c64ca7e7692c0aef3628d8e7cd4afee69093025f9ddd668653a4ce" + echo "grass.glb 3e3ec91132ad8519967aa2e4c0bedbab2aeed39b5714d66068bb75648ae73a52" + echo "pavement-fountain.glb ea2996089e90a79ba13d764106d374486bb8137666cf1cdbaebd1927ef365185" + echo "pavement.glb 60152776325d436761a13eb0c2ec9368b393bc82cf754b63cab810d79639ade9" + echo "road-corner.glb 85aec60d66c5084bb658274a1468139a5def1e9e9575c01286ed6c2069916789" + echo "road-intersection.glb 0212ffe9945b933535503372855c54dee20d54ddfc07e1c72963e67558a14183" + echo "road-split.glb 31bf582953db8315edfade4ba49d81145a78bda9757b6442dc417e14cc84b5ff" + echo "road-straight-lightposts.glb fc5340621fefdd43ab0bbf4bdaa41de9822dc545ce787fbfd92b75ce85d51e62" + echo "road-straight.glb 008a6305de778439d1a99be78a3e0945c72a9bc9cc0bade1b51a666d5db01d0b" + ;; + fps) + echo "blaster-repeater.glb e76220e5ad3877d879e70fe21bd5bc76a5988b8682aa5f7976739e71caf70e05" + echo "blaster.glb 9c2110d94c1bd7e01bfe6827a3d87e14962a1f721d8b300c67bdd36f492140a1" + echo "cloud.glb 4c667239958ae8950b6bc2f55bdb59814b24fc9f6d8531eb09411150e09c87f2" + echo "enemy-flying.glb e4868f6d8cac2a8f728430229789206841ca64727d94de2d808b468e4f182f8c" + echo "grass-small.glb 8dfe00761fa833daab3192148b9b3d573f2a10aa2eff2c01a8a386dd304e7fda" + echo "grass.glb 8e11283ca11894860a4dcb8f523165eda5de39f68fda43d892efc3a3d3eb567e" + echo "platform-large-grass.glb 19ee4b9e745f757d87d584094433e3f4d86be958bcd7eeba586874f088d2697b" + echo "platform.glb 6eac72ffbda64b4414833eb49d3b160fddf169c2caccca237a07e15a20731764" + echo "wall-high.glb 946a0f875bd515323afd883553729a29ac764b405e382bf9a30d6527dac74589" + echo "wall-low.glb c42c89dead3835fde97d1ede232bf1a69d297cd41e1d6bfe7e25c87b233a2ec0" + ;; + platformer) + echo "block-coin.glb ebe3a7be051566513039e6ba7d83d976fb3bf3e363fc8ee0633bdb872bb2d501" + echo "brick-particle.glb 981a84073e1dfa745a93db4ba550210049a144a4ac2c6cd333f289ff95630a72" + echo "brick.glb c9011dd30254c7a5dfdb29c61c22b6b3b4ad82186a096b90b824ab0b8dd58db8" + echo "character.glb 7112f6a08400914f9da546f3e6029e947cc9eab2b4a6da5eb99776111289efb1" + echo "cloud.glb 09c6f071d3a9ab64993248ee5c8d50debcd9c6cc4369236a01155ff7ff87f30a" + echo "coin.glb a3fb8f779a6af1cd75e5a02da77a87546ca8a59f6fe52f79f25179c4a68fd0e5" + echo "dust.glb 2f2ce449aca7791829bf2d93a7af534549f0bad1ee904d1a00df8d5329fc31dc" + echo "flag.glb 0ba84ff43f0fea0a9f0b080cfa1be54ebda36c8c35174d1ac2553ae6da9f65ae" + echo "grass-small.glb 4b94e8dd9cbdc3eedff99cbf614fe828f0c2342d21a7d0edb125b129d0422d51" + echo "grass.glb acb29df8a75b08e985e27a0ed8170b8f3d135abd24ccf5aa4d631cb2cd15cfda" + echo "platform-falling.glb d6dc9baba80af659e6d0fffc435bd1f08b1a7945942ccbb85dd9bb103b7a574d" + echo "platform-grass-large-round.glb 73c2b66eca6a36df6b5c25bb2c0594cf870066627275993a3a7857acc5a844bc" + echo "platform-large.glb d01f4eae24c895dfe467ef71c2ea510ca6bd6d8462f319741537936eae0b3f65" + echo "platform-medium.glb 64f81c8bd8bf07cc450dfcb25d18eedcc122d690028cca098bcdd48b00a02e46" + echo "platform.glb 78c5ed4da30c5a97f0747a54248bc9de05ba1b3fb6b94be527dd48599f5ef44c" + ;; + racing) + echo "decoration-empty.glb 3815b26a5274173934d37cf605e320450efad4e0034040868b6aea761cfd74bc" + echo "decoration-forest.glb 664a53f0f709fef9096af3bbfb1aa76536527a616b05170c0cf4e27e33358a00" + echo "decoration-tents.glb 19dbf2a778ad75f95c7d61f12866ef7174ab69cad169d3749c5400f5e14db8a3" + echo "track-bump.glb 6db020edc53532ebc0971c9d4ad9afb11c4de67c510649ca843db4616514f43c" + echo "track-corner.glb 0ffb3d83b60456fc5a2962447111a1a64bfe943f4ad95462e1edba107be81c80" + echo "track-finish.glb 2fec3b681658d6e77e20c4342d1e3ceeaab3e8d2fbf340942cde25cc2b21975b" + echo "track-straight.glb 2d8080df1fe27e39981480809f36eba7f239e814f96bff61bb4c99793221b701" + echo "track-tents.glb eaf68bbb44e362e291b71d13015fbafc4ce22a9102bda029614972d6cc843c7f" + echo "vehicle-motorcycle.glb c97911b8dbc2d5dd9d1961b46eb4d1f132b67a6c5a84f45593fea44e0595dd92" + echo "vehicle-truck-green.glb df362d027a09b19395be0e96abd7cc2dc54608dfb920b592e64357b589f9fcd6" + echo "vehicle-truck-purple.glb 03e32f5abcbbd03da3591f42fdaae2cf7d27ed379df7ce0d1f233bea02998565" + echo "vehicle-truck-red.glb eca99bd9ab0a2b02125f915e65d1ec8f1c5a93be7c6d4840efdac6633f47772c" + echo "vehicle-truck-yellow.glb 1ebd83174eab6d2fdf69eb5d8e32bd06a23a86d9edc4fe915a4160430a7c36a5" + ;; + *) + echo "unknown pack: $1" >&2 + return 1 + ;; + esac +} + +pack_count() { pack_files "$1" | wc -l | tr -d ' '; } + +# Read MIRROR.toml into parallel arrays: slug, url, sha, models, core, usable. +manifest_rows() { + awk ' + /^\[\[pack\]\]/ { slug=url=sha=""; models=0; core="false"; usable="false"; next } + /^slug =/ { gsub(/.*= "|"/, ""); slug=$0; next } + /^url =/ { gsub(/.*= "|"/, ""); url=$0; next } + /^sha256 =/ { gsub(/.*= "|"/, ""); sha=$0; next } + /^models =/ { gsub(/[^0-9]/, ""); models=$0; next } + /^core =/ { gsub(/.*= /, ""); core=$0; next } + /^usable =/ { gsub(/.*= /, ""); usable=$0 + if (slug != "") print slug "|" url "|" sha "|" models "|" core "|" usable + next } + ' "$MANIFEST" +} + +want_pack() { # + case "$PACKSEL" in + all) return 0 ;; + core) [[ "$2" == "true" ]] ;; + *) [[ ",$PACKSEL," == *",$1,"* ]] ;; + esac +} + +if [[ "${1:-}" == "--list" ]]; then + echo "Makepad Arcade stock asset library" + echo + printf '%-12s %-6s %-10s %s\n' "PACK" "FILES" "LICENCE" "SOURCE" + for entry in "${KENNEY_PACKS[@]}"; do + IFS='|' read -r pack repo _commit _dir <<<"$entry" + printf '%-12s %-6s %-10s %s\n' "$pack" "$(pack_count "$pack")" "CC0-1.0" "kenney.nl (KenneyNL/$repo)" + done + printf '%-12s %-6s %-10s %s\n' "characters" "${#KAYKIT_CHARS[@]}" "CC0-1.0" \ + "kaylousberg.com (KayKit Adventurers + Skeletons, one shared rig)" + +for entry in "${KENNEY_AUDIO[@]}"; do + IFS='|' read -r pack _hash _sha count <<<"$entry" + printf '%-12s %-6s %-10s %s\n' "$pack" "$count" "CC0-1.0" "kenney.nl (ogg)" + done + if [[ -f "$MANIFEST" ]]; then + echo + echo "3D packs (from MIRROR.toml; * = in the default core set):" + tm=0; tp=0 + while IFS='|' read -r slug _url _sha models core usable; do + [[ -z "$slug" ]] && continue + mark=" "; [[ "$core" == "true" ]] && mark="*" + note=""; [[ "$usable" == "true" ]] || note=" (FBX only — not fetched)" + printf '%s %-34s %5s models%s\n' "$mark" "$slug" "$models" "$note" + [[ "$usable" == "true" ]] || continue + tm=$((tm + models)); tp=$((tp + 1)) + done < <(manifest_rows) + echo " ${tp} usable packs, ${tm} models total" + fi + echo + echo "Run without --list to download. Files land in resources/models/kenney//," + echo "resources/audio/kenney// and resources/characters/ — all gitignored." + echo + echo "Audio is Ogg Vorbis (Kenney ships no WAV). This tree has no vorbis decoder," + echo "so sounds index and search but do not play yet; --transcode converts them to" + echo "WAV with ffmpeg if you have it installed." + exit 0 +fi + +TRANSCODE=0 +for arg in "$@"; do + case "$arg" in + --transcode) TRANSCODE=1 ;; + --packs=*) PACKSEL="${arg#--packs=}" ;; + --mirror=*) MIRROR="${arg#--mirror=}" ;; + --list) : ;; + *) echo "unknown option: $arg" >&2; exit 1 ;; + esac +done + + + +fetch() { #