romdevtools 0.16.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/AGENTS.md +75 -16
  2. package/CHANGELOG.md +316 -0
  3. package/examples/README.md +2 -0
  4. package/examples/atari2600/templates/platformer.asm +460 -0
  5. package/examples/atari2600/templates/racing.asm +463 -0
  6. package/examples/atari2600/templates/shmup.asm +386 -0
  7. package/examples/atari2600/templates/sports.asm +362 -0
  8. package/examples/atari7800/templates/default.c +49 -5
  9. package/examples/atari7800/templates/hello_sprite.c +48 -4
  10. package/examples/atari7800/templates/music_demo.c +47 -2
  11. package/examples/atari7800/templates/platformer.c +43 -4
  12. package/examples/atari7800/templates/puzzle.c +39 -4
  13. package/examples/atari7800/templates/racing.c +39 -4
  14. package/examples/atari7800/templates/shmup.c +40 -2
  15. package/examples/atari7800/templates/sports.c +36 -5
  16. package/examples/c64/templates/platformer.c +19 -5
  17. package/examples/c64/templates/puzzle.c +32 -2
  18. package/examples/c64/templates/shmup.c +28 -2
  19. package/examples/c64/templates/sports.c +30 -2
  20. package/examples/c64/templates/tile_engine.c +77 -27
  21. package/examples/gb/templates/default.c +110 -16
  22. package/examples/gb/templates/hello_sprite.c +15 -6
  23. package/examples/gb/templates/music_demo.c +36 -0
  24. package/examples/gb/templates/platformer.c +28 -6
  25. package/examples/gb/templates/puzzle.c +35 -4
  26. package/examples/gb/templates/racing.c +75 -10
  27. package/examples/gb/templates/shmup.c +41 -3
  28. package/examples/gb/templates/sports.c +51 -3
  29. package/examples/gb/templates/tile_engine.c +3 -2
  30. package/examples/gba/templates/gba_hello.c +29 -11
  31. package/examples/gba/templates/maxmod_demo.c +36 -2
  32. package/examples/gba/templates/platformer.c +3 -1
  33. package/examples/gba/templates/puzzle.c +15 -3
  34. package/examples/gba/templates/racing.c +65 -3
  35. package/examples/gba/templates/shmup.c +41 -4
  36. package/examples/gba/templates/sports.c +36 -2
  37. package/examples/gba/templates/tonc_hello.c +41 -5
  38. package/examples/gba/templates/tonc_hello_sprite.c +35 -1
  39. package/examples/gbc/templates/default.c +103 -26
  40. package/examples/gbc/templates/hello_sprite.c +12 -3
  41. package/examples/gbc/templates/music_demo.c +56 -12
  42. package/examples/gbc/templates/platformer.c +28 -6
  43. package/examples/gbc/templates/puzzle.c +35 -4
  44. package/examples/gbc/templates/racing.c +88 -21
  45. package/examples/gbc/templates/shmup.c +37 -3
  46. package/examples/gbc/templates/sports.c +48 -3
  47. package/examples/gbc/templates/tile_engine.c +3 -2
  48. package/examples/genesis/main.s +53 -1
  49. package/examples/genesis/templates/hello_sprite.c +25 -3
  50. package/examples/genesis/templates/puzzle.c +37 -3
  51. package/examples/genesis/templates/racing.c +44 -11
  52. package/examples/genesis/templates/sgdk_hello.c +34 -1
  53. package/examples/genesis/templates/shmup.c +31 -1
  54. package/examples/genesis/templates/shmup_2p.c +31 -0
  55. package/examples/genesis/templates/xgm2_demo.c +20 -0
  56. package/examples/gg/templates/default.c +56 -18
  57. package/examples/gg/templates/hello_sprite.c +25 -2
  58. package/examples/gg/templates/music_demo.c +24 -2
  59. package/examples/gg/templates/platformer.c +18 -12
  60. package/examples/gg/templates/puzzle.c +38 -7
  61. package/examples/gg/templates/racing.c +58 -9
  62. package/examples/gg/templates/shmup.c +47 -3
  63. package/examples/gg/templates/sports.c +57 -16
  64. package/examples/gg/templates/tile_engine.c +12 -6
  65. package/examples/lynx/templates/default.c +39 -8
  66. package/examples/lynx/templates/hello_sprite.c +15 -1
  67. package/examples/lynx/templates/music_demo.c +13 -1
  68. package/examples/lynx/templates/puzzle.c +28 -1
  69. package/examples/lynx/templates/racing.c +34 -7
  70. package/examples/lynx/templates/shmup.c +42 -3
  71. package/examples/lynx/templates/sports.c +29 -2
  72. package/examples/msx/platformer/main.c +213 -0
  73. package/examples/msx/puzzle/main.c +250 -0
  74. package/examples/msx/racing/main.c +249 -0
  75. package/examples/msx/shmup/main.c +288 -0
  76. package/examples/msx/sports/main.c +182 -0
  77. package/examples/nes/templates/default.c +67 -19
  78. package/examples/nes/templates/hello_sprite.c +35 -0
  79. package/examples/nes/templates/music_demo.c +40 -0
  80. package/examples/nes/templates/platformer.c +65 -6
  81. package/examples/nes/templates/puzzle.c +67 -6
  82. package/examples/nes/templates/racing.c +45 -13
  83. package/examples/nes/templates/shmup.c +51 -2
  84. package/examples/nes/templates/sports.c +51 -6
  85. package/examples/pce/catch_game/main.c +22 -3
  86. package/examples/pce/music_sfx/main.c +28 -1
  87. package/examples/pce/platformer/main.c +283 -0
  88. package/examples/pce/puzzle/main.c +304 -0
  89. package/examples/pce/racing/main.c +304 -0
  90. package/examples/pce/shmup/main.c +346 -0
  91. package/examples/pce/sports/main.c +254 -0
  92. package/examples/pce/sprite_move/main.c +7 -2
  93. package/examples/sms/main.c +35 -6
  94. package/examples/sms/templates/hello_sprite.c +29 -3
  95. package/examples/sms/templates/music_demo.c +18 -4
  96. package/examples/sms/templates/puzzle.c +34 -5
  97. package/examples/sms/templates/racing.c +39 -2
  98. package/examples/sms/templates/shmup.c +41 -2
  99. package/examples/sms/templates/shmup_2p.c +24 -1
  100. package/examples/sms/templates/sports.c +47 -4
  101. package/examples/snes/main.asm +108 -17
  102. package/examples/snes/templates/c-hello-data.asm +23 -0
  103. package/examples/snes/templates/c-hello.c +18 -1
  104. package/examples/snes/templates/default.c +50 -28
  105. package/examples/snes/templates/hello_sprite-data.asm +23 -0
  106. package/examples/snes/templates/hello_sprite.c +17 -1
  107. package/examples/snes/templates/music_demo-data.asm +23 -0
  108. package/examples/snes/templates/music_demo.c +22 -4
  109. package/examples/snes/templates/platformer-data.asm +22 -0
  110. package/examples/snes/templates/platformer.c +20 -2
  111. package/examples/snes/templates/puzzle-data.asm +22 -0
  112. package/examples/snes/templates/puzzle.c +21 -2
  113. package/examples/snes/templates/racing-data.asm +22 -0
  114. package/examples/snes/templates/racing.c +17 -1
  115. package/examples/snes/templates/shmup-data.asm +22 -0
  116. package/examples/snes/templates/shmup.c +20 -1
  117. package/examples/snes/templates/sports-data.asm +22 -0
  118. package/examples/snes/templates/sports.c +16 -1
  119. package/package.json +1 -1
  120. package/src/cheats/gamegenie.js +0 -1
  121. package/src/cli/smoke.js +1 -3
  122. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  123. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  124. package/src/host/LibretroHost.js +191 -16
  125. package/src/host/callbacks.js +9 -1
  126. package/src/host/chafa-render.js +2 -0
  127. package/src/host/dsp-state.js +2 -2
  128. package/src/host/gpgx-state.js +4 -0
  129. package/src/host/types.js +15 -8
  130. package/src/http/routes.js +1 -1
  131. package/src/http/tool-registry.js +26 -1
  132. package/src/mcp/server.js +1 -1
  133. package/src/mcp/state.js +36 -0
  134. package/src/mcp/tools/address-to-symbol.js +0 -1
  135. package/src/mcp/tools/art-loaders.js +1 -1
  136. package/src/mcp/tools/cart-parts.js +75 -4
  137. package/src/mcp/tools/classify-region.js +1 -1
  138. package/src/mcp/tools/diff-roms.js +1 -1
  139. package/src/mcp/tools/disasm-rebuild.js +507 -0
  140. package/src/mcp/tools/disasm.js +97 -9
  141. package/src/mcp/tools/find-references.js +1 -2
  142. package/src/mcp/tools/font-map.js +1 -1
  143. package/src/mcp/tools/frame.js +168 -3
  144. package/src/mcp/tools/index.js +0 -49
  145. package/src/mcp/tools/input-layout.js +0 -1
  146. package/src/mcp/tools/input.js +33 -3
  147. package/src/mcp/tools/lifecycle.js +18 -4
  148. package/src/mcp/tools/lospec.js +0 -19
  149. package/src/mcp/tools/platform-docs.js +1 -1
  150. package/src/mcp/tools/platform-tools.js +4 -4
  151. package/src/mcp/tools/project.js +54 -11
  152. package/src/mcp/tools/reinject.js +0 -1
  153. package/src/mcp/tools/rom-id.js +2 -2
  154. package/src/mcp/tools/snippets.js +2 -2
  155. package/src/mcp/tools/sprite-pipeline.js +1 -2
  156. package/src/mcp/tools/state.js +201 -14
  157. package/src/mcp/tools/tile-inspect.js +1 -1
  158. package/src/mcp/tools/toolchain.js +105 -12
  159. package/src/mcp/tools/watch-memory.js +137 -16
  160. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
  161. package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
  162. package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
  163. package/src/platforms/c64/MENTAL_MODEL.md +45 -1
  164. package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
  165. package/src/platforms/c64/d64.js +280 -0
  166. package/src/platforms/c64/sid.js +0 -2
  167. package/src/platforms/common/metasprite-adapters.js +1 -1
  168. package/src/platforms/common/metasprite-codegen.js +3 -3
  169. package/src/platforms/common/registers.js +5 -3
  170. package/src/platforms/gb/MENTAL_MODEL.md +10 -0
  171. package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
  172. package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
  173. package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
  174. package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
  175. package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
  176. package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
  177. package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
  178. package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
  179. package/src/platforms/msx/MENTAL_MODEL.md +10 -6
  180. package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +63 -2
  182. package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
  183. package/src/platforms/nes/image-to-tilemap.js +3 -0
  184. package/src/platforms/nes/lib/asm/famitone2.s +5 -1
  185. package/src/platforms/pce/MENTAL_MODEL.md +9 -4
  186. package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
  187. package/src/platforms/pce/lib/c/pce_video.c +1 -1
  188. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  189. package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
  190. package/src/platforms/snes/brr.js +0 -2
  191. package/src/playtest/playtest.js +0 -7
  192. package/src/rom-id/identifier.js +15 -0
  193. package/src/toolchains/asar/asar.js +0 -9
  194. package/src/toolchains/assemble-snippet.js +30 -12
  195. package/src/toolchains/cc65/ines.js +145 -0
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
  197. package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
  198. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
  199. package/src/toolchains/common/reassemble.js +10 -3
  200. package/src/toolchains/common/sdk-cache.js +1 -1
  201. package/src/toolchains/genesis-c/genesis-c.js +5 -3
  202. package/src/toolchains/index.js +27 -3
  203. package/src/toolchains/parse-errors.js +78 -1
  204. package/src/toolchains/sdcc/preflight-lint.js +5 -1
  205. package/src/toolchains/sdcc/sdcc.js +1 -1
  206. package/src/toolchains/sjasm/sjasm.js +1 -1
  207. package/src/toolchains/snes-c/snes-c.js +2 -2
  208. package/src/toolchains/vasm68k/vasm68k.js +2 -4
  209. package/src/toolchains/wladx/wladx.js +1 -1
package/AGENTS.md CHANGED
@@ -54,11 +54,11 @@ Skip playtest only when there's clearly no human in the loop: CI runs, automated
54
54
  - `input` — drive controllers, look up hardware bit layouts. `navigate` walks menus by advancing on SCREEN CHANGE (not fixed frames) and reports whether each press was consumed — the fast, reliable way to script a UI.
55
55
  - `state` — savestates and forensic state inspection (`state({op:'save'})`, `state({op:'load'})`, `state({op:'export'})` a slot to disk without touching the live host, `state({op:'list'})`, `state({op:'dump'})`)
56
56
  - `memory` — read/write VRAM/OAM/CGRAM/ARAM and other regions (all 14 platforms). `memory({op:'read'})` takes `offsets:[…]` to batch scattered reads in one call. **`memory({op:'search'})`/`memory({op:'searchNext'})`** = the Cheat-Engine value-search loop ("find the address of X, narrow as X changes"). **`memory({op:'readCart'})`** reads the loaded cart image to confirm a patch is live. **`memory({op:'classify'})`** says whether bytes look like ASCII/code/tile-data (kills the "found table that's really a string" trap). `memory({op:'snapshot'})` + `memory({op:'diff'})` answer "which bytes changed across this event?" (diff defaults to a clustered summary with stride detection); `state({op:'diff'})` is the coarse whole-machine version.
57
- - `debug` — `sprites({op:'inspect'})`, `palette({source:'live'})`, `cpu({op:'read'})` (all 14), `audioDebug({op:'inspect'})` (the 12 systems with a sound chip — all but Atari 2600/7800; pass `frames:N` to TRACE a per-channel note-timeline for headless melody asserts), `background({view:'renderState'})`, `breakpoint({on:'write'})` (write watchpoint, all 14), **`watch({on:'dma', precision:'sampled'})`** (Genesis: which ROM offset a VRAM graphic was DMA'd from), **`disasm({target:'bytes'|'rom'|'references'|'project'})`** (ALL 14 — native binutils objdump per CPU, incl. GBA ARM7/Thumb; the byte-exact `disasm({target:'project'})` reassembles through native as/ld/objcopy), `symbols({op})` lookup, `background({view:'rendered'})`, plus **`cheats({op})`** (`cheats({op:'lookup'})` = a free labeled RAM/code map for known ROMs, `cheats({op:'search'})` to fuzzy-find a game by name, `cheats({op:'apply'})`/`cheats({op:'clear'})` non-destructively, `cheats({op:'make'})` to create codes)
57
+ - `debug` — **`frame({op:'verify'})`** (NO-VISION render-health: one call answers "is the game actually rendering / alive?" on all 14 — fuses a framebuffer pixel scan with the per-platform render-enable/NMI decode; `{verified:true|false|null, issues[], pixels, render}`, frame-0-guarded so it never cries wolf on boot), `sprites({op:'inspect'})`, `palette({source:'live'})`, `cpu({op:'read'})` (all 14), `audioDebug({op:'inspect'})` (the 12 systems with a sound chip — all but Atari 2600/7800; pass `frames:N` to TRACE a per-channel note-timeline for headless melody asserts), `background({view:'renderState'})`, `breakpoint({on:'write'})` (write watchpoint, all 14), **`watch({on:'dma', precision:'sampled'})`** (Genesis: which ROM offset a VRAM graphic was DMA'd from), **`disasm({target:'bytes'|'rom'|'references'|'project'})`** (ALL 14 — native binutils objdump per CPU, incl. GBA ARM7/Thumb; the byte-exact `disasm({target:'project'})` reassembles through native as/ld/objcopy), `symbols({op})` lookup, `background({view:'rendered'})`, plus **`cheats({op})`** (`cheats({op:'lookup'})` = a free labeled RAM/code map for known ROMs, `cheats({op:'search'})` to fuzzy-find a game by name, `cheats({op:'apply'})`/`cheats({op:'clear'})` non-destructively, `cheats({op:'make'})` to create codes)
58
58
  - `assets` — convert PNGs to tiles (`encodeArt`/`importArt`), WAVs to BRR, identify ROMs (`cart({op:'identify'})`), plus the hacking toolkit (`romPatch({op})` — write/writeMany/spliceCHR/relocate/makeStored/findFree/findPointer/diff, `assembleSnippet`, `cart({op:'extract'})`, `cart({op:'wrap'})`)
59
59
  - `project` — starter snippets per platform
60
60
  - `show` — `playtest({op})`: `op:'open'` opens the live SDL window for a human, `op:'stop'` closes it, `op:'status'` reports liveness, `op:'framebuffer'` captures exactly what the human's window shows
61
- - `advanced` — `runUntil`, **`watch({on:'mem'|'range'|'pc'})`** (LOG-ALL tracing), **`breakpoint({on:'write'})`** (the EXACT instruction that wrote a byte, via a core watchpoint — fixes the frame-sampled-PC problem; `precision:'sampled'` is the cheap frame-PC version), **`breakpoint({on:'pc'})`** (execution breakpoint — freeze the CPU AT an instruction and read its registers), **`breakpoint({on:'read'})`** (the EXACT instruction that read a byte), **`frame({op:'stepInstruction'})`** (CPU single-step) — all 14 platforms; input recording
61
+ - `advanced` — `runUntil`, **`watch({on:'mem'|'range'|'pc'})`** (LOG-ALL tracing; `range`/`pc` take **`fromState`**/`fromStatePath` to trace from a restored savestate moment), **`breakpoint({on:'write'})`** (the EXACT instruction that wrote a byte, via a core watchpoint — fixes the frame-sampled-PC problem; `precision:'sampled'` is the cheap frame-PC version; on a `pressDuring` run pass **`abortIf:[{region,offset,label}]`** to stop early if the driven scenario derails — a guard byte changing returns `{aborted, abortedBy, before, after}` instead of burning all `maxFrames` on a meaningless `found:false`), **`breakpoint({on:'pc'})`** (execution breakpoint — freeze the CPU AT an instruction and read its registers), **`breakpoint({on:'read'})`** (the EXACT instruction that read a byte), **`frame({op:'stepInstruction'})`** (CPU single-step) — all 14 platforms; input recording
62
62
 
63
63
  **"Disassemble this NES ROM"** is now just: `disasm({target:'rom', path, startAddress, length})`. No discovery step.
64
64
 
@@ -124,7 +124,7 @@ Ergonomic exceptions:
124
124
  - **Small reads stay inline.** `memory({op:'read'})` of ≤4 KB returns hex inline with no path needed (peeking a few RAM/OAM/palette bytes is the common case). Only large reads require a path/inline.
125
125
  - **`build({output:'run'})` returns its screenshot inline by default** — its whole purpose is "build + run + show me." Pass `screenshotPath` only if your client can't display inline images.
126
126
 
127
- **On images specifically:** the `inline:true` image is only useful if YOUR client actually delivers inline images to you — some clients silently drop or down-convert image content. If you're not certain you can see them, **work from the structured data instead**: `sprites({op:'inspect'})` / `palette({source:'live'})` / `background({view:'renderState'})` always return their decoded JSON (sprite lists, palette entries, render flags) regardless of inline/path, and `frame({op:'screenshot', format:'ascii'})` gives a text render. The inline PNG is an opt-in luxury, not the primary signal.
127
+ **On images specifically:** the `inline:true` image is only useful if YOUR client actually delivers inline images to you — some clients silently drop or down-convert image content. If you're not certain you can see them, **work from the structured data instead**: start with **`frame({op:'verify'})`** — one call tells you `{verified:true|false|null}` whether the game is actually rendering (fuses a pixel scan with the render-enable registers), so you don't sit staring at a black frame wondering if it's broken or just blank. Then `sprites({op:'inspect'})` / `palette({source:'live'})` / `background({view:'renderState'})` always return their decoded JSON (sprite lists, palette entries, render flags) regardless of inline/path, and `frame({op:'screenshot', format:'ascii'})` gives a text render. The inline PNG is an opt-in luxury, not the primary signal.
128
128
 
129
129
  ## Trust hierarchy — where to find ground truth (R58 + R58b)
130
130
 
@@ -153,8 +153,10 @@ worry about ground truth:
153
153
  when you're scaffolding into a project dir.
154
154
 
155
155
  For most workflows, path A is all you need. Read MENTAL_MODEL.md +
156
- TROUBLESHOOTING.md when stuck. File a feedback round if the bundled
157
- examples are wrong.
156
+ TROUBLESHOOTING.md when stuck. **When a tool call FAILS, read the error
157
+ message and `issues[]` first — see "When a call fails" below; the error
158
+ usually names the fix.** File a feedback round if the bundled examples
159
+ are wrong.
158
160
 
159
161
  ### Path B — Debug when the bundled code disagrees with behavior
160
162
 
@@ -260,11 +262,11 @@ with explicit `sources` only when the files aren't on disk, e.g. generated in-co
260
262
 
261
263
  ## Supported platforms
262
264
 
263
- **13 tier-1 platforms** (build + run + screenshot + inspect + ≥5 genre scaffolds + sound + music + per-platform MENTAL_MODEL.md + TROUBLESHOOTING.md):
265
+ **14 tier-1 platforms** (build + run + screenshot + inspect + genre scaffolds + sound + music + per-platform MENTAL_MODEL.md + TROUBLESHOOTING.md):
264
266
 
265
- NES, Game Boy, Game Boy Color, SNES, Genesis, Game Boy Advance, SMS, Game Gear, C64, Atari 2600, Atari 7800, Lynx — all with `scaffold({op:'game', genre: shmup|platformer|puzzle|sports|racing})` available except Atari 2600 (asm-only — no genre scaffolds). The `platformer` scaffold side-scrolls (hardware camera + per-platform column streaming) on every one of these except NES, which is single-screen. Every tier-1 platform also ships a `music_demo` template using the platform's de-facto music engine: FamiTone2 (NES), hUGEDriver (GB/GBC), SPC700 driver (SNES), XGM2 via SGDK (Genesis), maxmod + .xm soundbank (GBA), PSG trackers (SMS/GG), SID sequencer (C64), `lynx_snd_play` (Lynx), 2-voice TIA (Atari 2600/7800).
267
+ NES, Game Boy, Game Boy Color, SNES, Genesis, Game Boy Advance, SMS, Game Gear, C64, Atari 7800, Lynx, PC Engine, MSX — all with the full `scaffold({op:'game', genre: shmup|platformer|puzzle|sports|racing})` set. The Atari 2600 is also tier-1 but ships **4** of those genres (no `puzzle` the TIA has no tilemap to draw a match-3 board). The `platformer` scaffold side-scrolls (hardware camera + per-platform column streaming) on every tier-1 platform except NES and the Atari 2600, which are single-screen (neither has hardware background scroll). Every tier-1 platform also ships a music demo using the platform's de-facto music engine — `music_demo` for most: FamiTone2 (NES), hUGEDriver (GB/GBC), SPC700 driver (SNES), XGM2 via SGDK (Genesis), maxmod + .xm soundbank (GBA), PSG trackers (SMS/GG), SID sequencer (C64), `lynx_snd_play` (Lynx), 2-voice TIA (Atari 2600/7800); PC Engine and MSX ship theirs as `music_sfx` (HuC6280 PSG; AY-3-8910 PSG). PC Engine and MSX additionally ship a hardware helper library plus `sprite_move` / `catch_game` example projects alongside the genre scaffolds.
266
268
 
267
- **Bring-up only** (build pipeline works, single `default` template, no genre scaffolds or sound/music wrappers yet): MSX, ColecoVision. Both use SDCC z80 same as SMS/GG — the genre scaffolds are queued.
269
+ **Bring-up only** (build pipeline works, single `default` template, no genre scaffolds or sound/music wrappers yet): ColecoVision. Uses SDCC z80 same as SMS/GG/MSX — the genre scaffolds are queued.
268
270
 
269
271
  **Delisted** (toolchain works but core-side issue blocks the run loop): Atari 5200 (atari800 BIOS-load path), ZX Spectrum (fuse tape-load path).
270
272
 
@@ -291,8 +293,10 @@ Different platforms have different levels of MCP-exposed debugging — different
291
293
  > to trip before the frame cap on the slow ~1MHz cores too. Pass `maxInstructions`
292
294
  > to override the budget, `presetMemory`/`stopAtPC` for codecs that read RAM globals
293
295
  > or need a mid-routine halt),
294
- > **`watch({on:'range'})`** (log EVERY read/write hitting an address range — discovery),
295
- > **`watch({on:'pc'})`** (coverage trace distinct PCs executed in a window),
296
+ > **`watch({on:'range'})`** (log EVERY read/write hitting an address range — discovery;
297
+ > pass **`fromState`**/`fromStatePath` to restore a savestate FIRST so the trace runs from a
298
+ > known, repeatable moment — jump to the boss, then see what writes HP),
299
+ > **`watch({on:'pc'})`** (coverage trace — distinct PCs executed in a window; also takes `fromState`),
296
300
  > **the RE-INJECT trio** (put an edited asset BACK, all 14): **`romPatch({op:'findPointer'})`**
297
301
  > (find every pointer to a ROM offset — Genesis 32-bit BE, SNES LoROM/HiROM, GBA
298
302
  > 0x08000000+offset incl. literal pools, banked 8-bit 16-bit-LE aliases),
@@ -321,7 +325,7 @@ Different platforms have different levels of MCP-exposed debugging — different
321
325
  - **Toolchains:** default is **C** via SDCC's sm83 port (same SDCC that powers SMS/GG/MSX/Coleco). For hand-tuned asm, pass `language:"asm"` to route through RGBDS. The C path uses `__sfr __at 0xFFNN` to bind GB I/O regs; helper headers under `src/platforms/gb/lib/c/gb_hardware.h` define LCDC/STAT/SCY/SCX/LY/BGP/OBP0/OBP1/etc. for both DMG and CGB. The SDCC 4.4.0 codegen quirk (`for (;;) { switch + write to __sfr }` crashes the register allocator) applies — use `do { ... } while (1)` and table-lookup writes instead.
322
326
  - **Atari 2600** (stella2014 patched): `palette({source:'live'})` (NTSC 128-color palette PNG; current background luma+hue extracted from TIA snapshot), `sprites({op:'inspect'})` (no OAM — returns the 5 graphics objects state P0/P1/M0/M1/Ball + a current-scanline PNG showing TIA composition), `cpu({op:'read'})` (6502 — A/X/Y/P/SP/PC from the M6502 internal regs), `background({view:'renderState'})` (decodes the 32-byte TIA snapshot into playfield/sprite/colors), `memory({op:'read'})` regions for `system_ram` (128 bytes of RIOT RAM), `a26_tia_regs` (32-byte TIA snapshot), `a26_cpu_regs` (7-byte 6502 snapshot). `disasm({target:'rom'})` + `disasm({target:'references'})` anchor to the top of the bank ($F000-$FFFF) with vector-table labels (NMI/RESET/IRQ at $FFFA).
323
327
  - **Atari 7800** (prosystem patched): `palette({source:'live'})` (256-color master PNG; MARIA palette block at $20-$3F decoded into 8 palettes × 3 colors + backdrop), `sprites({op:'inspect'})` (no OAM — returns the MARIA control regs + the DPP display-list-list pointer for the agent to walk), `cpu({op:'read'})` (6502 — A/X/Y/P/SP/PC from prosystem's sally globals), `background({view:'renderState'})` (MARIA CTRL bits + DPP + CHARBASE + dlistPtr), `memory({op:'read'})` regions for `system_ram` (the entire 64KB 6502 address space — MARIA regs, RAM, ROM all visible) + `a78_cpu_regs`. `disasm({target:'rom'})` + `disasm({target:'references'})` default to the top 16KB ($C000-$FFFF) where the reset vector lands.
324
- - **Commodore 64** (vice patched): `palette({source:'live'})` (the 16-color hardware-fixed palette PNG + current border/background/extra-bg indices decoded from VIC-II regs), `sprites({op:'inspect'})` (8 MOBs decoded into the generic shape with X/Y/color/multicolor/expand-X/expand-Y/priority + the screen-RAM sprite-data pointers at $07F8 so the agent can locate sprite pixel blocks), `cpu({op:'read'})` (6510 — A/X/Y/P/SP/PC from a `#define`-aliased live register file + the I/O port at $0001 decoded into LORAM/HIRAM/CHAREN), `audioDebug({op:'inspect', chip:'sid'})` (6581/8580 — 3 voices {waveform, freq→note, pulse-width, ADSR} + filter cutoff/resonance/mode), `background({view:'renderState'})` (VIC-II regs decoded into mode/scroll/colors/sprites, VIC bank from CIA2 $DD00, absolute screen + char base addresses), `memory({op:'read'})` regions for `system_ram` (64 KB RAM), `c64_color_ram` (1 KB), `c64_vic_regs` (64 B), `c64_sid_regs` (29 B via sid_peek), `c64_cia1_regs`/`c64_cia2_regs` (16 B each from `c_cia[]`), `c64_cpu_regs` (7 B). `disasm({target:'rom'})` + `disasm({target:'references'})` accept `.prg` files (2-byte load-address header) and the C64 register annotation table for VIC-II / SID / CIA registers. Starter snippets cover vic_init / sprite_table / sid_play / read_joystick / basic_stub.
328
+ - **Commodore 64** (vice patched): `palette({source:'live'})` (the 16-color hardware-fixed palette PNG + current border/background/extra-bg indices decoded from VIC-II regs), `sprites({op:'inspect'})` (8 MOBs decoded into the generic shape with X/Y/color/multicolor/expand-X/expand-Y/priority + the screen-RAM sprite-data pointers at $07F8 so the agent can locate sprite pixel blocks), `cpu({op:'read'})` (6510 — A/X/Y/P/SP/PC from a `#define`-aliased live register file + the I/O port at $0001 decoded into LORAM/HIRAM/CHAREN), `audioDebug({op:'inspect', chip:'sid'})` (6581/8580 — 3 voices {waveform, freq→note, pulse-width, ADSR} + filter cutoff/resonance/mode), `background({view:'renderState'})` (VIC-II regs decoded into mode/scroll/colors/sprites, VIC bank from CIA2 $DD00, absolute screen + char base addresses), `memory({op:'read'})` regions for `system_ram` (64 KB RAM), `c64_color_ram` (1 KB), `c64_vic_regs` (64 B), `c64_sid_regs` (29 B via sid_peek), `c64_cia1_regs`/`c64_cia2_regs` (16 B each from `c_cia[]`), `c64_cpu_regs` (7 B). `disasm({target:'rom'})` + `disasm({target:'references'})` accept `.prg` files (2-byte load-address header) and the C64 register annotation table for VIC-II / SID / CIA registers. Starter snippets cover vic_init / sprite_table / sid_play / read_joystick / basic_stub. **Disk images:** `loadMedia({platform:'c64', path:'game.d64'})` loads & autostarts real `.d64`/`.t64`/`.tap`/`.crt`/`.g64` games (drive 8, warp autostart — give it a few hundred frames; `status.mediaKind` reports disk/tape/cartridge/program); `cart({op:'packDisk', prgPath})` wraps a built `.prg` into a distributable autostart `.d64` (the format the Commodore 64 Ultimate hardware + the homebrew scene load), and `cart({op:'extract', path:'x.d64'})` lists/pulls its files. **Disk SAVES** (the C64 save medium is the floppy, not battery SRAM): a game's OWN KERNAL `SAVE` writes into the live disk (true-drive GCR write-back), so just run the game, let it save, then `state({op:'exportDisk', path})` to capture a `.d64` that includes the saved file (re-loadable to resume). `state({op:'importDisk', path})` pushes a `.d64` back into the running drive and `state({op:'putDiskFile', path, name})` injects one PRG file — for injecting a save made elsewhere. (On-disk filenames are PETSCII; the .d64 reader decodes them.)
325
329
  - **Game Boy Advance** (mgba patched): `sprites({op:'inspect'})` (128 OAM sprites → generic shape with shape/size, 9-bit signed X, affine/hidden, tile/palette/priority), `palette({source:'live'})` (256 BG + 256 OBJ 15-bit BGR555, `area:'bg'|'sprite'`), `cpu({op:'read'})` (ARM7TDMI — 16 gprs r0-r15 + cpsr/spsr + mode + ARM/THUMB, plus `execPc` adjusted for pipeline prefetch), `audioDebug({op:'inspect', chip:'gba'})` (4 DMG PSG channels + 2 Direct Sound DMA FIFOs, master/bias), `background({view:'renderState'})` (DISPCNT bg-mode + per-BG enable/priority/char-base/map-base/color-mode, forced-blank, OBJ enable), `memory({op:'read'})` regions for `gba_cpu_regs`, `gba_io_regs` (the IO page — video AND audio regs), `gba_palette`, `gba_oam`, plus system_ram/video_ram/save_ram. `disasm({target:'rom'})` + `disasm({target:'references'})` + `disasm({target:'project'})` run through the native binutils `arm-none-eabi-objdump` (WASM) — ARM by default, `thumb:true` for Thumb code; the byte-exact project reassembles through `arm-none-eabi-as`/`ld`/`objcopy`. (Note: GBA C compiles mostly to Thumb reached via an ARM crt0 stub, so an ARM-mode disasm of a full ROM decodes the Thumb spans as `.byte` — still byte-exact, just less readable until ARM/Thumb mode-tracking lands.)
326
330
  - **Atari Lynx** (handy patched): `palette({source:'live'})` (16-entry 12-bit Mikey palette → RGB), `cpu({op:'read'})` (65C02 — A/X/Y/P/SP/PC + flags), `audioDebug({op:'inspect', chip:'mikey'})` (4 channels — volume, timer→freq→note, 12-bit LFSR state), `background({view:'renderState'})` (DISPCTL DMA-enable/flip/color-mode + display base address), `memory({op:'read'})` regions for `lynx_cpu_regs`, `lynx_hw_regs` (the $FC00-$FDFF Suzy+Mikey window — sprite engine regs, LCD control, audio, palette), plus system_ram. **`sprites({op:'inspect'})` is a special case:** the Lynx has NO fixed OAM — sprites are SCB (Sprite Control Block) linked lists in RAM walked by Suzy, so `sprites({op:'inspect'})` returns the SCB list head (SCBNEXT $FC10/$FC11) and instructions to walk the chain over system_ram rather than a sprite table.
327
331
  - **MSX, ColecoVision**: standard system_ram + save_ram + video_ram. Deeper introspection not yet added — extend by patching their cores following the snes9x/gpgx/fceumm/vice pattern (see scripts/patches/).
@@ -395,9 +399,12 @@ When `build({output:'run'})` is too coarse, the long-form workflow:
395
399
  5. `input({op:'set'})` / `input({op:'press'})` / `input({op:'sequence'})` to drive the game
396
400
  6. `state({op:'save'}, "checkpoint")` / `state({op:'load'}, "checkpoint")` for try/undo
397
401
 
398
- ## Build errors
402
+ ## When a call fails: READ THE ERROR FIRST
399
403
 
400
- Every build tool returns `issues: [{file, line, col, severity, message, stage}, ...]`. Use that array, not the raw `log`. If `issues` is empty but `ok: false`, fall back to `log`.
404
+ romdev errors are written FOR you they name what went wrong AND how to recover. Read the message (and `issues[]`) before guessing, screenshotting, or retrying blindly. Two shapes:
405
+
406
+ - **Build/compile failures** return `issues: [{file, line, col, severity, message, stage}, ...]` — the structured error list. Use that array, NOT the raw `log`; it almost always names the exact line. Fall back to `log` only if `issues` is empty but `ok: false`. `issues[]` is RANKED most-dangerous first (**critical → error → warning → info**), so read it top-down: an entry flagged `critical: true` (e.g. a `WILL HANG:` `uint8`-loop-bound trap) is a latent crash even on a build that otherwise succeeded — fix those FIRST, never skip them as "just a warning". Link errors carry no `line` but include a `hint` naming the missing symbol + how to resolve it.
407
+ - **Tool/runtime errors** (thrown) carry the recovery step in the message itself. Examples: a "No ROM loaded" error after a session reconnect echoes the EXACT `loadMedia({...})` call to restore your state; a rejected `loadMedia` names the likely cause (wrong platform / truncated / unsupported mapper) and points you at `cart({op:'identify'})`; an `input({op:'set'})` with a typo'd button returns `ignoredButtons[]` so you see it pressed nothing. Don't discard these — they're the fix.
401
408
 
402
409
  **Crash isolation (R12).** Every WASM toolchain call runs in a child worker process. If a tool aborts (`_abort()`, SIGSEGV, OOM), only the worker dies — the MCP server keeps running, all other agent sessions are unaffected, tool registration + save states + playtest windows survive. The build response surfaces as `{ ok: false, stage: "crash", log: "[crash] worker exited unexpectedly — signal=… code=…", crash: { exitCode, signal } }`. Treat `stage: "crash"` as "the toolchain blew up — log the args + source somewhere durable so it can be triaged; you can keep iterating in this session without reconnecting".
403
410
 
@@ -428,6 +435,12 @@ romPatch({op:'diff', platform, a: original, b: patched }) // 6. verify the pa
428
435
  loadMedia({ platform, path: patched }) → frame({op:'screenshot'}) // 7. run it
429
436
  ```
430
437
 
438
+ **Driving input through a watched run.** A `watch`/`breakpoint` with NO
439
+ `pressDuring` INHERITS whatever `input({op:'set'})` last held — same as
440
+ `frame({op:'step'})`. But if you pass `pressDuring`, that schedule OWNS the pad
441
+ for the whole run and a prior `input({op:'set'})` is ignored. So to hold a button
442
+ *through* a watched window, put it in `pressDuring` — not a preceding `set`.
443
+
431
444
  **Finding which CODE wrote a byte.** Static disasm reading is the slow part —
432
445
  multiple `cmp #$XX` instructions look identical. Don't guess. Two tools, in order
433
446
  of precision:
@@ -776,14 +789,17 @@ post-processing layered on the objdump output.
776
789
  `disasm({target:'rom'})` gives you one routine as text. `disasm({target:'project'})` turns an
777
790
  **entire ROM into a complete, re-buildable project in one call**, across **all 14
778
791
  systems** (NES, SNES, GB/GBC, SMS/GG, Genesis, **GBA**, C64, Atari 2600/7800,
779
- **Lynx** — 65C02, **PC Engine** — HuC6280, and **MSX** — Z80; always byte-exact).
792
+ **Lynx** — 65C02, **PC Engine** — HuC6280, and **MSX** — Z80; byte-exact on 13,
793
+ PC Engine the one current exception — see caveats).
780
794
  Each region disassembles through the CPU's native objdump and reassembles through
781
795
  the matching native `as`/`ld`/`objcopy`, so the round-trip is guaranteed byte-for-byte:
782
796
 
783
797
  ```js
784
798
  disasm({ target:'project', path: "game.nes", outputDir: "./game-disasm" })
785
799
  // → { ok, platform, regions:[{file, startAddress, roundTripOk, readablePercent}],
786
- // roundTrip:{ allByteExact, failed:[] }, readablePercentAvg }
800
+ // roundTrip:{ allByteExact, failed:[] }, readablePercentAvg,
801
+ // rebuild:{ blobs:[{file,bytes}], buildCall:{...}|null, verifiable, buildDoc:"BUILD.md", notes } }
802
+ // Writes the .asm regions + chr.bin/header blobs + BUILD.md + rebuild.json to outputDir.
787
803
  ```
788
804
 
789
805
  It splits the ROM into regions (per-16KB bank for banked NES, per-32KB bank for
@@ -796,6 +812,38 @@ files ALWAYS rebuild to the original bytes (`roundTrip.allByteExact`). The
796
812
  vs. data. Each `.asm` carries a provenance + round-trip header and is ready to
797
813
  edit and rebuild with the platform's native toolchain.
798
814
 
815
+ **It also writes the REBUILD GLUE** so the project is turnkey, not just byte-exact
816
+ region files. Alongside the `.asm` files you get: any non-code DATA blobs the
817
+ rebuild needs (NES CHR-ROM → `chr.bin`; the stripped Genesis/GBA/Lynx/MSX
818
+ cartridge header → `*.bin`), a **`BUILD.md`** with the exact rebuild steps, and —
819
+ where a one-call rebuild exists — a **`rebuild.json`** holding the precise
820
+ `build({...})` args (absolute paths). The response carries the same under
821
+ `rebuild: { blobs, buildCall, verifiable, buildDoc, notes }`. So the RE loop is:
822
+ `disasm({target:'project'})` → edit a `.asm` → rebuild → `diffRoms` to confirm.
823
+
824
+ Two rebuild tiers (honest — the disasm emits each CPU's native-reassembler
825
+ syntax, which only some platforms' `build()` toolchains can consume):
826
+ - **One-call `build()` rebuild, byte-identical** — **NES, C64, Atari 7800, Lynx**.
827
+ Feed `rebuild.json` straight to `build`. (NES uses the new `inesHeader` option
828
+ — see below. Lynx: build() yields the headerless image; prepend the shipped
829
+ `lnx_header.bin` for the full `.lnx`.)
830
+ - **Native-recipe rebuild (`buildCall:null`), byte-identical, steps in `BUILD.md`**
831
+ — **SMS, GG, MSX, GB, GBC, Genesis, GBA, Atari 2600**. Their `build()`
832
+ toolchains (SDCC/RGBDS/asar/dasm/vasm) can't reassemble the disasm's ca65/GNU-as
833
+ syntax, so `BUILD.md` gives the proven native `as`/`ld`/`objcopy` chain.
834
+ - **PC Engine** is the one not-yet-byte-exact case (the region trims real padding /
835
+ doesn't strip a copier header) — `BUILD.md` says so.
836
+
837
+ **Rebuilding a commercial NES (NROM CHR-ROM) game — `build({inesHeader})`:** the
838
+ most common NES RE rebuild. `build({output:'rom', platform:'nes', inesHeader:{
839
+ prgBanks, chrBanks, mapper, mirroring}, sourcesPaths:{...the PRG...},
840
+ binaryIncludePaths:{ "chr.bin":... }})` auto-emits the 16-byte iNES header + the
841
+ CHARS segment wiring + the flat NROM `.cfg` — no hand-derived header bytes, no
842
+ glue `.s`/`.cfg`. (`disasm({target:'project'})` puts exactly this call in
843
+ `rebuild.json`.) For homebrew C that ships fixed tile art, `linkerConfig:"chr-rom"`
844
+ is the segment-split equivalent. See the NES MENTAL_MODEL.md "Rebuilding a CHR-ROM
845
+ NROM image" section.
846
+
799
847
  Reassembler per CPU family (all bundled WASM, no installs): **cc65** ca65/ld65
800
848
  for 6502 + 65816; native binutils **`as`/`ld`/`objcopy`** for the GNU CPUs —
801
849
  `m68k-elf` (Genesis), `arm-none-eabi` (GBA), and one `z80-elf` for both Z80
@@ -814,6 +862,10 @@ Caveats worth knowing up front:
814
862
  always correct. (The 192-byte GBA header is emitted as a clean data region.)
815
863
  - Banked-NES is the strongest case — per-bank regions come back ~100%
816
864
  instructions. GB/GBC, SMS/GG, C64, and Atari are also near-100%.
865
+ - **PC Engine** is the one platform that does NOT round-trip byte-exact yet: the
866
+ region trims real trailing $FF padding and doesn't strip a 512-byte copier
867
+ header, so the emitted region is a lossy view of the `.pce`. `BUILD.md` flags
868
+ this; a `planRegions` fix is the follow-up.
817
869
  - Platform is sniffed from the file extension; pass `platform:` to override.
818
870
 
819
871
  ## CHR/tile tools — file vs emulator source
@@ -881,6 +933,13 @@ OAM format: bytes per sprite are `[y, tileIndex, attributes, x]`.
881
933
  `state({op:'save'}, name)` / `state({op:'load'}, name)` slots are **in-memory** and discarded on `host({op:'shutdown'})` or new media. To persist a state across sessions:
882
934
  - `state({op:'save', path})` writes the CURRENT live host to a file directly.
883
935
  - `state({op:'export', fromSlot, path})` copies an EXISTING in-memory slot (e.g. one the human saved with a playtest emulator-hotkey — it appears in `state({op:'list'})`) to a file **without disturbing the live host** (no pause/resume needed). Reload either with `state({op:'load', path})`.
936
+ - **`path` resolution:** a RELATIVE `path` resolves against the **loaded ROM's directory** (so `path:"states/start.state"` lands next to your ROM), an absolute path is used as-is; the result echoes `resolvedPath` when they differ. (It is NOT resolved against the server's CWD.)
937
+
938
+ **SRAM (the cartridge BATTERY SAVE FILE — distinct from a savestate).** A savestate is the whole machine; SRAM is just the bytes a real cart keeps on its battery (the in-game save). romdev exposes it three ways, all on existing tools:
939
+ - **Live read/write:** `memory({op:'read'/'write', region:'save_ram'})` — poke/inspect the running game's save RAM.
940
+ - **Persist the `.sav`:** `state({op:'exportSram', path})` writes the save file; `state({op:'importSram', path})` loads one back (edit a save offline, or inject one a player made elsewhere). Same relative-path-resolves-to-ROM-dir rule.
941
+ - **Presence:** `cart({op:'identify'})` returns `saveRam:{hasBattery, bytes}` so you know whether a save even exists before reaching for it.
942
+ - **No battery save?** Many carts use passwords or no save (and Atari 2600/7800 + Lynx never had cartridge saves). `save_ram` is empty there and the tools say so plainly — use a full-machine savestate (`state({op:'save'/'load'})`) instead. **C64 is different:** its save medium is the floppy disk, not battery SRAM — use the disk ops (`state({op:'exportDisk'/'importDisk'/'putDiskFile'})`, see the C64 platform notes), not save_ram.
884
943
 
885
944
  `state({op:'load'})` removes any active cheats (a save-state blob doesn't carry frontend cheat state) and reports `cheatsCleared`. `host({op:'reset'})` resets the frame counter + core state (and clears cheats) but keeps the loaded ROM.
886
945
 
@@ -892,7 +951,7 @@ Three shapes, pick the one that matches what you're doing:
892
951
 
893
952
  - **`scaffold({op:'project', ..., withSnippets: true})`** — same as above, **plus** drops every vetted starter snippet for the platform alongside main.c. Use when you want "main.c + every helper file ready to edit" in one shot, without picking a genre. Snippets that overlap with the template's runtime are skipped (no double-writes). Response includes `snippetsCopied: string[]`.
894
953
 
895
- - **`scaffold({op:'game', platform, genre})`** — genre-shaped scaffold (`shmup` / `platformer` / `puzzle` / `sports` / `racing`). Higher-level than `scaffold({op:'project'})` — picks the right template + runtime + crt0 + linker config for the genre. Available on **NES, GB, GBC, SNES, Genesis, SMS, GG, C64, GBA, Lynx, Atari 7800**i.e. every platform that has genre templates. Availability is derived from the registered templates (not a hardcoded list), so the error message for an unsupported platform always names the current set; Atari 2600 (asm-only) + MSX + ColecoVision (bring-up only) have no genre scaffolds and are rejected. Ships a complete working ROM with state machine + sprite allocation + sound wired — fill in gameplay logic on top. **Want a side-scroller? Use `genre:"platformer"`** — and on every platform EXCEPT NES the scaffold already side-scrolls: a hardware camera follows the player (SCX/$D016/R8/BG?HOFS/REG_BG?HOFS/bgSetScroll depending on platform), with software tile-column streaming where the world is wider than one nametable/plane. NES is still single-screen (platforms drawn as sprites); to make it scroll, draw platforms into the background nametables + `ppu_scroll(camX,0)` (it flips the PPUCTRL nametable-select bit past 256 px) + stream columns past 512 px. Each platformer's `describe` text gives the per-platform specifics; the scroll-register details live in the platform's MENTAL_MODEL.md "Horizontal scrolling" section.
954
+ - **`scaffold({op:'game', platform, genre})`** — genre-shaped scaffold (`shmup` / `platformer` / `puzzle` / `sports` / `racing`). Higher-level than `scaffold({op:'project'})` — picks the right template + runtime + crt0 + linker config for the genre. Available on **all 14 tier-1 platforms** (NES, GB, GBC, SNES, Genesis, SMS, GG, C64, GBA, Lynx, Atari 7800, PC Engine, MSX full 5 each; Atari 2600 — 4, no `puzzle` since the TIA has no tilemap for a match-3 board). Availability is derived from the registered templates (not a hardcoded list), so the error message for an unsupported (platform, genre) pair always names the current set; e.g. `atari2600` + `puzzle` is rejected and the error lists the genres it *does* have. ColecoVision (bring-up only) has no genre scaffolds and is rejected wholesale. Ships a complete working ROM with state machine + sprite allocation + sound wired — fill in gameplay logic on top. **Want a side-scroller? Use `genre:"platformer"`** — and on every platform EXCEPT NES and the Atari 2600 the scaffold already side-scrolls: a hardware camera follows the player (SCX/$D016/R8/BXR/BG?HOFS/REG_BG?HOFS/bgSetScroll depending on platform), with software tile-column streaming where the world is wider than one nametable/plane. NES and the Atari 2600 are single-screen (no hardware background scroll — platforms drawn as sprites/playfield); to make NES scroll, draw platforms into the background nametables + `ppu_scroll(camX,0)` (it flips the PPUCTRL nametable-select bit past 256 px) + stream columns past 512 px. Each platformer's `describe` text gives the per-platform specifics; the scroll-register details live in the platform's MENTAL_MODEL.md "Horizontal scrolling" section.
896
955
 
897
956
  Then iterate with `build({output:'run'})` against the source you read from `path/main.*`.
898
957
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,289 @@ All notable changes to `romdevtools`. Dates are release dates.
4
4
  (Published as `romdev-mcp` through 0.11.0; renamed to `romdevtools` in 0.13.0 —
5
5
  the `romdev-mcp` bin is kept as an alias.)
6
6
 
7
+ ## 0.22.0
8
+
9
+ **Transparency + correctness pass: every tool failure is actionable, dangerous
10
+ warnings are ranked first, and all 14 platforms' scaffolds build clean AND
11
+ render visible content.** The theme: a coding agent should never be left guessing
12
+ by an opaque error, never skip a crash-class warning buried in noise, and never
13
+ copy a scaffold that ships with warnings or a blank screen.
14
+
15
+ ### Changed — actionable error messages across all 14 platforms
16
+ Failures now name the fix, not just the symptom:
17
+ - **`build`/assemble:** compile errors carry `{file, line, message, stage}`; LINK
18
+ errors (which have no source line) now reach `issues[]` too, each with a `hint`
19
+ naming the missing symbol + how to resolve it — on ALL FOUR linkers (GNU ld for
20
+ Genesis/GBA, ld65 for NES/C64/Lynx/A2600/A7800/PCE, sdld for GB/GBC/SMS/GG/MSX,
21
+ wlalink for SNES). The crt0 (startup-stub) assembly path and `assembleSnippet`
22
+ now surface the first `file:line: message` instead of dumping a raw log.
23
+ - **`loadMedia`:** a refused ROM names the likely cause (wrong platform / truncated
24
+ / unsupported mapper) and points at `cart({op:'identify'})`.
25
+ - **Runtime/host:** `getHost`'s "No ROM loaded" echoes the EXACT `loadMedia` call
26
+ to recover with after a session eviction; unknown memory region lists the valid
27
+ names; "no save state named X" lists the existing slots; `host({op:'unload'})`
28
+ no longer claims success when nothing was loaded.
29
+
30
+ ### Changed — build `issues[]` ranks the dangerous warnings FIRST
31
+ A weak agent skips a lethal warning when it's buried among unused-variable noise.
32
+ `issues[]` is now ordered **critical → error → warning → info** (stable within a
33
+ rank) on every platform. The SDCC pre-flight lint marks the unconditional
34
+ `uint8`-loop-bound trap as `critical: true` (it always hangs) with a `WILL HANG:`
35
+ message; the conditional VRAM byte-copy stays a plain warning (it can't be proven
36
+ unsafe statically, so it must not cry wolf).
37
+
38
+ ### Fixed — `watch`/`breakpoint` inherit held input (the movement-analysis bug)
39
+ A `watch`/`breakpoint` run with NO `pressDuring` now inherits whatever
40
+ `input({op:'set'})` last held — exactly like `frame({op:'step'})`. Previously the
41
+ first frame reset the pad to neutral, silently dropping a held button. A
42
+ `pressDuring` schedule still OWNS the pad for the run (deterministic capture).
43
+ Documented on the `input`/`watch`/`breakpoint` schemas.
44
+
45
+ ### Fixed — all 130 scaffolds: zero warnings AND render visible content
46
+ Swept every `scaffold({op:'project'})` template on all 14 platforms:
47
+ - **Warnings 65 → 0** (was concentrated in GB/GBC/Genesis/GG/GBA/SMS), fixed at
48
+ the SOURCE so scaffolds model the right pattern: GB/GBC VRAM tile copies use the
49
+ runtime's pointer-walk `memcpy_vram` (the indexed `dst[i]=src[i]` form SDCC sm83
50
+ miscompiles into VRAM); Genesis builds pass `-Wno-main` (SGDK mandates
51
+ `int main(bool)`); GBA/GG/SMS narrowing + dead-branch fixes.
52
+ - **Blank/broken renders 31 → 0** (verified via `frame({op:'verify'})`): added a
53
+ patterned background to lone-sprite/text scaffolds, and fixed real bugs found
54
+ along the way — **NES FamiTone2's `$0300` RAM collided with the C runtime BSS**
55
+ (zeroed PPUCTRL, killed rendering; the driver's RAM was relocated to `$0700`);
56
+ the **SNES `sfx_init()`-before-`setScreenOn()`** forced-blank trap; a **C64 cc65
57
+ screen-fill-loop hang** (rewritten via `memset`) + sprite-data/`$0801` overlap;
58
+ the **Lynx double-buffer** stale-page trap.
59
+
60
+ ### Added — ESLint over romdev's own JavaScript
61
+ Flat config (`npm run lint`) catching real bugs (undefined refs, unused
62
+ imports/vars, dupe keys, self-assignment) over the monorepo's plain-JS ESM
63
+ sources; vendored SDK/wasm/build trees ignored. Cleaned 114 pre-existing findings.
64
+
65
+ ## 0.21.0
66
+
67
+ **NES CHR-ROM / iNES rebuild ergonomics + turnkey `disasm({target:'project'})`
68
+ across all platforms.** Addresses the v0.16.0 feedback: rebuilding a commercial
69
+ NROM game from its disassembly into a byte-identical `.nes` no longer needs
70
+ hand-written iNES header bytes, a CHR-ROM `.incbin` glue source, or a 3-region
71
+ linker `.cfg`.
72
+
73
+ ### Added — `build({inesHeader:{prgBanks, chrBanks, mapper, mirroring, battery?}})`
74
+ NES NROM-rebuild convenience. Auto-emits the 16-byte iNES HEADER segment, wires
75
+ the CHR-ROM blob (from `binaryIncludePaths`) into a CHARS segment, and
76
+ synthesizes the flat NROM linker `.cfg` (HEADER + PRG + CHARS). The agent
77
+ supplies only the PRG disassembly + the CHR blob — no glue `.s`/`.cfg`, no
78
+ hand-derived header bytes. PRG start/size derive from `prgBanks` (NROM-128 →
79
+ $C000, NROM-256 → $8000). Mutually exclusive with `linkerConfig`. Proven
80
+ byte-identical against `nestest.nes`.
81
+
82
+ ### Added — `linkerConfig:"chr-rom"` NES preset
83
+ Sibling of `chr-ram`/`chr-ram-runtime`, for homebrew C that ships FIXED tile art:
84
+ segment split + a CHARS segment in an 8 KB ROM2 bank + a companion crt0 with an
85
+ 8 KB-CHR-ROM iNES header. (For other bank configs, prefer `inesHeader`.)
86
+
87
+ ### Changed — `disasm({target:'project'})` now emits a TURNKEY, rebuildable project
88
+ Previously it wrote only byte-exact `.asm` region files. Now, per platform, it
89
+ also writes the "rebuild glue": data blobs (NES CHR-ROM, MSX/Genesis/GBA/Lynx
90
+ headers), a human/agent-readable `BUILD.md`, and — where a one-call rebuild
91
+ exists — a `rebuild.json` (the exact `build()` args, absolute paths). Feed
92
+ `rebuild.json` back to `build` and you get a byte-identical ROM.
93
+ - **One-call `build()` rebuild (byte-identical):** NES, C64, Atari 7800, Lynx
94
+ (Lynx: build() yields the headerless image + a shipped `lnx_header.bin` to
95
+ prepend).
96
+ - **Native-recipe (byte-identical, documented in BUILD.md):** SMS, GG, MSX, GB,
97
+ GBC, Genesis, GBA, Atari 2600 — the disasm emits each CPU's native-reassembler
98
+ syntax (ca65 for 6502/65816, GNU `as` for z80/sm83/m68k/arm), which those
99
+ platforms' `build()` toolchains (SDCC/RGBDS/asar/dasm/vasm) can't consume, so
100
+ BUILD.md gives the proven native chain.
101
+ - **Not yet byte-exact:** PC Engine (planRegions trims real trailing padding +
102
+ doesn't strip a copier header — BUILD.md says so).
103
+
104
+ ### Fixed — disasm round-trip bugs surfaced by the rebuild work
105
+ - `reassemble.js`'s data-only floor omitted `.org`, silently truncating any
106
+ region with a non-zero start address — so multi-bank GB/GBC ($4000 banks) and
107
+ MSX ($4010) didn't round-trip at all. The floor now mirrors the linked path's
108
+ origin handling.
109
+ - `dataRegionSource` emitted `$`-prefixed hex that GNU assemblers (ARM/m68k)
110
+ reject (`$2E` read as an undefined symbol); it's now CPU-family-aware (`0x` hex
111
+ for z80/sm83/m68k/arm, `$` for 6502/65816).
112
+
113
+ ### Added — NES MENTAL_MODEL.md "Rebuilding a CHR-ROM NROM image" section
114
+ The iNES header bytes decoded, CHR-ROM vs CHR-RAM, NROM-128/-256 mapping, and the
115
+ three rebuild paths (`inesHeader` / `chr-rom` preset / `disasm({target:'project'})`).
116
+
117
+ ## 0.20.0
118
+
119
+ **Genre-scaffold parity across all 14 platforms + the MSX cartridge-boot fix +
120
+ PCE rendering fix + a higher blank-screen bar.**
121
+
122
+ ### Fixed — MSX cartridges now actually boot (`scaffold` + recipe)
123
+ Every MSX program had been booting to the C-BIOS "No cartridge found" screen:
124
+ `retro_load_game` returned true but the cart never reached a slot. Root cause was
125
+ NOT the wasm core, the C-BIOS, or the libretro API (all verified working against a
126
+ commercial MSX `.rom` in the same host) — it was our **build**. `projectBuildRecipe`
127
+ had no MSX branch, so `msx_crt0.s` was compiled as an ordinary translation unit
128
+ *alongside* SDCC's stock CP/M-style crt0; the cartridge `"AB"` header at $4000
129
+ got dropped and the INIT entry pointed at junk. Fix: route `msx_crt0.s` through
130
+ the crt0 slot (`crt0File`, `codeLoc = 0x4010`), exactly like the SMS/GG recipe.
131
+ MSX is now full tier-1.
132
+
133
+ ### Fixed — PC Engine "bottom half is vertical stripes" on every game
134
+ `vdc_init` set `VDC_MWR` to `0x0010`, which per the HuC6280 VDC spec selects the
135
+ **64×32** virtual screen, not the 32×32 the comment claimed. The BAT-clear loops
136
+ walk stride-32, so only the top ~16 rows of the 64-wide map were cleared — the
137
+ bottom half rendered uninitialized VRAM as vertical stripes. Set `VDC_MWR` to
138
+ `0x0000` (true 32×32) so the clear covers the whole visible map.
139
+
140
+ ### Added — genre-scaffold parity (PC Engine, MSX, Atari 2600)
141
+ PC Engine and MSX gained the full 5 canonical genre scaffolds
142
+ (`shmup` / `platformer` / `puzzle` / `sports` / `racing`); Atari 2600 gained 4
143
+ (no `puzzle` — the TIA has no tilemap to draw a match-3 board). Previously these
144
+ three shipped only ~3 ad-hoc starters while the other 11 platforms had the full
145
+ set. All 14 new scaffolds are verified rendering live (`frame({op:'verify'})` →
146
+ `verified:true`, ≥3 distinct colors, dominant under the blank threshold). The MSX
147
+ and PCE `platformer` scaffolds side-scroll (MSX via SCREEN 2 name-table column
148
+ streaming; PCE via the VDC BXR register). `scaffold({op:'game'})` now works on
149
+ all 14 platforms; only the per-(platform,genre) gaps (e.g. `atari2600` + `puzzle`)
150
+ are rejected, with the error naming the genres that platform *does* have.
151
+
152
+ ### Changed — blank-screen detection bar raised to 92%
153
+ `frame({op:'verify'})`'s `nearlyBlank` threshold went from 99.5% to **92%**
154
+ dominant-color coverage — 88–92% of one flat color still reads as "blank" to a
155
+ human. A sweep re-tuned the genre scaffolds across every platform to clear the
156
+ higher bar (real backgrounds/HUD instead of a lone sprite on a flat field).
157
+
158
+ ### Fixed — `frame({op:'verify'})` now emits its judged frame to the livestream
159
+ The REST/skill tool path (`runTool`) was dropping the observer image/frame
160
+ sidebands that only the MCP middleware handled, so `verify` (and any tool that
161
+ emits a deferred frame) never reached the `/livestream` UI over plain HTTP.
162
+ `runTool` now mirrors the middleware: it strips the sidebands from the result and
163
+ fires the deferred `call_frame` event.
164
+
165
+ ## 0.19.0
166
+
167
+ **Two one-stop-shop features for agents — both fold into existing tools, both
168
+ work on all 14 platforms.**
169
+
170
+ ### Added — `frame({op:'verify'})`: "is the game actually rendering / alive?"
171
+ A one-call render-health check for agents debugging WITHOUT vision (the spiral
172
+ where a black frame might be broken *or* fine and you can't tell). Pass `frames`
173
+ to boot-then-check in one call. Fuses two independent signals: a platform-agnostic
174
+ pixel-content scan of the live framebuffer (distinctColors, dominant-color %) and
175
+ the per-platform render-ENABLE/NMI decode (reused from the rendering-context
176
+ decoder — covers all 14 platforms). Returns `{verified:true|false|null, issues[],
177
+ pixels, render}`:
178
+ - `verified:null` + `unsettled` before any frame is stepped (frame-0 guard — never
179
+ cries wolf on boot; step first).
180
+ - `issues[]` flags `blankScreen` / `nearlyBlank` / `renderDisabled`. `renderDisabled`
181
+ is ONLY raised when the registers say so (never on a platform we can't decode —
182
+ there the pixel check carries the verdict).
183
+ - Pass/fail with zero image tokens; for WHAT to fix, getPlatformDoc(mental_model).
184
+ - Verified across all 14 platforms (`frame-verify-allplatforms.test.js`): the
185
+ verdict is internally consistent everywhere, and it correctly flags genuinely
186
+ blank scaffolds as broken. Implements the locked `renderHealth` spec, folded
187
+ into `frame` rather than a new top-level tool.
188
+
189
+ ### Added — `watch({on:'range'/'pc', fromState|fromStatePath})`: trace from a moment
190
+ The range/PC tracers can now restore a savestate FIRST, so the log runs from a
191
+ known, repeatable point (jump to the boss fight, then see exactly what writes HP)
192
+ instead of from wherever the live session happens to be. `fromState` = an in-memory
193
+ slot (state({op:'save', name})); `fromStatePath` = a savestate file on disk
194
+ (relative paths resolve to the ROM dir). Deterministic — same state → identical
195
+ trace. Platform-agnostic (rides the existing all-14-platform range/PC watch).
196
+ Result echoes `restoredFrom`. Tests in `watch-fromstate.test.js`.
197
+
198
+ ## 0.18.1
199
+
200
+ **C64: a game's OWN in-game disk SAVE works — and always did.** 0.18.0 shipped
201
+ the disk ops with a "known limit" claiming a running game's KERNAL `SAVE` doesn't
202
+ persist in WASM. That was WRONG — the cause was a bug in romdev's `.d64`
203
+ *directory reader*, not the emulator: the C64 KERNAL stores filenames in high-bit
204
+ PETSCII (A–Z = 0xC1–0xDA), and `readDirectory` dropped those bytes, so an
205
+ emulator-written `SCORE` parsed as an empty name and looked missing. VICE was
206
+ committing the save to the live disk the whole time (true-drive GCR write-back).
207
+
208
+ ### Fixed
209
+ - **`readDirectory`/`extractFile` decode high-bit PETSCII filenames** (and
210
+ lower-as-upper) — so files a game saves (KERNAL SAVE) are visible, not just
211
+ files romdev's own `prgToD64` wrote (which used plain ASCII). Verified end to
212
+ end: a cc65 program does `cbm_save("SCORE",8,…)`, and `exportDisk` reads the
213
+ `SCORE` file back with the right bytes. Locked by a regression test in
214
+ `d64.test.js` + a transparent-save test in `c64-disk-save.test.js`.
215
+ - The 0.18.0 "Known limit" is **retracted**: run the game, let it save, then
216
+ `state({op:'exportDisk', path})` captures a `.d64` that includes the save.
217
+ Docs + the `save_ram` n/a message corrected. (Confirmed against the native
218
+ vice-libretro core in RetroDECK, which produces a byte-identical saved disk.)
219
+
220
+ ## 0.18.0
221
+
222
+ **C64 disk SAVES — the floppy is the C64 save medium, and romdev now reads/writes
223
+ it on the live disk.** 0.17.0 added loading/running/distributing `.d64` disks;
224
+ this adds save/restore, the C64 analogue of SRAM `exportSram`/`importSram` (the
225
+ C64 has no battery RAM — games save by writing files to the floppy, so the disk
226
+ IS the save).
227
+
228
+ ### Added — `state` disk ops (C64 / VICE)
229
+ - **`state({op:'exportDisk', path})`** — write the LIVE mounted 1541 `.d64` to a
230
+ file (captures any files the game wrote to disk). Re-load it with `loadMedia`
231
+ (autostarts) or push it back with `importDisk`.
232
+ - **`state({op:'importDisk', path})`** — write a `.d64` back into the running
233
+ drive (inject a save disk made elsewhere). Enforces the standard 174848-byte
234
+ 35-track format.
235
+ - **`state({op:'putDiskFile', path, name})`** — inject ONE PRG file straight into
236
+ the live disk via the drive's filesystem (the "write a save" primitive).
237
+ - Backed by new VICE core exports (`romdev_disk_export`/`import`/`putfile`) that
238
+ operate on the live `disk_image_t` directly — captured in the reproducible
239
+ `vice-romdev-memory-regions.patch` (verified by a from-scratch re-fetch+build).
240
+ New `LibretroHost` methods: `exportDiskImage`/`importDiskImage`/`putDiskFile`/
241
+ `diskImageSupported`. Locked by `c64-disk-save.test.js`.
242
+
243
+ ### Known limit
244
+ - A game's OWN mid-run KERNAL `SAVE` does not yet auto-persist to disk in this
245
+ WASM build (the emulated 1541 serial-bus write stalls). Drive saves from the
246
+ host instead (`putDiskFile` / capture with `exportDisk`), or use a full-machine
247
+ savestate. The C64 `save_ram` n/a message now points at the disk ops.
248
+
249
+ ### Reproducibility hardening
250
+ - Every upstream pin in `versions.json` is now a full commit SHA or a verified
251
+ sha256 — closed two gaps: **cc65** was pinned to the mutable tag `V2.19`
252
+ (→ resolved to SHA `555282497c…`), and **sdcc** carried an unfilled
253
+ `UNVERIFIED-…` sha256 (→ real `ae8c1216…`). Zero weak pins remain.
254
+
255
+ ## 0.17.0
256
+
257
+ **C64 disk images — load real games & ship yours as `.d64`.** The Commodore
258
+ brand relaunched in 2025/26 (the FPGA Commodore 64 Ultimate / C64C Ultimate, on
259
+ the original 1986 tooling) and the homebrew/demo scene is booming — and that
260
+ world ships and loads games as **`.d64` disk images / `.crt` carts**, not bare
261
+ `.prg`. romdev was C64-`.prg`-only; now it handles disks end to end. No new
262
+ top-level tool and no core rebuild — the bundled VICE already does the work; the
263
+ gap was romdev's loader.
264
+
265
+ ### Added
266
+ - **Load & run disk/tape/cart:** `loadMedia({platform:'c64', path:'game.d64'})`
267
+ now accepts `.d64/.t64/.tap/.crt/.g64`. VICE attaches the disk to drive 8 and
268
+ **autostarts** it (= `LOAD"*",8,1 : RUN`) under warp (~100 frames vs sitting
269
+ at the BASIC `READY.` prompt). New c64 core-option defaults wire this up
270
+ (`vice_autostart` + `vice_autoloadwarp` + `vice_warp_boost`, write-protection
271
+ off). `status.mediaKind` now reflects the real medium (`disk`/`tape`/
272
+ `cartridge`/`program`) instead of always `program` — `defaultMediaKind` is
273
+ extension-aware.
274
+ - **Distribute as a disk:** `cart({op:'packDisk', prgPath})` wraps a built
275
+ `.prg` into an autostart-able `.d64` (a pure-JS 1541 codec — no `c1541`
276
+ dependency; exact `.prg` round-trip, standard 174848-byte image). `cart({op:
277
+ 'extract'})` on a `.d64` lists the directory; pass `name:` to pull a file off
278
+ the disk. So the full create→build→distribute loop produces the format the new
279
+ hardware and the scene actually load.
280
+
281
+ ### Known limit
282
+ - **In-emulator disk WRITES (a running game's own SAVE) are not yet persisted
283
+ back out of the core.** The write succeeds inside VICE but this WASM build
284
+ doesn't flush the modified image to the (MEM)FS on detach, and VICE exposes no
285
+ disk memory region. Loading/running/distributing disks is unaffected. The
286
+ honest C64 `save_ram` n/a message says so; for reliable persistence use a
287
+ full-machine savestate (`state({op:'save'/'load', path})`). A core patch to
288
+ add a disk export/flush entry point is tracked as a follow-up.
289
+
7
290
  ## 0.15.0
8
291
 
9
292
  **Scaffold audit: every scaffold on every platform now builds AND renders.** Two
@@ -98,6 +381,39 @@ the docs + source comments updated.)
98
381
  formats in. (Known limit: asar/SNES-asm only yields a wrapper "aborted"
99
382
  message — its WASM build aborts without printing line info.)
100
383
 
384
+ ### Added — SRAM (cartridge battery save) support, folded into existing tools
385
+ The cartridge battery SAVE FILE (in-game saves — distinct from a whole-machine
386
+ savestate) is now fully supported, with NO new top-level tool:
387
+ - **Live read/write** already worked via `memory({region:'save_ram'})` on every
388
+ battery-capable core (NES/GB/GBC/SNES/Genesis/GBA — verified against each core's
389
+ source; they all expose RETRO_MEMORY_SAVE_RAM).
390
+ - **Persist the `.sav`:** `state({op:'exportSram', path})` / `{op:'importSram', path}`
391
+ dump/restore the battery RAM as a real save file (relative path → ROM dir, size-
392
+ mismatch guard, zero-pad-smaller). The save-editor / inject-a-save capability that
393
+ previously forced agents out to local tooling.
394
+ - **Presence:** `cart({op:'identify'})` now returns `saveRam:{hasBattery, bytes}`
395
+ (from the iNES battery flag / GB cart-type) so an agent knows a save exists.
396
+ - **Honest "no save":** empty `save_ram` now says *why* — "this cart has no battery
397
+ save" / "Atari 2600/7800 & Lynx never had cartridge saves" / "C64 has no battery SRAM (disk/.prg)" — instead of a generic "core didn't expose it." (Confirmed via research +
398
+ core source: no core patches were needed; earlier "broken" readings were
399
+ password-game test carts like Metroid, which correctly have no battery.)
400
+
401
+ ### Fixed / Added — v0.15.0 session feedback
402
+ - **`state` file `path` resolution.** A RELATIVE `path` (save/load/export) used to
403
+ resolve against the server's CWD → silent ENOENT (and the docs use relative
404
+ paths). It now resolves against the LOADED ROM's directory ("states live next to
405
+ my ROM"); absolute paths are used as-is; the result echoes `resolvedPath`.
406
+ - **Abort-guard on input-driven `breakpoint({on:'write', precision:'exact'})`.** New
407
+ `abortIf:[{region,offset,label}]` — caller-named "is this scenario still valid?"
408
+ bytes. If any changes mid-run (player died → title screen, scene flipped) the
409
+ watchpoint stops IMMEDIATELY and returns `{aborted:true, abortedBy, before,
410
+ after}` instead of burning all `maxFrames` and returning a meaningless
411
+ `found:false`. Collapses the derailed-run recovery (breakpoint → screenshot →
412
+ N× memory read → reload) into one informative call.
413
+ - **No-hit note is now once-per-session.** `breakpoint` on:write used to repeat a
414
+ ~100-token "two common reasons" explainer on every miss; the full form now fires
415
+ only on the first miss per session, a one-liner after.
416
+
101
417
  ## 0.14.0
102
418
 
103
419
  **Two platform-specific top-level tools folded into their domain verbs, a
@@ -24,6 +24,8 @@ Each example fits the convention:
24
24
  | sms | `sms/main.c` (or `sms/templates/*.c`) | sdcc | Pair with `src/platforms/sms/lib/c/sms_crt0.s` (passed via `crt0` arg) — boots into a real cartridge with vector table + SP=$DFF0 + IM 1. Yellow 'H' on blue, scrollable with P1-B1. The 9 templates under `sms/templates/` (default, hello_sprite, tile_engine, shmup, shmup_2p, platformer, puzzle, sports, racing, music_demo) all use this crt0 — `scaffold({op:'project'})` copies it in automatically. |
25
25
  | gg | `gg/templates/default.c` (or any other template) | sdcc | R53: GG now ships `src/platforms/gg/lib/c/gg_crt0.s` (byte-identical to SMS's). Real visible-and-runnable default: VDP Mode 4 init + palette + yellow 'H' centered in the 160×144 visible viewport + B1 scroll loop. The 9 templates (default, hello_sprite, tile_engine, shmup, platformer, puzzle, sports, racing, music_demo) all link the GG runtime + crt0 via `scaffold({op:'project', platform:"gg"})`. |
26
26
  | gba | `gba/templates/*.c` | arm-none-eabi-gcc | Default runtime = **libtonc** (`#include <tonc.h>`). 9 scaffolds incl. `tonc_hello`, `tonc_hello_sprite`, the 5 genre scaffolds, and `maxmod_demo` (music). Pass `runtime:"libgba"` for the devkitPro API, `runtime:"none"` for bare newlib. **Always call `irq_init(NULL); irq_add(II_VBLANK, NULL);` before `VBlankIntrWait()`** — otherwise the BIOS halts forever. |
27
+ | pce | `pce/<template>/main.c` | cc65 (HuC6280) | HuCard homebrew, no BIOS. Ships a direct-register VDC/PSG helper lib (`pce.h` + `pce.lib`) — cc65 has no PCE sprite/sound library. Templates: `sprite_move`, `catch_game`, `music_sfx`, plus the 5 genre scaffolds (shmup/platformer/puzzle/sports/racing). **`#include <stdint.h>`** for int8/16/32_t — `pce.h` only typedefs u8/u16. Genre scaffolds fill the BAT (32×32 virtual screen); the platformer smooth-scrolls via the VDC BXR register. |
28
+ | msx | `msx/<template>/main.c` | sdcc (z80) | Boots cartridge homebrew on the open C-BIOS (no proprietary ROM). Ships an AY-3-8910 + TMS9918/V9938 VDP helper lib (`msx_hw.h` + `msx_vdp.c`). Templates: `sprite_move`, `catch_game`, `music_sfx`, plus the 5 genre scaffolds. The bundled `msx_crt0.s` (applied by the dir-build recipe automatically) emits the `"AB"` cartridge header at $4000 + INIT pointer — **C-BIOS shows its logo for ~2-3 s, then CALLs INIT**, so run ≥240 frames before screenshotting. The platformer column-streams the SCREEN 2 name table for a tile-by-tile scroll. |
27
29
 
28
30
  ## Guides
29
31