romdevtools 0.29.0 → 0.40.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 (103) hide show
  1. package/AGENTS.md +14 -5
  2. package/CHANGELOG.md +114 -12
  3. package/README.md +2 -1
  4. package/examples/gb/templates/tile_engine.c +1 -1
  5. package/examples/gbc/templates/tile_engine.c +1 -1
  6. package/examples/genesis/templates/two_plane_parallax.c +4 -4
  7. package/examples/nes/templates/tile_engine.c +1 -1
  8. package/package.json +14 -12
  9. package/src/analysis/analyze.js +263 -0
  10. package/src/analysis/decompile.js +108 -0
  11. package/src/analysis/decompiler/sleigh/6502.cspec +34 -0
  12. package/src/analysis/decompiler/sleigh/6502.ldefs +33 -0
  13. package/src/analysis/decompiler/sleigh/6502.pspec +16 -0
  14. package/src/analysis/decompiler/sleigh/6502.sla +3414 -0
  15. package/src/analysis/decompiler/sleigh/65816-snes.pspec +249 -0
  16. package/src/analysis/decompiler/sleigh/65816.cspec +17 -0
  17. package/src/analysis/decompiler/sleigh/65816.ldefs +15 -0
  18. package/src/analysis/decompiler/sleigh/65816.sla +23670 -0
  19. package/src/analysis/decompiler/sleigh/65c02.sla +4683 -0
  20. package/src/analysis/decompiler/sleigh/68000.cspec +67 -0
  21. package/src/analysis/decompiler/sleigh/68000.ldefs +68 -0
  22. package/src/analysis/decompiler/sleigh/68000.pspec +9 -0
  23. package/src/analysis/decompiler/sleigh/68000_register.cspec +118 -0
  24. package/src/analysis/decompiler/sleigh/68040.sla +81693 -0
  25. package/src/analysis/decompiler/sleigh/ARM.ldefs +377 -0
  26. package/src/analysis/decompiler/sleigh/ARM4t_le.sla +53758 -0
  27. package/src/analysis/decompiler/sleigh/ARM_v45.cspec +209 -0
  28. package/src/analysis/decompiler/sleigh/ARM_v45.pspec +40 -0
  29. package/src/analysis/decompiler/sleigh/ARMt_v45.pspec +43 -0
  30. package/src/analysis/decompiler/sleigh/HuC6280.cspec +67 -0
  31. package/src/analysis/decompiler/sleigh/HuC6280.ldefs +18 -0
  32. package/src/analysis/decompiler/sleigh/HuC6280.pspec +150 -0
  33. package/src/analysis/decompiler/sleigh/HuC6280.sla +7524 -0
  34. package/src/analysis/decompiler/sleigh/sm83.cspec +70 -0
  35. package/src/analysis/decompiler/sleigh/sm83.ldefs +29 -0
  36. package/src/analysis/decompiler/sleigh/sm83.pspec +19 -0
  37. package/src/analysis/decompiler/sleigh/sm83.sla +7695 -0
  38. package/src/analysis/decompiler/sleigh/z80.cspec +122 -0
  39. package/src/analysis/decompiler/sleigh/z80.ldefs +57 -0
  40. package/src/analysis/decompiler/sleigh/z80.pspec +43 -0
  41. package/src/analysis/decompiler/sleigh/z80.sla +20518 -0
  42. package/src/analysis/decompiler/wasm/decompile.js +2 -0
  43. package/src/analysis/decompiler/wasm/decompile.wasm +0 -0
  44. package/src/analysis/rizin.js +129 -0
  45. package/src/analysis/wasm/rizin.js +6032 -0
  46. package/src/analysis/wasm/rizin.wasm +0 -0
  47. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  48. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  49. package/src/cores/wasm/fceumm_libretro.js +1 -1
  50. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  51. package/src/cores/wasm/gambatte_libretro.js +1 -1
  52. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  53. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  54. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  55. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  56. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  57. package/src/cores/wasm/handy_libretro.js +1 -1
  58. package/src/cores/wasm/handy_libretro.wasm +0 -0
  59. package/src/cores/wasm/mgba_libretro.js +1 -1
  60. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  61. package/src/cores/wasm/prosystem_libretro.js +1 -1
  62. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  63. package/src/cores/wasm/snes9x_libretro.js +1 -1
  64. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  65. package/src/cores/wasm/stella2014_libretro.js +1 -1
  66. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  67. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  68. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  69. package/src/host/LibretroHost.js +25 -7
  70. package/src/http/routes.js +1 -1
  71. package/src/http/skill-doc.js +1 -1
  72. package/src/mcp/tools/cart-parts.js +5 -2
  73. package/src/mcp/tools/disasm.js +32 -5
  74. package/src/mcp/tools/font-map.js +3 -3
  75. package/src/mcp/tools/index.js +2 -2
  76. package/src/mcp/tools/memory.js +131 -24
  77. package/src/mcp/tools/project.js +1 -1
  78. package/src/mcp/tools/record.js +6 -7
  79. package/src/mcp/tools/reinject.js +1 -1
  80. package/src/mcp/tools/run-until.js +8 -2
  81. package/src/mcp/tools/symbols.js +10 -4
  82. package/src/mcp/tools/trace-vram-source.js +1 -1
  83. package/src/mcp/tools/watch-memory.js +50 -8
  84. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +80 -6
  85. package/src/platforms/atari2600/MENTAL_MODEL.md +6 -0
  86. package/src/platforms/atari7800/MENTAL_MODEL.md +6 -0
  87. package/src/platforms/c64/MENTAL_MODEL.md +6 -0
  88. package/src/platforms/gb/MENTAL_MODEL.md +6 -0
  89. package/src/platforms/gb/lib/c/README.md +1 -1
  90. package/src/platforms/gba/MENTAL_MODEL.md +7 -1
  91. package/src/platforms/gbc/MENTAL_MODEL.md +6 -0
  92. package/src/platforms/gbc/lib/c/README.md +1 -1
  93. package/src/platforms/genesis/MENTAL_MODEL.md +8 -2
  94. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  95. package/src/platforms/genesis/lib/wram.s +1 -1
  96. package/src/platforms/gg/MENTAL_MODEL.md +6 -0
  97. package/src/platforms/lynx/MENTAL_MODEL.md +6 -0
  98. package/src/platforms/msx/MENTAL_MODEL.md +6 -0
  99. package/src/platforms/nes/MENTAL_MODEL.md +6 -0
  100. package/src/platforms/pce/MENTAL_MODEL.md +6 -0
  101. package/src/platforms/sms/MENTAL_MODEL.md +6 -0
  102. package/src/platforms/snes/MENTAL_MODEL.md +10 -4
  103. package/src/toolchains/_worker/wasm-worker.js +5 -0
