romdevtools 0.16.0 → 0.21.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 (110) hide show
  1. package/AGENTS.md +60 -12
  2. package/CHANGELOG.md +258 -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/platformer.c +43 -4
  10. package/examples/atari7800/templates/puzzle.c +39 -4
  11. package/examples/atari7800/templates/racing.c +39 -4
  12. package/examples/atari7800/templates/shmup.c +40 -2
  13. package/examples/atari7800/templates/sports.c +36 -5
  14. package/examples/c64/templates/platformer.c +19 -5
  15. package/examples/c64/templates/puzzle.c +32 -2
  16. package/examples/c64/templates/shmup.c +28 -2
  17. package/examples/c64/templates/sports.c +30 -2
  18. package/examples/gb/templates/default.c +110 -16
  19. package/examples/gb/templates/platformer.c +25 -4
  20. package/examples/gb/templates/puzzle.c +32 -2
  21. package/examples/gb/templates/racing.c +72 -8
  22. package/examples/gb/templates/shmup.c +38 -1
  23. package/examples/gb/templates/sports.c +48 -1
  24. package/examples/gba/templates/gba_hello.c +29 -11
  25. package/examples/gba/templates/puzzle.c +15 -3
  26. package/examples/gba/templates/racing.c +65 -3
  27. package/examples/gba/templates/shmup.c +41 -4
  28. package/examples/gba/templates/sports.c +36 -2
  29. package/examples/gba/templates/tonc_hello.c +41 -5
  30. package/examples/gbc/templates/default.c +103 -26
  31. package/examples/gbc/templates/platformer.c +25 -4
  32. package/examples/gbc/templates/puzzle.c +32 -2
  33. package/examples/gbc/templates/racing.c +85 -19
  34. package/examples/gbc/templates/shmup.c +34 -1
  35. package/examples/gbc/templates/sports.c +45 -1
  36. package/examples/genesis/templates/puzzle.c +37 -3
  37. package/examples/genesis/templates/racing.c +44 -11
  38. package/examples/genesis/templates/sgdk_hello.c +34 -1
  39. package/examples/genesis/templates/shmup.c +31 -1
  40. package/examples/gg/templates/default.c +56 -18
  41. package/examples/gg/templates/platformer.c +18 -12
  42. package/examples/gg/templates/puzzle.c +38 -7
  43. package/examples/gg/templates/racing.c +51 -5
  44. package/examples/gg/templates/shmup.c +47 -3
  45. package/examples/gg/templates/sports.c +46 -3
  46. package/examples/lynx/templates/default.c +39 -8
  47. package/examples/lynx/templates/puzzle.c +28 -1
  48. package/examples/lynx/templates/racing.c +34 -7
  49. package/examples/lynx/templates/shmup.c +42 -3
  50. package/examples/lynx/templates/sports.c +29 -2
  51. package/examples/msx/platformer/main.c +213 -0
  52. package/examples/msx/puzzle/main.c +250 -0
  53. package/examples/msx/racing/main.c +249 -0
  54. package/examples/msx/shmup/main.c +288 -0
  55. package/examples/msx/sports/main.c +182 -0
  56. package/examples/nes/templates/default.c +67 -19
  57. package/examples/nes/templates/platformer.c +65 -6
  58. package/examples/nes/templates/puzzle.c +67 -6
  59. package/examples/nes/templates/racing.c +45 -13
  60. package/examples/nes/templates/shmup.c +51 -2
  61. package/examples/nes/templates/sports.c +51 -6
  62. package/examples/pce/platformer/main.c +283 -0
  63. package/examples/pce/puzzle/main.c +304 -0
  64. package/examples/pce/racing/main.c +304 -0
  65. package/examples/pce/shmup/main.c +346 -0
  66. package/examples/pce/sports/main.c +254 -0
  67. package/examples/sms/main.c +35 -6
  68. package/examples/sms/templates/puzzle.c +34 -5
  69. package/examples/sms/templates/racing.c +39 -2
  70. package/examples/sms/templates/shmup.c +41 -2
  71. package/examples/sms/templates/sports.c +43 -2
  72. package/examples/snes/templates/default.c +50 -28
  73. package/examples/snes/templates/platformer-data.asm +22 -0
  74. package/examples/snes/templates/platformer.c +16 -1
  75. package/examples/snes/templates/puzzle-data.asm +22 -0
  76. package/examples/snes/templates/puzzle.c +17 -1
  77. package/examples/snes/templates/racing-data.asm +22 -0
  78. package/examples/snes/templates/racing.c +17 -1
  79. package/examples/snes/templates/shmup-data.asm +22 -0
  80. package/examples/snes/templates/shmup.c +20 -1
  81. package/examples/snes/templates/sports-data.asm +22 -0
  82. package/examples/snes/templates/sports.c +16 -1
  83. package/package.json +1 -1
  84. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  85. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  86. package/src/host/LibretroHost.js +122 -1
  87. package/src/host/callbacks.js +9 -1
  88. package/src/host/types.js +15 -8
  89. package/src/http/tool-registry.js +26 -1
  90. package/src/mcp/tools/cart-parts.js +75 -3
  91. package/src/mcp/tools/disasm-rebuild.js +507 -0
  92. package/src/mcp/tools/disasm.js +95 -6
  93. package/src/mcp/tools/frame.js +168 -3
  94. package/src/mcp/tools/lifecycle.js +4 -2
  95. package/src/mcp/tools/project.js +54 -9
  96. package/src/mcp/tools/state.js +201 -14
  97. package/src/mcp/tools/toolchain.js +76 -3
  98. package/src/mcp/tools/watch-memory.js +125 -14
  99. package/src/platforms/c64/MENTAL_MODEL.md +45 -1
  100. package/src/platforms/c64/d64.js +281 -0
  101. package/src/platforms/gb/MENTAL_MODEL.md +10 -0
  102. package/src/platforms/msx/MENTAL_MODEL.md +10 -6
  103. package/src/platforms/nes/MENTAL_MODEL.md +63 -2
  104. package/src/platforms/pce/MENTAL_MODEL.md +9 -4
  105. package/src/platforms/pce/lib/c/pce_video.c +1 -1
  106. package/src/rom-id/identifier.js +15 -0
  107. package/src/toolchains/cc65/ines.js +145 -0
  108. package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
  109. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
  110. package/src/toolchains/common/reassemble.js +10 -2
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
 
