romdevtools 0.28.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 (154) hide show
  1. package/AGENTS.md +51 -41
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +1 -1
  88. package/src/host/LibretroHost.js +59 -1
  89. package/src/http/tool-registry.js +11 -11
  90. package/src/mcp/tools/cheats.js +2 -1
  91. package/src/mcp/tools/frame.js +3 -2
  92. package/src/mcp/tools/index.js +3 -3
  93. package/src/mcp/tools/input.js +5 -4
  94. package/src/mcp/tools/lifecycle.js +6 -4
  95. package/src/mcp/tools/platform-docs.js +1 -1
  96. package/src/mcp/tools/preview-tile.js +6 -2
  97. package/src/mcp/tools/project.js +1098 -130
  98. package/src/mcp/tools/rom-id.js +5 -1
  99. package/src/mcp/tools/run-until.js +4 -2
  100. package/src/mcp/tools/snippets.js +6 -6
  101. package/src/mcp/tools/sprite-pipeline.js +14 -2
  102. package/src/mcp/tools/state.js +2 -1
  103. package/src/mcp/tools/tile-inspect.js +8 -1
  104. package/src/mcp/tools/toolchain.js +12 -1
  105. package/src/mcp/tools/watch-memory.js +4 -3
  106. package/src/observer/bus.js +73 -0
  107. package/src/observer/livestream.html +4 -2
  108. package/src/observer/tool-wrap.js +17 -14
  109. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  110. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  111. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  112. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  113. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  114. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  115. package/src/platforms/gb/lib/c/README.md +10 -11
  116. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  117. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  118. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  119. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  120. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  121. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  122. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  123. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  124. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  125. package/src/platforms/gbc/lib/c/README.md +10 -11
  126. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  127. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  128. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  129. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  130. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  131. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  132. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  133. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  134. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  135. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  136. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  137. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  138. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  139. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  140. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  141. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  142. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  143. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  144. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  145. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  146. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  147. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  148. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  149. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  150. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  151. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  152. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  153. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  154. package/src/toolchains/index.js +27 -11
@@ -174,7 +174,7 @@ pokes the BG map "whenever the state changes" will have SOME of those pokes
174
174
  land mid-frame and vanish: stale cells, a piece that visually lags the
175
175
  logical grid, glitches that move around as code timing shifts.
176
176
 
177
- The robust pattern (used by the bundled puzzle scaffolds):
177
+ The robust pattern (used by the bundled puzzle example games):
178
178
 
179
179
  1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
180
180
  pairs to a small RAM queue whenever game state changes a cell.
@@ -195,7 +195,7 @@ pokes the BG map "whenever the state changes" will have SOME of those pokes
195
195
  land mid-frame and vanish: stale cells, a piece that visually lags the
196
196
  logical grid, glitches that move around as code timing shifts.
197
197
 
198
- The robust pattern (used by the bundled puzzle scaffolds):
198
+ The robust pattern (used by the bundled puzzle example games):
199
199
 
200
200
  1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
201
201
  pairs to a small RAM queue whenever game state changes a cell.
@@ -208,6 +208,60 @@ The robust pattern (used by the bundled puzzle scaffolds):
208
208
  If you must write outside that structure, turn the LCD off first (only
209
209
  acceptable during init/load screens — mid-game it flashes white).
210
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
+
211
265
  ## Debug recipes
212
266
 
213
267
  A few high-leverage tools you might not know exist:
@@ -247,14 +301,13 @@ Boot order that always works for GBC:
247
301
  }
248
302
  ```
249
303
 
250
- Cribbed from `examples/gbc/templates/tile_engine.c` — start a fresh
251
- game from that template with:
304
+ Cribbed from `examples/gbc/templates/tile_engine.c` — fork that
305
+ example into a fresh game with:
252
306
 
253
307
  ```js