package/AGENTS.md CHANGED
@@ -65,12 +65,12 @@ Skip playtest only when there's clearly no human in the loop: CI runs, automated
65
65
  - `run` — load ROMs, step frames, screenshot (works for existing ROMs you didn't compile)
66
66
  - `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.
67
67
  - `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'})`)
68
- - `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") — relative compares (`inc`/`dec`/`changed`) work as the FIRST narrow (baselines recorded at seed), and `as:'bcd'`/`as:'digits'` search packed-BCD scores and digit-per-byte HUD buffers (any constant tile base) when stored ≠ displayed. **`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; small clusters carry before/after hex, `minDelta` filters churn); **`memory({op:'diffRuns', portsA, portsB?})`** answers "which byte does this INPUT drive?" in one call (same start state run twice under two inputs, only the divergent bytes return); `state({op:'diff'})` is the coarse whole-machine version. Reads routed to disk take `echo:false` to skip the inline hex.
69
- - `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; EVERY hit on EVERY platform carries `registersAtHit` — the register file frozen at the hit instant, the only honest read since live regs drift after a hit — and the CPU stays frozen until the hit is cleared), **`watch({on:'dma', precision:'sampled'})`** (Genesis: which ROM offset a VRAM graphic was DMA'd from), **`watch({on:'copy'})`** (ALL 14: every write landing in a VRAM window logged with the EXECUTING instruction's PC — the generic 'which routine uploads this graphic?'; port-based video memory hooked in-core incl. the SNES DMA path, CPU-mapped VRAM via the range log), **`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; banked carts — NES mappers, SNES LoROM, GB MBC, Sega mapper, MSX megaROM, 2600 F8/F6/F4, 7800 SuperGame, >32KB HuCards — are split and reference-scanned PER BANK, refs tagged `prgBank`/`romBank`), `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)
68
+ - `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") — relative compares (`inc`/`dec`/`changed`) work as the FIRST narrow (baselines recorded at seed), and `as:'bcd'`/`as:'digits'` search packed-BCD scores and digit-per-byte HUD buffers (any constant tile base) when stored ≠ displayed. **`memory({op:'searchUnknown'})`** is the unknown-initial-value hunt — seed the whole region with no value, then narrow by `dec`/`inc`/`changed` across events (the value you can't read off the HUD). **`memory({op:'readCart'})`** reads the loaded cart image to confirm a patch is live (pass `{cpuAddress, bank}` to read a banked CPU address on NES/SNES). **`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; small clusters carry before/after hex, `minDelta` filters churn, and predicate filters — `changeDir:'inc'|'dec'`, `deltaEq`, `beforeMin/Max`, `afterMin/Max` — keep only the bytes that moved the way you expect; `outputPath`+`echo:false` route the full list to disk); **`memory({op:'diffRuns', portsA, portsB?})`** answers "which byte does this INPUT drive?" in one call (same start state run twice under two inputs, only the divergent bytes return); `state({op:'diff'})` is the coarse whole-machine version. Reads routed to disk take `echo:false` to skip the inline hex.
69
+ - `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; EVERY hit on EVERY platform carries `registersAtHit` — the register file frozen at the hit instant, the only honest read since live regs drift after a hit — and the CPU stays frozen until the hit is cleared), **`watch({on:'dma', precision:'sampled'})`** (Genesis: which ROM offset a VRAM graphic was DMA'd from), **`watch({on:'copy'})`** (ALL 14: every write landing in a VRAM window logged with the EXECUTING instruction's PC — the generic 'which routine uploads this graphic?'; port-based video memory hooked in-core incl. the SNES DMA path, CPU-mapped VRAM via the range log), **`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; banked carts — NES mappers, SNES LoROM, GB MBC, Sega mapper, MSX megaROM, 2600 F8/F6/F4, 7800 SuperGame, >32KB HuCards — are split and reference-scanned PER BANK, refs tagged `prgBank`/`romBank`), plus the **Rizin/Ghidra RE engine** `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` (ALL 14 — control-flow graphs, deep xrefs, auto-detected functions, and Ghidra C pseudocode; quality excellent on GBA/Genesis, rough on 6502) + `symbols({op:'analyze'})` (one-shot structural map), `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)
70
70
  - `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'})`)
