romdevtools 0.23.0 → 0.25.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 (40) hide show
  1. package/AGENTS.md +145 -498
  2. package/CHANGELOG.md +114 -3
  3. package/examples/atari7800/templates/sports.c +6 -2
  4. package/examples/sms/templates/shmup.c +5 -2
  5. package/package.json +2 -2
  6. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  7. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  8. package/src/host/LibretroHost.js +250 -1
  9. package/src/http/skill-doc.js +1 -1
  10. package/src/mcp/tools/index.js +4 -46
  11. package/src/mcp/tools/input-layout.js +10 -0
  12. package/src/mcp/tools/input.js +31 -2
  13. package/src/mcp/tools/playtest.js +17 -2
  14. package/src/mcp/tools/project.js +39 -6
  15. package/src/mcp/tools/record.js +9 -3
  16. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +155 -6
  17. package/src/platforms/atari2600/MENTAL_MODEL.md +37 -0
  18. package/src/platforms/atari7800/MENTAL_MODEL.md +36 -0
  19. package/src/platforms/c64/MENTAL_MODEL.md +103 -6
  20. package/src/platforms/gb/MENTAL_MODEL.md +56 -0
  21. package/src/platforms/gba/MENTAL_MODEL.md +57 -3
  22. package/src/platforms/gba/lib/arm-archives/libc.a +0 -0
  23. package/src/platforms/gba/lib/arm-archives/libgcc.a +0 -0
  24. package/src/platforms/gba/lib/arm-archives/libnosys.a +0 -0
  25. package/src/platforms/gbc/MENTAL_MODEL.md +21 -0
  26. package/src/platforms/genesis/MENTAL_MODEL.md +19 -0
  27. package/src/platforms/genesis/lib/c/libc.a +0 -0
  28. package/src/platforms/genesis/lib/c/libgcc.a +0 -0
  29. package/src/platforms/genesis/lib/c/libm.a +0 -0
  30. package/src/platforms/gg/MENTAL_MODEL.md +24 -0
  31. package/src/platforms/lynx/MENTAL_MODEL.md +33 -7
  32. package/src/platforms/msx/MENTAL_MODEL.md +27 -0
  33. package/src/platforms/nes/MENTAL_MODEL.md +35 -0
  34. package/src/platforms/sms/MENTAL_MODEL.md +51 -0
  35. package/src/platforms/snes/MENTAL_MODEL.md +21 -0
  36. package/src/platforms/snes/TROUBLESHOOTING.md +43 -0
  37. package/src/playtest/playtest.js +48 -0
  38. package/examples/msx/catch_game/_verify.mjs +0 -93
  39. package/examples/pce/catch_game/_verify.mjs +0 -75
  40. package/src/mcp/tool-manifest.js +0 -92
@@ -22,6 +22,32 @@ read that platform's `platform({op:'doc', platform, name:'mental_model'})`.
22
22
  The cheat DB is bundled (`romdev_game_codes`). Do **not** scan the user's disk for
23
23
  `.cht` files — if it's not in the bundled DB, treat it as absent and RE it.
24
24
 
25
+ **Reading a `cheats({op:'lookup'})` hit as a RAM/code map.** Each decoded part carries an
26
+ `address`, a `value`, a `kind` (`ram` = a labeled variable; `code` = a labeled ROM
27
+ patch site, has a `compare`), and a `device` (`game-genie`/`pro-action-replay`/
28
+ `gameshark`/`action-replay`/`raw`). So "which byte holds magic?" → one `lookup` call.
29
+ Filter a long list with `filter:"health"` or `kind:"ram"`. A match is by No-Intro
30
+ **name/filename, NOT a verified CRC** — it's PROBABLE (a different region/revision can
31
+ move addresses), so confirm before patching. The cheapest confirmation is to apply it
32
+ and watch: `cheats({op:'apply', path, desc})` → `frame({op:'screenshot'})`.
33
+
34
+ `cheats({op:'apply'})` is non-destructive (volatile core state, the RetroArch way — the ROM
35
+ file is never touched; `host({op:'reset'})`/`state({op:'load'})`/`cheats({op:'clear'})` removes it). It takes a
36
+ matched `desc`, a raw `code`, or `loadMedia({cheats:[…]})` to seed codes BEFORE frame 0.
37
+ `appliedAs` reports how it went in (`ram` poke / `rom` read-intercept / `raw` device
38
+ code). DB coverage is 13/14 (every tier-1 system except C64); GBA cheats are
39
+ encrypted, so apply-only (no labeled-address map — see `mapNote`).
40
+
41
+ **Creating a NEW code — `cheats({op:'make', platform, address, value, compare?})`.** The inverse of
42
+ decoding: turn a byte you found (via §1 or `breakpoint`) into a shareable, verified code,
43
+ for ANY ROM incl. your own homebrew/WIP. A RAM cheat needs just `address`+`value`; a ROM
44
+ patch adds `compare` (the byte currently there). It encodes for the platform's native
45
+ device(s) and labels each (NES/Genesis → Game Genie; SNES → Pro Action Replay **and**
46
+ Game Genie; GB/GBC → Game Genie + GameShark; SMS/GG → Action Replay) plus the raw
47
+ `ADDR:VAL`; each carries `verified:true` (round-trips against the full DB). Systems with no
48
+ letter-code device (Atari 2600/7800, Lynx, GBA, C64, PC Engine, MSX) get a verified raw
49
+ code. Works on all 14. Nothing is ever written to a ROM file.
50
+
25
51
  ---
26
52
 
27
53
  ## 1. To find the RAM address of a value (score / timer / stat / HP / record-id)
@@ -50,12 +76,20 @@ usually a struct/entity array, each island one record.
50
76
  The #1 trap: visible names/labels are often **pre-rendered tile GRAPHICS**, not
51
77
  font-rendered from an ASCII string. Patching the ASCII string then does nothing.
