romdevtools 0.27.0 → 0.29.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 (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -59,15 +59,34 @@ thousands of bytes and you'll drown).
59
59
  on-screen value. `region` defaults to `system_ram`.
60
60
  2. Change the value in-game (take damage, score a point), then
61
61
  `memory({op:'searchNext', compare:'eq', value})` — or `compare:'gt'|'lt'|'changed'|'unchanged'|
62
- 'inc'|'dec'` when you don't know the new value. Repeat until a handful remain.
62
+ 'inc'|'dec'` when you don't know the new value. The relative compares work as the
63
+ FIRST narrow too (baselines are recorded at seed). Repeat until a handful remain.
63
64
  3. Confirm: `memory({op:'write'})` the candidate and watch the screen react.
64
65
 
65
66
  This is the Cheat-Engine/RetroArch loop. It is THE bread-and-butter primitive.
66
67
 
68
+ **Stored ≠ displayed.** When a correct-looking seed returns 0, the byte usually isn't the
69
+ raw number: seed `as:'bcd'` for packed-BCD scores (2 decimal digits per byte — very common
70
+ on NES), or `as:'digits'` for one byte per ON-SCREEN digit at any constant tile base (HUD
71
+ tile-index buffers; the matched base is reported per candidate, and `searchNext` keeps
72
+ comparing in the same representation). For displayed−1 lives or ÷10 scores, just seed the
73
+ transformed number. If an INPUT drives the value (position, velocity, charge), skip the
74
+ loop entirely: `memory({op:'diffRuns', portsA:[{right:true}]})` isolates it in one call.
75
+
67
76
  `memory({op:'snapshot'})` + `memory({op:'diff'})` is for "which bytes did THIS one event touch?",
68
77
  not for value hunting. `memory({op:'diff'})` defaults to a **clustered summary** (ranges +
69
78
  stride) so it won't flood you — a reported stride (e.g. "islands at 0x80") is
70
- usually a struct/entity array, each island one record.
79
+ usually a struct/entity array, each island one record. Small clusters (≤8 bytes) carry
80
+ `before`/`after` hex inline, and `minDelta:N` drops |after−before| < N so RNG/counter
81
+ wiggle disappears from the report.
82
+
83
+ **"Which byte does this INPUT drive?" → `memory({op:'diffRuns'})`** — runs the same start
84
+ state twice (savestate restore in between) under two different held inputs (`portsA` vs
85
+ `portsB`, default released) for `frames` each, and returns only the bytes that DIVERGE
86
+ between the runs, with run-A/run-B values on small clusters. One call replaces the whole
87
+ save → hold → step → dump → restore → hold-other → dump → diff loop; the frame counter and
88
+ all input-independent churn cancel out automatically. (The emulator is left at the end of
89
+ run B.)
71
90
 
72
91
  ---
73
92
 
@@ -140,7 +159,12 @@ the copy reads from, then `breakpoint({on:'write'})` on THAT.
140
159
  **Precision — exact vs sampled.** The default `breakpoint({on:'write'})` is a core-level write
141
160
  watchpoint: it returns the EXACT writing instruction's PC, captured inside the CPU write
142
161
  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
162
+ is just the idle loop). On ALL 14 platforms, every hit (write/read/pc) also carries
163
+ **`registersAtHit`** — the full register file frozen AT the hit instant — and the CPU
164
+ stays FROZEN until the hit is cleared. Use registersAtHit instead of a follow-up
165
+ `cpu({op:'read'})`: pre-0.28.0 the live registers kept running after a hit (on gpgx they
166
+ drifted hundreds of instructions — address registers read that way were someone else's
167
+ values). On a banked mapper it reports the `bank` (NES/GB/SMS-GG) so you
144
168
  can pass `{startAddress, bank}` to `disasm({target:'rom'})`. The lighter
145
169
  `breakpoint({on:'write', precision:'sampled'})` (a.k.a. `watch({on:'mem'})`) steps until the byte changes
146
170
  and returns a frame-boundary PC — a lead, not a guarantee under interrupts; use it for the
@@ -197,6 +221,16 @@ pushes a sentinel return, and runs until it returns. Most of these formats have
197
221
  you can usually craft a replacement by hand. (sandbox:false leaves the dest buffer
198
222
  live for `memory({op:'read'})`; sandbox:true restores the game untouched.)
199
223
 
224
+ **Pass `pure:true` — on every platform.** A non-pure call that spans frames runs the
225
+ game's OWN frame logic concurrently (VBlank handlers via RAM vectors, music
226
+ drivers) — which can overwrite the dest buffer mid-call and hand you poisoned
227
+ "ground truth" (a real session spent hours diffing a CORRECT reimplementation
228
+ against it). With `pure:true` the game's handlers CANNOT run: Genesis/SMS/GG step
229
+ only the CPU (`pureMode:'cpu-only'`); everywhere else interrupt DELIVERY is
230
+ suppressed for the duration (`'irq-blocked'` — pending lines stay pending, video
231
+ advances harmlessly); the 2600 has no interrupts (`'no-interrupts'`). Non-pure
232
+ results carry a ⚠ caveat whenever frame logic ran.
233
+
200
234
  ## 5e. Re-inject an edited asset — the round-trip (don't reimplement the compressor)
201
235
 
202
236
  Once you can SEE the decompressed bytes (5c) and you've edited them, put them BACK
@@ -315,9 +349,14 @@ Once you know WHAT to change, the write loop is a handful of calls — no custom
315
349
  confirm a patch landed where you meant.
316
350
  - **`disasm({target:'references', path, platform, address})`** — find every instruction that
317
351
  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).
352
+ vector table too). The fast "who touches this?" for a STATIC image. EVERY banked format
353
+ is scanned PER BANK NES mappers (refs carry `prgBank`), and SNES multi-bank LoROM,
354
+ GB/GBC MBC, SMS/GG Sega-mapper, MSX megaROM, Atari 2600 F8/F6/F4, Atari 7800 SuperGame,
355
+ and >32KB HuCards (refs carry `romBank`) — so a hit in bank 12 of a 128KB cart shows up,
356
+ not just the first bank. Zero-page direct + indexed operands match, and `#$nn` immediates
357
+ are excluded (values, not addresses). Limitation: direct addressing only —
358
+ indirect/computed jumps aren't detected (use the runtime `watch`/`breakpoint` tools in
359
+ §5/§5d for those).
321
360
  - **`cart({op:'extract', path, outputDir})`** — split a ROM into standard parts (NES header/
322
361
  prg/chr; SNES copier_header+rom+internal header; Genesis vectors/header/body; GB boot/
323
362
  header/body) + a `manifest.json` (mapper, mirroring…). **`cart({op:'wrap'})`** is the inverse:
@@ -330,8 +369,9 @@ watch the screen react — cheaper than shipping a wrong ROM patch.
330
369
 
331
370
  For a STRUCTURAL hack (new logic, not a byte poke), turn the whole ROM into a
332
371
  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
372
+ the ROM into regions (per-bank on EVERY banked format: 16KB banks for NES/GB/SMS-GG/MSX/
373
+ 7800-SuperGame, 32KB for SNES LoROM, 4KB for banked 2600, 8KB pages for >32KB HuCards;
374
+ one flat region for Genesis/C64/Lynx/GBA and small carts), disassembles each through the CPU's
335
375
  native objdump, then **reassembles + verifies byte-exact** against the original; any line
336
376
  that won't reproduce faithfully heals to a `.byte`/`db` of its real bytes, so the emitted
337
377
  `.asm` ALWAYS rebuilds (`roundTrip.allByteExact`). `readablePercent` per region tells you
@@ -343,22 +383,27 @@ rebuild exists — a `rebuild.json` of the precise `build({...})` args. So the l
343
383
 
344
384
  **Two rebuild tiers** (the disasm emits each CPU's native-reassembler syntax — ca65 for
345
385
  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`.)
386
+ - **One-call `build()` rebuild, byte-identical** — **NES (NROM *and* banked mappers), C64,
387
+ Atari 7800 (flat *and* SuperGame banked), Lynx, PC Engine (flat *and* banked HuCards)**.
388
+ Feed `rebuild.json` straight to `build`. Banked projects ship a HEADER segment with the
389
+ original header bytes (16 iNES / 128 .a78 / 512 copier), per-bank segment wrappers, and a
390
+ generated multi-bank `.cfg` referenced via `linkerConfigPath` (so the cfg never streams
391
+ through context). (Lynx: `build()` yields the headerless image; prepend the shipped
392
+ `lnx_header.bin` for the full `.lnx`.)
349
393
  - **Native-recipe rebuild (`buildCall:null`), byte-identical, steps in `BUILD.md`** — **SMS,
350
394
  GG, MSX, GB, GBC, Genesis, GBA, Atari 2600**. Their `build()` toolchains (SDCC/RGBDS/asar/
351
395
  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.
396
+ `as`/`ld`/`objcopy` chain — per-bank on banked carts (Sega-mapper SMS/GG, MSX megaROMs,
397
+ banked 2600 get per-bank wrappers + cfg blobs and a bank-by-bank recipe).
355
398
 
356
399
  **Rebuilding a commercial NES (NROM CHR-ROM) game — `build({inesHeader})`:** the most common
357
400
  NES RE rebuild. `build({output:'rom', platform:'nes', inesHeader:{prgBanks, chrBanks, mapper,
358
401
  mirroring}, sourcesPaths:{…the PRG…}, binaryIncludePaths:{"chr.bin":…}})` auto-emits the
359
402
  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.)
403
+ `disasm({target:'project'})` puts exactly this call in `rebuild.json` for NROM; banked
404
+ mappers get the per-bank segment + multi-bank `.cfg` form instead (see the one-call tier
405
+ above). (For homebrew C that ships fixed tile art, `linkerConfig:"chr-rom"` is the
406
+ segment-split equivalent.)
362
407
 
363
408
  **Readability caveats** (the bytes are ALWAYS correct; only instruction-vs-`.byte` coverage
364
409
  varies): SNES and large Genesis ROMs come back byte-exact but DATA-ONLY (flat whole-ROM
@@ -398,6 +443,7 @@ For sprite/tile edits (not text), don't hand-roll the tile-format math:
398
443
  |---|---|
399
444
  | Find a value's address | `memory({op:'search'})` → `memory({op:'searchNext'})` (NOT full-RAM diff) |
400
445
  | Which bytes did one event touch | `memory({op:'snapshot'})` → `memory({op:'diff'})` (summary) |
446
+ | Which byte does an INPUT drive | `memory({op:'diffRuns', portsA, portsB?})` (A/B divergence, one call) |
401
447
  | Is on-screen text a string or a bitmap | `text({op:'learn'})` (reports pre-rendered graphic) |
402
448
  | Is a "table" really ASCII/code | `memory({op:'classify'})` |
403
449
  | Confirm a patch is in the running ROM | `memory({op:'readCart'})` |
@@ -406,7 +452,8 @@ For sprite/tile edits (not text), don't hand-roll the tile-format math:
406
452
  | Which instruction READ a byte | `breakpoint({on:'read', address})` (read-side `breakpoint({on:'write'})`) |
407
453
  | Single-step the CPU | `frame({op:'stepInstruction'})` (+ `cpu({op:'read'})` to watch regs) |
408
454
  | Set a CPU register | `cpu({op:'setReg', regId, value})` |
409
- | Decompress a compressed asset | `cpu({op:'decompress'})` / `cpu({op:'call'})` (run the ROM's own codec) |
455
+ | Decompress a compressed asset | `cpu({op:'decompress'})` / `cpu({op:'call', pure:true})` (run the ROM's own codec, interference-free) |
456
+ | Where does this on-screen graphic come from | `watch({on:'copy', start, end})` (all 14 — writer PC per VRAM write; Genesis DMA also via `watch({on:'dma'})`) |
410
457
  | Re-inject edited bytes the game accepts | `romPatch({op:'makeStored'})` (verbatim-expand block) → `romPatch({op:'findFree'})` → `romPatch({op:'relocate'})` |
411
458
  | Find the pointer that loads an asset | `romPatch({op:'findPointer', romOffset})` |
412
459
  | FIND the unknown routine touching X | `watch({on:'range', start,end})` (all hits) / `watch({on:'pc'})` (coverage) |
@@ -215,7 +215,11 @@ What you can read:
215
215
  registers.
216
216
  - **`disasm({target:'rom'})`** and **`disasm({target:'references'})`** —
217
217
  both anchor to the top of the bank (`$F000-$FFFF`) and label the vector
218
- table (NMI / RESET / IRQ at `$FFFA`).
218
+ table (NMI / RESET / IRQ at `$FFFA`). On banked carts (F8 = 8 KB,
219
+ F6 = 16 KB, F4 = 32 KB) `references` scans EVERY 4 KB bank at `$F000`,
220
+ refs tagged `romBank`; `disasm({target:'project'})` likewise emits one
221
+ region per bank plus per-bank `BANKn` wrappers and a multi-area `.cfg`
222
+ blob for the native ca65/ld65 rebuild.
219
223
 
220
224
  Memory regions for **`memory({op:'read'})`**:
221
225
 
@@ -159,3 +159,43 @@ snippet for one approach.
159
159
  ## "First build is slow but later ones are fast"
160
160
 
161
161
  Expected. dasm cold-load is ~500ms. Steady-state builds < 100ms.
162
+
163
+
164
+ ## Pressing RIGHT also "presses" LEFT (or the player can't move at all)
165
+
166
+ The classic `LDA SWCHA / ASL / BCS … / ASL / BCS …` carry-chain only works if
167
+ NOTHING between the shifts touches A. The moment a branch body does
168
+ `LDA P_X` (a bounds check, a compare), the next `ASL` shifts your *position*
169
+ instead of SWCHA — and since positions are < $80, carry comes back clear and
170
+ the "other direction" fires too. Net effect: moves cancel, the sprite sticks
171
+ to one edge. **Re-load SWCHA and AND a single bit per direction instead:**
172
+
173
+ ```asm
174
+ LDA SWCHA
175
+ AND #$80 ; bit7 = P0 Right (active LOW: 0 = pressed)
176
+ BNE .noRight
177
+ ...move right (clobber A freely)...
178
+ .noRight:
179
+ LDA SWCHA
180
+ AND #$40 ; bit6 = P0 Left
181
+ BNE .noLeft
182
+ ...
183
+ ```
184
+
185
+ ## Jump plays its sound but the player never leaves the ground
186
+
187
+ Signed-velocity clamps must check the SIGN first. An unsigned
188
+ `CMP #$F8 / BCS keep` "terminal velocity" clamp also catches every POSITIVE
189
+ (rising) velocity — +6 is less than $F8 unsigned — so the jump impulse is
190
+ instantly slammed to falling and the whole arc resolves inside one frame
191
+ (SFX plays, screen blips, no visible jump). Clamp only while falling:
192
+
193
+ ```asm
194
+ LDA P_VY
195
+ BPL .vyok ; rising → terminal clamp doesn't apply
196
+ CMP #$F8
197
+ BCS .vyok ; -8..-1 → fine
198
+ LDA #$F8 ; clamp to -8
199
+ STA P_VY
200
+ .vyok:
201
+ ```
@@ -58,7 +58,7 @@ and DLL; you do NOT poke pixels into a framebuffer.**
58
58
  and (worse) burns enough cycles that the CPU stops getting time.
59
59
  - **Y position = which zone the object lives in.** Each zone covers
60
60
  N scanlines. To move an object up/down, you move it between
61
- zones. (Or — in our scaffolds — you stamp the same sprite at
61
+ zones. (Or — in our example games — you stamp the same sprite at
62
62
  different row offsets within ONE zone's data block, which fakes
63
63
  Y movement.)
64
64
  - **Each DL header can pick a palette per object** (one of 8
@@ -136,7 +136,7 @@ The "loop continues" mask is `0x5F` (bits 0-4 + bit 6). Bit 5
136
136
  (indirect flag) and bit 7 (write-mode) do NOT keep the loop going
137
137
  by themselves.
138
138
 
139
- ### 5-byte extended form (the bundled scaffolds use this)
139
+ ### 5-byte extended form (the bundled example games use this)
140
140
 
141
141
  ```
142
142
  +0 pixel-data LOW byte
@@ -195,7 +195,7 @@ scanlines for the ENTIRE display area (243 scanlines on NTSC,
195
195
  including 10 lines of top overscan before the visible area).
196
196
 
197
197
  If your DLL is shorter than 243 entries, MARIA reads past the end
198
- into random memory and renders garbage zones. The bundled scaffold
198
+ into random memory and renders garbage zones. The bundled example
199
199
  allocates 243 entries × 3 bytes = 729 bytes (fits easily in 4 KB
200
200
  internal RAM) and points every zone with no objects at a shared
201
201
  `dl_empty[2] = {0, 0}` terminator.
@@ -213,11 +213,11 @@ for an 8-row sprite) unless you pack many sprites per page.
213
213
  **Easy work-around:** make every zone 1 scanline tall (offset=0)
214
214
  and use one DL entry per sprite ROW. Then `offset` is always 0, the
215
215
  address quirk goes away, and you can store sprite rows back-to-back.
216
- The bundled scaffold uses this pattern.
216
+ The bundled example uses this pattern.
217
217
 
218
218
  The cost is more DLL entries (one per scanline), but at 3 bytes each
219
219
  across 243 lines = 729 bytes total — trivial RAM cost. Worth it for
220
- the simpler mental model on a starter scaffold.
220
+ the simpler mental model on a starter example.
221
221
 
222
222
  ## Colour bytes (Atari NTSC palette)
223
223
 
@@ -237,10 +237,23 @@ read:
237
237
 
238
238
  ```c
239
239
  uint8_t pad = ~SWCHA;
240
- if (pad & JOY_UP) /* P1 up */
241
- if (pad & JOY_DOWN) /* P1 down */
242
- if (pad & JOY_LEFT) /* P1 left */
243
240
  if (pad & JOY_RIGHT) /* P1 right */
241
+ if (pad & JOY_LEFT) /* P1 left */
242
+ if (pad & JOY_DOWN) /* P1 down */
243
+ if (pad & JOY_UP) /* P1 up */
244
+ ```
245
+
246
+ **The bit order is the #1 7800 input footgun.** From bit 7 down the P1 nibble
247
+ is **Right ($80), Left ($40), Down ($20), Up ($10)** — same as the 2600. Defining
248
+ `JOY_UP 0x80 … JOY_RIGHT 0x10` (the "reads naturally" order) is exactly
249
+ REVERSED, and the symptom is bizarre enough to misdiagnose: up/down steer
250
+ left/right and vice versa. Always:
251
+
252
+ ```c
253
+ #define JOY_RIGHT 0x80
254
+ #define JOY_LEFT 0x40
255
+ #define JOY_DOWN 0x20
256
+ #define JOY_UP 0x10
244
257
  ```
245
258
 
246
259
  Fire button on `INPT4` at `$0C`, also active low.
@@ -343,9 +356,17 @@ What you can read:
343
356
  P / SP / PC) read from prosystem's `sally` globals.
344
357
  - **`background({view:'renderState'})`** — the MARIA CTRL bits, DPP,
345
358
  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.
359
+ - **`disasm({target:'rom'})`** defaults to the top 16 KB
360
+ (`$C000-$FFFF`), where the reset vector lands.
361
+ - **`disasm({target:'references'})`** — scans the WHOLE cart: flat carts
362
+ (≤48 KB) in one pass at their top-of-space org, SuperGame banked carts
363
+ (>48 KB) per 16 KB bank (last bank fixed at `$C000`, others at `$8000`),
364
+ refs tagged `romBank`. A 128-byte `.a78` header is stripped automatically.
365
+ - **`disasm({target:'project'})`** — flat carts rebuild with one flat cc65
366
+ build; SuperGame carts get per-bank regions + NES-style glue (HEADER
367
+ segment with the original 128 header bytes, `BANKn` wrappers, multi-bank
368
+ `.cfg` via `linkerConfigPath`) — a one-call byte-identical
369
+ `build()` rebuild either way.
349
370
 
350
371
  Memory regions for **`memory({op:'read'})`**:
351
372
 
@@ -56,14 +56,14 @@ you have to:
56
56
  2. Place the sprite's DL entry into the zone covering its Y range.
57
57
  3. Per-frame, move the entry's bytes from old-zone DL → new-zone DL.
58
58
 
59
- Our scaffolds use a single-zone DLL for simplicity. Vertical
59
+ Our example games use a single-zone DLL for simplicity. Vertical
60
60
  movement is faked by stamping the sprite at different row offsets
61
61
  within the canvas data — only works if the canvas is tall enough.
62
62
 
63
63
  ## "Memory overflow during link (RAM1 by N bytes)"
64
64
 
65
65
  The 7800 has **4 KB of RAM**. The `default.c` and `hello_sprite.c`
66
- scaffolds use very little; the `shmup.c` puzzle (and the older
66
+ example games use very little; the `shmup.c` puzzle (and the older
67
67
  canvas-buffer approach) easily blow past it.
68
68
 
69
69
  Symptoms:
@@ -77,7 +77,7 @@ Fixes:
77
77
  - Use ROM constants (`const uint8_t` at file scope) instead of
78
78
  RAM globals.
79
79
  - Replace canvas-buffer rendering with per-object DLs (see
80
- `shmup.c` scaffold for the canonical pattern).
80
+ `shmup.c` example for the canonical pattern).
81
81
  - Avoid per-frame `memset(canvas, 0, ...)` — instead, only stamp
82
82
  changed cells.
83
83
 
@@ -126,7 +126,7 @@ DL during active rendering; safe modification windows:
126
126
  Build a "next-frame" DL during the game-state update phase and
127
127
  swap pointers (DPPL/DPPH) at vblank — double-buffered.
128
128
 
129
- Our scaffolds rebuild the DL during vblank, which works for small
129
+ Our example games rebuild the DL during vblank, which works for small
130
130
  DLs (< ~100 bytes). Large DLs that take ~1 ms to rebuild may
131
131
  exceed vblank time and start corrupting the active frame.
132
132
 
@@ -197,5 +197,5 @@ Fix options (in order of how much they shrink BSS):
197
197
  if you only need one sprite at a time — see `default.c` and
198
198
  `hello_sprite.c`. No per-scanline pool needed.
199
199
 
200
- The bundled scaffolds size their pools to fit; if you scale up
200
+ The bundled example games size their pools to fit; if you scale up
201
201
  (more objects, taller play area), watch the build log.
@@ -115,9 +115,16 @@ which is what the KERNAL's IRQ uses to update key state every
115
115
 
116
116
  **Joystick.** One fire button. Press it with `input({op:'set', b: true})` (or
117
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.
118
+ (no second button). Drive fire with `b`/`south` + the d-pad.
119
+
120
+ **Two players.** BOTH C64 control ports are live at once, so 2P games just work:
121
+ **host port 0 = player 1** (control port 2, `$DC00`) and **host port 1 = player
122
+ 2** (control port 1, `$DC01`) — the universal "port 0 = P1" convention. Pass two
123
+ port entries: `input({op:'set', ports:[{up:true}, {down:true}]})` moves P1 up and
124
+ P2 down independently. (Under the hood the host enables the VICE userport-adapter
125
+ mapping so both ports route, and swaps them so P1 lands on control port 2 where
126
+ the games read it.) The legacy `input({op:'joyport', joyport:1|2})` still selects
127
+ which single port a ONE-stick setup drives, but you rarely need it now.
121
128
 
122
129
  **Keyboard (the C64-specific part — many games NEED it).** Unlike consoles, most
123
130
  C64 games (and cracktros) gate gameplay behind a KEYBOARD setup screen — **F1**
@@ -311,7 +318,7 @@ loads games, wrap the `.prg` into a `.d64`: `cart({op:'packDisk', prgPath})`
311
318
 
312
319
  ## Horizontal scrolling (for side-scrollers)
313
320
 
314
- The `platformer` scaffold is single-screen. C64 scrolling is the fiddliest of
321
+ The `platformer` example is single-screen. C64 scrolling is the fiddliest of
315
322
  the platforms because the VIC-II only does a 0-7 px *fine* scroll in hardware;
316
323
  moving further is a software char-cell shift.
317
324
 
@@ -83,6 +83,19 @@ KERNAL last selected. Result: ghost input.
83
83
 
84
84
  **Use port 2 (CIA1_PRA) by default.** All bundled C64 templates do.
85
85
 
86
+ ## "Player 2 input does nothing"
87
+
88
+ Both C64 control ports ARE live over MCP, so 2P works — the mapping is just
89
+ non-obvious: **host port 0 → control port 2 ($DC00) = player 1**, **host port 1
90
+ → control port 1 ($DC01) = player 2** (the universal "port 0 = P1" convention).
91
+ So a 2P game reads P1 from $DC00 and P2 from $DC01, and you drive them with two
92
+ port entries: `input({op:'set', ports:[{up:true},{down:true}]})` moves P1 up,
93
+ P2 down. If P2 seems dead, check you passed a SECOND `ports` entry (not just
94
+ port 0) and that the game actually entered 2P mode (its title pick, e.g. "PORT 1
95
+ FIRE = 2P"). The host enables the VICE userport-adapter mapping + swaps the two
96
+ RetroPad ports under the hood so this convention holds — you don't configure
97
+ anything.
98
+
86
99
  ## "Audio is silent / SID doesn't play"
87
100
 
88
101
  Three things to check:
@@ -68,7 +68,10 @@ returns nothing.
68
68
  `disasm({target:'project'})` route through the native binutils z80 `objdump` in
69
69
  its `gbz80` machine (WASM, `-m gbz80`) — full CB-prefix coverage plus the
70
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.
71
+ binutils serves both plain Z80 (SMS/GG/MSX) and the GB CPU. MBC-banked carts
72
+ (>32 KB) are scanned per 16 KB bank by `references` (bank 0 @ `$0000`, banks
73
+ 1+ @ their `$4000` window; refs tagged `romBank`) and split per-bank by
74
+ `disasm({target:'project'})`.
72
75
 
73
76
  ## Five silent-failure footguns to know before you start (R26 + R27)
74
77
 
@@ -103,6 +106,18 @@ check these first. All five have shipped fixes in the bundled runtime
103
106
  (volatile-safe by construction) or cast through `volatile uint8_t *`.
104
107
  See `gb_runtime/lib/c/SDCC_GOTCHAS.md` § "Writes to VRAM" for detail.
105
108
 
109
+ 3b. **OAM DMA goes FIRST after `wait_vblank()` — before any staging work.**
110
+ The vblank window is ~10 scanlines (~1140 cycles) and SDCC call overhead
111
+ is brutal: even a few dozen `oam_set()` CALLS before the flush push the
112
+ DMA out of vblank into active display, where it tears the sprites on one
113
+ FIXED scanline every frame (the "horizontal line a third of the way down"
114
+ glitch). The robust frame shape is: stage OAM/BG writes into RAM any time
115
+ during the frame, then `wait_vblank(); oam_dma_flush();` as the very first
116
+ thing, then a small bounded batch of BG map writes. One frame of sprite
117
+ latency is imperceptible. Also: statics belong at `dataLoc 0xC200` or
118
+ above (the project recipe sets this) so they can't collide with
119
+ `shadow_oam` at $C100.
120
+
106
121
  4. **OAM DMA must run from HRAM.** During the ~160 µs OAM DMA window
107
122
  the CPU can ONLY fetch from HRAM ($FF80-$FFFE). The bundled
108
123
  `oam_dma_copy()` now installs a 9-byte stub at $FF80 and CALLs it;
@@ -265,9 +280,9 @@ names also resolve (east→A, west→B). So `input({op:'set', a: true})` presses
265
280
  expected — unlike the genesis_plus_gx platforms (Genesis/SMS/GG), there's no
266
281
  surprise here. (Same for **GBC** — it shares the gambatte core.)
267
282
 
268
- ## What `scaffold({op:'project'})` copies into your project
283
+ ## What `examples({op:'fork'})` copies into your project
269
284
 
270
- `scaffold({op:'project', platform:"gb"|"gbc", template:...})` writes these files
285
+ `examples({op:'fork', example:"gb/..."|"gbc/...", name, path})` writes these files
271
286
  into your project directory. **They're yours** — every byte that compiles
272
287
  is in the repo. Edit, fork, replace; nothing is auto-injected at build time.
273
288
 
@@ -325,7 +340,7 @@ Most game patterns DON'T need any of this. Try the C path first.
325
340
 
326
341
  ## Horizontal scrolling (for side-scrollers)
327
342
 
328
- The `platformer` scaffold is single-screen. To make it a side-scroller:
343
+ The `platformer` example is single-screen. To make it a side-scroller:
329
344
 
330
345
  - **Hardware scroll:** write `SCX` (`$FF43`) each frame = camera X mod 256.
331
346
  The BG is a 32×32 tile map (256×256 px) that wraps, so `SCX` alone scrolls
@@ -166,6 +166,102 @@ at $0150. If you see code in that window, either:
166
166
  works; don't override it for GB/GBC unless you know what you're
167
167
  doing.
168
168
 
169
+ ## "BG map updates randomly don't stick" / a tile updates one frame late forever
170
+
171
+ The core (like real hardware mid-frame) DROPS writes to VRAM ($8000-$9FFF)
172
+ that land outside vblank while the LCD is on — silently. A game loop that
173
+ pokes the BG map "whenever the state changes" will have SOME of those pokes
174
+ land mid-frame and vanish: stale cells, a piece that visually lags the
175
+ logical grid, glitches that move around as code timing shifts.
176
+
177
+ The robust pattern (used by the bundled puzzle example games):
178
+
179
+ 1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
180
+ pairs to a small RAM queue whenever game state changes a cell.
181
+ 2. **FLUSH** — immediately after `wait_vblank()` (right after the OAM DMA),
182
+ drain the queue with pure writes. No scanning, no logic — vblank is only
183
+ ~1140 cycles, so the flush must be writes only and bounded.
184
+ 3. **Scrub** — repaint one or two rows per frame round-robin as insurance,
185
+ so any cell that ever got dropped self-heals within a second.
186
+
187
+ If you must write outside that structure, turn the LCD off first (only
188
+ acceptable during init/load screens — mid-game it flashes white).
189
+
190
+ ## "BG map updates randomly don't stick" / a tile updates one frame late forever
191
+
192
+ The core (like real hardware mid-frame) DROPS writes to VRAM ($8000-$9FFF)
193
+ that land outside vblank while the LCD is on — silently. A game loop that
194
+ pokes the BG map "whenever the state changes" will have SOME of those pokes
195
+ land mid-frame and vanish: stale cells, a piece that visually lags the
196
+ logical grid, glitches that move around as code timing shifts.
197
+
198
+ The robust pattern (used by the bundled puzzle example games):
199
+
200
+ 1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
201
+ pairs to a small RAM queue whenever game state changes a cell.
202
+ 2. **FLUSH** — immediately after `wait_vblank()` (right after the OAM DMA),
203
+ drain the queue with pure writes. No scanning, no logic — vblank is only
204
+ ~1140 cycles, so the flush must be writes only and bounded.
205
+ 3. **Scrub** — repaint one or two rows per frame round-robin as insurance,
206
+ so any cell that ever got dropped self-heals within a second.
207
+
208
+ If you must write outside that structure, turn the LCD off first (only
209
+ acceptable during init/load screens — mid-game it flashes white).
210
+
211
+ ## "My HUD scrolls with the background" / "the window ate the bottom of my screen"
212
+
213
+ The window layer (WX/WY + LCDC bit 5) is the GB's fixed-HUD mechanism: it has
214
+ no scroll registers, always draws its own map from (0,0) pinned to the screen,
215
+ on top of the BG. Three rules, all demonstrated in the shmup example
216
+ (`examples/gb/templates/shmup.c`, "HARDWARE IDIOM: window-layer HUD"):
217
+
218
+ - **WX is screen X plus 7.** `WX=7` is the left edge. WX 0-6 produces real
219
+ DMG pixel-pipeline glitches; WX ≥ 167 pushes the window off-screen.
220
+ - **The window has no height register.** From the first line it covers (WY)
221
+ it owns EVERY line to the bottom of the frame, full width from WX. That's
222
+ why GB HUDs live at the BOTTOM of the screen (`WY = 128` → lines 128-143 =
223
+ HUD, lines 0-127 = scrolling playfield). A top HUD needs a mid-frame
224
+ STAT/LYC interrupt to turn LCDC bit 5 back off — a different, fragile
225
+ idiom; don't fall into it by accident by setting WY=0.
226
+ - **Sprites are NOT clipped by the window** — they draw on top of it. Despawn
227
+ (or Y-clamp) everything before the HUD line, or your enemies fly across
228
+ the score bar.
229
+
230
+ Use a separate map for the window (LCDC bit 6 → $9C00) so it doesn't fight
231
+ the BG's $9800 map.
232
+
233
+ ## "Hi-score doesn't persist" / save_ram is empty or all $FF
234
+
235
+ Battery saves need BOTH halves:
236
+
237
+ 1. **The header must declare a battery cart.** The bundled `gb_crt0.s` emits
238
+ `$0147 = $03` (MBC1+RAM+BATTERY) and `$0149 = $02` (8 KB) as real bytes,
239
+ and the build's post-link header fix passes them through. The emulator
240
+ sizes its SAVE_RAM region from those two bytes — type $00 (ROM-only)
241
+ means no save_ram region at all, and writes to $A000 go nowhere.
242
+ 2. **Cart RAM is gated.** It boots DISABLED; writes are silently discarded
243
+ until you write `$0A` to $0000-$1FFF (any address there — it's a mapper
244
+ register, not memory). Write `$00` to the same range after saving
245
+ (battery hygiene: an enabled RAM bank can corrupt at power-off on real
246
+ hardware).
247
+
248
+ Working pattern with magic + checksum (a fresh cart is $FF garbage — never
249
+ trust raw bytes): shmup example, "HARDWARE IDIOM: battery SRAM". Verify
250
+ headlessly: play to a score, force game over, `memory({op:'read',
251
+ region:"save_ram"})` shows the record, and the hi-score still shows on the
252
+ title after a hard reset (power cycle).
253
+
254
+ ## "Boot takes seconds" / a screen repaint visibly stalls the game
255
+
256
+ The sm83 has no divide instruction. SDCC's software `%` / `/` costs ~700
257
+ cycles per call — one `(r*7+c*5) % 11` in a 32×32 map fill is 2048 calls
258
+ ≈ 1.5 MILLION cycles ≈ a 1.5-second frozen boot (measured, not theoretical;
259
+ the shmup example shipped exactly that for an hour). In any per-cell or
260
+ per-frame loop, replace modulo patterns with running counters +
261
+ subtract-on-overflow (see `paint_starfield` in the shmup example) and
262
+ decimal score display with power-of-ten subtraction (`u16_to_tiles` there).
263
+ A single `%` per event — e.g. per enemy spawn — is fine.
264
+
169
265
  ## Debug recipes
170
266
 
171
267
  A few high-leverage tools you might not know exist:
@@ -205,14 +301,13 @@ Boot order that always works for GBC:
205
301
  }
206
302
  ```
207
303
 
208
- Cribbed from `examples/gbc/templates/tile_engine.c` — start a fresh
209
- game from that template with:
304
+ Cribbed from `examples/gbc/templates/tile_engine.c` — fork that
305
+ example into a fresh game with:
210
306
 
211
307
  ```js
212
- scaffold({
213
- op: 'project',
214
- platform: "gbc",
215
- template: "tile_engine",
308
+ examples({
309
+ op: 'fork',
310
+ example: "gbc/tile_engine",
216
311
  name: "mygame",
217
312
  path: "/abs/path/to/dir",
218
313
  });
@@ -1,8 +1,8 @@
1
1
  # GB / GBC C runtime + headers
2
2
 
3
3
  These are the source files that back the GB/GBC C templates. They're
4
- **not** auto-injected at build time — `scaffold({op:'project', platform:"gb"|"gbc",
5
- template:"..."})` copies them into your project directory so the
4
+ **not** auto-injected at build time — `examples({op:'fork', example:"gb/<name>" or
5
+ "gbc/<name>", name, path})` copies them into your project directory so the
6
6
  project is self-describing. Build calls then point at your project's
7
7
  copy of these files via `sourcesPaths` / `includePaths` / `crt0Path`.
8
8
 
@@ -32,7 +32,7 @@ didn't produce, or to override a field:
32
32
  Fixes up / overrides the header of an existing ROM on disk (title, cart
33
33
  type, ROM/RAM size, CGB flag, etc.).
34
34
  - `node patch-header.js out.gb` — standalone Node script, copied into
35
- every GB project by `scaffold({op:'project'})`. Same logic, no MCP needed.
35
+ every GB project by `examples({op:'fork'})`. Same logic, no MCP needed.
36
36
  - `rgbfix -v -p 0 out.gb` — what the build pipeline runs under the hood;
37
37
  RGBDS asm projects can invoke it directly.
38
38
 
@@ -46,17 +46,16 @@ didn't produce, or to override a field:
46
46
  hardware, OAM DMA timing, joypad layout. Read this before your first
47
47
  GB/GBC project.
48
48
 
49
- ## Project templates
49
+ ## Forking an example
50
50
 
51
- Bootstrap a working game-loop skeleton with `scaffold({op:'project'})`:
51
+ Bootstrap a working game-loop skeleton by forking an example with `examples({op:'fork'})`:
52
52
 
53
53
  ```js
54
- scaffold({
55
- op: 'project',
56
- platform: "gbc",
57
- template: "tile_engine", // or "hello_sprite", or "default"
58
- name: "mygame",
59
- path: "/abs/path",
54
+ examples({
55
+ op: 'fork',
56
+ example: "gbc/tile_engine", // or "gbc/hello_sprite", or "gbc/default"
57
+ name: "mygame",
58
+ path: "/abs/path",
60
59
  })
61
60
  ```
62
61