71
71
  - `project` — the example-game library (`examples`: list / fork / show, plus the legacy snippet ops)
72
72
  - `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
73
- - `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
73
+ - `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; runs to END OF FRAME and reports the LAST matching write with `hits`=count; `condition:'increase'|'decrease'|'equals'`+`conditionValue` filters to the MEANINGFUL write — the score going UP, not the per-frame restore churn (core-level on all 14, `oldValueByte` reported); `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
74
74
 
75
75
  **"Disassemble this NES ROM"** is now just: `disasm({target:'rom', path, startAddress, length})`. No discovery step.
76
76
 
@@ -108,8 +108,17 @@ questions, and running both early is normal.** A good default:
108
108
  cheats don't cover: `disasm({target:'project'})` for a rebuildable project,
109
109
  `disasm({target:'references'})` for "what touches this address", `breakpoint({on:'write'})` for the exact
110
110
  instruction that wrote a byte, `watch({on:'mem'})`/`breakpoint({on:'write',precision:'sampled'})` to find an address
111
- empirically. For a no-cheats game or a logic/text/graphics change, this is
112
- where the real work is start here, don't wait on a cheat lookup.
111
+ empirically. For STRUCTURE "what are the functions, how do they call each
112
+ other, what's the control flow"use the Rizin analysis ops: `symbols({op:'analyze'})`
113
+ for a one-shot map (functions + entrypoints), `disasm({target:'functions'})` for the
114
+ auto-detected function list, `disasm({target:'cfg', address})` for a function's basic-block
115
+ graph, `disasm({target:'xrefs', address})` for every cross-reference TO an address (deeper
116
+ than the da65 `references` scan — it follows the analysis graph). For C-like PSEUDOCODE,
117
+ `disasm({target:'decompile', address})` runs Ghidra's decompiler (carries the decompiler's
118
+ own WARNINGs; quality excellent on GBA/Genesis, good on GB/Z80, rough on 6502). All
119
+ Rizin/Ghidra analysis ops cover 14/14 platforms. For a no-cheats game or a
120
+ logic/text/graphics change, this is where the real work is — start here, don't wait on a
121
+ cheat lookup.
113
122
  4. **VERIFY before patching**: `memory({op:'write'})` the address live and watch the effect
114
123
  (cheat labels are *probable* — matched by name, not verified CRC; static
115
124
  "matches the pattern" ≠ "actually runs").