52
78
 
53
- 1. `text({op:'learn'})` on the on-screen text. If it reports
54
- `likelyPreRenderedGraphic:true` (unique sequential tiles, no font reuse),
55
- **stop** the text is a bitmap. Editing it means changing tile pixels, not a
56
- string. Do not patch any ASCII string you found; it isn't the source.
57
- 2. If it IS font-rendered, find the string with `text({op:'find'})` /
58
- `text({op:'encode'})` and patch that.
79
+ 1. `text({op:'learn'})` on the on-screen text it infers the game's char→tile-ID map
80
+ (games use their own encoding: Excitebike A=$0A, Mario ASCII-offset, FF sparse). Two
81
+ modes: ROM mode `knownStrings:[{text, offset}]` when you found the bytes; **LIVE mode
82
+ `fromScreen:[{text, row, col}]`** reads the tile IDs straight off the live BG map at a
83
+ tile position (`background({view:'map'})` shows where the text sits) — this breaks the
84
+ chicken-and-egg of needing the offset you're still hunting. Live mode works on every
85
+ tilemap platform (NES/SNES/Genesis/GB/GBC/SMS/GG/C64); atari2600/7800, lynx, gba have
86
+ no text nametable → ROM mode only. If `learn` reports `likelyPreRenderedGraphic:true`
87
+ (unique sequential tiles, no font reuse), **stop** — the text is a bitmap. Editing it
88
+ means changing tile pixels, not a string. Do not patch any ASCII string you found.
89
+ 2. If it IS font-rendered: `text({op:'find', romPath, text, fontMap})` locates the string
90
+ (returns `fileOffset`, `prgFileOffset`, and a bank-aware `cpuAddress`+`bank` to feed
91
+ `disasm({target:'rom'})`; flags a likely length-prefix byte to avoid the overrun trap),
92
+ then `text({op:'encode', text, fontMap})` → bytes for `romPatch({op:'write'})`.
59
93
  3. To find where a graphic/text was sourced from: on **Genesis**, `watch({on:'dma', precision:'sampled'})`
60
94
  — drive to the screen that shows the graphic, and it reports the ROM offset(s)
61
95
  the tiles were DMA'd from (decoded from the VDP DMA registers). Edit the tile
@@ -103,6 +137,15 @@ from a SOURCE struct rather than written in place. Don't conclude "the address
103
137
  is wrong." Find the source: `memory({op:'search'})` the live value to locate the struct
104
138
  the copy reads from, then `breakpoint({on:'write'})` on THAT.
105
139
 
140
+ **Precision — exact vs sampled.** The default `breakpoint({on:'write'})` is a core-level write
141
+ watchpoint: it returns the EXACT writing instruction's PC, captured inside the CPU write
142
+ path — correct even for NMI/IRQ-driven writes (the common case where a frame-sampled PC
143
+ is just the idle loop). On a banked mapper it reports the `bank` (NES/GB/SMS-GG) so you
144
+ can pass `{startAddress, bank}` to `disasm({target:'rom'})`. The lighter
145
+ `breakpoint({on:'write', precision:'sampled'})` (a.k.a. `watch({on:'mem'})`) steps until the byte changes
146
+ and returns a frame-boundary PC — a lead, not a guarantee under interrupts; use it for the
147
+ value timeline or when you just want the change history, and cross-check the value trace.
148
+
106
149
  ---
107
150
 
108
151
  ## 5b. To READ a register at an instruction — execution breakpoints (all 14)
@@ -254,6 +297,101 @@ and `input({op:'set'})` state it too.)
254
297
 
255
298
  ---
256
299
 
