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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +144 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/analysis/analyze.js +320 -48
- package/src/analysis/backtrace.js +198 -0
- package/src/analysis/nes-ppu-runtime.js +220 -0
- package/src/analysis/nes-ppu-shim.js +263 -0
- package/src/analysis/pointer-table.js +84 -0
- package/src/analysis/recompile-65816.js +584 -0
- package/src/analysis/rizin.js +13 -1
- package/src/cheats/gamegenie.js +14 -2
- package/src/cores/capabilities.js +218 -0
- package/src/cores/registry.js +43 -51
- package/src/mcp/state.js +91 -2
- package/src/mcp/tools/cheats.js +10 -1
- package/src/mcp/tools/disasm.js +331 -24
- package/src/mcp/tools/find-references.js +93 -2
- package/src/mcp/tools/frame.js +440 -4
- package/src/mcp/tools/index.js +34 -1
- package/src/mcp/tools/lifecycle.js +53 -24
- package/src/mcp/tools/memory.js +1 -1
- package/src/mcp/tools/platform-tools.js +17 -5
- package/src/mcp/tools/platforms.js +18 -3
- package/src/mcp/tools/playtest.js +24 -1
- package/src/mcp/tools/project.js +1 -1
- package/src/mcp/tools/rendering-context.js +9 -6
- package/src/mcp/tools/watch-memory.js +231 -11
- package/src/mcp/util.js +82 -0
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +23 -8
- package/src/playtest/playtest.js +115 -46
- package/src/toolchains/_worker/pool.js +41 -3
- package/src/toolchains/cc65/da65.js +7 -0
- package/src/cores/wasm/bluemsx_libretro.js +0 -2
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +0 -2
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +0 -2
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +0 -2
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +0 -2
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +0 -2
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +0 -2
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +0 -2
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +0 -2
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +0 -2
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +0 -2
- 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.
|
|
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",
|