package/CHANGELOG.md CHANGED
@@ -4,6 +4,108 @@ 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.40.0 — 2026-06-11
8
+
9
+ ### Reverse-engineering analysis engine — control-flow graphs, deep xrefs, function detection, and a decompiler
10
+
11
+ A full open-source RE analysis layer, covering **all 14 platforms** with zero
12
+ proprietary dependencies. Two new binary packages carry the WebAssembly; the
13
+ main package gains four `disasm` targets and one `symbols` op that drive them.
14
+
15
+ - **`disasm({target:'functions'})`** — auto-detected function list
16
+ (`{address, size, nbbs, cc, callers, callees}`): the structural map of an
17
+ unknown ROM, the carve step before you label anything live.
18
+ - **`disasm({target:'cfg', address})`** — basic-block control-flow graph of the
19
+ function at `address` (nodes + typed edges: jump / branch_true / branch_false).
20
+ - **`disasm({target:'xrefs', address})`** — every cross-reference TO `address`,
21
+ following the analysis graph. Deeper than the flat `target:'references'` da65
22
+ operand scan — prefer `xrefs` once a function pass has run, `references` for a
23
+ quick header-less sweep.
24
+ - **`disasm({target:'decompile', address})`** — Ghidra C-like **pseudocode** for
25
+ the function at `address`, with the decompiler's own warnings and a per-CPU
26
+ `qualityNote`. Altitude rule: decompile is for UNDERSTANDING (and as a port
27
+ spec when retargeting to a bigger machine) — `target:'project'` stays the
28
+ byte-exact rebuildable edit path. Quality is excellent on ARM (GBA) and M68K
29
+ (Genesis), good on SM83 (GB/GBC) and Z80 (SMS/GG/MSX), medium on 65816 (SNES)
30
+ and HuC6280 (PC Engine), and rough on the 6502 family (an architecture limit —
31
+ every tool is rough on 6502).
32
+ - **`symbols({op:'analyze'})`** — one-shot structural map of a ROM
33
+ (auto-detected functions + strings + entrypoints), no `.dbg`/`.map` needed.
34
+
35
+ Built from pinned upstreams, fetch-on-demand, never vendored — only the compiled
36
+ artifacts ship (see `scripts/build-rizin.sh`, `scripts/build-decompiler.sh`,
37
+ `scripts/versions.json`):
38
+ - **`romdev-analysis`** — Rizin compiled to WASM (the CFG / xrefs / functions
39
+ engine). LGPL-3.0.
40
+ - **`romdev-analysis-decompiler`** — Ghidra's C++ decompiler compiled to WASM
41
+ (no JVM, no rizin) plus SLEIGH processor tables for all 14 CPUs. Apache-2.0,
42
+ with full per-component attribution in the package NOTICE (Ghidra/NSA, and the
43
+ community SM83 / 65816 / HuC6280 SLEIGH specs).
44
+
45
+ ### Documentation: no commercial game titles in shipped source
46
+
47
+ Swept every shipped tool description, doc, README, mental-model guide, and the
48
+ ROM-hacking playbook for commercial game/franchise names and replaced them with
49
+ generic platform + hardware + mechanic descriptions ("a banked NES racer", "a
50
+ top-down dungeon-crawler shape"). Console and chip names are unchanged — they're
51
+ not anyone's IP. The bundled cheat database (third-party crowd-sourced data) is
52
+ unaffected.
53
+
54
+ ### Tests build their own ROMs (no external fixtures)
55
+
56
+ Tests that needed a real ROM now build one from our **own example sources** at
57
+ runtime instead of depending on a ROM file on disk — so the whole suite runs
58
+ with no external/commercial ROM anywhere. Every previously-skipped fixture-gated
59
+ test now runs: **918 tests, 918 pass, 0 fail, 0 skipped**. (This also surfaced a
60
+ latent fidelity bug the silent skips had hidden: `wrapRomFromParts` dropped the
61
+ iNES battery-SRAM flag on round-trip — now preserved via a new `hasBattery`
62
+ field, and exposed on the `wrapRomFromParts` tool.)
63
+
64
+ ## 0.30.0 — 2026-06-11
65
+
66
+ ### RE-tooling round — the Cheat-Engine "locate a value + find its writer" workflow
67
+ From an NES reverse-engineering feedback batch. Six additions to the
68
+ memory/breakpoint primitives:
69
+ - **`memory({op:'searchUnknown'})`** — the unknown-initial-value hunt: seed the
70
+ WHOLE region with no value, then narrow across in-game events with
71
+ `searchNext({compare:'dec'|'inc'|'unchanged'|'changed'|'gt'|'lt'})`. Finds the
72
+ lives/timer/ammo address you can't see on the HUD (`op:'search'` needs a value;
73
+ this doesn't).
74
+ - **`memory({op:'diff'})` predicate filters** — `changeDir:'dec'|'inc'`,
75
+ `deltaEq:N` (signed exact delta, e.g. −1 = "lost a life"), `beforeMin/Max` +
76
+ `afterMin/Max` (value-range gates). A 500-byte death-window diff returns the
77
+ ~3 rows you want in one call.
78
+ - **`memory({op:'diff'})` honors `outputPath` + `echo:false`** (was a bug — diff
79
+ ignored `outputPath`; a big diff now routes to your path, not a harness path).
80
+ - **`memory({op:'readCart', cpuAddress, bank})`** — read cart ROM by a BANKED CPU
81
+ address (NES/SNES), the inverse of the breakpoint result's bank/prgOffset; no
82
+ more hand-computed `cpuAddr−0x8000+bank*0x4000`.
83
+ - **`breakpoint({on:'write', condition})`** — stop on the MEANINGFUL write, not
84
+ restoring churn: `condition:'increase'|'decrease'` (the stored byte actually
85
+ moved that way) or `'equals'` + `conditionValue` (became N, e.g. a $00→$01
86
+ re-arm). Reports `oldValueByte`→`valueByte`. Live on **all 14 platforms** (11
87
+ emulator cores rebuilt to capture the pre-write byte). Also clarified in the
88
+ tool note that `on:'write'` runs to end-of-frame and reports the LAST matching
89
+ write (`hits` = count).
90
+ - **Tool-schema slim** — dropped the inlined ~62-value region enum from every
91
+ SECONDARY region sub-param (`watch` per-range `ranges[].region`, `recordSession`
92
+ `memorySamples[].region`, and both `runUntil` memory conditions); they're now
93
+ runtime-validated strings, trimming the deferred-load schema cost the feedback
94
+ flagged. The ONE primary discoverable enum (`watch` on:'mem' single-range
95
+ `region`, where the region IS the choice) is intentionally kept. **Bonus fix:**
96
+ the `runUntil` region was a STALE hardcoded 8-value list that silently
97
+ schema-rejected valid non-NES regions (`genesis_cram`, `c64_color_ram`,
98
+ `nes_apu_regs`, …) the handler actually supports — now accepted.
99
+
100
+ ### Core rebuilds
101
+ 11 emulator cores rebuilt for the value-conditioned write breakpoint (bump +
102
+ republish each): fceumm, snes9x, genesis-plus-gx, gambatte, mgba, handy,
103
+ geargrafx, prosystem, stella2014, bluemsx, vice. **Build fix (latent, was on
104
+ main):** the bluemsx region patch carried a duplicate Makefile CFLAGS hunk
105
+ identical to the build patch's; since `git apply` is atomic, that made the whole
106
+ region patch silently fail to apply — the watchpoint/region exposure was being
107
+ dropped from the build. Removed the duplicate hunk.
108
+
7
109
  ## 0.29.0 — 2026-06-11
8
110
 
9
111
  ### Examples — the complete-game library, finished & made honest
@@ -119,7 +221,7 @@ three guarantees now hold across the whole platform matrix:
119
221
  - `test/regsnap-all-cores.test.js`: live single-step snapshot + freeze proof
120
222
  on 10 platforms (plus the existing gpgx suite for Genesis/SMS/GG).
121
223
 
122
- ### Fixed/Added — gpgx core round (the NBA-Jam-both-consoles feedback): break-instant truth on Genesis/SMS/GG
224
+ ### Fixed/Added — gpgx core round (a both-consoles sports-title feedback): break-instant truth on Genesis/SMS/GG
123
225
  The first core rebuild in this release (gpgx only; pins unchanged, patch extended).
124
226
  - **`registersAtHit` on Genesis/SMS/GG** — `breakpoint({on:'pc'|'write'|'read'})`
125
227
  hits now carry the FULL register file (m68k d0-d7/a0-a7/pc/sr/sp; z80
@@ -288,7 +390,7 @@ of repeated logic errors. The big ones:
288
390
  respond to input, and each platform's audio was captured and RMS-checked.
289
391
  (Atari 2600 has no puzzle genre by design.)
290
392
 
291
- ### Fixed/Added — the 0.27.0 Zanac RE feedback round (banked-NES rebuilds, A/B diff, token cuts)
393
+ ### Fixed/Added — the 0.27.0 NES RE feedback round (banked-NES rebuilds, A/B diff, token cuts)
292
394
  - **Banked NES `disasm({target:'project'})` now emits COMPLETE, working rebuild
293
395
  glue** (the headline ask): a `HEADER` segment with the original 16 iNES bytes,
294
396
  a per-bank `PRGn` segment wrapper for every bank, a multi-bank `nes_rebuild.cfg`
@@ -362,7 +464,7 @@ defaulted to native; this is the docs telling the truth about it.)
362
464
  ## 0.27.0
363
465
 
364
466
  ### Added — `breakpoint(on:'pc', captureMemory:[…])` reads named RAM at the hit
365
- Completes item 2 of the NES Rygar report. 0.26.0 shipped `registersAtHit` (the
467
+ Completes item 2 of an NES action-game RE report. 0.26.0 shipped `registersAtHit` (the
366
468
  break-instant register file) but not the memory half. Now `breakpoint(on:'pc')`
367
469
  takes `captureMemory:[{region,offset,length,label}]` and returns those reads inline
368
470
  as `capturedMemory`, so register + RAM inspection at a PC collapses into ONE call —
@@ -373,7 +475,7 @@ hit frame (stable + what RE needs), documented as such.
373
475
  ## 0.26.0
374
476
 
375
477
  ### Fixed — NES `breakpoint(on:'pc')` now returns reliable break-instant registers
376
- An agent RE'ing NES Rygar found that after a `pc` breakpoint hit, a follow-up
478
+ An agent RE'ing an NES action game found that after a `pc` breakpoint hit, a follow-up
377
479
  `cpu({op:'read'})` returned the **idle-loop PC**, not the breakpoint instruction —
378
480
  the documented "break, then read the live register file" workflow gave end-of-frame
379
481
  state. Root cause: fceumm drains the cycle budget on hit but `retro_run` still
@@ -402,7 +504,7 @@ own frame correctly. Null until a ROM is loaded.
402
504
  ## 0.25.0
403
505
 
404
506
  ### Added — C64 input scripting + verification (RE startup-flow telemetry)
405
- Follow-up to the 0.24.0 C64 keyboard work: an agent RE'ing C64 Uridium could now
507
+ Follow-up to the 0.24.0 C64 keyboard work: an agent RE'ing a C64 shoot-'em-up could now
406
508
  press keys, but couldn't (a) script a keyboard+joystick startup TIMELINE in one
407
509
  call, or (b) tell whether a non-responsive key reached VICE at all. Both added — no
408
510
  core rebuild (the `c64_cia1_regs` region + key matrix already existed):
@@ -475,7 +577,7 @@ or the tool list); full release notes remain in CHANGELOG.md for humans.
475
577
  ## 0.24.0
476
578
 
477
579
  ### Added — C64 keyboard + joyport input (VICE core patch)
478
- An agent RE'ing C64 Uridium could reach the intro via joystick but couldn't ENTER
580
+ An agent RE'ing a C64 shoot-'em-up could reach the intro via joystick but couldn't ENTER
479
581
  gameplay — the game needs **F1** (1 player) + fire on **port 2**, and romdev's
480
582
  input was joypad-mask-only. Many C64 games gate gameplay behind KEYBOARD setup
481
583
  screens that joystick can't reach. The VICE core now exports
@@ -557,7 +659,7 @@ training agents to ignore lint. Now: provably-VRAM dest → warning, plain RAM a
557
659
  repeated starfield + player sprite, hardware scroll only, zero loop-time tilemap
558
660
  writes. Builds clean, renders, scrolls (verified).
559
661
  - Genesis MENTAL_MODEL/TROUBLESHOOTING: "do NOT rewrite tilemaps in the frame
560
- loop", logical-vs-hardware plane size, the correct parallax loop, Sonic-style
662
+ loop", logical-vs-hardware plane size, the correct parallax loop, large-scroller-style
561
663
  column streaming, and a "why does movement feel choppy?" recipe.
562
664
 
563
665
  ### Changed — discoverability (the recurring root cause)
@@ -975,7 +1077,7 @@ savestate) is now fully supported, with NO new top-level tool:
975
1077
  - **Honest "no save":** empty `save_ram` now says *why* — "this cart has no battery