300
+ ## 7. Authoring & verifying the byte patch
301
+
302
+ Once you know WHAT to change, the write loop is a handful of calls — no custom scripts:
303
+
304
+ - **`assembleSnippet({cpu, origin, code})`** — assemble a tiny asm chunk to raw bytes (no
305
+ header/linker/segments). CPUs: `6502 / 65c02 / 65816 / 68k / z80 / sm83 / gb / gbc /
306
+ huc6280`. **Z80 gotcha:** the sdas dialect requires `#` on immediates (`ld a,#5`, not
307
+ `ld a,5`).
308
+ - **`romPatch({op:'write', path, offset, hex, expect})`** — the splicer THE other hack tools
309
+ compose through. **Always pass `expect`** (the current bytes) — it refuses the write if
310
+ they don't match, catching a hex/dec slip or a patch authored against region A applied to
311
+ region B. `allowExpand` for size-changing edits.
312
+ - **`romPatch({op:'diff', platform, a, b})`** — mapper-aware ROM diff: reports CPU addresses
313
+ (NROM-128 mirrors, SNES LoROM `XX:XXXX`), per-region tallies (PRG vs CHR vs header), and
314
+ `tile:N` annotations on CHR changes for direct sprite-hack identification. Use it to
315
+ confirm a patch landed where you meant.
316
+ - **`disasm({target:'references', path, platform, address})`** — find every instruction that
317
+ references a target address, classified `call/jump/branch/read/write/use/ref` (walks the
318
+ vector table too). The fast "who touches this?" for a STATIC image. Limitation: direct
319
+ addressing only — indirect/computed jumps aren't detected (use the runtime `watch`/
320
+ `breakpoint` tools in §5/§5d for those).
321
+ - **`cart({op:'extract', path, outputDir})`** — split a ROM into standard parts (NES header/
322
+ prg/chr; SNES copier_header+rom+internal header; Genesis vectors/header/body; GB boot/
323
+ header/body) + a `manifest.json` (mapper, mirroring…). **`cart({op:'wrap'})`** is the inverse:
324
+ emits `wrapperSource` + `linkerConfig` ready for `build({output:'rom'})`.
325
+
326
+ Verify-before-patch: `memory({op:'write', region:'system_ram', offset, hex})` on the LIVE emulator and
327
+ watch the screen react — cheaper than shipping a wrong ROM patch.
328
+
329
+ ## 7b. Whole-ROM rebuildable disassembly — `disasm({target:'project'})`
330
+
331
+ For a STRUCTURAL hack (new logic, not a byte poke), turn the whole ROM into a
332
+ re-buildable project in one call: `disasm({target:'project', path, outputDir})`. It splits
333
+ the ROM into regions (per-16KB bank for banked NES, per-32KB for SNES LoROM, slot0+slotX
334
+ for GB, one flat region for SMS/Genesis/C64/Atari), disassembles each through the CPU's
335
+ native objdump, then **reassembles + verifies byte-exact** against the original; any line
336
+ that won't reproduce faithfully heals to a `.byte`/`db` of its real bytes, so the emitted
337
+ `.asm` ALWAYS rebuilds (`roundTrip.allByteExact`). `readablePercent` per region tells you
338
+ how much came back as real instructions vs. data. Alongside the `.asm` it writes the
339
+ turnkey **rebuild glue**: data blobs (NES CHR-ROM → `chr.bin`; stripped Genesis/GBA/Lynx/MSX
340
+ cartridge header → `*.bin`), a `BUILD.md` with the exact steps, and — where a one-call
341
+ rebuild exists — a `rebuild.json` of the precise `build({...})` args. So the loop is
342
+ `disasm({target:'project'})` → edit a `.asm` → rebuild → `romPatch({op:'diff'})` to confirm.
343
+
344
+ **Two rebuild tiers** (the disasm emits each CPU's native-reassembler syntax — ca65 for
345
+ 6502/65816, GNU `as` for m68k/arm/z80/gbz80 — which only some `build()` toolchains consume):
346
+ - **One-call `build()` rebuild, byte-identical** — **NES, C64, Atari 7800, Lynx**. Feed
347
+ `rebuild.json` straight to `build`. (Lynx: `build()` yields the headerless image; prepend
348
+ the shipped `lnx_header.bin` for the full `.lnx`.)
349
+ - **Native-recipe rebuild (`buildCall:null`), byte-identical, steps in `BUILD.md`** — **SMS,
350
+ GG, MSX, GB, GBC, Genesis, GBA, Atari 2600**. Their `build()` toolchains (SDCC/RGBDS/asar/
351
+ dasm/vasm) can't reassemble ca65/GNU-as syntax, so `BUILD.md` gives the proven native
352
+ `as`/`ld`/`objcopy` chain.
353
+ - **PC Engine** is the one not-yet-byte-exact case (the region trims real padding / doesn't
354
+ strip a copier header) — `BUILD.md` flags it.
355
+
356
+ **Rebuilding a commercial NES (NROM CHR-ROM) game — `build({inesHeader})`:** the most common
357
+ NES RE rebuild. `build({output:'rom', platform:'nes', inesHeader:{prgBanks, chrBanks, mapper,
358
+ mirroring}, sourcesPaths:{…the PRG…}, binaryIncludePaths:{"chr.bin":…}})` auto-emits the
359
+ 16-byte iNES header + CHARS-segment wiring + flat NROM `.cfg` — no hand-derived header bytes.
360
+ `disasm({target:'project'})` puts exactly this call in `rebuild.json`. (For homebrew C that
361
+ ships fixed tile art, `linkerConfig:"chr-rom"` is the segment-split equivalent.)
362
+
363
+ **Readability caveats** (the bytes are ALWAYS correct; only instruction-vs-`.byte` coverage
364
+ varies): SNES and large Genesis ROMs come back byte-exact but DATA-ONLY (flat whole-ROM
365
+ disasm of a mostly-data image heals to `.byte` — meaningful coverage needs recursive
366
+ entry-point following, a known follow-up). GBA reads LOW because GBA C compiles mostly to
367
+ Thumb reached via an ARM crt0 stub, so an ARM-mode disasm decodes Thumb spans as `.byte`.
368
+ Banked-NES is the strongest case (~100% instructions); GB/GBC, SMS/GG, C64, Atari are also
369
+ near-100%.
370
+
371
+ ## 8. Graphics swaps — PNG ↔ tiles round-trip
372
+
373
+ For sprite/tile edits (not text), don't hand-roll the tile-format math:
374
+
375
+ - **`tiles({op:'png', source:'path', platform, path, bank, paletteFromEmulator, paletteIndex})`** —
376
+ a source ROM's tiles → PNG. `bank:N` (NES 4 KB CHR bank) replaces magic file-offset math;
377
+ `paletteFromEmulator:true`+`paletteIndex` colors the export with the LIVE palette (vs
378
+ grayscale) so the art is recognizable to edit.
379
+ - **`importArt({from:'rom', sourceRom, sourcePlatform, sourceBank, sourceTileX/Y/W/H, targetPlatform,
380
+ outputPng, intent, paletteIndex})`** — one-call lift of a tile region from a source ROM into
381
+ the target platform's format (extract+crop+quantize). `intent:"homebrew"` reads the live
382
+ source palette; `intent:"rom-hack"` preserves source bytes verbatim.
383
+ - **`encodeArt({stage:'tiles', platform, pngBase64})`** → target-platform tile bytes.
384
+ - **`romPatch({op:'spliceCHR', path, platform, pngBase64, tileIndex, expect, bank, paletteHint})`** —
385
+ PNG → tile bytes → splice into CHR at tile slot N (auto-locates iNES CHR base; `expect`
386
+ checks the existing tile bytes; `paletteHint:["#RRGGBB",…]` gives explicit RGB→index
387
+ mapping). Composes the `encodeArt`+`romPatch({op:'write'})` step in one call.
388
+ - **`background({view:'rendered'})`** — at the current state, the set of tile IDs actually drawn
389
+ (BG nametable + OAM). Sample at title/gameplay/menu and diff the sets to map tile IDs to
390
+ assets without scanning sheets by eye. (`romPatch({op:'findFree'})` locates $FF/$00 runs for asm
391
+ overlays, longest-first.)
392
+
393
+ ---
394
+
257
395
  ## Quick reference
