romdevtools 0.15.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.
- package/AGENTS.md +61 -13
- package/CHANGELOG.md +289 -0
- package/README.md +1 -1
- package/examples/README.md +2 -0
- package/examples/atari2600/templates/platformer.asm +460 -0
- package/examples/atari2600/templates/racing.asm +463 -0
- package/examples/atari2600/templates/shmup.asm +386 -0
- package/examples/atari2600/templates/sports.asm +362 -0
- package/examples/atari7800/templates/default.c +49 -5
- package/examples/atari7800/templates/platformer.c +43 -4
- package/examples/atari7800/templates/puzzle.c +39 -4
- package/examples/atari7800/templates/racing.c +39 -4
- package/examples/atari7800/templates/shmup.c +40 -2
- package/examples/atari7800/templates/sports.c +36 -5
- package/examples/c64/templates/platformer.c +19 -5
- package/examples/c64/templates/puzzle.c +32 -2
- package/examples/c64/templates/shmup.c +28 -2
- package/examples/c64/templates/sports.c +30 -2
- package/examples/gb/templates/default.c +110 -16
- package/examples/gb/templates/platformer.c +25 -4
- package/examples/gb/templates/puzzle.c +32 -2
- package/examples/gb/templates/racing.c +72 -8
- package/examples/gb/templates/shmup.c +38 -1
- package/examples/gb/templates/sports.c +48 -1
- package/examples/gba/templates/gba_hello.c +29 -11
- package/examples/gba/templates/puzzle.c +15 -3
- package/examples/gba/templates/racing.c +65 -3
- package/examples/gba/templates/shmup.c +41 -4
- package/examples/gba/templates/sports.c +36 -2
- package/examples/gba/templates/tonc_hello.c +41 -5
- package/examples/gbc/templates/default.c +103 -26
- package/examples/gbc/templates/platformer.c +25 -4
- package/examples/gbc/templates/puzzle.c +32 -2
- package/examples/gbc/templates/racing.c +85 -19
- package/examples/gbc/templates/shmup.c +34 -1
- package/examples/gbc/templates/sports.c +45 -1
- package/examples/genesis/templates/puzzle.c +37 -3
- package/examples/genesis/templates/racing.c +44 -11
- package/examples/genesis/templates/sgdk_hello.c +34 -1
- package/examples/genesis/templates/shmup.c +31 -1
- package/examples/gg/templates/default.c +56 -18
- package/examples/gg/templates/platformer.c +18 -12
- package/examples/gg/templates/puzzle.c +38 -7
- package/examples/gg/templates/racing.c +51 -5
- package/examples/gg/templates/shmup.c +47 -3
- package/examples/gg/templates/sports.c +46 -3
- package/examples/lynx/templates/default.c +39 -8
- package/examples/lynx/templates/puzzle.c +28 -1
- package/examples/lynx/templates/racing.c +34 -7
- package/examples/lynx/templates/shmup.c +42 -3
- package/examples/lynx/templates/sports.c +29 -2
- package/examples/msx/platformer/main.c +213 -0
- package/examples/msx/puzzle/main.c +250 -0
- package/examples/msx/racing/main.c +249 -0
- package/examples/msx/shmup/main.c +288 -0
- package/examples/msx/sports/main.c +182 -0
- package/examples/nes/templates/default.c +67 -19
- package/examples/nes/templates/platformer.c +65 -6
- package/examples/nes/templates/puzzle.c +67 -6
- package/examples/nes/templates/racing.c +45 -13
- package/examples/nes/templates/shmup.c +51 -2
- package/examples/nes/templates/sports.c +51 -6
- package/examples/pce/platformer/main.c +283 -0
- package/examples/pce/puzzle/main.c +304 -0
- package/examples/pce/racing/main.c +304 -0
- package/examples/pce/shmup/main.c +346 -0
- package/examples/pce/sports/main.c +254 -0
- package/examples/sms/main.c +35 -6
- package/examples/sms/templates/puzzle.c +34 -5
- package/examples/sms/templates/racing.c +39 -2
- package/examples/sms/templates/shmup.c +41 -2
- package/examples/sms/templates/sports.c +43 -2
- package/examples/snes/templates/default.c +50 -28
- package/examples/snes/templates/platformer-data.asm +22 -0
- package/examples/snes/templates/platformer.c +16 -1
- package/examples/snes/templates/puzzle-data.asm +22 -0
- package/examples/snes/templates/puzzle.c +17 -1
- package/examples/snes/templates/racing-data.asm +22 -0
- package/examples/snes/templates/racing.c +17 -1
- package/examples/snes/templates/shmup-data.asm +22 -0
- package/examples/snes/templates/shmup.c +20 -1
- package/examples/snes/templates/sports-data.asm +22 -0
- package/examples/snes/templates/sports.c +16 -1
- package/package.json +1 -1
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +122 -1
- package/src/host/callbacks.js +9 -1
- package/src/host/types.js +15 -8
- package/src/http/skill-doc.js +1 -1
- package/src/http/tool-registry.js +27 -2
- package/src/mcp/tools/cart-parts.js +75 -3
- package/src/mcp/tools/disasm-rebuild.js +507 -0
- package/src/mcp/tools/disasm.js +95 -6
- package/src/mcp/tools/frame.js +168 -3
- package/src/mcp/tools/index.js +4 -4
- package/src/mcp/tools/lifecycle.js +4 -2
- package/src/mcp/tools/project.js +54 -9
- package/src/mcp/tools/state.js +201 -14
- package/src/mcp/tools/toolchain.js +89 -4
- package/src/mcp/tools/watch-memory.js +125 -14
- package/src/platforms/c64/MENTAL_MODEL.md +45 -1
- package/src/platforms/c64/d64.js +281 -0
- package/src/platforms/gb/MENTAL_MODEL.md +10 -0
- package/src/platforms/msx/MENTAL_MODEL.md +10 -6
- package/src/platforms/nes/MENTAL_MODEL.md +63 -2
- package/src/platforms/pce/MENTAL_MODEL.md +9 -4
- package/src/platforms/pce/lib/c/pce_video.c +1 -1
- package/src/rom-id/identifier.js +15 -0
- package/src/toolchains/cc65/cc65.js +8 -1
- package/src/toolchains/cc65/ines.js +145 -0
- package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
- package/src/toolchains/common/reassemble.js +10 -2
- package/src/toolchains/gba-c/gba-c.js +6 -1
- package/src/toolchains/genesis-c/genesis-c.js +10 -2
- package/src/toolchains/parse-errors.js +67 -5
package/AGENTS.md
CHANGED
|
@@ -43,7 +43,7 @@ Skip playtest only when there's clearly no human in the loop: CI runs, automated
|
|
|
43
43
|
|
|
44
44
|
## Tool surface: everything is loaded — just call the tool
|
|
45
45
|
|
|
46
|
-
**All ~
|
|
46
|
+
**All ~32 tools are registered and callable from session init — there is no loading step.** If you see a tool name anywhere in this doc or via `catalog({op:'categories'})`, you can call it right now. Each tool is a small VERB with an operation axis — `memory({op})`, `build({output})`, `sprites({op})`, `breakpoint({on})`, `cpu({op})` — so the whole surface is a few dozen names, not a few hundred.
|
|
47
47
|
|
|
48
48
|
(We used to lazy-load tools behind a `loadCategory` call. It caused more harm than good — agents burned round-trips re-loading categories, and dynamic registration never propagated reliably to clients anyway. The consolidation shrank the surface enough that the entire thing loads up front; the old `loadCategory`/`describeTool` discovery tools are gone.)
|
|
49
49
|
|
|
@@ -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
|
-
**
|
|
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
|
|
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):
|
|
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
|
-
> **`
|
|
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;
|
|
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
|
|
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
|
|
@@ -67,6 +292,70 @@ instead of assuming the server is broken and installing their own tools.
|
|
|
67
292
|
- The `uint8-loop-bound` preflight lint is scope-aware (no longer false-flags a
|
|
68
293
|
`uint16_t` loop counter that shares a name with a `uint8_t` in another function).
|
|
69
294
|
|
|
295
|
+
## 0.16.0
|
|
296
|
+
|
|
297
|
+
**Build diagnostics: agents were building blind — errors AND warnings now reach
|
|
298
|
+
the response as structured `issues[]`.** An agent can only fix what the toolchain
|
|
299
|
+
tells it, where it tells it. Audited the whole build surface and closed the gaps
|
|
300
|
+
so diagnostics (file/line/message/stage) come back in the tool result, not buried
|
|
301
|
+
in the raw log. (Also bumps a doc count: the surface is 32 tools after 0.15.0's
|
|
302
|
+
dmaTrace→watch / patchGbHeader→romPatch consolidation; stale "34" references in
|
|
303
|
+
the docs + source comments updated.)
|
|
304
|
+
|
|
305
|
+
### Fixed
|
|
306
|
+
- **Warnings were OFF.** No C compiler was being asked for them. gcc (GBA/Genesis)
|
|
307
|
+
now compiles USER source with `-Wall -Wextra -Wno-unused-parameter` (the bundled
|
|
308
|
+
SDK stays warning-free so its noise doesn't bury the agent's); cc65 enables its
|
|
309
|
+
valid high-value `-W` set. So unused vars, implicit declarations, etc. are now
|
|
310
|
+
emitted and surfaced.
|
|
311
|
+
- **Swallowed errors now structured:** SDCC's keyword-less `file:line: syntax
|
|
312
|
+
error: …` and `warning NNN: …` (GB/GBC/SMS/MSX previously returned an empty
|
|
313
|
+
`issues[]` on a syntax error); the sdld/ASlink `Undefined Global '_x'` link
|
|
314
|
+
error; vasm errors (Genesis asm emits no stage marker, so they hit the
|
|
315
|
+
fallback, which had skipped the vasm parser); and a **missing `incbin` asset**
|
|
316
|
+
— the #1 thing an agent forgets to pass — now reports `could not open <x.bin>`
|
|
317
|
+
with the exact filename.
|
|
318
|
+
- **Fixed a build crash that ate the real error:** `build({output:'rom', path})`
|
|
319
|
+
fell into the source builder with no source and threw "Cannot read properties
|
|
320
|
+
of undefined (reading 'split')" instead of the compiler error; it now routes to
|
|
321
|
+
the project-dir builder like `output:'run'`/`'project'`.
|
|
322
|
+
- Verified live across all 14 platforms; a `parse-errors-coverage` test locks the
|
|
323
|
+
formats in. (Known limit: asar/SNES-asm only yields a wrapper "aborted"
|
|
324
|
+
message — its WASM build aborts without printing line info.)
|
|
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
|
+
|
|
70
359
|
## 0.14.0
|
|
71
360
|
|
|
72
361
|
**Two platform-specific top-level tools folded into their domain verbs, a
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Agents: the server delivers [`AGENTS.md`](./AGENTS.md) as connection-time instru
|
|
|
52
52
|
Most agents support MCP, but you don't have to use it. Run the server
|
|
53
53
|
(`npx romdevtools`) and **skip wiring it into your agent's MCP
|
|
54
54
|
config** — no `claude mcp add`, no `mcp.json` entry, no MCP client at all. The
|
|
55
|
-
same
|
|
55
|
+
same 32 tools are reachable over plain HTTP / as an Agent Skill against the
|
|
56
56
|
running server:
|
|
57
57
|
|
|
58
58
|
- **Plain HTTP:** `POST http://127.0.0.1:7331/tool/{name}` with the args as a JSON
|
package/examples/README.md
CHANGED
|
@@ -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
|
|