976
1078
  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 +
977
1079
  core source: no core patches were needed; earlier "broken" readings were
978
- password-game test carts like Metroid, which correctly have no battery.)
1080
+ password-based NES carts, which correctly have no battery.)
979
1081
 
980
1082
  ### Fixed / Added — v0.15.0 session feedback
981
1083
  - **`state` file `path` resolution.** A RELATIVE `path` (save/load/export) used to
@@ -1187,7 +1289,7 @@ discoverable rename table).
1187
1289
  searches only the widest form. The suppression matches on offset-overlap AND
1188
1290
  pointer-value (so two coincidentally co-located but distinct pointers are never
1189
1291
  falsely merged). The other 12 platforms emit a single width, so this is a
1190
- verified no-op there. (NBA-Jam-TE agent nit: 20 hits → the 10 distinct
1292
+ verified no-op there. (sports-title agent nit: 20 hits → the 10 distinct
1191
1293
  relocation handles, no hand-dedupe.)
1192
1294
  - **`cpu({op:'call'})` watchdog now trips on a wrong-entry free-run, not just a
1193
1295
  tight loop — on EVERY CPU.** Two cross-system gaps fixed:
@@ -1204,7 +1306,7 @@ discoverable rename table).
1204
1306
  just `m68k_run`** — so it actually fires on **SMS/GG** (where the Z80 is the
1205
1307
  active CPU). Before, `callSubroutine` armed a watchdog that could never trip
