romdevtools 0.26.0 → 0.28.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 +5 -3
- package/CHANGELOG.md +322 -3
- package/README.md +1 -1
- package/examples/README.md +1 -1
- package/examples/atari2600/templates/platformer.asm +18 -9
- package/examples/atari2600/templates/racing.asm +25 -4
- package/examples/atari2600/templates/shmup.asm +30 -5
- package/examples/atari2600/templates/sports.asm +41 -9
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +12 -8
- package/examples/atari7800/templates/puzzle.c +7 -4
- package/examples/atari7800/templates/racing.c +5 -2
- package/examples/atari7800/templates/shmup.c +8 -4
- package/examples/atari7800/templates/sports.c +6 -3
- package/examples/c64/templates/platformer.c +28 -24
- package/examples/c64/templates/puzzle.c +77 -16
- package/examples/c64/templates/racing.c +9 -0
- package/examples/c64/templates/shmup.c +13 -1
- package/examples/c64/templates/sports.c +9 -4
- package/examples/gb/templates/platformer.c +6 -2
- package/examples/gb/templates/puzzle.c +279 -101
- package/examples/gb/templates/racing.c +13 -1
- package/examples/gb/templates/shmup.c +13 -1
- package/examples/gb/templates/sports.c +9 -3
- package/examples/gba/templates/platformer.c +7 -13
- package/examples/gba/templates/puzzle.c +93 -15
- package/examples/gba/templates/racing.c +13 -1
- package/examples/gba/templates/shmup.c +13 -1
- package/examples/gba/templates/sports.c +17 -5
- package/examples/gbc/templates/platformer.c +6 -2
- package/examples/gbc/templates/puzzle.c +878 -178
- package/examples/gbc/templates/racing.c +13 -1
- package/examples/gbc/templates/shmup.c +13 -1
- package/examples/gbc/templates/sports.c +9 -3
- package/examples/genesis/templates/puzzle.c +76 -15
- package/examples/genesis/templates/racing.c +13 -1
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/gg/templates/platformer.c +4 -0
- package/examples/gg/templates/puzzle.c +80 -14
- package/examples/gg/templates/racing.c +17 -1
- package/examples/gg/templates/shmup.c +17 -1
- package/examples/gg/templates/sports.c +4 -0
- package/examples/lynx/templates/platformer.c +25 -6
- package/examples/lynx/templates/puzzle.c +77 -14
- package/examples/lynx/templates/shmup.c +13 -1
- package/examples/lynx/templates/sports.c +5 -2
- package/examples/msx/platformer/main.c +2 -0
- package/examples/msx/puzzle/main.c +78 -15
- package/examples/msx/racing/main.c +1 -0
- package/examples/msx/shmup/main.c +1 -0
- package/examples/msx/sports/main.c +3 -2
- package/examples/nes/templates/platformer.c +11 -3
- package/examples/nes/templates/puzzle.c +81 -21
- package/examples/nes/templates/racing.c +15 -1
- package/examples/nes/templates/shmup.c +1 -0
- package/examples/nes/templates/sports.c +1 -0
- package/examples/pce/platformer/main.c +3 -1
- package/examples/pce/puzzle/main.c +78 -12
- package/examples/pce/racing/main.c +1 -0
- package/examples/pce/shmup/main.c +5 -4
- package/examples/pce/sports/main.c +4 -3
- package/examples/sms/templates/platformer.c +4 -0
- package/examples/sms/templates/puzzle.c +80 -14
- package/examples/sms/templates/racing.c +17 -1
- package/examples/sms/templates/shmup.c +17 -1
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +4 -0
- package/examples/snes/templates/platformer.c +32 -15
- package/examples/snes/templates/puzzle.c +84 -16
- package/examples/snes/templates/racing.c +20 -1
- package/examples/snes/templates/shmup.c +20 -2
- package/examples/snes/templates/sports.c +7 -0
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- 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 +245 -10
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +11 -4
- package/src/mcp/tools/index.js +15 -1
- package/src/mcp/tools/input.js +26 -3
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/project.js +35 -9
- package/src/mcp/tools/toolchain.js +43 -10
- package/src/mcp/tools/watch-memory.js +172 -25
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +27 -6
- package/src/platforms/gb/MENTAL_MODEL.md +16 -1
- package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
- package/src/platforms/gb/lib/c/patch-header.js +7 -4
- package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/patch-header.js +7 -4
- package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/TROUBLESHOOTING.md +13 -17
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +6 -0
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +2 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
- package/src/platforms/nes/MENTAL_MODEL.md +10 -3
- package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
- package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
- package/src/platforms/pce/MENTAL_MODEL.md +9 -0
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +2 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/sms/MENTAL_MODEL.md +5 -0
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +5 -0
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/index.js +37 -8
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ This is romdev's GENERIC orientation — read it once. The platform-specific det
|
|
|
4
4
|
|
|
5
5
|
## What this server does
|
|
6
6
|
|
|
7
|
-
Drives the full homebrew ROM dev loop for 14 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace
|
|
7
|
+
Drives the full homebrew ROM dev loop for 14 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace where an on-screen graphic comes from (`watch({on:'copy'})` on all 14 — writer PC per VRAM write; `watch({on:'dma'})` for Genesis DMA sources), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
|
|
8
8
|
|
|
9
9
|
You drive the work. The human is a director — they may want a game, a ROM disassembly, a tool-assisted reverse-engineering session, or anything else this server can do.
|
|
10
10
|
|
|
@@ -40,6 +40,8 @@ A couple of optional features load a native Node addon (most notably the `playte
|
|
|
40
40
|
|
|
41
41
|
If a human is sitting next to you during this session — and that's most sessions in practice — open the playtest window as soon as your first build succeeds. `playtest()` opens a native SDL window that runs your ROM live and accepts USB gamepads (hot-plugged controllers are picked up automatically). It returns **immediately** — the render loop runs in the background, so you keep calling other tools while the human plays. Every other MCP tool keeps working against that same running ROM, and **`build({output:'run'})`/`loadMedia` rebuilds update the window in place** — the window follows your latest build, no relaunch and no crash on rebuild. A human sitting next to you should be **playing the game** while you iterate, not watching screenshots scroll past.
|
|
42
42
|
|
|
43
|
+
**Co-driving is detected for you.** While the human is actively pressing (pad or keyboard), the window's input wins over yours and its real-time loop races your frame-stepping — and you'll KNOW: `frame`/`input` responses carry a `humanCoDriveWarning` while they pressed within the last ~2s, and `catalog({op:'status'})` / `playtest({op:'status'})` expose `humanInputActive`. When the human is idle the window leaves your `input({op:'set'})` alone. For deterministic stepping while they play, either `host({op:'pause'})` (the window keeps rendering, frozen) or use a SECOND session (a different `x-romdev-session` header = a fully isolated emulator).
|
|
44
|
+
|
|
43
45
|
```
|
|
44
46
|
playtest() // opens the SDL window (returns immediately). op:'open' is the default;
|
|
45
47
|
// playtest({op:'stop'|'status'|'framebuffer'}) close / check / capture-what-the-human-sees
|
|
@@ -63,8 +65,8 @@ Skip playtest only when there's clearly no human in the loop: CI runs, automated
|
|
|
63
65
|
- `run` — load ROMs, step frames, screenshot (works for existing ROMs you didn't compile)
|
|
64
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.
|
|
65
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'})`)
|
|
66
|
-
- `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.
|
|
67
|
-
- `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)
|
|
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
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'})`)
|
|
69
71
|
- `project` — starter snippets per platform
|
|
70
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
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,326 @@ 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.28.0
|
|
8
|
+
|
|
9
|
+
The reverse-engineering release: the three RE primitives — break-instant
|
|
10
|
+
`registersAtHit`, interference-free `pure` CPU calls, and the
|
|
11
|
+
`watch({on:'copy'})` graphics source-trace — now work on ALL 14 platforms
|
|
12
|
+
(every emulator core rebuilt; upstream pins unchanged, everything carried by
|
|
13
|
+
the patches in `scripts/patches/`). Plus the full scaffold overhaul from real
|
|
14
|
+
RetroDECK playtesting, banked-cart parity for disasm/rebuild, the value-search
|
|
15
|
+
upgrades, and the playtest co-drive detection. Details per section below.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Added — pure calls + the generic copy trace on ALL 14 platforms (primitives #2 and #3)
|
|
19
|
+
The other two primitives from the all-platforms RE proposal, completing the
|
|
20
|
+
set (registersAtHit was #1):
|
|
21
|
+
- **`cpu({op:'call', pure:true})` works everywhere.** The guarantee is the
|
|
22
|
+
same on every platform — the game's own NMI/IRQ/VBlank logic CANNOT run
|
|
23
|
+
during the call and stomp the routine's output buffer — with the mechanism
|
|
24
|
+
reported as `pureMode`: Genesis/SMS/GG step ONLY the CPU (`'cpu-only'`,
|
|
25
|
+
the gpgx separable-loop path); every other core suppresses interrupt
|
|
26
|
+
DELIVERY for the duration (`'irq-blocked'` via a new `romdev_irqblock_set`
|
|
27
|
+
export — pending lines stay pending, video/timers advance harmlessly, no
|
|
28
|
+
game handler executes); the 2600's 6507 has no interrupt lines at all
|
|
29
|
+
(`'no-interrupts'`). Proven live on NES: NMI delivery verified firing,
|
|
30
|
+
then silent under the block, then a planted routine pure-called
|
|
31
|
+
end-to-end with its write landing.
|
|
32
|
+
- **`watch({on:'copy'})` — the generic "where does this graphic come
|
|
33
|
+
from?".** Logs every write landing in a VRAM/dest address window with the
|
|
34
|
+
EXECUTING instruction's PC. Port-based video memory is hooked INSIDE the
|
|
35
|
+
cores — NES $2007, SNES $2118/19 (BOTH CPU port writes and the DMA path —
|
|
36
|
+
the PC is the DMA-triggering instruction), PCE VWR, MSX VDP data port,
|
|
37
|
+
SMS/GG/Genesis VDP data port (the CPU-port complement of the Genesis DMA
|
|
38
|
+
watch). Direct-mapped platforms (GB/GBC, GBA, C64, Lynx, 7800) route
|
|
39
|
+
through the CPU-address range log automatically. Follow a hit with
|
|
40
|
+
breakpoint({on:'pc', address: pc}) for registersAtHit at the uploader.
|
|
41
|
+
- Cores rebuilt again (same pins; the scripts/patches/ diffs carry
|
|
42
|
+
everything — all 11 verified to apply clean to pristine checkouts).
|
|
43
|
+
- `test/pure-copy-primitives.test.js`: the 13-core irq-block/run-pure
|
|
44
|
+
feature matrix, NES NMI-delivery proof + end-to-end pure call, MSX
|
|
45
|
+
block-safety, and copy traces on NES (port), SNES (port+DMA), GB (mapped).
|
|
46
|
+
|
|
47
|
+
### Fixed/Added — registersAtHit + freeze-after-hit on ALL 14 platforms (every core rebuilt)
|
|
48
|
+
The gpgx round's break-instant fixes, extended to every other core — the same
|
|
49
|
+
three guarantees now hold across the whole platform matrix:
|
|
50
|
+
- **`registersAtHit` everywhere** — every breakpoint hit (pc-break, watchdog,
|
|
51
|
+
write-watch, read-watch) on every platform freezes the FULL register file at
|
|
52
|
+
the hit instant inside the core hook, exported via `romdev_regsnap_get` and
|
|
53
|
+
surfaced in the breakpoint hit response. Per-CPU register sets: 6502 family
|
|
54
|
+
(NES/2600/7800/C64/Lynx/PCE) A/X/Y/P/S/PC; 65816 (SNES) +DB/D; sm83 (GB/GBC)
|
|
55
|
+
A/F/B/C/D/E/H/L/SP; Z80 (SMS/GG/MSX) +IX/IY; m68k (Genesis) D0-7/A0-7/SR;
|
|
56
|
+
ARM7 (GBA) r0-r15/CPSR. NES previously snapshotted pc-breaks only — its
|
|
57
|
+
write/read hits now snapshot too.
|
|
58
|
+
- **Freeze-after-hit everywhere** — once a hit fires, the CPU run loop stays
|
|
59
|
+
frozen (across re-entries and frames) until the host clears the hit, so even
|
|
60
|
+
live register reads agree with the snapshot. Previously each core resumed on
|
|
61
|
+
the next loop re-entry and the registers drifted.
|
|
62
|
+
- **Executing-instruction PC everywhere** — write/read watchpoints and range
|
|
63
|
+
logs report the EXECUTING instruction's first byte, latched at dispatch
|
|
64
|
+
(sm83/Z80/65816/6502 PCs advance past operands mid-instruction — the same
|
|
65
|
+
off-by-one class the gpgx round fixed for m68k; GBA reports the pipeline PC,
|
|
66
|
+
matching its breakpoint-address convention).
|
|
67
|
+
- Cores rebuilt: fceumm, snes9x, gambatte, mGBA, handy, vice, stella2014,
|
|
68
|
+
prosystem, geargrafx, bluemsx (pins unchanged; the romdev patches in
|
|
69
|
+
scripts/patches/ carry all of it — the whole stack reproduces from a clean
|
|
70
|
+
clone). `cpu({op:'call', pure:true})` remains gpgx-only (the other systems'
|
|
71
|
+
CPU/video loops are not separable without deeper core surgery); their calls
|
|
72
|
+
carry the ⚠ frame-logic caveat instead.
|
|
73
|
+
- `test/regsnap-all-cores.test.js`: live single-step snapshot + freeze proof
|
|
74
|
+
on 10 platforms (plus the existing gpgx suite for Genesis/SMS/GG).
|
|
75
|
+
|
|
76
|
+
### Fixed/Added — gpgx core round (the NBA-Jam-both-consoles feedback): break-instant truth on Genesis/SMS/GG
|
|
77
|
+
The first core rebuild in this release (gpgx only; pins unchanged, patch extended).
|
|
78
|
+
- **`registersAtHit` on Genesis/SMS/GG** — `breakpoint({on:'pc'|'write'|'read'})`
|
|
79
|
+
hits now carry the FULL register file (m68k d0-d7/a0-a7/pc/sr/sp; z80
|
|
80
|
+
a/f/b/c/d/e/h/l/ix/iy/pc/sp) frozen by the core AT the hit instant. gpgx
|
|
81
|
+
schedules CPUs per scanline, so the live register file used to drift
|
|
82
|
+
hundreds of instructions past a hit before the host could read it — the
|
|
83
|
+
"wrong-pointer chases" that cost a real RE session ~2h. On a pc-break the
|
|
84
|
+
CPU now also stays FROZEN for the remainder of the frame (and across
|
|
85
|
+
frames until the hit is cleared), so even live reads agree.
|
|
86
|
+
- **Write/read watchpoint PC is the EXECUTING instruction** — the hooks now
|
|
87
|
+
record the instruction's first-byte address latched at dispatch, not the
|
|
88
|
+
post-prefetch PC (the orb-at-$2A7216-reported-as-$2A721C off-by-one).
|
|
89
|
+
`breakpoint({on:'write'})` also renames `value`→`valueByte` (it's the one
|
|
90
|
+
byte that landed, not the operand) and explains its `hits` semantics.
|
|
91
|
+
- **`cpu({op:'call', pure:true})`** — steps ONLY the active CPU (new
|
|
92
|
+
`romdev_run_pure` export): no VDP line processing, no co-CPU, no interrupts
|
|
93
|
+
raised — so the game's own VBlank logic can NOT run "concurrently" and
|
|
94
|
+
stomp the driven routine's output buffer (a real session diffed a correct
|
|
95
|
+
codec reimplementation against that poisoned output for ~1.5h). Non-pure
|
|
96
|
+
calls that spanned frames now carry a loud ⚠ caveat naming the risk and
|
|
97
|
+
the fix.
|
|
98
|
+
- **Genesis `system_ram` normalized to CPU byte order** — gpgx stores 68k
|
|
99
|
+
work RAM host-LE word-swapped (`work_ram[A^1]`); the raw layout leaked
|
|
100
|
+
through every byte-granular tool. Self-consistent within search→write
|
|
101
|
+
loops (which is why it hid — even a test had the swapped bytes baked in as
|
|
102
|
+
the expected value), but off-by-XOR-1 the moment an offset crossed to/from
|
|
103
|
+
disassembly addresses or cheat-DB maps. Offset X now IS the byte the 68k
|
|
104
|
+
sees at $FF0000+X; words read big-endian as documented. This also fixes
|
|
105
|
+
`cpu({op:'call'})` sentinel pushes / `presetMemory` writes for any non-zero
|
|
106
|
+
sentinel address (the default $0 sentinel was swap-invariant, hiding it).
|
|
107
|
+
- breakpoint hit responses normalize `hits` (a watchdog stop no longer
|
|
108
|
+
reports the contradictory `hit:true, hits:0`).
|
|
109
|
+
- Docs: the held-input menu trick (when a `pressDuring` schedule never
|
|
110
|
+
registers on a menu screen, hold via `input({op:'set'})` and omit
|
|
111
|
+
pressDuring — runs inherit held input) is now in the breakpoint/watch tool
|
|
112
|
+
docs; the server banner prints a one-line headless note when no display is
|
|
113
|
+
available (so an agent knows before promising a playtest window).
|
|
114
|
+
- `test/gpgx-registers-at-hit.test.js`: live-core coverage for all of it,
|
|
115
|
+
including a per-platform Genesis memory-read smoke (the earlier
|
|
116
|
+
"info is not defined" regression was invisible to a fake-host-only suite).
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
### Fixed/Added — value-search upgrades (from the locate-value skill review)
|
|
120
|
+
- **Relative compares work as the FIRST `searchNext`.** `op:'search'` now
|
|
121
|
+
baselines every candidate at seed time, so `compare:'inc'/'dec'/'changed'/
|
|
122
|
+
'unchanged'` no longer silently return 0 candidates on the first narrow
|
|
123
|
+
(the footgun a real session burned rounds on and a skill had to document —
|
|
124
|
+
the "do one eq round first" workaround is obsolete).
|
|
125
|
+
- **Representation-aware search** — `memory({op:'search', as:'bcd'|'digits'})`
|
|
126
|
+
for the stored≠displayed cases: `'bcd'` matches packed-BCD values (2 decimal
|
|
127
|
+
digits/byte, region endianness — classic NES scores); `'digits'` matches one
|
|
128
|
+
byte per ON-SCREEN digit at ANY constant tile base (HUD digit/tile-index
|
|
129
|
+
buffers; the base is auto-detected per candidate and reported; single-digit
|
|
130
|
+
seeds only accept base 0/0x30 to avoid matching everything). `searchNext`
|
|
131
|
+
narrows in the seed's representation automatically, including numeric
|
|
132
|
+
`inc`/`dec` on decoded values. Works on all platforms/regions (endianness
|
|
133
|
+
per region, big-endian m68k included).
|
|
134
|
+
- **search/searchNext response notes fixed** — they recommended the dead
|
|
135
|
+
`searchValue` name and a `writeMemory({bytes})` form that op:'write'
|
|
136
|
+
REJECTS; now they name the live ops with a `hex` payload, mention the
|
|
137
|
+
scene-changed-mid-step empty-round trap, and point input-driven values at
|
|
138
|
+
`diffRuns`. Same stale-name fix in two `watch` tool notes.
|
|
139
|
+
- `test/search-representations.test.js` covers all of it.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
### Added — banked-cart parity across ALL platforms (per-bank references + rebuild glue)
|
|
143
|
+
The 0.27.0 feedback round fixed per-bank reference scanning and one-call banked
|
|
144
|
+
rebuild glue for NES only. Every other banked-cart platform now gets the same
|
|
145
|
+
treatment:
|
|
146
|
+
- **`disasm({target:'references'})` scans EVERY bank on every banked format** —
|
|
147
|
+
SNES multi-bank LoROM (was: only the first 32KB bank), GB/GBC MBC and SMS/GG
|
|
148
|
+
Sega-mapper and MSX megaROM (was: only the first 32KB), Atari 2600 F8/F6/F4
|
|
149
|
+
(was: only the boot bank), Atari 7800 (was: only the top 16KB — flat carts now
|
|
150
|
+
scan the WHOLE image, SuperGame carts per-bank), and >32KB HuCards (was: a
|
|
151
|
+
wrapped, garbage start address). Non-NES refs carry a `romBank` tag (NES keeps
|
|
152
|
+
`prgBank`). Very large carts scan the first 64 banks and SAY SO in `notes`.
|
|
153
|
+
- **`disasm({target:'project'})` splits every banked format per-bank** so
|
|
154
|
+
instructions never straddle a bank edge: Sega-mapper SMS/GG (16KB banks),
|
|
155
|
+
MSX megaROMs (16KB banks + the "AB" header as its own data region), banked
|
|
156
|
+
2600 (4KB banks), 7800 SuperGame (16KB banks + the .a78 header split out),
|
|
157
|
+
>32KB HuCards (8KB pages + optional copier header split out).
|
|
158
|
+
- **Atari 7800 SuperGame and PC Engine HuCards (flat AND banked) get one-call
|
|
159
|
+
byte-identical `build()` rebuilds** — their asm toolchain is cc65/ca65, the
|
|
160
|
+
same match that made NES one-call. NES-style glue: HEADER segment carrying
|
|
161
|
+
the original header bytes, per-bank segment wrappers, generated multi-bank
|
|
162
|
+
`.cfg` via `linkerConfigPath`. **PCE was previously the one honestly-LOSSY
|
|
163
|
+
case** (planRegions trimmed real $FF padding and didn't strip copier
|
|
164
|
+
headers) — both fixed, `verifiable:true` now.
|
|
165
|
+
- **SMS/GG, MSX, and 2600 banked carts get per-bank native rebuild recipes**
|
|
166
|
+
(their `build()` is SDCC/DASM — can't consume the disasm syntax): per-bank
|
|
167
|
+
wrappers + cfg blobs (2600), bank-by-bank `as`/`objcopy`/`dd`/`cat` recipes
|
|
168
|
+
in `BUILD.md` (SMS/GG/MSX), all byte-exact.
|
|
169
|
+
- Proven by `test/banked-parity.test.js`: synthetic banked carts on 7 platforms;
|
|
170
|
+
byte-identical one-call rebuilds verified end-to-end for 7800 SuperGame,
|
|
171
|
+
banked PCE, and flat-PCE-with-real-padding.
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
### Fixed — scaffold overhaul from real RetroDECK/Bazzite playtesting (all 14 platforms)
|
|
175
|
+
A full human playtest of every genre scaffold on real hardware surfaced clusters
|
|
176
|
+
of repeated logic errors. The big ones:
|
|
177
|
+
- **SMS/GG: every `build({output:'project'})` ROM black-screened** — the project
|
|
178
|
+
recipe skipped the dir's `*_crt0.s` believing `buildForPlatform` auto-injects
|
|
179
|
+
the bundled crt0 (it doesn't; only the rom/run handlers do), so SDCC's stock
|
|
180
|
+
z80 crt0 linked instead and `main()` never ran. Also: the bundled crt0's reset
|
|
181
|
+
block was 9 bytes (overflowed into the `.org 0x0008` RST slot, corrupting
|
|
182
|
+
`jp gsinit`), `_CODE` linked at `$0000` ON TOP of the vector table, and `.gg`
|
|
183
|
+
ROMs got an SMS region nibble (`$4C`) that flips Genesis-Plus-GX into SMS-compat
|
|
184
|
+
mode. Project builds now route/fall back to the bundled crt0, `_CODE` sits at
|
|
185
|
+
`$0100`, GG ROMs get region `$7C`, ROMs pad to 32KB before the TMR SEGA header,
|
|
186
|
+
and a regression test pins the boot byte + header. The SMS scaffold now ships
|
|
187
|
+
`sms_crt0.s` like GG/MSX.
|
|
188
|
+
- **"All enemies spawn on the left"** (18 shmup/racing templates): spawn X/lane
|
|
189
|
+
came from `spawn_timer`, which the caller resets to 0 immediately before
|
|
190
|
+
`spawn()` — a constant. Each template now has a Galois-LFSR `rand8()`.
|
|
191
|
+
- **Puzzle genre**: the gbc template is now the polished falling-jewel reference
|
|
192
|
+
game (4-direction matches, gravity + cascade chains, magic piece, SFX + music,
|
|
193
|
+
collect/flush vblank rendering, dataLoc `$C200` via the gb/gbc project recipe);
|
|
194
|
+
the DMG gb template is rebuilt around the same core; and the
|
|
195
|
+
mark/clear/gravity/cascade core is ported to all 10 other platforms (PCE: H+V
|
|
196
|
+
in its 8KB boot bank). Replaces a horizontal-only scan that missed vertical/
|
|
197
|
+
diagonal matches, half-cleared 4+ runs, and never dropped survivors.
|
|
198
|
+
- **Atari 2600**: SWCHA ASL carry-chains clobbered A between shifts (pressing
|
|
199
|
+
RIGHT also "pressed" LEFT — the stuck-to-the-left-edge bug) in three templates;
|
|
200
|
+
the platformer's terminal-velocity clamp caught POSITIVE velocities (unsigned
|
|
201
|
+
CMP), killing every jump within one frame; sports' paddle axis was inverted vs
|
|
202
|
+
the kernel's Y convention and RESBL was never strobed (the ball NEVER moved
|
|
203
|
+
horizontally — per-frame div-15 + HMBL positioning added); racing re-randomizes
|
|
204
|
+
both lanes on crash; shmup aliens reaching the cannon reset the wave.
|
|
205
|
+
- **Atari 7800**: the SWCHA joystick bit defines were exactly REVERSED on every
|
|
206
|
+
template (up/down steered left/right; sports' left/right moved the paddle
|
|
207
|
+
vertically). Plus speed tuning (platformer movement + jump, puzzle fall rate,
|
|
208
|
+
sports serve).
|
|
209
|
+
- **Platformers**: GBA fell through every platform (the `blocked_below` gate
|
|
210
|
+
only matched a 1px window at 20px/frame fall speeds); SNES platforms are now
|
|
211
|
+
visibly drawn on the scrolled text layer (were invisible collision rects);
|
|
212
|
+
Lynx landing uses a crossing test (exact-equality check tunnelled); C64
|
|
213
|
+
`render_view` rewritten ~20x faster (a per-CELL platform scan + 16-bit modulo
|
|
214
|
+
cost ~2s per 8px scroll step at 1MHz — froze the game and ate jump presses);
|
|
215
|
+
NES player is red (was sky-blue on sky-blue) and moves 2px/frame; GB/GBC jump
|
|
216
|
+
height tamed.
|
|
217
|
+
- **GBA sports "never starts"**: `tte_printf` (broken in this libtonc — the
|
|
218
|
+
documented GBA-1 issue) ran every frame and crashed with an undefined-
|
|
219
|
+
instruction exception on iteration 1. Replaced with the `tte_write` digit path
|
|
220
|
+
the other templates already use.
|
|
221
|
+
- **SNES**: each genre now gets a distinct backdrop tint (every scaffold shipped
|
|
222
|
+
the same blue checkered wallpaper).
|
|
223
|
+
- **Sound everywhere**: every scaffold now has a continuous background-music
|
|
224
|
+
loop plus audible SFX, verified per platform by recording + RMS analysis.
|
|
225
|
+
Genesis/Lynx tick a melody inside `sfx_update()` (no template wiring; Lynx
|
|
226
|
+
voices 64→100), NES adds a triangle-channel melody to `nes_runtime`, PCE a
|
|
227
|
+
ch5 melody with corrected volume (the 5-bit field is ~-1.5dB/step from 31 —
|
|
228
|
+
the old 13 was -27dB, near-silence; the shmup SFX are maxed), and the SMS/GG
|
|
229
|
+
3-voice tracker that already shipped is now actually STARTED by all 11
|
|
230
|
+
templates. **MSX root cause**: `msx_crt0.s` had the same `_INITIALIZER`-in-RAM
|
|
231
|
+
bug fixed for SMS/GG (every `static x = N` booted 0) plus a BIOS-KEYINT
|
|
232
|
+
PSGADDR-latch race (PSG writes now DI/EI-guarded) — both fixed; this likely
|
|
233
|
+
also explains the reported MSX sprite flakiness.
|
|
234
|
+
- **GB/GBC sports scanline tear**: the OAM DMA now fires at the vblank leading
|
|
235
|
+
edge (45 staged `oam_set` calls used to push it a third of the frame into
|
|
236
|
+
active display — the "horizontal line a 3rd of the way down" glitch).
|
|
237
|
+
- Misc per-genre polish: PCE gameplay speeds, C64 racing clears the BASIC
|
|
238
|
+
startup text, C64 sports court widened to the 9-bit sprite range, MSX/Lynx
|
|
239
|
+
sports contrast, GBA puzzle well border.
|
|
240
|
+
- **Verification**: all 69 existing platform×genre scaffolds were swept —
|
|
241
|
+
scaffold → project build → boot → render-health green, all 14 platforms
|
|
242
|
+
respond to input, and each platform's audio was captured and RMS-checked.
|
|
243
|
+
(Atari 2600 has no puzzle genre by design.)
|
|
244
|
+
|
|
245
|
+
### Fixed/Added — the 0.27.0 Zanac RE feedback round (banked-NES rebuilds, A/B diff, token cuts)
|
|
246
|
+
- **Banked NES `disasm({target:'project'})` now emits COMPLETE, working rebuild
|
|
247
|
+
glue** (the headline ask): a `HEADER` segment with the original 16 iNES bytes,
|
|
248
|
+
a per-bank `PRGn` segment wrapper for every bank, a multi-bank `nes_rebuild.cfg`
|
|
249
|
+
(switchable banks at `$8000`, fixed top bank at `$C000`, CHR wired when
|
|
250
|
+
present), and a `rebuild.json` `build()` call referencing all of it. Proven
|
|
251
|
+
byte-identical on a synthetic 4-bank mapper-2 ROM fed straight back to
|
|
252
|
+
`build()` — what previously took an hour of hand-written segments + cfg is
|
|
253
|
+
now zero glue. (NROM keeps the existing proven `inesHeader` one-call path.)
|
|
254
|
+
- **`build({linkerConfigPath})`** reads the `.cfg` from disk so a large
|
|
255
|
+
multi-bank config never streams through context (and `rebuild.json` uses it).
|
|
256
|
+
- **`disasm({target:'references'})` scans every PRG bank on banked NES** —
|
|
257
|
+
the old flat-blob-at-`$8000` disassembly returned `refsFound:0` on >32KB
|
|
258
|
+
ROMs. Refs now carry a `prgBank` tag, and `#$nn` immediates no longer count
|
|
259
|
+
as references (they're values, not addresses).
|
|
260
|
+
- **`memory({op:'diffRuns'})`** — the A/B input-diff primitive: runs the same
|
|
261
|
+
start state twice under two different held inputs (savestate restore in
|
|
262
|
+
between) and returns only the divergent bytes, with run-A/run-B values for
|
|
263
|
+
small clusters. Replaces the save/run/dump/restore/run/dump/python-diff loop
|
|
264
|
+
(~6 calls + a 4KB context hit) with one call; live-verified isolating an NES
|
|
265
|
+
player-X byte.
|
|
266
|
+
- **`memory({op:'read'/'readCart', outputPath, echo:false})`** returns just
|
|
267
|
+
`{path, bytes}` — no more ~4KB hex echo on a 2KB dump that was explicitly
|
|
268
|
+
routed to disk.
|
|
269
|
+
- **`memory({op:'diff'})`**: summary clusters ≤8 bytes now include
|
|
270
|
+
`before`/`after` hex (no more falling back to `view:'raw'` for the values),
|
|
271
|
+
and `minDelta` filters RNG/counter wiggle.
|
|
272
|
+
- **`input({op:'press'})` guarantees a released→pressed edge** (one released
|
|
273
|
+
frame first), so edge-triggered handlers (START pause) can't miss the press
|
|
274
|
+
when the button was already held.
|
|
275
|
+
- **`breakpoint({on:'pc'})` misses now diagnose**: report `pcNow`, stop
|
|
276
|
+
suggesting `pressDuring` when input WAS supplied (wrong-address is then the
|
|
277
|
+
likely story), and point at `watch({on:'pc'})` coverage tracing.
|
|
278
|
+
|
|
279
|
+
### Added — human co-drive detection: agents now KNOW when a human is playing in the playtest window
|
|
280
|
+
The long-standing confusion ("they get confused when I try to play while they're
|
|
281
|
+
coding") had a real mechanism: the playtest window shares the session's ONE
|
|
282
|
+
emulator host with the agent, and its 60fps tick wrote the human's pad state —
|
|
283
|
+
including all-zeros when nobody was pressing — over the agent's `input({op:'set'})`
|
|
284
|
+
every frame. The agent had no signal a human was co-driving and no warning that
|
|
285
|
+
its input was being clobbered. Now:
|
|
286
|
+
- **The window only writes input while the human is actually pressing** (pad,
|
|
287
|
+
keyboard, or rewind-scrub), plus one release write after they let go. An idle
|
|
288
|
+
window no longer silently clobbers the agent's held input. The human still wins
|
|
289
|
+
the instant they press.
|
|
290
|
+
- **The window tracks human activity** ("pressed within the last ~2 s" ≈ 120
|
|
291
|
+
ticks) and exposes it: `catalog({op:'status'})` reports `playtestWindowOpen` +
|
|
292
|
+
`humanInputActive` (+ `framesSinceHumanInput`), and `playtest({op:'status'})`
|
|
293
|
+
reports the same.
|
|
294
|
+
- **`frame({op:'step'/'stepAndShot'})` and `input(set/press/sequence/navigate)`
|
|
295
|
+
responses carry a `humanCoDriveWarning`** while the human is actively playing,
|
|
296
|
+
telling the agent the conflict is happening NOW and pointing at the escape
|
|
297
|
+
hatches: `host({op:'pause'})` to inspect frozen, or a second session
|
|
298
|
+
(different `x-romdev-session` = fully isolated emulator) for deterministic work.
|
|
299
|
+
- The playtest tool's FOOTGUN doc now describes the real contract (real-time
|
|
300
|
+
stepping always races; input only clobbered while the human presses).
|
|
301
|
+
|
|
302
|
+
### Changed — `screenshot` scale docs: native is the accurate default, upscale adds no detail
|
|
303
|
+
The `scale` param's docs oversold integer UPscaling as making tiny handheld shots
|
|
304
|
+
"legible." That was misleading: nearest-neighbor upscale just duplicates pixels —
|
|
305
|
+
it adds **no information** the native frame doesn't already have, costs more image
|
|
306
|
+
tokens, and since VLM vision encoders resize every input to their own fixed
|
|
307
|
+
resolution it may not change what the model sees (and can slightly degrade it via a
|
|
308
|
+
bicubic downscale of stretched pixels). Reworded the param + tool description to
|
|
309
|
+
lead with **native (`scale:1`, the default) = perfect pixels = the accurate
|
|
310
|
+
representation**, keep the genuinely-useful DOWNscale (`<1`, fewer tokens for
|
|
311
|
+
"did it change?" checks), and frame upscale honestly as a last resort for clients
|
|
312
|
+
that can't zoom a small image. (No behavior change — `scale` was already opt-in and
|
|
313
|
+
defaulted to native; this is the docs telling the truth about it.)
|
|
314
|
+
(Committed during the 0.27.0 cycle but AFTER 0.27.0 published — ships in 0.28.0.)
|
|
315
|
+
|
|
316
|
+
## 0.27.0
|
|
317
|
+
|
|
318
|
+
### Added — `breakpoint(on:'pc', captureMemory:[…])` reads named RAM at the hit
|
|
319
|
+
Completes item 2 of the NES Rygar report. 0.26.0 shipped `registersAtHit` (the
|
|
320
|
+
break-instant register file) but not the memory half. Now `breakpoint(on:'pc')`
|
|
321
|
+
takes `captureMemory:[{region,offset,length,label}]` and returns those reads inline
|
|
322
|
+
as `capturedMemory`, so register + RAM inspection at a PC collapses into ONE call —
|
|
323
|
+
no follow-up `cpu`/`memory` round trips. `registersAtHit` is the true break instant
|
|
324
|
+
(core snapshot); `capturedMemory` reflects the routine's RAM side effects for the
|
|
325
|
+
hit frame (stable + what RE needs), documented as such.
|
|
326
|
+
|
|
7
327
|
## 0.26.0
|
|
8
328
|
|
|
9
329
|
### Fixed — NES `breakpoint(on:'pc')` now returns reliable break-instant registers
|
|
@@ -17,9 +337,8 @@ them (the schema's "CPU is FROZEN at this instruction" was wrong for NES).
|
|
|
17
337
|
SNAPSHOTS A/X/Y/P/S at the hit instant, exposed via `romdev_pcbreak_get`.
|
|
18
338
|
- **`breakpoint(on:'pc')` returns `registersAtHit`** — the reliable break-instant
|
|
19
339
|
register file. The schema + hit note now steer to it and explicitly warn that a
|
|
20
|
-
live `cpu({op:'read'})` after a hit is end-of-frame state on fceumm. (
|
|
21
|
-
|
|
22
|
-
hit, so there's no freeze-durability race and no extra round trip.)
|
|
340
|
+
live `cpu({op:'read'})` after a hit is end-of-frame state on fceumm. (The
|
|
341
|
+
`captureMemory` companion that reads named RAM inline at the hit landed in 0.27.0.)
|
|
23
342
|
- **NES `cpu({op:'read'})` core-internal fields relabeled** (item 3): `DB`,
|
|
24
343
|
`IRQlow`, `tcount`, `count` are fceumm internals (data-bus latch / IRQ bitmask /
|
|
25
344
|
cycle counters), not 6502 registers — moved out of `registers` into a labeled
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ claude mcp add --transport http romdev http://127.0.0.1:7331/mcp
|
|
|
45
45
|
|
|
46
46
|
It's a standard **streamable-HTTP** MCP server at `http://127.0.0.1:7331/mcp`. For opencode, Codex CLI, and other clients, see **[Connect](https://github.com/monteslu/romdev#connect)** in the repository README. An optional human observer (live tool-call view) is at `/livestream`.
|
|
47
47
|
|
|
48
|
-
Agents: the server delivers [`AGENTS.md`](./AGENTS.md) as connection-time instructions — the workflow guide for the full tool surface. Or just connect your agent and call `catalog({op:'categories'})` to explore the tools live
|
|
48
|
+
Agents: the server delivers [`AGENTS.md`](./AGENTS.md) as connection-time instructions — the workflow guide for the full tool surface. Or just connect your agent and call `catalog({op:'categories'})` to explore the tools live, and `catalog({op:'status'})` for the running version + session snapshot.
|
|
49
49
|
|
|
50
50
|
## Prefer not to use MCP? Use HTTP or a Skill
|
|
51
51
|
|
package/examples/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Each example fits the convention:
|
|
|
21
21
|
| genesis | `genesis/main.s` | vasm68k | |
|
|
22
22
|
| gb | `gb/main.c` (default) or `gb/main.asm` (`language:"asm"`) | sdcc sm83 port (C, default) / rgbds (asm) | C example cycles the BG palette every 32 frames. Asm example shows yellow 'H' on light BG, scrollable with A. SDCC GB hardware-register headers under `src/platforms/gb/lib/c/gb_hardware.h`. |
|
|
23
23
|
| gbc | `gbc/main.asm` | rgbds (asm) / sdcc sm83 (C) | The bundled example is an asm CGB-color demo (yellow 'H' on a true-blue BG, only possible on GBC). C is also supported via SDCC sm83 — same as GB. |
|
|
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
|
|
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 10 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
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. |
|
|
@@ -162,17 +162,21 @@ MAIN:
|
|
|
162
162
|
LDA FRAME
|
|
163
163
|
AND #$01
|
|
164
164
|
BNE .skipmove
|
|
165
|
+
; SWCHA is active-LOW; RE-LOAD per direction (the old ASL carry-chain
|
|
166
|
+
; clobbered A with LDA P_X between shifts → RIGHT also triggered LEFT
|
|
167
|
+
; and the moves cancelled — the player couldn't move).
|
|
165
168
|
LDA SWCHA
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
AND #$80 ; bit7 = Right (0 = pressed)
|
|
170
|
+
BNE .nr
|
|
168
171
|
LDA P_X
|
|
169
172
|
CMP #140
|
|
170
173
|
BCS .nr
|
|
171
174
|
INC P_X
|
|
172
175
|
INC P_X
|
|
173
176
|
.nr:
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
LDA SWCHA
|
|
178
|
+
AND #$40 ; bit6 = Left (0 = pressed)
|
|
179
|
+
BNE .nl
|
|
176
180
|
LDA P_X
|
|
177
181
|
CMP #16
|
|
178
182
|
BCC .nl
|
|
@@ -212,12 +216,17 @@ MAIN:
|
|
|
212
216
|
LDA ON_GND
|
|
213
217
|
BNE .skipgrav
|
|
214
218
|
DEC P_VY ; gravity: velocity drifts toward falling each frame
|
|
215
|
-
;
|
|
219
|
+
; Clamp terminal FALL speed to -8 px/frame — but ONLY while falling.
|
|
220
|
+
; The old unsigned compare (CMP #$F8 / BCS keep) also caught every
|
|
221
|
+
; POSITIVE velocity (5 < $F8 unsigned!), so the instant you jumped the
|
|
222
|
+
; clamp slammed P_VY from +6 to -8: the whole "jump" rose 0 frames,
|
|
223
|
+
; fell 8px and re-landed within ONE frame — jump sfx played, screen
|
|
224
|
+
; blipped, player never left the ground.
|
|
216
225
|
LDA P_VY
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
;
|
|
220
|
-
LDA #$F8
|
|
226
|
+
BPL .vyok ; rising (positive) → terminal clamp doesn't apply
|
|
227
|
+
CMP #$F8
|
|
228
|
+
BCS .vyok ; -8..-1 → within terminal speed, keep
|
|
229
|
+
LDA #$F8 ; -128..-9 → clamp to -8
|
|
221
230
|
STA P_VY
|
|
222
231
|
.vyok:
|
|
223
232
|
; P_Y += P_VY (signed add: sign-extend P_VY into the add)
|
|
@@ -165,17 +165,23 @@ MAIN:
|
|
|
165
165
|
LDA FRAME
|
|
166
166
|
AND #$01
|
|
167
167
|
BNE .skipmove
|
|
168
|
+
; SWCHA is active-LOW (0 = pressed). RE-LOAD it for each direction —
|
|
169
|
+
; the old ASL carry-chain clobbered A with LDA P_X between shifts, so
|
|
170
|
+
; the second ASL shifted P_X instead of SWCHA: pressing RIGHT also
|
|
171
|
+
; "pressed" LEFT (P_X < $80 -> carry clear) and the moves cancelled.
|
|
172
|
+
; That was the "ship/car stuck to the left edge" bug.
|
|
168
173
|
LDA SWCHA
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
AND #$80 ; bit7 = P0 Right (0 = pressed)
|
|
175
|
+
BNE .nr
|
|
171
176
|
LDA P_X
|
|
172
177
|
CMP #128
|
|
173
178
|
BCS .nr
|
|
174
179
|
INC P_X
|
|
175
180
|
INC P_X
|
|
176
181
|
.nr:
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
LDA SWCHA
|
|
183
|
+
AND #$40 ; bit6 = P0 Left (0 = pressed)
|
|
184
|
+
BNE .nl
|
|
179
185
|
LDA P_X
|
|
180
186
|
CMP #28
|
|
181
187
|
BCC .nl
|
|
@@ -270,6 +276,21 @@ MAIN:
|
|
|
270
276
|
STA E1_Y
|
|
271
277
|
LDA #182
|
|
272
278
|
STA E2_Y
|
|
279
|
+
; Re-randomize BOTH lanes on crash (FRAME-derived). The old code only
|
|
280
|
+
; reset Y, so after a crash the enemy kept its old X — crash into it
|
|
281
|
+
; once near the left edge and it respawned in the same column forever
|
|
282
|
+
; ("enemy car stuck to the left edge").
|
|
283
|
+
LDA FRAME
|
|
284
|
+
AND #$3F
|
|
285
|
+
CLC
|
|
286
|
+
ADC #40
|
|
287
|
+
STA E1_X
|
|
288
|
+
LDA FRAME
|
|
289
|
+
EOR #$2A
|
|
290
|
+
AND #$3F
|
|
291
|
+
CLC
|
|
292
|
+
ADC #44
|
|
293
|
+
STA E2_X
|
|
273
294
|
LDA #$08 ; noisy crash
|
|
274
295
|
STA AUDC0
|
|
275
296
|
LDA #$1F
|
|
@@ -131,17 +131,23 @@ MAIN:
|
|
|
131
131
|
LDA FRAME
|
|
132
132
|
AND #$01
|
|
133
133
|
BNE .skipmove
|
|
134
|
+
; SWCHA is active-LOW (0 = pressed). RE-LOAD it for each direction —
|
|
135
|
+
; the old ASL carry-chain clobbered A with LDA P_X between shifts, so
|
|
136
|
+
; the second ASL shifted P_X instead of SWCHA: pressing RIGHT also
|
|
137
|
+
; "pressed" LEFT (P_X < $80 -> carry clear) and the moves cancelled.
|
|
138
|
+
; That was the "ship/car stuck to the left edge" bug.
|
|
134
139
|
LDA SWCHA
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
AND #$80 ; bit7 = P0 Right (0 = pressed)
|
|
141
|
+
BNE .nr
|
|
137
142
|
LDA P_X
|
|
138
143
|
CMP #140
|
|
139
144
|
BCS .nr
|
|
140
145
|
INC P_X
|
|
141
146
|
INC P_X
|
|
142
147
|
.nr:
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
LDA SWCHA
|
|
149
|
+
AND #$40 ; bit6 = P0 Left (0 = pressed)
|
|
150
|
+
BNE .nl
|
|
145
151
|
LDA P_X
|
|
146
152
|
CMP #10
|
|
147
153
|
BCC .nl
|
|
@@ -220,10 +226,29 @@ MAIN:
|
|
|
220
226
|
STA ALIEN_DIR
|
|
221
227
|
LDA ALIEN_Y
|
|
222
228
|
CMP #30
|
|
223
|
-
BCC .
|
|
229
|
+
BCC .invaded ; reached the cannon's row — the aliens GOT YOU
|
|
224
230
|
SEC
|
|
225
231
|
SBC #6
|
|
226
232
|
STA ALIEN_Y
|
|
233
|
+
JMP .noMarch
|
|
234
|
+
.invaded:
|
|
235
|
+
; Game over: the old code just CLAMPED here, so the aliens sat on top
|
|
236
|
+
; of the cannon doing nothing ("gets hit by aliens which don't kill
|
|
237
|
+
; it"). Now: harsh buzz + the wave resets to the top, like a life lost.
|
|
238
|
+
LDA #40
|
|
239
|
+
STA ALIEN_X
|
|
240
|
+
LDA #60
|
|
241
|
+
STA ALIEN_Y
|
|
242
|
+
LDA #1
|
|
243
|
+
STA ALIEN_DIR
|
|
244
|
+
LDA #$08 ; noise
|
|
245
|
+
STA AUDC0
|
|
246
|
+
LDA #$1F
|
|
247
|
+
STA AUDF0
|
|
248
|
+
LDA #$0E
|
|
249
|
+
STA AUDV0
|
|
250
|
+
LDA #20
|
|
251
|
+
STA SFX_LEFT
|
|
227
252
|
.noMarch:
|
|
228
253
|
|
|
229
254
|
; sfx countdown
|