diff --git a/.gitignore b/.gitignore index 8d012a217..ad13aaa04 100644 --- a/.gitignore +++ b/.gitignore @@ -52,31 +52,3 @@ 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 6d14b3718..eff1c3ddd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,7 @@ workspace.members = [ - # === app === + "widgets/dll", + # === apps === "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", @@ -53,19 +40,46 @@ workspace.members = [ "examples/cad", "examples/box3d", "examples/hello_world", - # === xr app === - "xr", - # === studio === - "studio/hub", - "studio/desktop", - # === necessary tools === - "tools/cargo_makepad", - "tools/map_tiles", - "tools/map_bake", - "tools/remote", - "tools/arcade_eval", - # === tests === + "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", + "libs/cef", ] workspace.exclude = [ diff --git a/README.md b/README.md index ce322c8d4..f17960fd4 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 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 +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. This repository contains the core engine, widgets, tools, and examples. @@ -17,7 +17,6 @@ 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 @@ -53,7 +52,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 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 +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 ``` ## Build And Run Makepad Studio diff --git a/aigame.md b/aigame.md new file mode 100644 index 000000000..d6335cb67 --- /dev/null +++ b/aigame.md @@ -0,0 +1,308 @@ +# 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 new file mode 100644 index 000000000..89693876c --- /dev/null +++ b/aigame_parity_gap.md @@ -0,0 +1,52 @@ +# 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 new file mode 100644 index 000000000..0aa6bc8d8 --- /dev/null +++ b/aigame_port_findings.md @@ -0,0 +1,179 @@ +# 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 new file mode 100644 index 000000000..9030a6d47 --- /dev/null +++ b/aigame_port_inventory.md @@ -0,0 +1,70 @@ +# 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 deleted file mode 100644 index 795c72bc9..000000000 --- a/apps/arcade/BUDGETS.md +++ /dev/null @@ -1,210 +0,0 @@ -# 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 deleted file mode 100644 index 6f7bf8067..000000000 --- a/apps/arcade/Cargo.toml +++ /dev/null @@ -1,46 +0,0 @@ -[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 deleted file mode 100644 index db998b1f9..000000000 --- a/apps/arcade/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# 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 deleted file mode 100644 index 520bbbac5..000000000 --- a/apps/arcade/build.rs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100755 index c40b941a9..000000000 --- a/apps/arcade/download_assets.sh +++ /dev/null @@ -1,448 +0,0 @@ -#!/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() { #