1206
1308
  on SMS/GG (the counter only incremented on the m68k), so a Z80 free-run fell
1207
- to `maxFrames`. Requires the rebuilt `romdev-core-gpgx` WASM. (NBA-Jam-TE
1309
+ to `maxFrames`. Requires the rebuilt `romdev-core-gpgx` WASM. (sports-title
1208
1310
  agent nit, generalized to all 14 platforms.)
1209
1311
 
1210
1312
  ## 0.10.0
@@ -1282,7 +1384,7 @@ Requires the bumped core packages.
1282
1384
 
1283
1385
  ## 0.7.0
1284
1386
 
1285
- Reverse-engineering follow-ups from the NBA Jam (Genesis) agent's decompress
1387
+ Reverse-engineering follow-ups from a Genesis sports-title agent's decompress
1286
1388
  feedback. Genesis reference for the hang-fix (the watchdog is a core hook — it
1287
1389
  fans out to every core in 0.8.0); the JS-layer fixes (watchDma, previewTileArt)
1288
1390
  are all-platform.
@@ -1521,7 +1623,7 @@ sprite-inspection bug fix.
1521
1623
  ## 0.3.0
1522
1624
 
1523
1625
  The reverse-engineering / romhacking release — a full RE toolkit driven by a real
1524
- NBA Jam Tournament Edition (Genesis) session, plus PC Engine + MSX cheat coverage
1626
+ a Genesis sports-title session, plus PC Engine + MSX cheat coverage
1525
1627
  and a cleaner package split.
1526
1628
 
1527
1629
  ### Added — RE / romhacking toolkit
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # romdev
2
2
 
3
- The entry point for **romdev** — vibe-code real retro games. Lets a coding agent build, run, and inspect actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, and more) with one command.
3
+ The entry point for **romdev** — vibe-code real retro games. Build, run, inspect, and reverse-engineer actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, and more) with one command — drive it yourself or let a coding assistant do it.
4
4
 
