romdevtools 0.40.2 → 0.42.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 (55) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +1 -1
  4. package/package.json +3 -1
  5. package/src/analysis/analyze.js +320 -48
  6. package/src/analysis/backtrace.js +198 -0
  7. package/src/analysis/nes-ppu-runtime.js +220 -0
  8. package/src/analysis/nes-ppu-shim.js +263 -0
  9. package/src/analysis/pointer-table.js +84 -0
  10. package/src/analysis/recompile-65816.js +584 -0
  11. package/src/analysis/rizin.js +13 -1
  12. package/src/cheats/gamegenie.js +14 -2
  13. package/src/cores/capabilities.js +218 -0
  14. package/src/cores/registry.js +43 -51
  15. package/src/mcp/state.js +91 -2
  16. package/src/mcp/tools/cheats.js +10 -1
  17. package/src/mcp/tools/disasm.js +331 -24
  18. package/src/mcp/tools/find-references.js +93 -2
  19. package/src/mcp/tools/frame.js +440 -4
  20. package/src/mcp/tools/index.js +34 -1
  21. package/src/mcp/tools/lifecycle.js +53 -24
  22. package/src/mcp/tools/memory.js +1 -1
  23. package/src/mcp/tools/platform-tools.js +17 -5
  24. package/src/mcp/tools/platforms.js +18 -3
  25. package/src/mcp/tools/playtest.js +24 -1
  26. package/src/mcp/tools/project.js +1 -1
  27. package/src/mcp/tools/rendering-context.js +9 -6
  28. package/src/mcp/tools/watch-memory.js +231 -11
  29. package/src/mcp/util.js +82 -0
  30. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +23 -8
  31. package/src/playtest/playtest.js +115 -46
  32. package/src/toolchains/_worker/pool.js +41 -3
  33. package/src/toolchains/cc65/da65.js +7 -0
  34. package/src/cores/wasm/bluemsx_libretro.js +0 -2
  35. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  36. package/src/cores/wasm/fceumm_libretro.js +0 -2
  37. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  38. package/src/cores/wasm/gambatte_libretro.js +0 -2
  39. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  40. package/src/cores/wasm/geargrafx_libretro.js +0 -2
  41. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  42. package/src/cores/wasm/genesis_plus_gx_libretro.js +0 -2
  43. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  44. package/src/cores/wasm/handy_libretro.js +0 -2
  45. package/src/cores/wasm/handy_libretro.wasm +0 -0
  46. package/src/cores/wasm/mgba_libretro.js +0 -2
  47. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  48. package/src/cores/wasm/prosystem_libretro.js +0 -2
  49. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  50. package/src/cores/wasm/snes9x_libretro.js +0 -2
  51. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  52. package/src/cores/wasm/stella2014_libretro.js +0 -2
  53. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  54. package/src/cores/wasm/vice_x64_libretro.js +0 -2
  55. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