258
396
 
259
397
  | Goal | Tool |
@@ -276,4 +414,15 @@ and `input({op:'set'})` state it too.)
276
414
  | Where did a VRAM graphic come from (Genesis) | `watch({on:'dma', precision:'sampled'})` (ROM offset of the DMA source) |
277
415
  | Drive a menu fast | `input({op:'navigate'})` (advances on screen change) |
278
416
  | Free RAM map for a known game | `cheats({op:'lookup'})` / `cheats({op:'search'})` |
417
+ | Apply a cheat live (non-destructive) | `cheats({op:'apply'})` (verify a label / fun) |
418
+ | Create a shareable code from a byte | `cheats({op:'make'})` (verified, all 14) |
419
+ | Read on-screen text's tile map | `text({op:'learn', fromScreen})` (live, no offset needed) |
420
+ | Find / encode a font-rendered string | `text({op:'find'})` → `text({op:'encode'})` |
421
+ | Assemble asm → raw patch bytes | `assembleSnippet({cpu, origin, code})` |
422
+ | Mapper-aware diff of two ROMs | `romPatch({op:'diff'})` (CPU addrs, CHR `tile:N`) |
423
+ | Who references this address (static) | `disasm({target:'references'})` (direct modes only) |
424
+ | Split / rebuild a ROM into parts | `cart({op:'extract'})` / `cart({op:'wrap'})` |
425
+ | Swap a sprite/tile (PNG round-trip) | `tiles({op:'png'})` → edit → `romPatch({op:'spliceCHR'})` |
426
+ | Lift art from another game's ROM | `importArt({from:'rom'})` |
427
+ | Tile IDs actually being drawn now | `background({view:'rendered'})` |
279
428
  | Safe patch | `romPatch({op:'write'})`/`romPatch({op:'writeMany'})` with `expect` |
@@ -192,3 +192,40 @@ When you call `build({output:'rom', platform:"atari2600", source: ...})`:
192
192
  2. The result is `.a26` — loadable in stella (`loadMedia`).
193
193
 
194
194
  There's no linker — dasm produces a complete cart in one pass.
195
+
196
+ ## MCP debug & inspection tooling
197
+
198
+ The 2600 runs on the **stella2014 (patched)** core. Because the 2600 has
199
+ no framebuffer and no standard sound chip, its inspectors look different
200
+ from the tilemap consoles — they decode the live TIA snapshot instead.
201
+
202
+ What you can read:
203
+
204
+ - **`palette({source:'live'})`** — the NTSC 128-color palette as a PNG,
205
+ with the *current* TIA background luma+hue extracted from the live
206
+ snapshot so you can see what color the beam is painting right now. This
207
+ is the same 128-entry `HHHHLLLL` palette the 7800 uses.
208
+ - **`sprites({op:'inspect'})`** — there is **no OAM** on the 2600, so this
209
+ returns the state of the 5 TIA graphics objects (P0, P1, M0, M1, Ball)
210
+ plus a current-scanline PNG showing how the TIA is composing that line.
211
+ - **`cpu({op:'read'})`** — the 6502 register file (A / X / Y / P / SP / PC)
212
+ pulled from the M6502 core's internal regs.
213
+ - **`background({view:'renderState'})`** — decodes the 32-byte TIA snapshot
214
+ into the playfield pattern, per-object enables/positions, and the color
215
+ registers.
216
+ - **`disasm({target:'rom'})`** and **`disasm({target:'references'})`** —
217
+ both anchor to the top of the bank (`$F000-$FFFF`) and label the vector
218
+ table (NMI / RESET / IRQ at `$FFFA`).
219
+
220
+ Memory regions for **`memory({op:'read'})`**:
221
+
222
+ | Region | Size | What it is |
223
+ | --- | --- | --- |
224
+ | `system_ram` | 128 bytes | the RIOT RAM — that's the *entire* console RAM |
225
+ | `a26_tia_regs` | 32 bytes | the live TIA register snapshot |
226
+ | `a26_cpu_regs` | 7 bytes | the 6502 register snapshot |
227
+
228
+ **No `audioDebug` inspector.** The 2600's sound comes from the two TIA
229
+ audio voices (`AUDC/AUDF/AUDV` at `$15-$1A`), not a standard PSG/FM chip,
230
+ so there's no `audioDebug` decode — read the audio state directly out of
231
+ the `a26_tia_regs` snapshot instead.
@@ -322,3 +322,39 @@ When you call `build({output:'rom', platform:"atari7800", language:"c"})`:
322
322
  3. ld65 links + atari7800.cfg → flat `.a78` ROM.
323
323
 
324
324
  Loadable via prosystem (`loadMedia`).