5
5
  ```bash
6
6
  npx romdevtools
@@ -31,6 +31,7 @@ This package contains all the JavaScript — the tool surface, the WASM emulator
31
31
  - Cores: `romdev-core-{fceumm,gambatte,gpgx,vice,handy,prosystem,geargrafx,bluemsx}`
32
32
  - Platforms: `romdev-platform-{snes,gba,atari2600}`
33
33
  - Toolchains: `romdev-toolchain-{cc65,sdcc,m68k-gcc,vasm,rgbds}`
34
+ - Analysis: `romdev-analysis` (Rizin → WASM: control-flow graphs, cross-references, function detection) and `romdev-analysis-decompiler` (Ghidra's C++ decompiler → WASM + SLEIGH processor specs for all 14 CPUs). Power `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. Lazy-loaded on first use.
34
35
  - Data: `romdev_game_codes` — the bundled game-code / cheat database (a free labeled RAM/code map for thousands of known ROMs), split out so it can grow independently. Lazy-loaded one platform at a time.
35
36
 
36
37
  `@kmamal/sdl` is used only by `playtest()` / `romdevtools-cli play` (the live window). It ships its native binary via its own install script, which npm skips when romdev is a transitive dep (e.g. under `npx`) — so romdev's `postinstall` fetches it, and `playtest()` also self-heals at runtime if the binary is still missing (downloading the prebuilt before the first window open). Either way, if the binary can't be fetched (offline/locked-down network), the headless server is unaffected — only the live window degrades, and the error tells you the one command to fix it.
@@ -1,6 +1,6 @@
1
1
  /* ── tile_engine.c — GBC starter with a tile map + multiple rooms ──
2
2
  *
3
- * Single-screen-per-room layout (Adventure / Zelda-1 / Sokoban shape).
3
+ * Single-screen-per-room layout (top-down dungeon-crawler / room-puzzle shape).
4
4
  * - 20×18 BG map rendered from a `room[]` array
5
5
  * - Walk a sprite around with the d-pad
6
6
  * - Crossing the screen edge transitions to the next room
@@ -1,6 +1,6 @@
1
1
  /* ── tile_engine.c — GBC starter with a tile map + multiple rooms ──
2
2
  *
3
- * Single-screen-per-room layout (Adventure / Zelda-1 / Sokoban shape).
3
+ * Single-screen-per-room layout (top-down dungeon-crawler / room-puzzle shape).
4
4
  * - 20×18 BG map rendered from a `room[]` array
5
5
  * - Walk a sprite around with the d-pad
6
6
  * - Crossing the screen edge transitions to the next room
@@ -1,6 +1,6 @@
1
1
  /* ── two_plane_parallax.c — Genesis SGDK two-plane parallax scaffold ──
2
2
  *
3
- * The "Uridium / Sonic-feel" starting point: a side-scrolling world that
3
+ * A smooth-scrolling side-scroller starting point: a side-scrolling world that
4
4
  * moves SMOOTHLY because the frame loop does HARDWARE SCROLL ONLY. There
5
5
  * are ZERO tilemap writes inside the loop — the two planes are painted
6
6
  * ONCE at setup, and every frame we just nudge two scroll registers and
@@ -30,11 +30,11 @@
30
30
  * plane — you don't get an independent per-plane size. See the Genesis
31
31
  * MENTAL_MODEL.md "Scrolling, parallax & the feel trap".
32
32
  *
33
- * To go WIDER than 512 px (a true Sonic-size level) you keep this exact
33
+ * To go WIDER than 512 px (a large multi-screen level) you keep this exact
34
34
  * loop and add ONE thing: stream the single offscreen column that's about
35
35
  * to scroll into view each time the camera crosses an 8-px tile boundary
36
36
  * (a circular buffer in the 64-cell plane) — NOT a whole-plane redraw.
37
- * See MENTAL_MODEL.md "How Sonic-style large maps really work".
37
+ * See MENTAL_MODEL.md "How large scrolling maps REALLY work".
38
38
  */
39
39
 
40
40
  #include <genesis.h>
@@ -94,7 +94,7 @@ int main(bool hard) {
94
94
  (void)hard;
95
95
 
96
96
  /* Palettes (BGR, 3 bits/chan). PAL0 = sprite, PAL1 = planes. */
97
- PAL_setColor(0 + 5, 0x008E); /* player orange (Uridium-ish) */
97
+ PAL_setColor(0 + 5, 0x008E); /* player orange */
98
98
  PAL_setColor(16 + 1, 0x0A86); /* ground top (light) */
99
99
  PAL_setColor(16 + 2, 0x0530); /* ground body (dark) */
100
100
  PAL_setColor(16 + 3, 0x0CCC); /* block edge (white) */
@@ -1,6 +1,6 @@
1
1
  /* ── tile_engine.c — NES starter with a tile map + multiple rooms ──
2
2
  *
3
- * Single-screen-per-room layout (Adventure / Zelda-1 / Sokoban shape).
3
+ * Single-screen-per-room layout (top-down dungeon-crawler / room-puzzle shape).
4
4
  * - 32×30 BG nametable rendered from a `room[]` array
5
5
  * - Walk a sprite around with the d-pad
6
6
  * - Crossing the screen edge transitions to the next room
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "romdevtools",
3
- "version": "0.29.0",
3
+ "version": "0.40.0",
4
4
  "description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 14 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
@@ -50,19 +50,21 @@
50
50
  "fuse.js": "^7.4.1",
51
51
  "omggif": "^1.0.10",
52
52
  "pngjs": "^7.0.0",
53
- "romdev-core-bluemsx": "0.5.0",
54
- "romdev-core-fceumm": "0.9.0",
55
- "romdev-core-gambatte": "0.8.0",
56
- "romdev-core-geargrafx": "0.6.0",
57
- "romdev-core-gpgx": "0.11.0",
58
- "romdev-core-handy": "0.6.0",
59
- "romdev-core-prosystem": "0.7.0",
60
- "romdev-core-vice": "0.8.0",
53
+ "romdev-analysis": "0.1.0",
54
+ "romdev-analysis-decompiler": "0.1.0",
55
+ "romdev-core-bluemsx": "0.6.0",
56
+ "romdev-core-fceumm": "0.10.0",
57
+ "romdev-core-gambatte": "0.9.0",
58
+ "romdev-core-geargrafx": "0.7.0",
59
+ "romdev-core-gpgx": "0.12.0",
60
+ "romdev-core-handy": "0.7.0",
61
+ "romdev-core-prosystem": "0.8.0",
62
+ "romdev-core-vice": "0.9.0",
61
63
  "romdev-famitone": "0.1.0",
62
64
  "romdev-maxmod": "0.1.0",
63
- "romdev-platform-atari2600": "0.7.0",
64
- "romdev-platform-gba": "0.7.0",
65
- "romdev-platform-snes": "0.7.0",
65
+ "romdev-platform-atari2600": "0.8.0",
66
+ "romdev-platform-gba": "0.8.0",
67
+ "romdev-platform-snes": "0.8.0",
66
68
  "romdev-toolchain-cc65": "0.1.1",
67
69
  "romdev-toolchain-m68k-gcc": "0.2.0",
68
70
  "romdev-toolchain-rgbds": "0.1.0",