254
- scaffold({
255
- op: 'project',
256
- platform: "gbc",
257
- template: "tile_engine",
308
+ examples({
309
+ op: 'fork',
310
+ example: "gbc/tile_engine",
258
311
  name: "mygame",
259
312
  path: "/abs/path/to/dir",
260
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
 
@@ -86,11 +86,35 @@
86
86
  nop
87
87
  jp init
88
88
 
89
- ;; ─── Header bytes at $0104-$014F — host pipeline fills these ──────
89
+ ;; ─── Header bytes at $0104-$014F — host pipeline fills most of these
90
+ ;; The logo / title / checksums are patched post-link (rgbfix in the
91
+ ;; build pipeline, or patch-header.js when rebuilding outside romdev).
92
+ ;; The CART TYPE and RAM SIZE bytes are DECLARED HERE as real bytes so
93
+ ;; the build's header fixup can read and preserve them:
94
+ ;;
95
+ ;; $0147 = $03 MBC1 + RAM + BATTERY
96
+ ;; $0149 = $02 8 KB external cart RAM at $A000-$BFFF
97
+ ;;
98
+ ;; This is what makes battery saves (persistent hi-scores) work: the
99
+ ;; emulator sizes its SAVE_RAM from these two bytes. The RAM is gated —
100
+ ;; games must write $0A to $0000-$1FFF before touching $A000 and write
101
+ ;; $00 after (see the SRAM idiom in the shmup example). Games that never
102
+ ;; touch $A000 are completely unaffected by the mapper declaration: a
103
+ ;; 32 KB image fits in MBC1 banks 0-1 exactly as it does in a ROM-only
104
+ ;; cart, and the MBC registers only react to ROM-area WRITES (which
105
+ ;; normal code never performs).
106
+ ;;
107
+ ;; If you rebuild OUTSIDE romdev, keep these bytes: rgbfix flags are
108
+ ;; `-m 0x03 -r 0x02` (patch-header.js defaults to ROM-only — pass
109
+ ;; cartType/ramSize through patchGbHeader() if you script it).
90
110
  .area _HEADERe (ABS)
91
111
  .org 0x0104
92
- ;; 76 bytes total: Nintendo logo (48) + title (16) + flags+checksums (12)
93
- .ds 0x4C
112
+ .ds 0x43 ; $0104-$0146 logo/title/CGB flag/licensee/SGB (patched in)
113
+ .org 0x0147
114
+ .db 0x03 ; $0147 cart type: MBC1+RAM+BATTERY (see above)
115
+ .db 0x00 ; $0148 ROM size (rgbfix -p recomputes)
116
+ .db 0x02 ; $0149 RAM size: 8 KB
117
+ .ds 0x06 ; $014A-$014F dest/licensee/version/checksums (patched in)
94
118
 
95
119
  ;; ─── init: real boot code, lives in _CODE starting at $0150 ────────
96
120
  .area _CODE
@@ -13,7 +13,7 @@
13
13
  // runs a bundled rgbfix after every gb/gbc link — see the
14
14
  // "rgbfix (auto header fix)" line in build logs), so you only need this
15
15
  // script when rebuilding the project OUTSIDE romdev with stock SDCC and
16
- // no RGBDS installed. It's what keeps the scaffold self-contained.
16
+ // no RGBDS installed. It's what keeps the forked project self-contained.
17
17
  //
18
18
  // The bundled gb_crt0.s reserves $0100-$014F for the header window,
19
19
  // so the bytes patched in here land on actual cartridge-header
@@ -151,7 +151,17 @@ if (isCli) {
151
151
  const rom = new Uint8Array(readFileSync(inPath));
152
152
  // Auto-detect CGB based on file extension.
153
153
  const cgb = /\.gbc$/i.test(inPath) || /\.gbc$/i.test(outPath);
154
- patchGbHeader(rom, { cgb });
154
+ // Battery-cart passthrough: the bundled gb_crt0.s emits the cart-type and
155
+ // RAM-size bytes EXPLICITLY ($0147=$03 MBC1+RAM+BATTERY, $0149=$02 8KB) so
156
+ // battery hi-score saves work. Preserve a known battery-MBC declaration
157
+ // instead of stomping it back to ROM-only; unknown/garbage bytes (a crt0
158
+ // that left the header window as a .ds gap) still get the safe defaults.
159
+ const BATTERY_TYPES = new Set([0x03, 0x06, 0x0F, 0x10, 0x13, 0x1B, 0x1E]);
160
+ const declType = rom.length > 0x149 ? rom[0x147] : 0x00;
161
+ const declRam = rom.length > 0x149 ? rom[0x149] : 0x00;
162
+ const cartType = BATTERY_TYPES.has(declType) ? declType : 0x00;
163
+ const ramSize = cartType !== 0x00 && declRam >= 0x01 && declRam <= 0x05 ? declRam : 0x00;
164
+ patchGbHeader(rom, { cgb, cartType, ramSize });
155
165
  writeFileSync(outPath, rom);
156
- console.log(`patched ${inPath}${outPath !== inPath ? ` → ${outPath}` : ""} (${rom.length} bytes${cgb ? ", CGB" : ""})`);
166
+ console.log(`patched ${inPath}${outPath !== inPath ? ` → ${outPath}` : ""} (${rom.length} bytes${cgb ? ", CGB" : ""}${cartType ? `, cart $${cartType.toString(16)}+RAM` : ""})`);
157
167
  }
@@ -129,11 +129,11 @@ Sound FIFO states. See "MCP debug & inspection tooling" below for the rest of
129
129
  the live-debug loop (sprites / palette / background / cpu / breakpoint + the
130
130
  memory regions and disasm pipeline).
131
131
 
132
- **For scaffold-level sfx**, the libtonc runtime ships a minimal
132
+ **For starter-level sfx**, the libtonc runtime ships a minimal
133
133
  `gba_sfx.h` / `gba_sfx.c` pair (3 functions: `sfx_init`, `sfx_tone`,
134
134
  `sfx_noise`) that wraps the DMG-compatible APU directly. Same shape
135
- as the NES/GB scaffold sound API, so cross-platform game ports feel
136
- the same. All 5 GBA genre scaffolds (shmup/platformer/puzzle/sports/
135
+ as the NES/GB example sound API, so cross-platform game ports feel
136
+ the same. All 5 GBA genre example games (shmup/platformer/puzzle/sports/
137
137
  racing) use it.
138
138
 
139
139
  ## MCP debug & inspection tooling
@@ -210,7 +210,7 @@ the first call** (`irq_init(NULL)` + `irq_add(II_VBLANK, NULL)` with
210
210
  libtonc — `irqInit(NULL)` + `irqEnable(IRQ_VBLANK)` with libgba).
211
211
  Without this, the BIOS halts the CPU forever waiting for an IRQ that
212
212
  never fires. ROM appears to compile + load but freezes on frame 1 —
213
- single most common GBA gotcha. Every bundled scaffold does it; copy
213
+ single most common GBA gotcha. Every bundled example does it; copy
214
214
  the pattern.
215
215
 
216
216
  ## Cart header format
@@ -36,7 +36,7 @@ installs the master handler + `irq_add(II_VBLANK, NULL)` registers a
36
36
  no-op for vblank (just so the IRQ fires + the BIOS counter increments).
37
37
  With libgba, `irqInit()` does both steps.
38
38
 
39
- Every bundled R28 scaffold (`tonc_hello`, `tonc_hello_sprite`, `shmup`,
39
+ Every bundled R28 example (`tonc_hello`, `tonc_hello_sprite`, `shmup`,
40
40
  `platformer`, `puzzle`, `sports`, `racing`) sets this up — copy the
41
41
  pattern.
42
42
 
@@ -132,10 +132,10 @@ install promise but everything else still works.
132
132
  A deferred enhancement is to port libsysbase into our build so
133
133
  iprintf "just works."
134
134
 
135
- ## Adding sound to a scaffold
135
+ ## Adding sound to your game
136
136
 
137
137
  Both runtimes bundle `gba_sfx.h` + `gba_sfx.c` next to your `main.c`
138
- (courtesy of `scaffold({op:'project'})`). The shape:
138
+ (courtesy of `examples({op:'fork'})`). The shape:
139
139
 
140
140
  ```c
141
141
  #include "gba_sfx.h"
@@ -76,6 +76,46 @@ void sfx_noise(u8 length_frames) {
76
76
  REG_SND4FREQ = 0xC033;
77
77
  }
78
78
 
79
+ /* ── background music: 16-step melody loop on channel 2 ─────────────
80
+ * Mirrors the Genesis/NES example pattern: games get continuous music
81
+ * for free by calling sfx_music_tick() once per frame. The melody is a
82
+ * gentle A-minor arpeggio at reduced envelope volume (0xA of 0xF) so
83
+ * one-shot SFX on channel 1 / noise on 4 read clearly over it.
84
+ * Note values are GBA 11-bit frequency codes: Hz = 131072/(2048-code),
85
+ * code = 2048 - 131072/Hz. 0 = rest. */
86
+ static const u16 music_code[16] = {
87
+ 1452, 1548, 1651, 1750, /* A3 C4 E4 A4 */
88
+ 1714, 1651, 1548, 0, /* G4 E4 C4 - */
89
+ 1452, 1546, 1620, 1714, /* A3 C4 D4 G4 */
90
+ 1651, 1548, 1452, 0, /* E4 C4 A3 - */
91
+ };
92
+ static u8 music_enabled = 1;
93
+ static u8 music_step, music_timer;
94
+
95
+ void sfx_music(u8 on) {
96
+ music_enabled = on;
97
+ music_step = 0;
98
+ music_timer = 0;
99
+ if (!on) REG_SND2CNT = 0x0000; /* zero envelope = silence now */
100
+ }
101
+
102
+ void sfx_music_tick(void) {
103
+ if (!music_enabled) return;
104
+ if (music_timer == 0) {
105
+ u16 code = music_code[music_step & 15];
106
+ if (code) {
107
+ /* vol 0xA, 50% duty, length 40/64 steps (~156ms) — the
108
+ * length-enable auto-silences before the next note so
109
+ * rests actually rest. */
110
+ REG_SND2CNT = (u16)(0xA080 | (64 - 40));
111
+ REG_SND2FREQ = (u16)(0xC000 | (code & 0x07FF));
112
+ }
113
+ music_step++;
114
+ }
115
+ music_timer++;
116
+ if (music_timer >= 10) music_timer = 0; /* 6 notes/sec at 60fps */
117
+ }
118
+
79
119
  void sfx_off(void) {
80
120
  REG_SNDSTAT = 0x0000;
81
121
  }
@@ -42,6 +42,16 @@ void sfx_tone(u8 channel, u16 freq_period, u8 length_frames);
42
42
  * length_frames: 1..63 (same scaling as sfx_tone). */
43
43
  void sfx_noise(u8 length_frames);
44
44
 
45
+ /* ── background music ────────────────────────────────────────────────
46
+ * A 16-step square-wave melody loop on channel 2 (so keep one-shot SFX
47
+ * on channel 1 + noise on 4 and nothing fights for the channel).
48
+ * Call sfx_music_tick() once per frame from your main loop — it steps
49
+ * the melody. ON by default after sfx_init(); sfx_music(0) silences it.
50
+ * "No sound" feedback in playtests is nearly always a missing per-frame
51
+ * tick, not broken registers. */
52
+ void sfx_music(u8 on);
53
+ void sfx_music_tick(void);
54
+
45
55
  /* Power down the APU. */
46
56
  void sfx_off(void);
47
57
 
@@ -114,7 +114,7 @@ The CGB boot ROM checks header byte **`$0143`**:
114
114
  - `$80` → CGB-enhanced mode (color works, DMG-compat fallback)
115
115
  - `$C0` → CGB-only mode (refuses to boot on a DMG)
116
116
 
117
- **Every bundled GBC scaffold is built with `$0143 = $80`** — `build({output:'rom'})`
117
+ **Every bundled GBC example game is built with `$0143 = $80`** — `build({output:'rom'})`
118
118
  / `build({output:'run'})` set this automatically at build time when `platform:"gbc"`,
119
119
  so a freshly built `.gbc` boots in color with no extra step. (Build it as
120
120
  `platform:"gb"` instead and the flag stays `$00` → DMG green-shade mode,
@@ -197,12 +197,12 @@ inversion: `{a}`→A, `{b}`→B, `{start}`/`{select}`, plus the d-pad (spatial
197
197
  east→A, west→B). So `input({op:'set', a: true})` presses GBC A as expected — unlike
198
198
  the genesis_plus_gx platforms (Genesis/SMS/GG), there's no surprise here.
199
199
 
200
- ## Scaffolds
200
+ ## Example games
201
201
 
202
- All GB scaffolds (`shmup`, `platformer`, `puzzle`, `sports`, `racing`,
202
+ All GB example games (`shmup`, `platformer`, `puzzle`, `sports`, `racing`,
203
203
  `hello_sprite`, `tile_engine`) compile identically as GBC ROMs — the
204
204
  bundled GB runtime is already CGB-aware (writes OCPD/OCPS for color).
205
- The genre scaffolds inherit from GB via `TEMPLATES.gbc = TEMPLATES.gb`;
205
+ The genre examples inherit from GB via `TEMPLATES.gbc = TEMPLATES.gb`;
206
206
  the only differences at build time are:
207
207
 
208
208
  - ROM extension: `.gbc` (vs `.gb`)
@@ -128,7 +128,7 @@ pokes the BG map "whenever the state changes" will have SOME of those pokes
128
128
  land mid-frame and vanish: stale cells, a piece that visually lags the
129
129
  logical grid, glitches that move around as code timing shifts.
130
130
 
131
- The robust pattern (used by the bundled puzzle scaffolds):
131
+ The robust pattern (used by the bundled puzzle example games):
132
132
 
133
133
  1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
134
134
  pairs to a small RAM queue whenever game state changes a cell.
@@ -149,7 +149,7 @@ sound channels or extra waveforms.
149
149
 
150
150
  ## "ROM size > 32 KB needed"
151
151
 
152
- The bundled GBC scaffolds all fit in 32 KB (single bank, no MBC).
152
+ The bundled GBC example games all fit in 32 KB (single bank, no MBC).
153
153
  For larger projects use an MBC (memory bank controller). MBC1 / MBC3
154
154
  work in gambatte; set the `$0147` cartridge type byte accordingly.
155
155
  romPatch({op:'gbHeader'}) doesn't set this — you write it from your asm/C.
@@ -159,11 +159,11 @@ romPatch({op:'gbHeader'}) doesn't set this — you write it from your asm/C.
159
159
  Default GBC speed is the same as DMG (~4 MHz Z80). Double-speed mode
160
160
  via KEY1 ($FF4D) doubles CPU but halves audio sample rate + breaks
161
161
  cycle-counted code. Most homebrew leaves it off; if you need the
162
- extra clocks, change the GB scaffold pattern to:
162
+ extra clocks, change the GB example pattern to:
163
163
 
164
164
  ```c
165
165
  KEY1 = 1; /* request speed switch */
166
166
  __asm__("stop"); /* arm the switch (compiler-specific syntax) */
167
167
  ```
168
168
 
169
- Not bundled in any scaffold — use only if you've measured a need.
169
+ Not bundled in any example — use only if you've measured a need.
@@ -3,7 +3,7 @@
3
3
  GBC shares its toolchain (SDCC sm83) + emulator (gambatte) + most
4
4
  of the runtime (`gb_runtime.c`, `gb_crt0.s`, `patch-header.js`,
5
5
  hUGEDriver) with DMG. The CGB tree (`src/platforms/gbc/`) holds
6
- the color-aware scaffolds; everything below is in lockstep with
6
+ the color-aware example games; everything below is in lockstep with
7
7
  the GB tree.
8
8
 
9
9
  CGB-specific:
@@ -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
 
@@ -86,11 +86,34 @@
86
86
  nop
87
87
  jp init
88
88
 
89
- ;; ─── Header bytes at $0104-$014F — host pipeline fills these ──────
89
+ ;; ─── Header bytes at $0104-$014F — host pipeline fills most of these ──
90
90
  .area _HEADERe (ABS)
91
91
  .org 0x0104
92
- ;; 76 bytes total: Nintendo logo (48) + title (16) + flags+checksums (12)
93
- .ds 0x4C
92
+ ;; Nintendo logo ($0104-$0133, 48 bytes) + title/manufacturer/CGB
93
+ ;; flag ($0134-$0143) + licensee/SGB ($0144-$0146): left as a gap —
94
+ ;; the post-link header fix (bundled rgbfix, or patch-header.js when
95
+ ;; rebuilding outside romdev) writes the canonical logo, the CGB
96
+ ;; flag, and both checksums.
97
+ .ds 0x43
98
+ ;; Cartridge TYPE + RAM size ($0147-$0149) are emitted EXPLICITLY so
99
+ ;; the post-link fix can preserve them (it reads these bytes and
100
+ ;; passes them through; unknown/garbage values fall back to ROM-only).
101
+ ;; This is the GB equivalent of the NES crt0's iNES BATTERY bit:
102
+ ;; declaring the cart in the boot file makes battery saves part of
103
+ ;; the project source, not a build flag someone has to remember.
104
+ ;;
105
+ ;; $0147 = $03 MBC1 + RAM + BATTERY → the core exposes the 8KB
106
+ ;; at $A000-$BFFF as persistent SAVE_RAM (.srm).
107
+ ;; Writes only stick after the $0A enable sequence —
108
+ ;; see the SRAM HARDWARE IDIOM in the puzzle example.
109
+ ;; $0148 = $00 32 KB ROM (2 banks — no banking needed)
110
+ ;; $0149 = $02 8 KB cart RAM (one bank at $A000)
111
+ .org 0x0147
112
+ .db 0x03 ; cart type: MBC1+RAM+BATTERY
113
+ .db 0x00 ; ROM size: 32 KB
114
+ .db 0x02 ; RAM size: 8 KB
115
+ ;; $014A-$014F (destination/licensee/version/checksums): host fills.
116
+ .ds 0x06
94
117
 
95
118
  ;; ─── init: real boot code, lives in _CODE starting at $0150 ────────
96
119
  .area _CODE
@@ -13,7 +13,7 @@
13
13
  // runs a bundled rgbfix after every gb/gbc link — see the
14
14
  // "rgbfix (auto header fix)" line in build logs), so you only need this
15
15
  // script when rebuilding the project OUTSIDE romdev with stock SDCC and
16
- // no RGBDS installed. It's what keeps the scaffold self-contained.
16
+ // no RGBDS installed. It's what keeps the forked project self-contained.
17
17
  //
18
18
  // The bundled gb_crt0.s reserves $0100-$014F for the header window,
19
19
  // so the bytes patched in here land on actual cartridge-header
@@ -151,7 +151,17 @@ if (isCli) {
151
151
  const rom = new Uint8Array(readFileSync(inPath));
152
152
  // Auto-detect CGB based on file extension.
153
153
  const cgb = /\.gbc$/i.test(inPath) || /\.gbc$/i.test(outPath);
154
- patchGbHeader(rom, { cgb });
154
+ // Battery-cart passthrough: the bundled gb_crt0.s emits the cart-type and
155
+ // RAM-size bytes EXPLICITLY ($0147=$03 MBC1+RAM+BATTERY, $0149=$02 8KB) so
156
+ // battery hi-score saves work. Preserve a known battery-MBC declaration
157
+ // instead of stomping it back to ROM-only; unknown/garbage bytes (a crt0
158
+ // that left the header window as a .ds gap) still get the safe defaults.
159
+ const BATTERY_TYPES = new Set([0x03, 0x06, 0x0F, 0x10, 0x13, 0x1B, 0x1E]);
160
+ const declType = rom.length > 0x149 ? rom[0x147] : 0x00;
161
+ const declRam = rom.length > 0x149 ? rom[0x149] : 0x00;
162
+ const cartType = BATTERY_TYPES.has(declType) ? declType : 0x00;
163
+ const ramSize = cartType !== 0x00 && declRam >= 0x01 && declRam <= 0x05 ? declRam : 0x00;
164
+ patchGbHeader(rom, { cgb, cartType, ramSize });
155
165
  writeFileSync(outPath, rom);
156
- console.log(`patched ${inPath}${outPath !== inPath ? ` → ${outPath}` : ""} (${rom.length} bytes${cgb ? ", CGB" : ""})`);
166
+ console.log(`patched ${inPath}${outPath !== inPath ? ` → ${outPath}` : ""} (${rom.length} bytes${cgb ? ", CGB" : ""}${cartType ? `, cart $${cartType.toString(16)}+RAM` : ""})`);
157
167
  }
@@ -178,7 +178,7 @@ software mistake, not a hardware limit:
178
178
  > burst or a per-frame DMA), you overrun vblank, drop frames, and the
179
179
  > scroll judders. **Paint the planes ONCE at setup; the loop only nudges
180
180
  > scroll registers and re-stages sprites.** Use the
181
- > `template:"two_plane_parallax"` scaffold as the known-good shape.
181
+ > `two_plane_parallax` example as the known-good shape.
182
182
 
183
183
  ### Hardware scroll, the whole loop
184
184
 
@@ -241,7 +241,7 @@ if (newTileCol != lastTileCol) {
241
241
  ```
242
242
 
243
243
  That's ~28 tile writes per 8 px of travel, not a 1792-cell plane redraw.
244
- The `template:"platformer"` scaffold scrolls within one plane (no
244
+ The `platformer` example scrolls within one plane (no
245
245
  streaming); add the column-stream above to go wider. (Real Sonic also
246
246
  splits the screen with H-blank raster effects for independent strips —
247
247
  that's an IRQ/raster topic, see the `asm` template.)
@@ -481,7 +481,7 @@ build pipeline computes the checksum on link.
481
481
 
482
482
  ## Where the SDK lives (and how to read it)
483
483
 
484
- `scaffold({op:'project', platform:"genesis"})` ships the full SGDK include
484
+ `examples({op:'fork'})` (any Genesis example) ships the full SGDK include
485
485
  tree into the new project at `vendor/sgdk/`. So when your code does
486
486
  `#include <genesis.h>`, those headers come from
487
487
  `vendor/sgdk/include/`:
@@ -239,8 +239,8 @@ Genesis scrolls in HARDWARE — moving the world is two register writes
239
239
  plane every frame (a `VDP_fillTileMapRect` / `VDP_loadTileMap` / big
240
240
  `DMA_*` each frame), you overrun the vblank DMA budget and drop frames →
241
241
  judder. Fix: paint the planes ONCE at setup; the loop only nudges scroll
242
- registers + re-stages sprites. The `template:"two_plane_parallax"`
243
- scaffold is the known-good shape.
242
+ registers + re-stages sprites. The `two_plane_parallax`
243
+ example is the known-good shape.
244
244
 
245
245
  Diagnose it without guessing (no core rebuild):
246
246
 
@@ -14,7 +14,7 @@ $E000-$FFFB Work RAM mirror
14
14
  $FFFC-$FFFF Mapper control registers
15
15
  ```
16
16
 
17
- Most 32 KB scaffolds fit in banks 0+1 and never touch the mapper.
17
+ Most 32 KB example games fit in banks 0+1 and never touch the mapper.
18
18
 
19
19
  ## VDP (display)
20
20
 
@@ -36,7 +36,7 @@ GG VDP = SMS VDP in Mode 4, smaller visible viewport.
36
36
  OR draw the text via the BG name table (no per-line limit).
37
37
 
38
38
  **Always render gameplay content inside (48, 24)..(207, 167)** so it's
39
- visible on real hardware. The bundled scaffolds work without this
39
+ visible on real hardware. The bundled example games work without this
40
40
  because gpgx shows the full framebuffer.
41
41
 
42
42
  ### Sprite coords are hardware-space, NOT visible-space
@@ -78,9 +78,9 @@ on its own anymore.
78
78
  `gg_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
79
79
  sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
80
80
  from `$2000-$3FFF`, in their **own bank** separate from BG tiles at
81
- $0000. This is the baseline because every bundled scaffold uploads
81
+ $0000. This is the baseline because every bundled example uploads
82
82
  its sprite tiles to `$2000` (`gg_load_tiles(0x2000, …)`) — the
83
- default and the scaffolds match, so sprites Just Show Up.
83
+ default and the examples match, so sprites Just Show Up.
84
84
 
85
85
  Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
86
86
  (sharing the BG bank). If you ever set R6=0xFB you MUST also upload
@@ -16,7 +16,7 @@ Anything you draw outside `(48, 24)..(207, 167)` is in the border —
16
16
  visible in headless emulator screenshots (gpgx shows the whole frame)
17
17
  but invisible on real hardware.
18
18
 
19
- The bundled scaffolds are direct ports of the SMS scaffolds and target
19
+ The bundled example games are direct ports of the SMS examples and target
20
20
  the full 256×192 area. Works fine for development under gpgx; for
21
21
  shipping to a real GG, reposition sprite + tilemap content to the
22
22
  visible center.
@@ -28,7 +28,7 @@ active low), separate from the D-pad/A/B which are on `$DC` like
28
28
  SMS. `gg_joypad_read()` already merges them — START shows up in bit 7
29
29
  of the returned byte (`JOY_START` mask).
30
30
 
31
- If you copied an SMS scaffold that uses PAUSE-as-START semantics
31
+ If you copied an SMS example that uses PAUSE-as-START semantics
32
32
  (SMS pause button is at port $DD bit 4 IIRC), it won't work on GG;
33
33
  swap to JOY_START.
34
34
 
@@ -85,7 +85,7 @@ two-byte little-endian CRAM entry.
85
85
 
86
86
  ## "Linking error: undefined reference to sms_joypad_read_p2"
87
87
 
88
- GG only has one controller. The SMS scaffolds use `sms_joypad_read_p2`
88
+ GG only has one controller. The SMS examples use `sms_joypad_read_p2`
89
89
  for the two-controller patterns (Pong, 2P shmup). When porting, drop
90
90
  the P2 read + force `p2 = 0` so the AI fallback always engages.
91
91
 
@@ -3,7 +3,7 @@
3
3
  GG shares its toolchain (SDCC z80) + emulator (genesis_plus_gx) +
4
4
  most of the runtime with SMS (`sms_crt0.s` ≡ `gg_crt0.s` byte-for-
5
5
  byte; PSG protocol identical). The GG tree at `src/platforms/gg/`
6
- holds the GG-specific scaffolds and runtime helpers; SMS docs apply
6
+ holds the GG-specific example games and runtime helpers; SMS docs apply
7
7
  for everything else.
8
8
 
9
9
  GG-specific: