Skip to content

Publish the 51 prefabs gmx used to bundle - #4

Draft
opera-jimmym wants to merge 6 commits into
mainfrom
jimmym/bundled-prefabs
Draft

Publish the 51 prefabs gmx used to bundle#4
opera-jimmym wants to merge 6 commits into
mainfrom
jimmym/bundled-prefabs

Conversation

@opera-jimmym

@opera-jimmym opera-jimmym commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Moves the 51 prefabs gmx used to bundle inside its binary into this catalog, then fixes what that move exposed.

The move

gmx shipped 51 prefabs welded into its binary as a second format (recipe.toml + skeleton/) with its own loader — 3.5 MB and 862 files nobody could update without cutting a release. They land here as 21 games and components (install = "copy") and 30 libraries (install = "namespace"), each with a README.

What changed after that

The goldens are gone. Each prefab carried an expected.txt hashing its install output. A game project never sees the file, consumers pin a version tag anyway, and the drift it guarded is self-inflicted: the 108 prefabs already here compose through a pinned [parent-link] source = "transition_base@v0.6.4" and have no drift problem, while the imported 51 compose through requires = ["kernel"] — a bare name resolved at catalog HEAD. The gate also recorded a bug rather than catching it, reporting 51/51 passing while shipping it. gmx prefab check no longer needs a golden to run, so the checks worth having — dead [art] slots, stale README claims, the gametests — survive.

One real bug. exclude sat under a [tuning] header in 20 manifests, so TOML parsed it as a data knob and it leaked into generated GML (exclude: ["expected.txt"] inside bullet_hell_tuning()).

Conformance. A LICENSE for all 51 (every other prefab had one; six READMEs cited one that did not exist), a category on all 51, [roles] for the nine components missing them, and a regenerated prefab-index.json — which was stale independently, still carrying install: null on two prefabs. The repo README now documents what a prefab must carry.

The level is the tile layer. Three prefabs authored their level in levels/*.txt and generated room data from it. That is a second copy of the shape; gmx's own model has no room for it. maze_chase's 222 wall instances become an autotiled tile layer — obj_wall had no GML at all — and pathgrid gains pathgrid_block_tilemap. Bricks and dots stay instances, because the game counts them.

Nine of the fourteen gametests never passed. They jumped straight to the play room, skipping the title room where kernel_boot() runs, and crashed on boot while steps claimed they passed. They now enter through the title room. Their frame budgets were guesses too: the runner is real-time and breakout ran 610 frames for a result final at 80. A full sweep is 540s → 206s.

Verification

All 14 gametests pass, run the way CI runs them. gmx prefab check is clean across all 51, every prefab installs and gmx validates, and gmx prefab reindex is idempotent.

Not done

  • Thumbnails — 0 of 51 have one; all 108 native prefabs do. Its own pass.
  • Tags — the 51 <name>@1.0.0 tags are local-only, so these prefabs are unreleased.
  • Versioned requires — the actual fix for the drift the goldens proxied.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YMkJC93GC8J4kUzTRfh7N1

These shipped inside the gmx binary. A catalog prefab is the same code
reached by version instead of by release, so a project pins what it
composed against rather than tracking whatever gmx last built.

Each installs under `prefabs/<ns>/` with its symbols renamed, and is
called as `::<ns>::thing()` — namespacing is what lets two libraries
own a name like `state` without colliding. Each carries the manifest,
a README, a LICENSE, a `project.toml` naming its demo rooms, and an
`obj_demo`/`rm_demo` pair excluded from what a consumer receives.

`exclude` is a top-level key. Written under a `[tuning]` or `[content]`
header it parses as a knob instead, and leaks into the generated GML as
a tuning value that excludes nothing.
`install = "copy"` rather than namespaced: these land in the project
root and the project owns them from the moment they arrive. A file the
project already has is never overwritten — the incoming copy goes to
`.gmx/incoming/` — and rooms are appended to `[room_order]`.

Terrain is a tile layer, not a text file beside one. A second copy of a
level's shape is a thing that can disagree with the tiles, so the layer
is the only copy: `platformer` and `maze_chase` carry a 47-mask
auto-tile tileset, and the shape is edited through `gmx autotile shape`.
Bricks and dots stay instances, because something counted or destroyed
needs an identity a tilemap cell does not have.

`[roles]` names what each object and room is for, which is the only
thing that says which object is the game and which room it plays in.
`traps` records what fails silently — writing `x` on a physics body,
which the solver discards so the object simply never moves.
A deterministic run — fixed seed, scripted inputs — that boots the
prefab and asserts the loop it exists to demonstrate. This is what has
teeth: it checks behaviour rather than that the bytes did not change,
and a golden can only ever tell you output moved, never that it was
right.

Each boots through the prefab's own title room and presses to start,
rather than jumping to the play room. `::ns::` does not resolve inside
a chunk — chunks are injected after namespace rewriting — so a chunk
that calls into a library dies with a bare "runtime error" naming
neither the line nor the identifier.

Watch chunks record a running extreme, not the last sample: a check
that reads the final frame passes for a player who fell through the
floor and came back.
All 108 prefabs already here had one and none of the 51 did, which is
the half a browser most wants a picture of.

Taking one needs the prefab to run on its own, and that is what found
the real gap: five components had no demo at all. Writing one for each
turned up a prefab that could not work — `main_menu` declared
`[[content.items]]` with no `id`, while `obj_menu` reads `items[i].id`
in its Create, so placing it in any room killed it. Nothing had ever
instantiated it.

Two things a demo has to supply itself: the kernel is not booted, so
anything reaching `::kernel::` dies naming a kernel script rather than
the caller; and `<prefab>_tuning()` is generated at install, so a demo
run from the prefab directory cannot call it.

`gmx run` writes capture bookkeeping into the prefab it ran, and `gmx
test` bakes `foo.gametest.json` to a `foo.json` of base64 bytecode.
Neither belongs in the tree, and the baked one is not in `NOT_PAYLOAD`,
so it installs into every consumer project.
The repo README was one line, which is how the imported set diverged
without anyone noticing. It now records what a prefab carries, what
each manifest field is for, and — the part that was wrong everywhere —
how publishing actually works.

Two different tags are involved and shipping one of them publishes
nothing. Consumers load `prefab-index.json` at the newest tag with no
`<prefab>@` prefix, so without a new `vX.Y.Z` a consumer sees the
previous release's index however many per-prefab tags were pushed.
`git push --tags` uploads tags and not the commit they point at, and a
tag on a branch that never merged keeps resolving, quietly, to the
wrong tree.

Also drops `online_leaderboard`'s copy of the gmx scaffold — 878 lines
pinned to gmx 0.3.27, teaching eight commands that no longer exist.
@opera-jimmym
opera-jimmym force-pushed the jimmym/bundled-prefabs branch from 640301a to 198e649 Compare September 1, 2026 09:34
A four-lane rhythm game: 120 BPM, one note per beat from beat four after
a count-in, arrow keys one per lane, perfect/good/miss judging with score
and combo. The chart is an array in `obj_rhythm`'s Create and nothing
else has to move with it.

Its value beyond a new genre is that it is the only thing in the catalog
that reaches GameMaker's headless audio path. A run with no audio device
had `audio_play_sound` hand back a negative handle, and
`audio_sound_get_track_position` then read 0 every frame — silently, so
a rhythm game looked like it was working and simply never spawned a
note. The runner's own self-test drives the transport struct directly
and never reaches the entry points where that happened, which is why it
shipped broken and reported OK. This reaches them through a real game.

The eight asserts bite in both directions, and both were checked: all
eight pass on a runtime with the fix, and all eight fail without it,
`actual 0.0` on every one. Confirmed outside the gametest too — frames
60/150/240/330 of an install on an unfixed runtime are byte-identical at
14386 bytes with the HUD frozen at `0.00 / 8.00s`, so the failure is
upstream of the chart entirely. `steps[]` says which runtime it needs and
what its own failure looks like, so eight red asserts read as the bug
being reported rather than as a recipe to repair.

It composes on kernel like every other playable prefab, rather than
carrying its own scoreboard. `install = "copy"` puts it in the project
root beside whatever else is installed, so a private score and combo
would have meant two counters in one project with the HUD showing one of
them. Score goes through `kernel_score_add`, which scales by the combo
multiplier — the combo is bumped first, so the hit that extends a run is
the one that benefits.

Kernel's pause had to be turned off explicitly, and that is recorded as
a trap rather than left as a comment: it stops the game while the track
keeps playing, so the song runs on behind a frozen screen and the chart
is never in step again. Nothing errors. Anything driven by audio rather
than by frames has the same problem.

The song is a real 120 BPM track — kick on the beat, snare off it, a
bass line — so the chart is checkable by ear rather than against a test
tone. Art and audio are generated placeholders, CC0; the LICENSE is MIT
like the other code prefabs, with that note kept underneath.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants