rom-forge
Authors and compiles Puck cartridges (puck.cartridge.v1, *.cgb.puck, *.dmg.puck, *.agb.puck, forge.play, forge.open, puck compile --validate), native compilers, cost. Emulator core is gaming-bricks.
SKILL.md.claude/skills/rom-forge/SKILL.md
GitHubROM documents and native compilers
A map of cartridge authoring: where it lives, the contracts that bite, and how to see a cartridge run. When this file and the code disagree, the code wins and this file is corrected in the same change.
Where things live
| Thing | Where |
|---|---|
The puck.cartridge.v1 model, validation, canonicalization, the CartridgeDraft editor, ICartridgeCompiler, the cost model |
src/Puck.GamingBricks.Forge (CartridgeDocuments, CartridgeValidation, CartridgeCost, CartridgeLimits); its README has the source contract |
The cartridge .puck vocabulary |
src/Puck.GamingBricks.Transpiler |
| Native SM83 compiler for the cgb and dmg targets (code, 2bpp tiles, maps, palettes or shade registers) | HgbCartridgeCompiler in src/Puck.HumbleGamingBrick.Forge |
| Native AGB compiler (Thumb code, mode-0 4bpp graphics) | AgbCartridgeCompiler in src/Puck.AdvancedGamingBrick.Forge |
| Shared audio compilation | AudioDocumentCompiler, ApuNotePeriod in the shared forge package; TuneRom.Build for world audio |
| Boot ROMs | BootRomBuilder, BootDivPrediction, BootRomLayout in Puck.HumbleGamingBrick.Forge; the native ARM7TDMI firmware in Puck.AdvancedGamingBrick/Firmware |
| The in-engine console | src/Puck.World/ForgeCommandModule.cs (forge.new/open/show/set/remove/undo/check/build/save/export/play) |
| Shipped sources | worlds/debug/cartridges/<target>/*.<target>.puck (one folder per target, cgb/, dmg/ and agb/, which a world lists in a machine's contentFolders), authored source only; shared/ holds the modules several targets import, each declaring schema: "puck.cartridge.v1" so a world-source walk of the package never takes it for a world module |
Both compilers consume the same rules, variables, sprites and input conditions.
Source data determines behavior: never add a game-name switch or copy gameplay
into a C# template. Puck.GamingBricks.Forge depends on Abstractions, Assets and
Puck.State, never on World.
From source to a running cartridge
- Edit the
.pucksource. puck compile <name>.cgb.puck --output .claude/scratch/<lane>-cartridge.cartridge.json --validate.--validaterunsCartridgeDocuments.Validateover the lowered JSON; lowering alone proves only that the source was well formed.- In a running
Puck.World,forge.open <file>.cartridge.jsonloads the compiled JSON as the console's draft, thenforge.play <screen-index> <file>.cartridge.jsonhot-loads the draft into a screen.forge.openreads JSON only; machine content binding accepts authored.pucksources directly.forge.playneeds a draft first (forge: create or open a draft firstotherwise), and its path is an output: it writes the draft's canonical source there, then inserts it, and refuses a path not ending in.cartridge.json. A world screen whosesourceis a machine may instead name the.pucksource as itscontentPath; its content provider resolves imports and compiles the source at bind. - Look at it with
world.screenshot. Cartridges are verified in a world, never in a bespoke driver.
puck decompile <name>.cartridge.json --output <name>.cgb.puck turns a document
back into source; the source it writes compiles back to the same bytes.
The owner's ruling treats cartridge JSON twins as derived caches: never commit them or compare a run against them. Commit the authored source, compile when needed, and keep semantic compile/decompile round trips over freshly compiled documents. The machine content provider accepts source directly; the forge console still opens a compiled JSON draft.
forge.play <screen> <path> saves the source and inserts it through the
screen's named machine (WorldMachineCommandModule.InsertContent); a screen
with no declared machine refuses. The path must be a file in one of that
machine's contentFolders, as every mount must, and the play needs Control
over the screen and the machine. Failed draft edits leave the draft intact; check, save, build,
export and play always validate.
Contracts that bite
- Cartridge rules admit what world rules refuse.
repeat <n> as <name> { }withbreak, call-form gates (key(<button>, held|pressed|released)) and every compound assignment are legal in a cartridge rule. World rules also admit compound assignment; loops and cartridge call-form gates are PUCK037 and PUCK038 there.key(...)is a gate only, not an operand. Arepeatindex name must be a declared variable. - Rule bodies are step trees (
set/if/repeat/break/map/blit/play/stop/save/load). An operand is aPuck.State.ExpressionProgram;CartridgeExpressions.Readsis the admitted operation subset and anything outside it is refused at its own spelling. - Targets differ; validation says how. cgb, dmg and agb; byte and two-byte
state; no ROM banking. Consult the validator before calling a feature
portable. The light sensor
$lightexists only oncgb. - Colour is per-palette: up to 8 palettes on cgb and 16 on agb,
mapPalettesper background cell, a sprite picks an object palette. The dmg target validates like cgb but shades monochrome: every cell from the first background palette whatever it selects, a sprite from the first or second object palette by its palette's low bit, each colour reduced to one of four shades by its luma (CartridgeGraphics.MonochromePalette); a fade is a table of shade registers. A dmg image runs at single speed, so it sustains half the cgb work per frame (CartridgeCostProfile.Monochrome). - One game, several targets: a shared module. A cartridge source may
importa.puckmodule; its templates and constants come in (CartridgeDocumentEmitter.LowerSource, whichpuck compile, the language server and the round-trip test use), never rows. The game lives once as a template inworlds/debug/cartridges/shared/(tetromino.puck), and each target's source imports it, declarestargetandpalettes, and calls it. A module contributes no standalone document. - A Color-only image cannot run on the DMG. The cgb image arms the speed
switch and executes STOP, which parks a monochrome processor until a key is
pressed, and it writes palette memory and video bank one, which the DMG does
not have. Port the source to
target: "dmg"instead. - Map writes land a frame late on both targets: they queue and drain in the next vertical blank. Validation bounds a frame's map writes to the queue's 24 entries.
- A sound is a music track or a one-shot effect. A track is one to four voice parts, one to a channel, and validation refuses an effect on any voice a track occupies, since the effect would cut that part off. An effect may sit only on the pulse-1, noise or wave voice (never pulse 2), so with music on pulse 1 and pulse 2 the choices are an effect on noise or wave, or moving the music part off pulse 1 and the effect onto it.
- Keep the two sound drivers in register sync. Stream step widths are
durationplus five register bytes for pulse one and wave, four for pulse two and noise;AgbSoundDrivermaps the same registers to the AGB's legacy sound ports. - Saves.
AgbSaveModuleneeds its identifier text in the image or nothing persists; saves persist every byte.
Cost is measured, never estimated
CartridgeCost prices a document in abstract work units against
CartridgeCostProfile's per-target weights; CartridgeDocuments.Estimate
reports the frame bound and nothing refuses on it. Cost is advice, never a gate:
do not add a cost refusal, because the one hard refusal is capacity
(CartridgeCapacityException, an image that outgrew its machine). Weights come from
puck cartridge-cost, which boots CartridgeCostMeasurement's probe documents
on both machines. Never hand-count an emitter's instructions into a weight or
adjust a weight to make a document fit; a new primitive is measured by adding
its shape to CartridgeCostMeasurement.Shapes. An unmeasured primitive prices
as CostBound.Unmodeled, which poisons the whole estimate. Never compare unit
totals across targets. A blit repaints with the display off, so frames stop
advancing while it runs; CartridgeLimits.BlitCellCount bounds its size.
Boot ROMs
BootRomBuilder.Build(ConsoleModel) emits each revision's boot program. The
handoff counter (BootDivPrediction's tables) is the contract, pinned from
outside by the boot-rom-handoff Post stage against the mooneye boot_div-*
cartridges; never move a table value to make a boot agree with it. The builder
solves its straight-line tail against the emitted image; never hand-count
instruction timings into its constants. Runtime packages embed generated images
(HgbFirmware.GetImage) and never reference Forge. Regenerate with
puck firmware hgb --output <dir> (or puck firmware agb) and check the bytes
with --verify, which compares without writing.
Seeing it work
- Compile a cartridge and play it in a world (above). For World integration,
drive the real host over stdin (
puck-world); a build alone is not a play test. - The cartridge suites are
tests/Puck.AdvancedGamingBrick.Forge.Tests(CartridgeCompilerTests,CartridgeMemoryTestsand the per-featureCartridge*Testsrun cartridges on both real emulators),tests/Puck.GamingBricks.Transpiler.Tests(the round trip) andtests/Puck.HumbleGamingBrick.Forge.Tests(boot ROM and bundle container). Run them when they reach the change:dotnet build <project> -c Release -nodeReuse:false, thendotnet test <project> -c Release --no-build. - Settle out of OAM DMA before reading the bus. A fixed stepping boundary
can land inside OAM DMA, where WRAM reads 0xFF. Every SM83 verifier calls
VerifyMachineSettle.SettleOutOfOamDmaafter stepping frames; useFramework.VerifyMachineDriver, which already does. - Wait on the observable fact, not a duration.
RunFramesUntiladvances until a condition holds and fails at a frame cap. Reach a shared starting position once and hand it out withVerifyMachineDriver.Snapshot/Restore.
references/backend-contracts.md holds memory addresses, register bits and the native compilers' instruction-level behavior.
Route adjacent work
| Skill | Route there for |
|---|---|
puck-dsl |
.puck grammar, the compile/lint/format verbs, PUCKnnn diagnostics. |
gaming-bricks |
Emulator core behavior and the Post batteries. |
puck-world |
Driving the World over stdin; screens and authority. |