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.
Files changed (144) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +322 -3
  3. package/README.md +1 -1
  4. package/examples/README.md +1 -1
  5. package/examples/atari2600/templates/platformer.asm +18 -9
  6. package/examples/atari2600/templates/racing.asm +25 -4
  7. package/examples/atari2600/templates/shmup.asm +30 -5
  8. package/examples/atari2600/templates/sports.asm +41 -9
  9. package/examples/atari7800/templates/hello_sprite.c +8 -4
  10. package/examples/atari7800/templates/platformer.c +12 -8
  11. package/examples/atari7800/templates/puzzle.c +7 -4
  12. package/examples/atari7800/templates/racing.c +5 -2
  13. package/examples/atari7800/templates/shmup.c +8 -4
  14. package/examples/atari7800/templates/sports.c +6 -3
  15. package/examples/c64/templates/platformer.c +28 -24
  16. package/examples/c64/templates/puzzle.c +77 -16
  17. package/examples/c64/templates/racing.c +9 -0
  18. package/examples/c64/templates/shmup.c +13 -1
  19. package/examples/c64/templates/sports.c +9 -4
  20. package/examples/gb/templates/platformer.c +6 -2
  21. package/examples/gb/templates/puzzle.c +279 -101
  22. package/examples/gb/templates/racing.c +13 -1
  23. package/examples/gb/templates/shmup.c +13 -1
  24. package/examples/gb/templates/sports.c +9 -3
  25. package/examples/gba/templates/platformer.c +7 -13
  26. package/examples/gba/templates/puzzle.c +93 -15
  27. package/examples/gba/templates/racing.c +13 -1
  28. package/examples/gba/templates/shmup.c +13 -1
  29. package/examples/gba/templates/sports.c +17 -5
  30. package/examples/gbc/templates/platformer.c +6 -2
  31. package/examples/gbc/templates/puzzle.c +878 -178
  32. package/examples/gbc/templates/racing.c +13 -1
  33. package/examples/gbc/templates/shmup.c +13 -1
  34. package/examples/gbc/templates/sports.c +9 -3
  35. package/examples/genesis/templates/puzzle.c +76 -15
  36. package/examples/genesis/templates/racing.c +13 -1
  37. package/examples/genesis/templates/shmup_2p.c +13 -1
  38. package/examples/gg/templates/platformer.c +4 -0
  39. package/examples/gg/templates/puzzle.c +80 -14
  40. package/examples/gg/templates/racing.c +17 -1
  41. package/examples/gg/templates/shmup.c +17 -1
  42. package/examples/gg/templates/sports.c +4 -0
  43. package/examples/lynx/templates/platformer.c +25 -6
  44. package/examples/lynx/templates/puzzle.c +77 -14
  45. package/examples/lynx/templates/shmup.c +13 -1
  46. package/examples/lynx/templates/sports.c +5 -2
  47. package/examples/msx/platformer/main.c +2 -0
  48. package/examples/msx/puzzle/main.c +78 -15
  49. package/examples/msx/racing/main.c +1 -0
  50. package/examples/msx/shmup/main.c +1 -0
  51. package/examples/msx/sports/main.c +3 -2
  52. package/examples/nes/templates/platformer.c +11 -3
  53. package/examples/nes/templates/puzzle.c +81 -21
  54. package/examples/nes/templates/racing.c +15 -1
  55. package/examples/nes/templates/shmup.c +1 -0
  56. package/examples/nes/templates/sports.c +1 -0
  57. package/examples/pce/platformer/main.c +3 -1
  58. package/examples/pce/puzzle/main.c +78 -12
  59. package/examples/pce/racing/main.c +1 -0
  60. package/examples/pce/shmup/main.c +5 -4
  61. package/examples/pce/sports/main.c +4 -3
  62. package/examples/sms/templates/platformer.c +4 -0
  63. package/examples/sms/templates/puzzle.c +80 -14
  64. package/examples/sms/templates/racing.c +17 -1
  65. package/examples/sms/templates/shmup.c +17 -1
  66. package/examples/sms/templates/shmup_2p.c +17 -1
  67. package/examples/sms/templates/sports.c +4 -0
  68. package/examples/snes/templates/platformer.c +32 -15
  69. package/examples/snes/templates/puzzle.c +84 -16
  70. package/examples/snes/templates/racing.c +20 -1
  71. package/examples/snes/templates/shmup.c +20 -2
  72. package/examples/snes/templates/sports.c +7 -0
  73. package/package.json +12 -12
  74. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  75. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  76. package/src/cores/wasm/fceumm_libretro.js +1 -1
  77. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  78. package/src/cores/wasm/gambatte_libretro.js +1 -1
  79. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  80. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  81. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  82. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  83. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  84. package/src/cores/wasm/handy_libretro.js +1 -1
  85. package/src/cores/wasm/handy_libretro.wasm +0 -0
  86. package/src/cores/wasm/mgba_libretro.js +1 -1
  87. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  88. package/src/cores/wasm/prosystem_libretro.js +1 -1
  89. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  90. package/src/cores/wasm/snes9x_libretro.js +1 -1
  91. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  92. package/src/cores/wasm/stella2014_libretro.js +1 -1
  93. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  94. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  95. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  96. package/src/host/LibretroHost.js +245 -10
  97. package/src/mcp/server.js +6 -0
  98. package/src/mcp/tools/disasm-rebuild.js +315 -65
  99. package/src/mcp/tools/disasm.js +149 -28
  100. package/src/mcp/tools/find-references.js +216 -51
  101. package/src/mcp/tools/frame.js +11 -4
  102. package/src/mcp/tools/index.js +15 -1
  103. package/src/mcp/tools/input.js +26 -3
  104. package/src/mcp/tools/memory.js +208 -39
  105. package/src/mcp/tools/playtest.js +56 -4
  106. package/src/mcp/tools/project.js +35 -9
  107. package/src/mcp/tools/toolchain.js +43 -10
  108. package/src/mcp/tools/watch-memory.js +172 -25
  109. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  110. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  111. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  112. package/src/platforms/atari7800/MENTAL_MODEL.md +27 -6
  113. package/src/platforms/gb/MENTAL_MODEL.md +16 -1
  114. package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
  115. package/src/platforms/gb/lib/c/patch-header.js +7 -4
  116. package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
  117. package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
  118. package/src/platforms/gbc/lib/c/font.h +43 -0
  119. package/src/platforms/gbc/lib/c/patch-header.js +7 -4
  120. package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
  121. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  122. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  123. package/src/platforms/gg/TROUBLESHOOTING.md +13 -17
  124. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  125. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  126. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  127. package/src/platforms/msx/MENTAL_MODEL.md +6 -0
  128. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  129. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  130. package/src/platforms/msx/lib/c/msx_hw.h +2 -0
  131. package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
  132. package/src/platforms/nes/MENTAL_MODEL.md +10 -3
  133. package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
  134. package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
  135. package/src/platforms/pce/MENTAL_MODEL.md +9 -0
  136. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  137. package/src/platforms/pce/lib/c/pce_hw.h +2 -1
  138. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  139. package/src/platforms/sms/MENTAL_MODEL.md +5 -0
  140. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  141. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  142. package/src/platforms/snes/MENTAL_MODEL.md +5 -0
  143. package/src/playtest/playtest.js +73 -3
  144. 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 which ROM offset a Genesis graphic was DMA'd from (`watch({on:'dma', precision:'sampled'})`), 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.
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. (Item 1+2
21
- of the report, collapsed: the snapshot is taken in the same call that detects the
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 (and `catalog({op:'whatsNew'})` for the recent CHANGELOG + a rename table if you're resuming work against an older version).
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
 
@@ -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 9 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. |
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
- ASL ; bit7 = Right
167
- BCS .nr
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
- ASL ; bit6 = Left
175
- BCS .nl
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
- ; clamp terminal fall speed to -8 px/frame (P_VY is signed; -8 = $F8).
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
- CMP #$F8 ; if P_VY (unsigned) < $F8 AND it's negative → too fast
218
- BCS .vyok ; >= $F8 (covers -8..-1 and 0..127) → keep
219
- ; here P_VY is in $80..$F7 = -128..-9clamp to -8
220
- LDA #$F8
226
+ BPL .vyok ; rising (positive) terminal clamp doesn't apply
227
+ CMP #$F8
228
+ BCS .vyok ; -8..-1within 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
- ASL ; bit7 = P0 Right
170
- BCS .nr
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
- ASL ; bit6 = P0 Left
178
- BCS .nl
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
- ASL ; bit7 = P0 Right
136
- BCS .nr
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
- ASL ; bit6 = P0 Left
144
- BCS .nl
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 .noMarch ; already near the bottomdon't go further
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