package/AGENTS.md CHANGED
@@ -66,11 +66,11 @@ Skip playtest only when there's clearly no human in the loop: CI runs, automated
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
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)
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'|'resolveJumptable'})` (ALL 14 — control-flow graphs, deep xrefs, auto-detected functions [sorted real-code-first, `looksLikeData` flagged], and Ghidra C pseudocode; quality excellent on GBA/Genesis, rough on 6502; decompile output NAMES hardware registers [`PPUMASK` not `*0x2001`] and on the 6502 family folds SLEIGH clutter to readable C99 [`uint8_t`, `zp_FD`]; `resolveJumptable` resolves computed dispatchers live via `breakpoint({on:'jumptable'})`) + `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; 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
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), **`breakpoint({on:'jumptable'})`** (RESOLVE a computed-jump dispatcher static analysis can't follow — `JMP (table,X)` / RTS-trick state machines, script/battle VMs: break at the dispatcher, single-step through the indirect transfer, record the COMPUTED targets live across frames/inputs; the varying arms are isolated from fixed trampolines; `disasm({target:'resolveJumptable'})` points here. No static-only tool can do this — it needs the live emulator), **`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
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,150 @@ 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.42.0 — 2026-06-24
8
+
9
+ ### v0.41.0 feedback batch + the NES→SNES port engine
10
+
11
+ A round driven by RE-session feedback (cheats, ergonomics, the human-playtest
12
+ eviction case) plus the flagship port-engine work.
13
+
14
+ #### Fixed — `cheats` raw RAM codes were inert
15
+
16
+ `cheats({op:'make'})` for a sub-`$0100` RAM address emitted the short `32:09`
17
+ form, which libretro cores PARSE but never bind — and `apply` then falsely
18
+ reported `applied:true`. `encodeRaw` now pads the address to a binding width
19
+ (`0032:09`); `apply` re-encodes a hand-typed short code too. Verified on fceumm.
20
+
21
+ #### New — hex-string forms on address params
22
+
23
+ A hex literal in a JSON arg (`address: 0xC06C`) was a hard parse error. All
24
+ address-like params (`address`/`offset`/`pc`/`compare`/…) now accept the STRING
25
+ forms `"0xC06C"` / `"$C06C"` / decimal strings, coerced before validation —
26
+ centralized so every tool gets it.
27
+
28
+ #### New — `catalog({op:'status'}).capabilities`
29
+
30
+ A map of which debug ops the loaded core/toolchain implement (pcBreakpoint,
31
+ watchpointExact, rangeWatch, cheats, da65Toolchain, …) so an agent picks a
32
+ working trace strategy up front instead of probing by failure.
33
+
34
+ #### New — `callStack` on breakpoint hits (13 of 14 platforms)
35
+
36
+ `breakpoint({on:'pc'|'write'})` hits carry a decoded call stack: the server walks
37
+ the stack from the captured stack pointer and returns each caller PC. Covers the
38
+ 6502 family (JSR-opcode validated), m68k (Genesis — 4-byte BE return), and Z80/
39
+ SM83 (SMS/GG/MSX/GB/GBC — 2-byte LE return). GBA (ARM) is the lone exclusion: its
40
+ calls return through the link register, not the stack.
41
+
42
+ #### New — `references` finds inline jump-table / trampoline call sites
43
+
44
+ When no `jsr/jmp/branch` names a CODE address (it's reached via a computed jump),
45
+ `references` now scans the raw ROM for the address as a 16-bit pointer (LE/BE) and
46
+ reports `tableHits`. Per-platform header skip (NES/SNES/7800/Lynx); the 6502
47
+ RTS-trick `addr-1` form is scanned only on the 6502 family.
48
+
49
+ #### New — `disasm({target:'pointerTable'})` (all mapped platforms)
50
+
51
+ Static index→handler decode of a jump/pointer table: contiguous `dw`, SPLIT
52
+ lo/hi arrays at two bases, the RTS-trick (`+1`), and a REVERSE lookup (handler →
53
+ dispatch index). The static complement to the live `breakpoint({on:'jumptable'})`.
54
+ Works on every platform with an address mapper (nes/snes/sms/gg/gb/gbc/2600/7800/
55
+ c64/genesis); the endian default follows the CPU (Genesis/m68k → BE).
56
+
57
+ #### New — playtest eviction survivability
58
+
59
+ While a playtest window is open, romdev rolls a `.state` to disk every ~15s (and
60
+ on F2), so a session eviction can't lose a human's manual progress — the recovery
61
+ hint points at `state({op:'load', path})`. Surfaced in `playtest({op:'status'})`.
62
+
63
+ #### Leaner schemas
64
+
65
+ The last inline 62-value region enum in the watch/breakpoint tool schemas is now
66
+ a runtime-validated string — the full enum lives once on `memory`'s `region`.
67
+
68
+ #### Port engine — NES→SNES static recompile renders + plays
69
+
70
+ `disasm({target:'recompile'})` now draws the original ROM's screen on SNES
71
+ (`withShim`, phase-1 static — fixed the `cpx.w` upload bug) and animates sprites
72
+ + runs the game's NMI each vblank (`withRuntime`, phase-2 live). The recompiler
73
+ follows the real reset vector and anchors the entry to the routine's first
74
+ instruction. (Plus the `frame` port-compare oracles: compareRam/compareRender/
75
+ findDiverge/portStatus + side-by-side two-core capture.)
76
+
77
+ ## 0.41.0 — 2026-06-12
78
+
79
+ ### RE engine round — bank-aware decompile, live jumptable recovery, readable 6502 output
80
+
81
+ A correctness + readability pass across the whole reverse-engineering engine,
82
+ plus the differentiator no static tool has: **resolving computed jumps with the
83
+ live emulator.** All 14 platforms; no `romdev-analysis*` package changes (the
84
+ work is in the address-mapping JS, the decompile post-passes, and the live-debug
85
+ tools).
86
+
87
+ #### New — `breakpoint({on:'jumptable', address})`: live computed-jumptable recovery
88
+
89
+ Static analysis follows direct addressing only, so a game's *hottest* routines —
90
+ state machines, script / event VMs, battle engines that dispatch through
91
+ `JMP (table,X)` or an RTS-trick — decompile to `(*_IRQ)()` + "Could not recover
92
+ jumptable." romdev has a **live emulator**, so it resolves them dynamically: break
93
+ at the dispatcher, single-step through the indirect transfer, and record the PC
94
+ it actually lands on — accumulated across frames/inputs. Fixed trampolines (the
95
+ compiler's pointer-call shim, return paths) are filtered out by what *doesn't*
96
+ vary; the destinations that vary hit-to-hit are the real switch arms, ranked by
97
+ hit count. Drive more game states (`pressDuring` / `fromState`) to surface rarer
98
+ arms. **No standalone tool (IDA / Ghidra / Binary Ninja) can do this** — it needs
99
+ an emulator in the loop. `disasm({target:'resolveJumptable', address})` is the
100
+ static-side alias that redirects to it.
101
+
102
+ #### New — `disasm({target:'decompile'})` reads cleaner
103
+
104
+ - **Hardware registers are named.** MMIO refs Ghidra emits as raw addresses
105
+ (`*0x2001`, `uRAM400e`) become the register name (`PPUMASK`, `NOISE_LO`), with a
106
+ `/* hw registers: … */` legend — on the 9 platforms with a register map
107
+ (NES/SNES/Genesis/GB/GBC/SMS/GG/2600/7800/C64).
108
+ - **6502 SLEIGH clutter folds to readable C** (NES/2600/7800/C64/Lynx/PCE). Width
109
+ types become C99 stdint (`uint1`→`uint8_t`, `uint2`→`uint16_t`), redundant
110
+ nested casts collapse (`(uint16_t)(uint8_t)x`→`(uint8_t)x`), and zero-page byte
111
+ refs are named (`cRAM00fd`→`zp_FD`), with a `/* 6502 fold: … */` legend. A real
112
+ banked NES function went from `*(xunknown1 *)(uint2)(uint1)(param_2 - 0xb)` to
113
+ `*(uint8_t *)(zp_FE - 0xb)` — same semantics, far more readable. (The
114
+ carry-flag-16-bit / BCD reconstruction is left to an LLM reading the output;
115
+ rewriting it textually would risk changing semantics.)
116
+
117
+ #### Improved — bank-aware decompile + honest function ranking
118
+
119
+ - **Banked NES `decompile` resolves the bank.** Rizin reports flat-PRG VAs, so a
120
+ flat decode was bank-blind (cross-bank `JSR`/`JMP` landed on the wrong bank).
121
+ `decompile` now lays a real 32 KB CPU window (selected bank @ `$8000` + fixed
122
+ top bank @ `$C000`) so in-bank *and* fixed-bank calls resolve; NROM falls
123
+ through to the flat path. On a real banked game this moved a top-12 function
124
+ list from ~1 readable / 11 garbage to ~10 readable / 2.
125
+ - **`disasm({target:'functions'})` is ranked real-code-first** with a
126
+ `looksLikeData` flag (+ `dataCount`), so giant single-block data folds stop
127
+ crowding out the actual control-flow routines you want.
128
+
129
+ #### Fixed / hardened
130
+
131
+ - **SMD-interleaved Genesis dumps auto-deinterleave.** A `.bin` in the SMD copier
132
+ format (size = N·16 KB + 512, `0xAA 0xBB` magic) read flat decodes to pure
133
+ "bad instruction" garbage; analysis now detects + reverses the interleave and
134
+ warns, so a flat disasm isn't silently wrong.
135
+ - **C64 `.prg` load-address header is stripped** before analysis (the 2-byte load
136
+ address was being analyzed as code), with the base applied so addresses line up.
137
+ - **Worker-pool timeout + recycle.** A whole-ROM `aaa` on a multi-MB ROM that
138
+ never returns no longer wedges the shared WASM analysis pool — the call times
139
+ out, the worker is killed + respawned, and a clean `{ timedOut }` result comes
140
+ back (with a "use a scoped pass" hint) instead of every later `disasm` hanging
141
+ until a manual server restart.
142
+
143
+ #### New op discovery
144
+
145
+ - **`platform({op:'capabilities', platform?})`** — the per-platform op-support
146
+ matrix (CPU family, rendering kind, which introspection/debug ops each core
147
+ actually wires), so an agent can check support before calling instead of
148
+ catching a failure. Unsupported ops now throw a typed, structured error
149
+ (`{ unsupported, platform, op, reason, alternative }`) rather than a bare string.
150
+
7
151
  ## 0.40.2 — 2026-06-11
8
152
 
9
153
  ### Fixed — SNES `disasm({target:'decompile'})` treated the address as a raw file offset
package/README.md CHANGED
@@ -11,7 +11,7 @@ npx romdevtools
11
11
  - **Build** — bundled per-platform toolchains (cc65, SDCC, RGBDS, asar, vasm, SGDK, PVSnesLib, libtonc, …) as WASM. Write source, compile, get a real ROM.
12
12
  - **Run + see + drive** — load the ROM into an emulated console (libretro cores as WASM), step frames, screenshot, script controller input.
13
13
  - **Inspect + romhack** — read CPU/video/save RAM, watch memory, write-breakpoints, the Cheat-Engine value-search loop, a bundled cheat database, mapper-aware disassembly, and a byte-exact rebuildable-project disassembler.
14
- - **Reverse-engineering analysis engine (all 14 platforms)** — control-flow graphs, deep cross-references, auto-detected functions, a one-shot structural map, and a Ghidra **decompiler** (C-like pseudocode): `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. Understand *how* a routine works before you touch it — no $3,000 IDA license, no install.
14
+ - **Reverse-engineering analysis engine (all 14 platforms)** — control-flow graphs, deep cross-references, auto-detected functions (ranked real-code-first), a one-shot structural map, and a Ghidra **decompiler** (C-like pseudocode, with hardware registers named and 6502 SLEIGH clutter folded to readable C): `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. And the piece no static tool has: **live computed-jumptable recovery** — `breakpoint({on:'jumptable'})` runs the emulator to resolve the `JMP (table,X)` / RTS-trick dispatchers (state machines, script/battle VMs) that static analysis collapses to "could not recover." Understand *how* a routine works before you touch it — no $3,000 IDA license, no install.
15
15
  - **Convert assets** — PNG → platform tiles/tilemaps, quantize-to-palette, audio importers (BRR for SNES, XGM2 PCM for Genesis).
16
16
 
17
17
  Point any coding agent at it three ways:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "romdevtools",
3
- "version": "0.40.2",
3
+ "version": "0.42.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",
@@ -80,6 +80,8 @@
80
80
  "files": [
81
81
  "src",
82
82
  "!src/**/*.test.js",
83
+ "!src/cores/wasm/**",
84
+ "!src/toolchains/*/wasm/**",
83
85
  "examples",
84
86
  "AGENTS.md",
85
87
  "README.md",