@@ -260,11 +260,11 @@ with explicit `sources` only when the files aren't on disk, e.g. generated in-co
260
260
 
261
261
  ## Supported platforms
262
262
 
263
- **13 tier-1 platforms** (build + run + screenshot + inspect + ≥5 genre scaffolds + sound + music + per-platform MENTAL_MODEL.md + TROUBLESHOOTING.md):
263
+ **14 tier-1 platforms** (build + run + screenshot + inspect + genre scaffolds + sound + music + per-platform MENTAL_MODEL.md + TROUBLESHOOTING.md):
264
264
 
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).
265
+ 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
266
 
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.
267
+ **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
268
 
269
269
  **Delisted** (toolchain works but core-side issue blocks the run loop): Atari 5200 (atari800 BIOS-load path), ZX Spectrum (fuse tape-load path).
270
270
 
@@ -291,8 +291,10 @@ Different platforms have different levels of MCP-exposed debugging — different
291
291
  > to trip before the frame cap on the slow ~1MHz cores too. Pass `maxInstructions`
292
292
  > to override the budget, `presetMemory`/`stopAtPC` for codecs that read RAM globals
293
293
  > 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),
294
+ > **`watch({on:'range'})`** (log EVERY read/write hitting an address range — discovery;
295
+ > pass **`fromState`**/`fromStatePath` to restore a savestate FIRST so the trace runs from a
296
+ > known, repeatable moment — jump to the boss, then see what writes HP),
297
+ > **`watch({on:'pc'})`** (coverage trace — distinct PCs executed in a window; also takes `fromState`),
296
298
  > **the RE-INJECT trio** (put an edited asset BACK, all 14): **`romPatch({op:'findPointer'})`**
297
299
  > (find every pointer to a ROM offset — Genesis 32-bit BE, SNES LoROM/HiROM, GBA
298
300
  > 0x08000000+offset incl. literal pools, banked 8-bit 16-bit-LE aliases),
@@ -321,7 +323,7 @@ Different platforms have different levels of MCP-exposed debugging — different
321
323
  - **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
324
  - **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
325
  - **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.
326
+ - **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
327
  - **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
328
  - **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
329
  - **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/).
@@ -776,14 +778,17 @@ post-processing layered on the objdump output.
776
778
  `disasm({target:'rom'})` gives you one routine as text. `disasm({target:'project'})` turns an