325
+
326
+ ## MCP debug & inspection tooling
327
+
328
+ The 7800 runs on the **prosystem (patched)** core. Like the 2600 it has
329
+ no framebuffer and no tile/sprite-attribute tables, so its inspectors
330
+ decode MARIA's display-list machinery rather than a tilemap.
331
+
332
+ What you can read:
333
+
334
+ - **`palette({source:'live'})`** — a 256-color master palette PNG, with
335
+ the live MARIA palette block at `$20-$3F` decoded into the 8 palettes ×
336
+ 3 colors each, plus the backdrop. This is the Atari NTSC palette shared
337
+ with the 2600.
338
+ - **`sprites({op:'inspect'})`** — there is **no OAM** on the 7800. Instead
339
+ this returns the MARIA control registers and the **DPP** display-list-
340
+ list pointer, leaving the agent to walk the DLL → DL hierarchy itself
341
+ (the same structure described under "MARIA: the unusual one" above).
342
+ - **`cpu({op:'read'})`** — the 6502 ("Sally") register file (A / X / Y /
343
+ P / SP / PC) read from prosystem's `sally` globals.
344
+ - **`background({view:'renderState'})`** — the MARIA CTRL bits, DPP,
345
+ CHARBASE, and the current `dlistPtr`.
346
+ - **`disasm({target:'rom'})`** and **`disasm({target:'references'})`** —
347
+ both default to the top 16 KB (`$C000-$FFFF`), where the reset vector
348
+ lands.
349
+
350
+ Memory regions for **`memory({op:'read'})`**:
351
+
352
+ | Region | Size | What it is |
353
+ | --- | --- | --- |
354
+ | `system_ram` | 64 KB | the *entire* 6502 address space — MARIA regs, RAM, and ROM are all visible through this one region |
355
+ | `a78_cpu_regs` | — | the 6502 register snapshot |
356
+
357
+ **No `audioDebug` inspector.** The 7800's standard audio is the same TIA
358
+ chip carried over from the 2600 (`$15-$1A`), not a decodable PSG/FM chip,
359
+ so there's no `audioDebug` decode. (Some carts add a POKEY, but it's
360
+ non-standard — don't assume it's present.)
@@ -41,6 +41,12 @@ Default after boot = `$37` (all three set) — BASIC + KERNAL ROMs +
41
41
  I/O regs. Set CHAREN=0 to read the character-set bitmaps from ROM;
42
42
  set HIRAM=0 to swap KERNAL out for RAM (useful for low-memory tricks).
43
43
 
44
+ > **cc65 zero-page starts at $02 (same cc65 trap as NES/Atari/Lynx).** cc65
45
+ > reserves `$00-$01` (here it's also the 6510 I/O port), so your first
46
+ > `.res 1` in `ZEROPAGE` lands at **$02**, not $00. Don't hand-write asm that
47
+ > assumes a ZP var is at $00. Confirm with `symbols({op:'map'})` after
48
+ > `build({output:'romWithDebug'})`.
49
+
44
50
  ## VIC-II — character cells, NOT tiles
45
51
 
46
52
  The C64's video chip displays 25 rows × 40 cols of 8×8 character
@@ -107,12 +113,55 @@ which is what the KERNAL's IRQ uses to update key state every
107
113
 
108
114
  ### Driving input over MCP
109
115
 
110
- The C64 joystick has **one** fire button. Over MCP press it with
111
- `input({op:'set', b: true})` or the spatial `input({op:'set', south: true})` both clear
112
- `$DC00` bit 4 (verified live against vice). `input({op:'set', a: true})` is a **no-op**
113
- (no second button not in `input({op:'layout'})`'s `physicalButtons`). So drive fire
114
- with `b`/`south`, plus the d-pad. ⚠ A cc65 `.prg` only starts after BASIC
115
- auto-`RUN`s it — step ~70+ frames past load before input/reads register.
116
+ **Joystick.** One fire button. Press it with `input({op:'set', b: true})` (or
117
+ spatial `south`) both clear `$DC00` bit 4 (verified live). `a` is a **no-op**
118
+ (no second button). Drive fire with `b`/`south` + the d-pad. The joystick reads
119
+ **port 2** by default; switch with `input({op:'joyport', joyport:1})` /
120
+ `input({op:'joyport'})` to read it.
121
+
122
+ **Keyboard (the C64-specific part — many games NEED it).** Unlike consoles, most
123
+ C64 games (and cracktros) gate gameplay behind a KEYBOARD setup screen — **F1**
124
+ to pick 1 player, RUN/STOP, SPACE/RETURN — that the joystick can't reach. So if a
125
+ joystick gets you to a title/intro but not into play, you almost certainly need a
126
+ key:
127
+ - `input({op:'pressKey', key:'f1'})` — press one C64 key (held + auto-released).
128
+ Keys: `f1/f3/f5/f7`, `return`, `space`, `run/stop`, `a-z`, `0-9`, `ctrl`, `cbm`,
129
+ `home`, `down`, `right`, `lshift`, `rshift`.
130
+ - `input({op:'typeText', text:'LOAD"*",8,1\rRUN\r'})` — type a string (`\r` =
131
+ RETURN). For BASIC commands / filenames.
132
+ - `input({op:'layout', platform:'c64'})` lists the keyboard keys + the joyport.
133
+
134
+ A typical C64 RE startup: load → step to the title → `pressKey f1` (1 player) →
135
+ `set {b:true}` (fire to start) → step → you're in gameplay → `state({op:'save'})`.
136
+
137
+ **Script the whole startup in one call.** `recordSession`'s `inputScript` takes
138
+ C64 `keys` alongside joystick `ports` — held from each entry until the next — so a
139
+ key+joystick startup is one deterministic timeline instead of many calls:
140
+ ```js
141
+ recordSession({ frames:600, sampleEvery:60, outputDir:'…', inputScript:[
142
+ { atFrame:0, keys:['f1'] }, // 1 player
143
+ { atFrame:30, ports:[{ b:true }] }, // fire (port 2)
144
+ { atFrame:90, keys:['run/stop'] }, // start
145
+ { atFrame:120, keys:[] } ] }) // release
146
+ ```
147
+ A step may set just `keys`, just `ports`, or both; `keys:[]` releases all. An
148
+ unknown key is rejected with a clear error (not a silent no-op).
149
+
150
+ **When a key seems ignored, probe it.** `input({op:'pressKey', key:'run/stop',
151
+ verify:true})` returns the matrix coords, active joyport, and a CIA1 snapshot
152
+ (`$DC00`/`$DC01`) **before / during (held) / after**. `before==during` ⇒ the key
153
+ never moved the matrix line (didn't reach VICE); they differ but the game didn't
154
+ react ⇒ it scanned a different key/port, or that screen (crack/doc/trainer)
155
+ ignores it. This is how you tell a romdev problem from a game-flow problem.
156
+
157
+ **Controller-alone (playtest):** a human in `playtest` needs no keyboard — the
158
+ pad's spare buttons map to the C64 keys (X=Space, L2=Run/Stop, R2=Return,
159
+ right-stick=F1/F3/F5/F7, top face=F1; d-pad+Fire=joystick). The same mapping
160
+ applies to the agent's `setInput`, so e.g. `input({op:'set', c64_f1:true})` also
161
+ presses F1. `playtest({op:'open'})` returns `c64Controls` to relay to the user.
162
+
163
+ ⚠ A cc65 `.prg` only starts after BASIC auto-`RUN`s it — step ~70+ frames past
164
+ load before input/reads register.
116
165
 
117
166
  ## SID — three voices of fame
118
167
 
@@ -131,6 +180,54 @@ the `sid_play.s` starter snippet.
131
180
  waveform, freq→note, pulse-width, ADSR — plus the filter cutoff/resonance/mode.
132
181
  Handy for verifying a `sid_play` routine is actually gating notes.
133
182
 
183
+ ## MCP debug & inspection tooling
184
+
185
+ romdev runs the C64 on a patched VICE (`vice_x64`) core that exposes deep live
186
+ state. The inspectors all read the **running** machine — use them to confirm what
187
+ your code actually did to the hardware, not what you think it did.
188
+
189
+ **Live visual / state inspectors:**
190
+
191
+ - `palette({source:'live'})` — the 16-color hardware-fixed C64 palette as a PNG,
192
+ plus the current border / background / extra-background color indices decoded
193
+ straight from the VIC-II registers.
194
+ - `sprites({op:'inspect'})` — all 8 MOBs decoded into the generic sprite shape:
195
+ X/Y, color, multicolor flag, expand-X / expand-Y, priority, AND the screen-RAM
196
+ sprite-data pointers at `$07F8` so you can locate each sprite's pixel block in
197
+ the VIC bank.
198
+ - `cpu({op:'read'})` — the 6510 registers (A/X/Y/P/SP/PC) read from a live,
199
+ `#define`-aliased register file, plus the `$0001` I/O port decoded into its
200
+ LORAM / HIRAM / CHAREN bits (so you can see which ROMs are banked in).
201
+ - `background({view:'renderState'})` — the VIC-II registers decoded into
202
+ mode / scroll / colors / sprites, the VIC bank resolved from CIA2 `$DD00`, and
203
+ the **absolute** screen-RAM + character-base addresses (no manual bank math).
204
+ - `audioDebug({op:'inspect', chip:'sid'})` — the SID voice/filter decode covered
205
+ in the SID section above.
206
+
207
+ **`c64_*` memory regions** (via `memory({op:'read'})`) — exact, named windows
208
+ onto the hardware, decoded live:
209
+
210
+ | region | size | notes |
211
+ | --------------- | ----- | ----------------------------------------------- |
212
+ | `system_ram` | 64 KB | full RAM |
213
+ | `c64_color_ram` | 1 KB | the nibble color RAM |
214
+ | `c64_vic_regs` | 64 B | VIC-II registers |
215
+ | `c64_sid_regs` | 29 B | SID registers (read via `sid_peek`) |
216
+ | `c64_cia1_regs` | 16 B | CIA1, from the `c_cia[]` array |
217
+ | `c64_cia2_regs` | 16 B | CIA2, from the `c_cia[]` array |
218
+ | `c64_cpu_regs` | 7 B | 6510 register file |
219
+
220
+ **Disassembly:** `disasm({target:'rom'})` and `disasm({target:'references'})`
221
+ accept `.prg` files (they understand the 2-byte little-endian load-address
222
+ header), and apply the C64 register-annotation table so VIC-II / SID / CIA
223
+ register accesses come back named rather than as bare addresses.
224
+
225
+ **Starter snippets** cover `vic_init` / `sprite_table` / `sid_play` /
226
+ `read_joystick` / `basic_stub`.
227
+
228
+ Disk-image loading and disk SAVE/restore tooling have their own sections below
229
+ ("Disk images" and "Disk SAVES").
230
+
134
231
  ## Cartridge / load file format
135
232
 
136
233
  The .prg format is dead simple:
@@ -14,6 +14,62 @@ one call fuses a framebuffer pixel scan with the live LCDC and returns
14
14
  `verified:null` = step a frame first. Zero image tokens, frame-0-guarded — use it
15
15
  as the first move when a change "did nothing."
16
16
 
17
+ ## Toolchains
18
+
19
+ Default is **C** via SDCC's sm83 port (the same SDCC that powers SMS/GG/MSX/
20
+ Coleco). For hand-tuned asm, pass `language:"asm"` to route through RGBDS. The C
21
+ path uses `__sfr __at 0xFFNN` to bind GB I/O regs; the helper headers under
22
+ `src/platforms/gb/lib/c/gb_hardware.h` define LCDC/STAT/SCY/SCX/LY/BGP/OBP0/OBP1/
23
+ etc. for both DMG and CGB. ⚠ SDCC 4.4.0 codegen quirk: `for (;;) { switch + write
24
+ to __sfr }` crashes the register allocator — use `do { ... } while (1)` and
25
+ table-lookup writes instead. (See the GB/GBC SDCC_GOTCHAS for the full set of
26
+ sm83 codegen footguns.)
27
+
28
+ ## MCP debug & inspection tooling
29
+
30
+ The bundled gambatte core is patched to expose deep live state — sprites,
31
+ palettes, tiles, background/LCDC, CPU, and raw memory regions. This applies to
32
+ **both `gb` and `gbc`** builds (one shared gambatte core).
33
+
34
+ **Live inspectors (decode hardware state, no manual byte-twiddling):**
35
+
36
+ - **`sprites({op:'inspect'})`** — decodes all 40 OAM slots and renders a
37
+ sprite-sheet PNG with sprite-priority + horizontal/vertical flip applied.
38
+ - **`palette({source:'live'})`** — DMG path decodes the BGP / OBP0 / OBP1 bytes
39
+ into 4 shades each; CGB path decodes the 64-byte BCPS/OCPS palette RAM into
40
+ 8 palettes × 4 colors in BGR555.
41
+ - **`tiles({op:'png'})`** — renders all 384 tiles from $8000-$97FF.
42
+ - **`cpu({op:'read'})`** — SM83 register file: A/F/BC/DE/HL + flags + IME/halt.
43
+ - **`audioDebug({op:'inspect', chip:'gb'})`** — DMG APU decode: 2 pulse + wave
44
+ + noise channels, with timer→freq→note conversion, sweep, duty, and panning,
45
+ read straight from the live `NR*` registers.
46
+ - **`background({view:'renderState'})`** — LCDC bit-by-bit, scroll (SCX/SCY),
47
+ LY/LYC, window state, plus CGB extras: current VRAM bank, KEY1, and the
48
+ BCPS/OCPS palette index.
49
+
50
+ **Raw memory regions** via `memory({op:'read', region:...})`:
51
+
52
+ | Region | Contents |
53
+ |---|---|
54
+ | `gb_vram` | VRAM ($8000-$9FFF) — tile data + BG maps (CGB: the active bank) |
55
+ | `gb_oam` | OAM ($FE00-$FE9F) — 40 sprites × 4 bytes |
56
+ | `gb_io` | I/O register page ($FF00-$FF7F) — LCDC, BGP, JOYP, CGB regs, etc. |
57
+ | `gb_hram` | HRAM ($FF80-$FFFE) — fast scratch |
58
+ | `gb_bgpdata` | CGB BG palette RAM (64 bytes) |
59
+ | `gb_objpdata` | CGB OBJ palette RAM (64 bytes) |
60
+ | `gb_cpu_regs` | SM83 register snapshot |
61
+
62
+ ⚠ **Gotcha: gambatte exposes `gb_vram`, NOT the generic `video_ram` region.**
63
+ Other platforms' cores expose video memory under `video_ram`; on GB/GBC you must
64
+ ask for `gb_vram` (and the other `gb_*` names above). A `video_ram` read here
65
+ returns nothing.
66
+
67
+ **Disassembly:** `disasm({target:'rom'})` + `disasm({target:'references'})` +
68
+ `disasm({target:'project'})` route through the native binutils z80 `objdump` in
69
+ its `gbz80` machine (WASM, `-m gbz80`) — full CB-prefix coverage plus the
70
+ SM83-specific opcodes (`ld (hl+),a`, `ldh`, `reti`, `ld hl,sp+e8`). One z80-elf
71
+ binutils serves both plain Z80 (SMS/GG/MSX) and the GB CPU.
72
+
17
73
  ## Five silent-failure footguns to know before you start (R26 + R27)
18
74
 
19
75
  If your ROM compiles cleanly but doesn't render — or sprites land in
@@ -125,9 +125,9 @@ maxmod (separate library, not bundled here).
125
125
 
126
126
  **Debugging sound:** `audioDebug({op:'inspect', chip:"gba"})` decodes the live APU —
127
127
  per-channel freq→note/duty/volume for the 4 tone channels plus the 2 Direct
128
- Sound FIFO states. Pair with `sprites({op:'inspect'})`/`palette({source:'live'})`/
129
- `background({view:'renderState'})`/`cpu({op:'read'})` (ARM7) and `breakpoint({on:'write'})` for the rest of the
130
- live-debug loop.
128
+ Sound FIFO states. See "MCP debug & inspection tooling" below for the rest of
129
+ the live-debug loop (sprites / palette / background / cpu / breakpoint + the
130
+ memory regions and disasm pipeline).
131
131
 
132
132
  **For scaffold-level sfx**, the libtonc runtime ships a minimal
133
133
  `gba_sfx.h` / `gba_sfx.c` pair (3 functions: `sfx_init`, `sfx_tone`,
@@ -136,6 +136,60 @@ as the NES/GB scaffold sound API, so cross-platform game ports feel
136
136
  the same. All 5 GBA genre scaffolds (shmup/platformer/puzzle/sports/
137
137
  racing) use it.
138
138
 
139
+ ## MCP debug & inspection tooling
140
+
141
+ GBA runs on mGBA (patched). These inspectors read the *live* core state —
142
+ reach for them when a sprite, palette, or BG renders wrong and the source
143
+ alone doesn't explain it. (The audio inspector is also summarized under
144
+ "Sound" above.)
145
+
146
+ - **`sprites({op:'inspect'})`** — decodes all **128 OAM sprites** into a
147
+ generic shape: attr0/1/2 unpacked to shape + size, **9-bit signed X**,
148
+ the affine and hidden flags, and tile / palette / priority.
149
+ - **`palette({source:'live'})`** — reads the palette as **15-bit BGR555**:
150
+ 256 BG entries + 256 OBJ entries. Pass `area:'bg'` or `area:'sprite'` to
151
+ pick the half.
152
+ - **`cpu({op:'read'})`** — ARM7TDMI dump: the 16 general regs **r0-r15**,
153
+ `cpsr` + `spsr`, the processor mode, the ARM/THUMB state bit, and an
154
+ **`execPc`** field that is r15 adjusted back for the pipeline prefetch
155
+ (r15 reads ahead of the executing instruction, so raw r15 is misleading —
156
+ use `execPc` for "where am I really").
157
+ - **`audioDebug({op:'inspect', chip:'gba'})`** — the 4 DMG-compatible PSG
158
+ channels (per-channel freq→note / duty / volume) plus the **2 Direct Sound
159
+ DMA FIFO** states, and master / bias. See "Sound" above.
160
+ - **`background({view:'renderState'})`** — decodes DISPCNT: the BG mode, and
161
+ per-BG enable / priority / char-base / map-base / color-mode, the
162
+ forced-blank bit, and OBJ enable. Use it to confirm REG_DISPCNT and the
163
+ REG_BGxCNT bases match where you uploaded tiles + maps.
164
+
165
+ ### Memory regions (`memory({op:'read', region:…})`)
166
+
167
+ | Region | Address / size | Contents |
168
+ |-----------------|------------------------------------|-------------------------------------------|
169
+ | `gba_cpu_regs` | — | ARM7TDMI register snapshot |
170
+ | `gba_io_regs` | $04000000-$040003FE (1 KB) | the I/O page — **video AND audio** MMIO |
171
+ | `gba_palette` | $05000000-$050003FF (1 KB) | 256 BG + 256 OBJ BGR555 entries |
172
+ | `gba_oam` | $07000000-$070003FF (1 KB) | 128 sprite attribute entries (8 B each) |
173
+ | `system_ram` | $02000000 EWRAM / $03000000 IWRAM | main + on-chip work RAM |
174
+ | `video_ram` | $06000000-$06017FFF (96 KB) | BG + sprite tile data + framebuffer |
175
+ | `save_ram` | $0E000000-$0E00FFFF (64 KB) | battery-backed SRAM |
176
+
177
+ Pair `sprites` / `palette` / `background` / `cpu` with
178
+ `breakpoint({on:'write'})` for the full live-debug loop.
179
+
180
+ ### Disassembly (`disasm({target:…})`)
181
+
182
+ `disasm({target:'rom'})`, `disasm({target:'references'})`, and
183
+ `disasm({target:'project'})` run the native binutils
184
+ **`arm-none-eabi-objdump`** (WASM) — **ARM mode by default**, pass
185
+ `thumb:true` for Thumb code. The byte-exact project reassembles through
186
+ `arm-none-eabi-as` / `ld` / `objcopy`.
187
+
188
+ **Gotcha (until ARM/Thumb mode-tracking lands):** GBA C compiles mostly to
189
+ **Thumb** reached via an **ARM crt0 stub**, so an ARM-mode disasm of a full
190
+ ROM decodes the Thumb spans as `.byte` — still byte-exact, just less readable.
191
+ Disasm the Thumb spans with `thumb:true` to get real mnemonics.
192
+
139
193
  ## Frame heartbeat
140
194
 
141
195
  ```c
@@ -74,6 +74,27 @@ the same wall.
74
74
  - **HDMA** ($FF51-$FF55) for fast block transfers during HBlank —
75
75
  used for live tile streaming.
76
76
 
77
+ ## MCP debug & inspection tooling
78
+
79
+ GBC shares the patched gambatte core with DMG, so **all the live inspectors
80
+ and `gb_*` memory regions documented in the GB MENTAL_MODEL apply unchanged
81
+ here** — `sprites({op:'inspect'})`, `tiles({op:'png'})`, `cpu({op:'read'})`,
82
+ `audioDebug({op:'inspect', chip:'gb'})`, and the `gb_vram` / `gb_oam` / `gb_io`
83
+ / `gb_hram` / `gb_cpu_regs` regions (same gotcha: it's `gb_vram`, NOT the
84
+ generic `video_ram`). Disassembly routes through the same `-m gbz80` objdump.
85
+ See the GB MENTAL_MODEL for the shared gambatte debug tooling.
86
+
87
+ CGB-only deltas on top of that shared set:
88
+
89
+ - **`palette({source:'live'})`** on a CGB ROM decodes the **64-byte BCPS/OCPS
90
+ palette RAM** into **8 palettes × 4 colors in BGR555** (the DMG path that
91
+ decodes BGP/OBP0/OBP1 bytes is what runs on a `gb` build instead). The raw
92
+ CGB palette RAM is also readable directly via the **`gb_bgpdata`** (BG, 64
93
+ bytes) and **`gb_objpdata`** (OBJ, 64 bytes) memory regions.
94
+ - **`background({view:'renderState'})`** reports the CGB extras the DMG path
95
+ doesn't have: the current **VRAM bank** (VBK), **KEY1** (double-speed state),
96
+ and the live **BCPS/OCPS palette index**.
97
+
77
98
  ## CGB vs DMG mode
78
99
 
79
100
  The CGB boot ROM checks header byte **`$0143`**:
@@ -413,6 +413,25 @@ headless per-PCM-channel "is it playing" readout for Genesis yet (it would need
413
413
  core patch to expose the XGM2 Z80 driver state), so audio verification here is
414
414
  record-and-listen, not assert.
415
415
 
416
+ ## MCP debug & inspection tooling
417
+
418
+ The shipped genesis_plus_gx (gpgx) core is patched for live introspection.
419
+ Video is deeply readable; the FM audio chip is only partially exposed:
420
+
421
+ - **Sprites:** `sprites({op:'inspect'})` decodes the live SAT.
422
+ - **Palette:** `palette({source:'live'})` reads live CRAM.
423
+ - **CPU:** `cpu({op:'read', cpu:'main'})` reads the 68000.
424
+ - **Audio (limited):** `getYm2612State` returns the YM2612's internal
425
+ struct as a raw blob — gpgx doesn't expose it in a safely per-channel
426
+ decodable form (good for frame-to-frame diffing, see "Debugging sound").
427
+ `getPsgState` decodes the SN76489 (3 tone + 1 noise channels).
428
+ - **Memory regions:** `memory({op:'read'})` exposes CRAM, VSRAM, VDP_REGS,
429
+ Z80_RAM (the sound CPU's RAM), M68K work RAM, YM2612, PSG, and VRAM.
430
+ Remember the gpgx byte-swap quirk: VRAM and WRAM read host-LE
431
+ word-byte-swapped (a 16-bit value's two bytes are swapped at the offset)
432
+ — account for it or read single bytes (see "Reading your C globals
433
+ headlessly").
434
+
416
435
  ## ROM layout
417
436
 
418
437
  ```
Binary file
Binary file