777
779
  **entire ROM into a complete, re-buildable project in one call**, across **all 14
778
780
  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).
781
+ **Lynx** — 65C02, **PC Engine** — HuC6280, and **MSX** — Z80; byte-exact on 13,
782
+ PC Engine the one current exception — see caveats).
780
783
  Each region disassembles through the CPU's native objdump and reassembles through
781
784
  the matching native `as`/`ld`/`objcopy`, so the round-trip is guaranteed byte-for-byte:
782
785
 
783
786
  ```js
784
787
  disasm({ target:'project', path: "game.nes", outputDir: "./game-disasm" })
785
788
  // → { ok, platform, regions:[{file, startAddress, roundTripOk, readablePercent}],
786
- // roundTrip:{ allByteExact, failed:[] }, readablePercentAvg }
789
+ // roundTrip:{ allByteExact, failed:[] }, readablePercentAvg,
790
+ // rebuild:{ blobs:[{file,bytes}], buildCall:{...}|null, verifiable, buildDoc:"BUILD.md", notes } }
791
+ // Writes the .asm regions + chr.bin/header blobs + BUILD.md + rebuild.json to outputDir.
787
792
  ```
788
793
 
789
794
  It splits the ROM into regions (per-16KB bank for banked NES, per-32KB bank for
@@ -796,6 +801,38 @@ files ALWAYS rebuild to the original bytes (`roundTrip.allByteExact`). The
796
801
  vs. data. Each `.asm` carries a provenance + round-trip header and is ready to
797
802
  edit and rebuild with the platform's native toolchain.
798
803
 
804
+ **It also writes the REBUILD GLUE** so the project is turnkey, not just byte-exact
805
+ region files. Alongside the `.asm` files you get: any non-code DATA blobs the
806
+ rebuild needs (NES CHR-ROM → `chr.bin`; the stripped Genesis/GBA/Lynx/MSX
807
+ cartridge header → `*.bin`), a **`BUILD.md`** with the exact rebuild steps, and —
808
+ where a one-call rebuild exists — a **`rebuild.json`** holding the precise
809
+ `build({...})` args (absolute paths). The response carries the same under
810
+ `rebuild: { blobs, buildCall, verifiable, buildDoc, notes }`. So the RE loop is:
811
+ `disasm({target:'project'})` → edit a `.asm` → rebuild → `diffRoms` to confirm.
812
+
813
+ Two rebuild tiers (honest — the disasm emits each CPU's native-reassembler
814
+ syntax, which only some platforms' `build()` toolchains can consume):
815
+ - **One-call `build()` rebuild, byte-identical** — **NES, C64, Atari 7800, Lynx**.
816
+ Feed `rebuild.json` straight to `build`. (NES uses the new `inesHeader` option
817
+ — see below. Lynx: build() yields the headerless image; prepend the shipped
818
+ `lnx_header.bin` for the full `.lnx`.)
819
+ - **Native-recipe rebuild (`buildCall:null`), byte-identical, steps in `BUILD.md`**
820
+ — **SMS, GG, MSX, GB, GBC, Genesis, GBA, Atari 2600**. Their `build()`
821
+ toolchains (SDCC/RGBDS/asar/dasm/vasm) can't reassemble the disasm's ca65/GNU-as
822
+ syntax, so `BUILD.md` gives the proven native `as`/`ld`/`objcopy` chain.
823
+ - **PC Engine** is the one not-yet-byte-exact case (the region trims real padding /
824
+ doesn't strip a copier header) — `BUILD.md` says so.
825
+
826
+ **Rebuilding a commercial NES (NROM CHR-ROM) game — `build({inesHeader})`:** the
827
+ most common NES RE rebuild. `build({output:'rom', platform:'nes', inesHeader:{
828
+ prgBanks, chrBanks, mapper, mirroring}, sourcesPaths:{...the PRG...},
829
+ binaryIncludePaths:{ "chr.bin":... }})` auto-emits the 16-byte iNES header + the
830
+ CHARS segment wiring + the flat NROM `.cfg` — no hand-derived header bytes, no
831
+ glue `.s`/`.cfg`. (`disasm({target:'project'})` puts exactly this call in
832
+ `rebuild.json`.) For homebrew C that ships fixed tile art, `linkerConfig:"chr-rom"`
833
+ is the segment-split equivalent. See the NES MENTAL_MODEL.md "Rebuilding a CHR-ROM
834
+ NROM image" section.
835
+
799
836
  Reassembler per CPU family (all bundled WASM, no installs): **cc65** ca65/ld65
800
837
  for 6502 + 65816; native binutils **`as`/`ld`/`objcopy`** for the GNU CPUs —
801
838
  `m68k-elf` (Genesis), `arm-none-eabi` (GBA), and one `z80-elf` for both Z80
@@ -814,6 +851,10 @@ Caveats worth knowing up front:
814
851
  always correct. (The 192-byte GBA header is emitted as a clean data region.)
815
852
  - Banked-NES is the strongest case — per-bank regions come back ~100%
816
853
  instructions. GB/GBC, SMS/GG, C64, and Atari are also near-100%.
854
+ - **PC Engine** is the one platform that does NOT round-trip byte-exact yet: the
855
+ region trims real trailing $FF padding and doesn't strip a 512-byte copier
856
+ header, so the emitted region is a lossy view of the `.pce`. `BUILD.md` flags
857
+ this; a `planRegions` fix is the follow-up.
817
858
  - Platform is sniffed from the file extension; pass `platform:` to override.
818
859
 
819
860
  ## CHR/tile tools — file vs emulator source
@@ -881,6 +922,13 @@ OAM format: bytes per sprite are `[y, tileIndex, attributes, x]`.
881
922
  `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
923
  - `state({op:'save', path})` writes the CURRENT live host to a file directly.
883
924
  - `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})`.
925
+ - **`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.)
926
+
927
+ **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:
928
+ - **Live read/write:** `memory({op:'read'/'write', region:'save_ram'})` — poke/inspect the running game's save RAM.
929
+ - **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.
930
+ - **Presence:** `cart({op:'identify'})` returns `saveRam:{hasBattery, bytes}` so you know whether a save even exists before reaching for it.
931
+ - **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
932
 
885
933
  `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
934
 
@@ -892,7 +940,7 @@ Three shapes, pick the one that matches what you're doing:
892
940
 
893
941
  - **`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
942
 
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.
943
+ - **`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
944
 
897
945
  Then iterate with `build({output:'run'})` against the source you read from `path/main.*`.
898
946
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,231 @@ 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.21.0
8
+
9
+ **NES CHR-ROM / iNES rebuild ergonomics + turnkey `disasm({target:'project'})`
10
+ across all platforms.** Addresses the v0.16.0 feedback: rebuilding a commercial
11
+ NROM game from its disassembly into a byte-identical `.nes` no longer needs
12
+ hand-written iNES header bytes, a CHR-ROM `.incbin` glue source, or a 3-region
13
+ linker `.cfg`.
14
+
15
+ ### Added — `build({inesHeader:{prgBanks, chrBanks, mapper, mirroring, battery?}})`
16
+ NES NROM-rebuild convenience. Auto-emits the 16-byte iNES HEADER segment, wires
17
+ the CHR-ROM blob (from `binaryIncludePaths`) into a CHARS segment, and
18
+ synthesizes the flat NROM linker `.cfg` (HEADER + PRG + CHARS). The agent
19
+ supplies only the PRG disassembly + the CHR blob — no glue `.s`/`.cfg`, no
20
+ hand-derived header bytes. PRG start/size derive from `prgBanks` (NROM-128 →
21
+ $C000, NROM-256 → $8000). Mutually exclusive with `linkerConfig`. Proven
22
+ byte-identical against `nestest.nes`.
23
+
24
+ ### Added — `linkerConfig:"chr-rom"` NES preset
25
+ Sibling of `chr-ram`/`chr-ram-runtime`, for homebrew C that ships FIXED tile art:
26
+ segment split + a CHARS segment in an 8 KB ROM2 bank + a companion crt0 with an
27
+ 8 KB-CHR-ROM iNES header. (For other bank configs, prefer `inesHeader`.)
28
+
29
+ ### Changed — `disasm({target:'project'})` now emits a TURNKEY, rebuildable project
30
+ Previously it wrote only byte-exact `.asm` region files. Now, per platform, it
31
+ also writes the "rebuild glue": data blobs (NES CHR-ROM, MSX/Genesis/GBA/Lynx
32
+ headers), a human/agent-readable `BUILD.md`, and — where a one-call rebuild
33
+ exists — a `rebuild.json` (the exact `build()` args, absolute paths). Feed
34
+ `rebuild.json` back to `build` and you get a byte-identical ROM.
35
+ - **One-call `build()` rebuild (byte-identical):** NES, C64, Atari 7800, Lynx
36
+ (Lynx: build() yields the headerless image + a shipped `lnx_header.bin` to
37
+ prepend).
38
+ - **Native-recipe (byte-identical, documented in BUILD.md):** SMS, GG, MSX, GB,
39
+ GBC, Genesis, GBA, Atari 2600 — the disasm emits each CPU's native-reassembler
40
+ syntax (ca65 for 6502/65816, GNU `as` for z80/sm83/m68k/arm), which those
41
+ platforms' `build()` toolchains (SDCC/RGBDS/asar/dasm/vasm) can't consume, so
42
+ BUILD.md gives the proven native chain.
43
+ - **Not yet byte-exact:** PC Engine (planRegions trims real trailing padding +
44
+ doesn't strip a copier header — BUILD.md says so).
45
+
46
+ ### Fixed — disasm round-trip bugs surfaced by the rebuild work
47
+ - `reassemble.js`'s data-only floor omitted `.org`, silently truncating any
48
+ region with a non-zero start address — so multi-bank GB/GBC ($4000 banks) and
49
+ MSX ($4010) didn't round-trip at all. The floor now mirrors the linked path's
50
+ origin handling.
51
+ - `dataRegionSource` emitted `$`-prefixed hex that GNU assemblers (ARM/m68k)
52
+ reject (`$2E` read as an undefined symbol); it's now CPU-family-aware (`0x` hex
53
+ for z80/sm83/m68k/arm, `$` for 6502/65816).
54
+
55
+ ### Added — NES MENTAL_MODEL.md "Rebuilding a CHR-ROM NROM image" section
56
+ The iNES header bytes decoded, CHR-ROM vs CHR-RAM, NROM-128/-256 mapping, and the
57
+ three rebuild paths (`inesHeader` / `chr-rom` preset / `disasm({target:'project'})`).
58
+
59
+ ## 0.20.0
60
+
61
+ **Genre-scaffold parity across all 14 platforms + the MSX cartridge-boot fix +
62
+ PCE rendering fix + a higher blank-screen bar.**
63
+
64
+ ### Fixed — MSX cartridges now actually boot (`scaffold` + recipe)
65
+ Every MSX program had been booting to the C-BIOS "No cartridge found" screen:
66
+ `retro_load_game` returned true but the cart never reached a slot. Root cause was
67
+ NOT the wasm core, the C-BIOS, or the libretro API (all verified working against a
68
+ commercial MSX `.rom` in the same host) — it was our **build**. `projectBuildRecipe`
69
+ had no MSX branch, so `msx_crt0.s` was compiled as an ordinary translation unit
70
+ *alongside* SDCC's stock CP/M-style crt0; the cartridge `"AB"` header at $4000
71
+ got dropped and the INIT entry pointed at junk. Fix: route `msx_crt0.s` through
72
+ the crt0 slot (`crt0File`, `codeLoc = 0x4010`), exactly like the SMS/GG recipe.
73
+ MSX is now full tier-1.
74
+
75
+ ### Fixed — PC Engine "bottom half is vertical stripes" on every game
76
+ `vdc_init` set `VDC_MWR` to `0x0010`, which per the HuC6280 VDC spec selects the
77
+ **64×32** virtual screen, not the 32×32 the comment claimed. The BAT-clear loops
78
+ walk stride-32, so only the top ~16 rows of the 64-wide map were cleared — the
79
+ bottom half rendered uninitialized VRAM as vertical stripes. Set `VDC_MWR` to
80
+ `0x0000` (true 32×32) so the clear covers the whole visible map.
81
+
82
+ ### Added — genre-scaffold parity (PC Engine, MSX, Atari 2600)
83
+ PC Engine and MSX gained the full 5 canonical genre scaffolds
84
+ (`shmup` / `platformer` / `puzzle` / `sports` / `racing`); Atari 2600 gained 4
85
+ (no `puzzle` — the TIA has no tilemap to draw a match-3 board). Previously these
86
+ three shipped only ~3 ad-hoc starters while the other 11 platforms had the full
87
+ set. All 14 new scaffolds are verified rendering live (`frame({op:'verify'})` →
88
+ `verified:true`, ≥3 distinct colors, dominant under the blank threshold). The MSX
89
+ and PCE `platformer` scaffolds side-scroll (MSX via SCREEN 2 name-table column
90
+ streaming; PCE via the VDC BXR register). `scaffold({op:'game'})` now works on
91
+ all 14 platforms; only the per-(platform,genre) gaps (e.g. `atari2600` + `puzzle`)
92
+ are rejected, with the error naming the genres that platform *does* have.
93
+
94
+ ### Changed — blank-screen detection bar raised to 92%
95
+ `frame({op:'verify'})`'s `nearlyBlank` threshold went from 99.5% to **92%**
96
+ dominant-color coverage — 88–92% of one flat color still reads as "blank" to a
97
+ human. A sweep re-tuned the genre scaffolds across every platform to clear the
98
+ higher bar (real backgrounds/HUD instead of a lone sprite on a flat field).
99
+
100
+ ### Fixed — `frame({op:'verify'})` now emits its judged frame to the livestream
101
+ The REST/skill tool path (`runTool`) was dropping the observer image/frame
102
+ sidebands that only the MCP middleware handled, so `verify` (and any tool that
103
+ emits a deferred frame) never reached the `/livestream` UI over plain HTTP.
104
+ `runTool` now mirrors the middleware: it strips the sidebands from the result and
105
+ fires the deferred `call_frame` event.
106
+
107
+ ## 0.19.0
108
+
109
+ **Two one-stop-shop features for agents — both fold into existing tools, both
110
+ work on all 14 platforms.**
111
+
112
+ ### Added — `frame({op:'verify'})`: "is the game actually rendering / alive?"
113
+ A one-call render-health check for agents debugging WITHOUT vision (the spiral
114
+ where a black frame might be broken *or* fine and you can't tell). Pass `frames`
115
+ to boot-then-check in one call. Fuses two independent signals: a platform-agnostic
116
+ pixel-content scan of the live framebuffer (distinctColors, dominant-color %) and
117
+ the per-platform render-ENABLE/NMI decode (reused from the rendering-context
118
+ decoder — covers all 14 platforms). Returns `{verified:true|false|null, issues[],
119
+ pixels, render}`:
120
+ - `verified:null` + `unsettled` before any frame is stepped (frame-0 guard — never
121
+ cries wolf on boot; step first).
122
+ - `issues[]` flags `blankScreen` / `nearlyBlank` / `renderDisabled`. `renderDisabled`
123
+ is ONLY raised when the registers say so (never on a platform we can't decode —
124
+ there the pixel check carries the verdict).
125
+ - Pass/fail with zero image tokens; for WHAT to fix, getPlatformDoc(mental_model).
126
+ - Verified across all 14 platforms (`frame-verify-allplatforms.test.js`): the
127
+ verdict is internally consistent everywhere, and it correctly flags genuinely
128
+ blank scaffolds as broken. Implements the locked `renderHealth` spec, folded
129
+ into `frame` rather than a new top-level tool.
130
+
131
+ ### Added — `watch({on:'range'/'pc', fromState|fromStatePath})`: trace from a moment
132
+ The range/PC tracers can now restore a savestate FIRST, so the log runs from a
133
+ known, repeatable point (jump to the boss fight, then see exactly what writes HP)
134
+ instead of from wherever the live session happens to be. `fromState` = an in-memory
135
+ slot (state({op:'save', name})); `fromStatePath` = a savestate file on disk
136
+ (relative paths resolve to the ROM dir). Deterministic — same state → identical
137
+ trace. Platform-agnostic (rides the existing all-14-platform range/PC watch).
138
+ Result echoes `restoredFrom`. Tests in `watch-fromstate.test.js`.
139
+
140
+ ## 0.18.1
141
+
142
+ **C64: a game's OWN in-game disk SAVE works — and always did.** 0.18.0 shipped
143
+ the disk ops with a "known limit" claiming a running game's KERNAL `SAVE` doesn't
144
+ persist in WASM. That was WRONG — the cause was a bug in romdev's `.d64`
145
+ *directory reader*, not the emulator: the C64 KERNAL stores filenames in high-bit
146
+ PETSCII (A–Z = 0xC1–0xDA), and `readDirectory` dropped those bytes, so an
147
+ emulator-written `SCORE` parsed as an empty name and looked missing. VICE was
148
+ committing the save to the live disk the whole time (true-drive GCR write-back).
149
+
150
+ ### Fixed
151
+ - **`readDirectory`/`extractFile` decode high-bit PETSCII filenames** (and
152
+ lower-as-upper) — so files a game saves (KERNAL SAVE) are visible, not just
153
+ files romdev's own `prgToD64` wrote (which used plain ASCII). Verified end to
154
+ end: a cc65 program does `cbm_save("SCORE",8,…)`, and `exportDisk` reads the
155
+ `SCORE` file back with the right bytes. Locked by a regression test in
156
+ `d64.test.js` + a transparent-save test in `c64-disk-save.test.js`.
157
+ - The 0.18.0 "Known limit" is **retracted**: run the game, let it save, then
158
+ `state({op:'exportDisk', path})` captures a `.d64` that includes the save.
159
+ Docs + the `save_ram` n/a message corrected. (Confirmed against the native
160
+ vice-libretro core in RetroDECK, which produces a byte-identical saved disk.)
161
+
162
+ ## 0.18.0
163
+
164
+ **C64 disk SAVES — the floppy is the C64 save medium, and romdev now reads/writes
165
+ it on the live disk.** 0.17.0 added loading/running/distributing `.d64` disks;
166
+ this adds save/restore, the C64 analogue of SRAM `exportSram`/`importSram` (the
167
+ C64 has no battery RAM — games save by writing files to the floppy, so the disk
168
+ IS the save).
169
+
170
+ ### Added — `state` disk ops (C64 / VICE)
171
+ - **`state({op:'exportDisk', path})`** — write the LIVE mounted 1541 `.d64` to a
172
+ file (captures any files the game wrote to disk). Re-load it with `loadMedia`
173
+ (autostarts) or push it back with `importDisk`.
174
+ - **`state({op:'importDisk', path})`** — write a `.d64` back into the running
175
+ drive (inject a save disk made elsewhere). Enforces the standard 174848-byte
176
+ 35-track format.
177
+ - **`state({op:'putDiskFile', path, name})`** — inject ONE PRG file straight into
178
+ the live disk via the drive's filesystem (the "write a save" primitive).
179
+ - Backed by new VICE core exports (`romdev_disk_export`/`import`/`putfile`) that
180
+ operate on the live `disk_image_t` directly — captured in the reproducible
181
+ `vice-romdev-memory-regions.patch` (verified by a from-scratch re-fetch+build).
182
+ New `LibretroHost` methods: `exportDiskImage`/`importDiskImage`/`putDiskFile`/
183
+ `diskImageSupported`. Locked by `c64-disk-save.test.js`.
184
+
185
+ ### Known limit
186
+ - A game's OWN mid-run KERNAL `SAVE` does not yet auto-persist to disk in this
187
+ WASM build (the emulated 1541 serial-bus write stalls). Drive saves from the
188
+ host instead (`putDiskFile` / capture with `exportDisk`), or use a full-machine
189
+ savestate. The C64 `save_ram` n/a message now points at the disk ops.
190
+
191
+ ### Reproducibility hardening
192
+ - Every upstream pin in `versions.json` is now a full commit SHA or a verified
193
+ sha256 — closed two gaps: **cc65** was pinned to the mutable tag `V2.19`
194
+ (→ resolved to SHA `555282497c…`), and **sdcc** carried an unfilled
195
+ `UNVERIFIED-…` sha256 (→ real `ae8c1216…`). Zero weak pins remain.
196
+
197
+ ## 0.17.0
198
+
199
+ **C64 disk images — load real games & ship yours as `.d64`.** The Commodore
200
+ brand relaunched in 2025/26 (the FPGA Commodore 64 Ultimate / C64C Ultimate, on
201
+ the original 1986 tooling) and the homebrew/demo scene is booming — and that
202
+ world ships and loads games as **`.d64` disk images / `.crt` carts**, not bare
203
+ `.prg`. romdev was C64-`.prg`-only; now it handles disks end to end. No new
204
+ top-level tool and no core rebuild — the bundled VICE already does the work; the
205
+ gap was romdev's loader.
206
+
207
+ ### Added
208
+ - **Load & run disk/tape/cart:** `loadMedia({platform:'c64', path:'game.d64'})`
209
+ now accepts `.d64/.t64/.tap/.crt/.g64`. VICE attaches the disk to drive 8 and
210
+ **autostarts** it (= `LOAD"*",8,1 : RUN`) under warp (~100 frames vs sitting
211
+ at the BASIC `READY.` prompt). New c64 core-option defaults wire this up
212
+ (`vice_autostart` + `vice_autoloadwarp` + `vice_warp_boost`, write-protection
213
+ off). `status.mediaKind` now reflects the real medium (`disk`/`tape`/
214
+ `cartridge`/`program`) instead of always `program` — `defaultMediaKind` is
215
+ extension-aware.
216
+ - **Distribute as a disk:** `cart({op:'packDisk', prgPath})` wraps a built
217
+ `.prg` into an autostart-able `.d64` (a pure-JS 1541 codec — no `c1541`
218
+ dependency; exact `.prg` round-trip, standard 174848-byte image). `cart({op:
219
+ 'extract'})` on a `.d64` lists the directory; pass `name:` to pull a file off
220
+ the disk. So the full create→build→distribute loop produces the format the new
221
+ hardware and the scene actually load.
222
+
223
+ ### Known limit
224
+ - **In-emulator disk WRITES (a running game's own SAVE) are not yet persisted
225
+ back out of the core.** The write succeeds inside VICE but this WASM build
226
+ doesn't flush the modified image to the (MEM)FS on detach, and VICE exposes no
227
+ disk memory region. Loading/running/distributing disks is unaffected. The
228
+ honest C64 `save_ram` n/a message says so; for reliable persistence use a
229
+ full-machine savestate (`state({op:'save'/'load', path})`). A core patch to
230
+ add a disk export/flush entry point is tracked as a follow-up.
231
+
7
232
  ## 0.15.0
8
233
 
9
234
  **Scaffold audit: every scaffold on every platform now builds AND renders.** Two
@@ -98,6 +323,39 @@ the docs + source comments updated.)
98
323
  formats in. (Known limit: asar/SNES-asm only yields a wrapper "aborted"
99
324
  message — its WASM build aborts without printing line info.)
100
325
 
326
+ ### Added — SRAM (cartridge battery save) support, folded into existing tools
327
+ The cartridge battery SAVE FILE (in-game saves — distinct from a whole-machine
328
+ savestate) is now fully supported, with NO new top-level tool:
329
+ - **Live read/write** already worked via `memory({region:'save_ram'})` on every
330
+ battery-capable core (NES/GB/GBC/SNES/Genesis/GBA — verified against each core's
331
+ source; they all expose RETRO_MEMORY_SAVE_RAM).
332
+ - **Persist the `.sav`:** `state({op:'exportSram', path})` / `{op:'importSram', path}`
333
+ dump/restore the battery RAM as a real save file (relative path → ROM dir, size-
334
+ mismatch guard, zero-pad-smaller). The save-editor / inject-a-save capability that
335
+ previously forced agents out to local tooling.
336
+ - **Presence:** `cart({op:'identify'})` now returns `saveRam:{hasBattery, bytes}`
337
+ (from the iNES battery flag / GB cart-type) so an agent knows a save exists.
338
+ - **Honest "no save":** empty `save_ram` now says *why* — "this cart has no battery
339
+ 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 +
340
+ core source: no core patches were needed; earlier "broken" readings were
341
+ password-game test carts like Metroid, which correctly have no battery.)
342
+
343
+ ### Fixed / Added — v0.15.0 session feedback
344
+ - **`state` file `path` resolution.** A RELATIVE `path` (save/load/export) used to
345
+ resolve against the server's CWD → silent ENOENT (and the docs use relative
346
+ paths). It now resolves against the LOADED ROM's directory ("states live next to
347
+ my ROM"); absolute paths are used as-is; the result echoes `resolvedPath`.
348
+ - **Abort-guard on input-driven `breakpoint({on:'write', precision:'exact'})`.** New
349
+ `abortIf:[{region,offset,label}]` — caller-named "is this scenario still valid?"
350
+ bytes. If any changes mid-run (player died → title screen, scene flipped) the
351
+ watchpoint stops IMMEDIATELY and returns `{aborted:true, abortedBy, before,
352
+ after}` instead of burning all `maxFrames` and returning a meaningless
353
+ `found:false`. Collapses the derailed-run recovery (breakpoint → screenshot →
354
+ N× memory read → reload) into one informative call.
355
+ - **No-hit note is now once-per-session.** `breakpoint` on:write used to repeat a
356
+ ~100-token "two common reasons" explainer on every miss; the full form now fires
357
+ only on the first miss per session, a one-liner after.
358
+
101
359
  ## 0.14.0
102
360
 
103
361
  **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