romdevtools 0.16.0 → 0.22.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 (209) hide show
  1. package/AGENTS.md +75 -16
  2. package/CHANGELOG.md +316 -0
  3. package/examples/README.md +2 -0
  4. package/examples/atari2600/templates/platformer.asm +460 -0
  5. package/examples/atari2600/templates/racing.asm +463 -0
  6. package/examples/atari2600/templates/shmup.asm +386 -0
  7. package/examples/atari2600/templates/sports.asm +362 -0
  8. package/examples/atari7800/templates/default.c +49 -5
  9. package/examples/atari7800/templates/hello_sprite.c +48 -4
  10. package/examples/atari7800/templates/music_demo.c +47 -2
  11. package/examples/atari7800/templates/platformer.c +43 -4
  12. package/examples/atari7800/templates/puzzle.c +39 -4
  13. package/examples/atari7800/templates/racing.c +39 -4
  14. package/examples/atari7800/templates/shmup.c +40 -2
  15. package/examples/atari7800/templates/sports.c +36 -5
  16. package/examples/c64/templates/platformer.c +19 -5
  17. package/examples/c64/templates/puzzle.c +32 -2
  18. package/examples/c64/templates/shmup.c +28 -2
  19. package/examples/c64/templates/sports.c +30 -2
  20. package/examples/c64/templates/tile_engine.c +77 -27
  21. package/examples/gb/templates/default.c +110 -16
  22. package/examples/gb/templates/hello_sprite.c +15 -6
  23. package/examples/gb/templates/music_demo.c +36 -0
  24. package/examples/gb/templates/platformer.c +28 -6
  25. package/examples/gb/templates/puzzle.c +35 -4
  26. package/examples/gb/templates/racing.c +75 -10
  27. package/examples/gb/templates/shmup.c +41 -3
  28. package/examples/gb/templates/sports.c +51 -3
  29. package/examples/gb/templates/tile_engine.c +3 -2
  30. package/examples/gba/templates/gba_hello.c +29 -11
  31. package/examples/gba/templates/maxmod_demo.c +36 -2
  32. package/examples/gba/templates/platformer.c +3 -1
  33. package/examples/gba/templates/puzzle.c +15 -3
  34. package/examples/gba/templates/racing.c +65 -3
  35. package/examples/gba/templates/shmup.c +41 -4
  36. package/examples/gba/templates/sports.c +36 -2
  37. package/examples/gba/templates/tonc_hello.c +41 -5
  38. package/examples/gba/templates/tonc_hello_sprite.c +35 -1
  39. package/examples/gbc/templates/default.c +103 -26
  40. package/examples/gbc/templates/hello_sprite.c +12 -3
  41. package/examples/gbc/templates/music_demo.c +56 -12
  42. package/examples/gbc/templates/platformer.c +28 -6
  43. package/examples/gbc/templates/puzzle.c +35 -4
  44. package/examples/gbc/templates/racing.c +88 -21
  45. package/examples/gbc/templates/shmup.c +37 -3
  46. package/examples/gbc/templates/sports.c +48 -3
  47. package/examples/gbc/templates/tile_engine.c +3 -2
  48. package/examples/genesis/main.s +53 -1
  49. package/examples/genesis/templates/hello_sprite.c +25 -3
  50. package/examples/genesis/templates/puzzle.c +37 -3
  51. package/examples/genesis/templates/racing.c +44 -11
  52. package/examples/genesis/templates/sgdk_hello.c +34 -1
  53. package/examples/genesis/templates/shmup.c +31 -1
  54. package/examples/genesis/templates/shmup_2p.c +31 -0
  55. package/examples/genesis/templates/xgm2_demo.c +20 -0
  56. package/examples/gg/templates/default.c +56 -18
  57. package/examples/gg/templates/hello_sprite.c +25 -2
  58. package/examples/gg/templates/music_demo.c +24 -2
  59. package/examples/gg/templates/platformer.c +18 -12
  60. package/examples/gg/templates/puzzle.c +38 -7
  61. package/examples/gg/templates/racing.c +58 -9
  62. package/examples/gg/templates/shmup.c +47 -3
  63. package/examples/gg/templates/sports.c +57 -16
  64. package/examples/gg/templates/tile_engine.c +12 -6
  65. package/examples/lynx/templates/default.c +39 -8
  66. package/examples/lynx/templates/hello_sprite.c +15 -1
  67. package/examples/lynx/templates/music_demo.c +13 -1
  68. package/examples/lynx/templates/puzzle.c +28 -1
  69. package/examples/lynx/templates/racing.c +34 -7
  70. package/examples/lynx/templates/shmup.c +42 -3
  71. package/examples/lynx/templates/sports.c +29 -2
  72. package/examples/msx/platformer/main.c +213 -0
  73. package/examples/msx/puzzle/main.c +250 -0
  74. package/examples/msx/racing/main.c +249 -0
  75. package/examples/msx/shmup/main.c +288 -0
  76. package/examples/msx/sports/main.c +182 -0
  77. package/examples/nes/templates/default.c +67 -19
  78. package/examples/nes/templates/hello_sprite.c +35 -0
  79. package/examples/nes/templates/music_demo.c +40 -0
  80. package/examples/nes/templates/platformer.c +65 -6
  81. package/examples/nes/templates/puzzle.c +67 -6
  82. package/examples/nes/templates/racing.c +45 -13
  83. package/examples/nes/templates/shmup.c +51 -2
  84. package/examples/nes/templates/sports.c +51 -6
  85. package/examples/pce/catch_game/main.c +22 -3
  86. package/examples/pce/music_sfx/main.c +28 -1
  87. package/examples/pce/platformer/main.c +283 -0
  88. package/examples/pce/puzzle/main.c +304 -0
  89. package/examples/pce/racing/main.c +304 -0
  90. package/examples/pce/shmup/main.c +346 -0
  91. package/examples/pce/sports/main.c +254 -0
  92. package/examples/pce/sprite_move/main.c +7 -2
  93. package/examples/sms/main.c +35 -6
  94. package/examples/sms/templates/hello_sprite.c +29 -3
  95. package/examples/sms/templates/music_demo.c +18 -4
  96. package/examples/sms/templates/puzzle.c +34 -5
  97. package/examples/sms/templates/racing.c +39 -2
  98. package/examples/sms/templates/shmup.c +41 -2
  99. package/examples/sms/templates/shmup_2p.c +24 -1
  100. package/examples/sms/templates/sports.c +47 -4
  101. package/examples/snes/main.asm +108 -17
  102. package/examples/snes/templates/c-hello-data.asm +23 -0
  103. package/examples/snes/templates/c-hello.c +18 -1
  104. package/examples/snes/templates/default.c +50 -28
  105. package/examples/snes/templates/hello_sprite-data.asm +23 -0
  106. package/examples/snes/templates/hello_sprite.c +17 -1
  107. package/examples/snes/templates/music_demo-data.asm +23 -0
  108. package/examples/snes/templates/music_demo.c +22 -4
  109. package/examples/snes/templates/platformer-data.asm +22 -0
  110. package/examples/snes/templates/platformer.c +20 -2
  111. package/examples/snes/templates/puzzle-data.asm +22 -0
  112. package/examples/snes/templates/puzzle.c +21 -2
  113. package/examples/snes/templates/racing-data.asm +22 -0
  114. package/examples/snes/templates/racing.c +17 -1
  115. package/examples/snes/templates/shmup-data.asm +22 -0
  116. package/examples/snes/templates/shmup.c +20 -1
  117. package/examples/snes/templates/sports-data.asm +22 -0
  118. package/examples/snes/templates/sports.c +16 -1
  119. package/package.json +1 -1
  120. package/src/cheats/gamegenie.js +0 -1
  121. package/src/cli/smoke.js +1 -3
  122. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  123. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  124. package/src/host/LibretroHost.js +191 -16
  125. package/src/host/callbacks.js +9 -1
  126. package/src/host/chafa-render.js +2 -0
  127. package/src/host/dsp-state.js +2 -2
  128. package/src/host/gpgx-state.js +4 -0
  129. package/src/host/types.js +15 -8
  130. package/src/http/routes.js +1 -1
  131. package/src/http/tool-registry.js +26 -1
  132. package/src/mcp/server.js +1 -1
  133. package/src/mcp/state.js +36 -0
  134. package/src/mcp/tools/address-to-symbol.js +0 -1
  135. package/src/mcp/tools/art-loaders.js +1 -1
  136. package/src/mcp/tools/cart-parts.js +75 -4
  137. package/src/mcp/tools/classify-region.js +1 -1
  138. package/src/mcp/tools/diff-roms.js +1 -1
  139. package/src/mcp/tools/disasm-rebuild.js +507 -0
  140. package/src/mcp/tools/disasm.js +97 -9
  141. package/src/mcp/tools/find-references.js +1 -2
  142. package/src/mcp/tools/font-map.js +1 -1
  143. package/src/mcp/tools/frame.js +168 -3
  144. package/src/mcp/tools/index.js +0 -49
  145. package/src/mcp/tools/input-layout.js +0 -1
  146. package/src/mcp/tools/input.js +33 -3
  147. package/src/mcp/tools/lifecycle.js +18 -4
  148. package/src/mcp/tools/lospec.js +0 -19
  149. package/src/mcp/tools/platform-docs.js +1 -1
  150. package/src/mcp/tools/platform-tools.js +4 -4
  151. package/src/mcp/tools/project.js +54 -11
  152. package/src/mcp/tools/reinject.js +0 -1
  153. package/src/mcp/tools/rom-id.js +2 -2
  154. package/src/mcp/tools/snippets.js +2 -2
  155. package/src/mcp/tools/sprite-pipeline.js +1 -2
  156. package/src/mcp/tools/state.js +201 -14
  157. package/src/mcp/tools/tile-inspect.js +1 -1
  158. package/src/mcp/tools/toolchain.js +105 -12
  159. package/src/mcp/tools/watch-memory.js +137 -16
  160. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
  161. package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
  162. package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
  163. package/src/platforms/c64/MENTAL_MODEL.md +45 -1
  164. package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
  165. package/src/platforms/c64/d64.js +280 -0
  166. package/src/platforms/c64/sid.js +0 -2
  167. package/src/platforms/common/metasprite-adapters.js +1 -1
  168. package/src/platforms/common/metasprite-codegen.js +3 -3
  169. package/src/platforms/common/registers.js +5 -3
  170. package/src/platforms/gb/MENTAL_MODEL.md +10 -0
  171. package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
  172. package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
  173. package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
  174. package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
  175. package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
  176. package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
  177. package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
  178. package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
  179. package/src/platforms/msx/MENTAL_MODEL.md +10 -6
  180. package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +63 -2
  182. package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
  183. package/src/platforms/nes/image-to-tilemap.js +3 -0
  184. package/src/platforms/nes/lib/asm/famitone2.s +5 -1
  185. package/src/platforms/pce/MENTAL_MODEL.md +9 -4
  186. package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
  187. package/src/platforms/pce/lib/c/pce_video.c +1 -1
  188. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  189. package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
  190. package/src/platforms/snes/brr.js +0 -2
  191. package/src/playtest/playtest.js +0 -7
  192. package/src/rom-id/identifier.js +15 -0
  193. package/src/toolchains/asar/asar.js +0 -9
  194. package/src/toolchains/assemble-snippet.js +30 -12
  195. package/src/toolchains/cc65/ines.js +145 -0
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
  197. package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
  198. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
  199. package/src/toolchains/common/reassemble.js +10 -3
  200. package/src/toolchains/common/sdk-cache.js +1 -1
  201. package/src/toolchains/genesis-c/genesis-c.js +5 -3
  202. package/src/toolchains/index.js +27 -3
  203. package/src/toolchains/parse-errors.js +78 -1
  204. package/src/toolchains/sdcc/preflight-lint.js +5 -1
  205. package/src/toolchains/sdcc/sdcc.js +1 -1
  206. package/src/toolchains/sjasm/sjasm.js +1 -1
  207. package/src/toolchains/snes-c/snes-c.js +2 -2
  208. package/src/toolchains/vasm68k/vasm68k.js +2 -4
  209. package/src/toolchains/wladx/wladx.js +1 -1
@@ -145,6 +145,48 @@ stub that does a `SYS` to the C entry point.
145
145
  For game ROMs you typically just let the user load the .prg into the
146
146
  emulator and the rest takes care of itself.
147
147
 
148
+ ## Disk images (.d64) — loading real games & distributing yours
149
+
150
+ A bare `.prg` is fine for dev iteration, but the real C64 world — the new
151
+ Commodore 64 Ultimate / C64C Ultimate FPGA hardware and the homebrew/demo
152
+ scene — ships and loads games as **`.d64` disk images** (and `.crt` carts /
153
+ `.tap` tapes). romdev handles them:
154
+
155
+ - **Load & run a disk/cart/tape:** `loadMedia({platform:'c64', path:'game.d64'})`
156
+ (also `.t64 .tap .crt .g64`). VICE attaches it to drive 8 and **autostarts**
157
+ it (equivalent to `LOAD"*",8,1 : RUN`) under warp — give it a few hundred
158
+ frames to finish the emulated 1541 load, then it's running. `mediaKind` in
159
+ status reports `disk` / `tape` / `cartridge` / `program` so you know what you
160
+ loaded.
161
+ - **Distribute YOUR game as a disk:** build your `.prg` as usual, then
162
+ `cart({op:'packDisk', prgPath:'game.prg'})` → an autostart-able `game.d64`
163
+ in the exact format the Ultimate hardware and the scene load. (`cart({op:
164
+ 'extract', path:'x.d64'})` lists a disk's files; add `name:` to pull one out.)
165
+
166
+ ## Disk SAVES (the C64 save medium)
167
+
168
+ The C64 has no battery SRAM — games save by **writing files to the floppy**. The
169
+ disk IS the save, so romdev exposes the LIVE mounted `.d64` for save/restore
170
+ (the C64 analogue of SRAM `exportSram`/`importSram`):
171
+
172
+ - **Snapshot the disk** (captures any files the game wrote): `state({op:
173
+ 'exportDisk', path:'save.d64'})`. Re-load it later with `loadMedia` (autostarts)
174
+ or push it back into a running session with `state({op:'importDisk', path})`.
175
+ - **Inject a save file** a player made elsewhere, straight into the running disk:
176
+ `state({op:'putDiskFile', path:'progress.prg', name:'PROGRESS'})` writes one PRG
177
+ file via the drive. Read it back with `exportDisk` or `cart({op:'extract'})`.
178
+
179
+ These work on the standard 35-track 1541 `.d64` (174848 bytes).
180
+
181
+ **A game's OWN in-emulator `SAVE` works too.** When a running program does a
182
+ KERNAL `SAVE` to drive 8, VICE commits it into the live disk image (true-drive
183
+ GCR write-back) — so after the game saves, `state({op:'exportDisk', path})`
184
+ captures a `.d64` that includes the new file, and you can re-load it later to
185
+ resume. (The on-disk filename is stored in PETSCII; romdev's reader decodes it.)
186
+ So the normal flow is just: run the game, let it save, `exportDisk` to persist.
187
+ `putDiskFile`/`importDisk` are for *injecting* a save from outside (a save a
188
+ player made elsewhere), not a requirement for the game's own saves.
189
+
148
190
  ## Frame heartbeat
149
191
 
150
192
  The C64 has no dedicated vblank interrupt by default. Two approaches:
@@ -166,7 +208,9 @@ When you call `build({output:'rom', platform:"c64", language:"c"})`:
166
208
  3. ld65 links + the bundled c64.cfg → `.prg` with a 2-byte load-address
167
209
  header.
168
210
 
169
- Loadable via vice_x64 (`loadMedia`).
211
+ Loadable via vice_x64 (`loadMedia`). To ship it the way the scene/hardware
212
+ loads games, wrap the `.prg` into a `.d64`: `cart({op:'packDisk', prgPath})`
213
+ (see "Disk images" above).
170
214
 
171
215
  ## Horizontal scrolling (for side-scrollers)
172
216
 
@@ -1,5 +1,11 @@
1
1
  # Commodore 64 — troubleshooting
2
2
 
3
+ > **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
4
+ > `issues: [{file, line, col, severity, message, stage}]` — the structured error
5
+ > list. It almost always names the exact line to fix. Read that before matching a
6
+ > symptom below or touching your source. Fall back to the raw `log` only if
7
+ > `issues[]` is empty but `ok:false`.
8
+
3
9
  When something's broken. Read MENTAL_MODEL.md first
4
10
  (via `platform({op:'doc', platform:"c64", name:"mental_model"})`).
5
11
 
@@ -0,0 +1,280 @@
1
+ // C64 1541 disk image (.d64) codec — pure JS, no external tools.
2
+ //
3
+ // Why this exists: romdev builds C64 homebrew as a bare `.prg` (cc65's output),
4
+ // but the real C64 world — the new Commodore 64 Ultimate / C64C Ultimate FPGA
5
+ // hardware and the entire homebrew/demo scene — loads games as `.d64` disk
6
+ // images (and saves by writing files back INTO the disk). A `.prg` with no
7
+ // drive can't save and isn't how anything ships. This module is the bridge:
8
+ //
9
+ // prgToD64(prg, {name}) — pack a .prg into a fresh, autostart-able .d64
10
+ // readDirectory(d64) — list the files on a disk image
11
+ // extractFile(d64, name) — pull a file's bytes back out (post-save read)
12
+ //
13
+ // Format reference: the standard 35-track 1541 image (174848 bytes). 256-byte
14
+ // sectors, variable sectors per track. Track 18 holds the BAM (sector 0) and
15
+ // the directory (sectors 1+). Files are PETSCII-named, stored as linked sector
16
+ // chains where each sector's first two bytes are (nextTrack, nextSector) — or
17
+ // (0x00, lastByteIndex) on the final sector. This is the well-documented "D64"
18
+ // layout used by VICE's c1541 and every C64 emulator.
19
+
20
+ const SECTOR_SIZE = 256;
21
+ const NUM_TRACKS = 35;
22
+ const DIR_TRACK = 18;
23
+ const BAM_SECTOR = 0;
24
+ const DIR_START_SECTOR = 1;
25
+
26
+ // Sectors per track for a 35-track 1541 disk (zones 1-4).
27
+ // Tracks 1-17: 21, 18-24: 19, 25-30: 18, 31-35: 17.
28
+ const SECTORS_PER_TRACK = (() => {
29
+ const a = new Array(NUM_TRACKS + 1).fill(0); // 1-indexed
30
+ for (let t = 1; t <= NUM_TRACKS; t++) {
31
+ if (t <= 17) a[t] = 21;
32
+ else if (t <= 24) a[t] = 19;
33
+ else if (t <= 30) a[t] = 18;
34
+ else a[t] = 17;
35
+ }
36
+ return a;
37
+ })();
38
+
39
+ const TOTAL_SECTORS = (() => {
40
+ let n = 0;
41
+ for (let t = 1; t <= NUM_TRACKS; t++) n += SECTORS_PER_TRACK[t];
42
+ return n; // 683
43
+ })();
44
+
45
+ const IMAGE_SIZE = TOTAL_SECTORS * SECTOR_SIZE; // 174848
46
+
47
+ /** Byte offset of (track, sector) within the flat image. track is 1-indexed. */
48
+ function offsetOf(track, sector) {
49
+ let off = 0;
50
+ for (let t = 1; t < track; t++) off += SECTORS_PER_TRACK[t] * SECTOR_SIZE;
51
+ return off + sector * SECTOR_SIZE;
52
+ }
53
+
54
+ /** Convert an ASCII string to PETSCII-ish bytes, padded/truncated to `len` with 0xA0 (shifted space). */
55
+ function petsciiName(name, len = 16) {
56
+ const out = new Uint8Array(len).fill(0xa0);
57
+ const s = String(name || "").toUpperCase();
58
+ for (let i = 0; i < len && i < s.length; i++) {
59
+ const c = s.charCodeAt(i);
60
+ // ASCII A-Z, 0-9, space, and common punctuation map ~1:1 to PETSCII for
61
+ // these ranges; anything exotic falls back to a space.
62
+ out[i] = c >= 0x20 && c <= 0x5f ? c : 0x20;
63
+ }
64
+ return out;
65
+ }
66
+
67
+ /**
68
+ * Convert a PETSCII directory name (as stored on disk) back to a trimmed ASCII
69
+ * string. Filenames written by the C64 KERNAL SAVE use the DEFAULT uppercase
70
+ * charset, where letters A–Z are 0xC1–0xDA (high bit set), not 0x41–0x5A — so we
71
+ * must translate that range, otherwise an emulator-written "SCORE" reads as
72
+ * empty. (Our own prgToD64 writes plain 0x41–0x5A; both must decode.)
73
+ */
74
+ function asciiFromPetscii(bytes) {
75
+ let s = "";
76
+ for (const b of bytes) {
77
+ if (b === 0xa0 || b === 0x00) break; // shifted-space pad / terminator
78
+ if (b >= 0xc1 && b <= 0xda) {
79
+ s += String.fromCharCode(b - 0x80); // PETSCII upper A–Z (0xC1..) → ASCII
80
+ } else if (b >= 0x20 && b <= 0x5f) {
81
+ s += String.fromCharCode(b); // plain ASCII / digits / punctuation
82
+ } else if (b >= 0x61 && b <= 0x7a) {
83
+ s += String.fromCharCode(b - 0x20); // PETSCII lower-as-upper → ASCII upper
84
+ }
85
+ // anything else (graphics chars etc.) is dropped from the readable name
86
+ }
87
+ return s.trim();
88
+ }
89
+
90
+ /**
91
+ * Pack a `.prg` (2-byte little-endian load address + body) into a fresh,
92
+ * autostart-able 1541 `.d64` image. The file is written as a single PRG-type
93
+ * directory entry named `name` (default "GAME"). The disk is otherwise empty.
94
+ *
95
+ * @param {Uint8Array|Buffer} prg the raw .prg bytes (load addr + program)
96
+ * @param {object} [opts]
97
+ * @param {string} [opts.name] file name (PETSCII, ≤16 chars) — default "GAME"
98
+ * @param {string} [opts.diskName] disk label (≤16 chars) — default = name
99
+ * @param {string} [opts.diskId] 2-char disk id — default "RD"
100
+ * @returns {Uint8Array} a 174848-byte .d64 image
101
+ */
102
+ export function prgToD64(prg, opts = {}) {
103
+ const body = prg instanceof Uint8Array ? prg : new Uint8Array(prg);
104
+ if (body.length < 2) throw new Error("prgToD64: .prg too small (need ≥2 bytes load address)");
105
+ const fileName = opts.name || "GAME";
106
+ const diskName = opts.diskName || fileName;
107
+ const diskId = (opts.diskId || "RD").slice(0, 2).padEnd(2, " ");
108
+
109
+ const img = new Uint8Array(IMAGE_SIZE);
110
+
111
+ // ---- Lay the file out as a linked sector chain ----------------------------
112
+ // Files conventionally start on track 1; we walk forward, skipping the
113
+ // directory track (18). 254 data bytes per sector (2 bytes are the link).
114
+ const dataPerSector = SECTOR_SIZE - 2;
115
+ const numSectors = Math.ceil(body.length / dataPerSector) || 1;
116
+
117
+ // Pick a sector list (track, sector) for the file, skipping the dir track.
118
+ const chain = [];
119
+ let track = 1;
120
+ let sector = 0;
121
+ for (let i = 0; i < numSectors; i++) {
122
+ // advance to a free (track,sector), skipping the directory track
123
+ while (track === DIR_TRACK || sector >= SECTORS_PER_TRACK[track]) {
124
+ if (sector >= SECTORS_PER_TRACK[track]) { track++; sector = 0; }
125
+ if (track === DIR_TRACK) { track++; sector = 0; }
126
+ if (track > NUM_TRACKS) throw new Error("prgToD64: file too large for a 35-track disk");
127
+ }
128
+ chain.push([track, sector]);
129
+ sector++;
130
+ }
131
+
132
+ // Write the chain.
133
+ for (let i = 0; i < chain.length; i++) {
134
+ const [t, s] = chain[i];
135
+ const base = offsetOf(t, s);
136
+ const isLast = i === chain.length - 1;
137
+ const sliceStart = i * dataPerSector;
138
+ const sliceEnd = Math.min(sliceStart + dataPerSector, body.length);
139
+ const chunk = body.subarray(sliceStart, sliceEnd);
140
+ if (isLast) {
141
+ img[base] = 0x00; // next track = 0 → end of file
142
+ img[base + 1] = chunk.length + 1; // bytes-used-in-this-sector index
143
+ } else {
144
+ const [nt, ns] = chain[i + 1];
145
+ img[base] = nt;
146
+ img[base + 1] = ns;
147
+ }
148
+ img.set(chunk, base + 2);
149
+ }
150
+
151
+ const fileBlocks = chain.length;
152
+
153
+ // ---- BAM (track 18, sector 0) --------------------------------------------
154
+ const bam = offsetOf(DIR_TRACK, BAM_SECTOR);
155
+ img[bam + 0] = DIR_TRACK; // first directory track
156
+ img[bam + 1] = DIR_START_SECTOR; // first directory sector
157
+ img[bam + 2] = 0x41; // 'A' = 1541 disk format
158
+ img[bam + 3] = 0x00;
159
+
160
+ // Per-track free-sector bitmap: 4 bytes each for tracks 1..35 at +4.
161
+ // byte0 = free count, bytes1-3 = bitmap (bit set = sector free).
162
+ for (let t = 1; t <= NUM_TRACKS; t++) {
163
+ const e = bam + 4 + (t - 1) * 4;
164
+ const spt = SECTORS_PER_TRACK[t];
165
+ let freeMask = 0;
166
+ for (let s = 0; s < spt; s++) freeMask |= (1 << s);
167
+ // mark used: any sector in our file chain, plus track 18 sectors 0 & 1
168
+ let used = new Set();
169
+ if (t === DIR_TRACK) { used.add(BAM_SECTOR); used.add(DIR_START_SECTOR); }
170
+ for (const [ct, cs] of chain) if (ct === t) used.add(cs);
171
+ for (const s of used) freeMask &= ~(1 << s);
172
+ let freeCount = 0;
173
+ for (let s = 0; s < spt; s++) if (freeMask & (1 << s)) freeCount++;
174
+ img[e + 0] = freeCount;
175
+ img[e + 1] = freeMask & 0xff;
176
+ img[e + 2] = (freeMask >> 8) & 0xff;
177
+ img[e + 3] = (freeMask >> 16) & 0xff;
178
+ }
179
+
180
+ // Disk name (+0x90, 16 bytes, 0xA0 padded), then id + dos type.
181
+ img.set(petsciiName(diskName, 16), bam + 0x90);
182
+ img[bam + 0xa0] = 0xa0;
183
+ img[bam + 0xa1] = 0xa0;
184
+ img[bam + 0xa2] = diskId.charCodeAt(0);
185
+ img[bam + 0xa3] = diskId.charCodeAt(1);
186
+ img[bam + 0xa4] = 0xa0;
187
+ img[bam + 0xa5] = 0x32; // '2' DOS version
188
+ img[bam + 0xa6] = 0x41; // 'A'
189
+ for (let i = 0xa7; i <= 0xaa; i++) img[bam + i] = 0xa0;
190
+
191
+ // ---- Directory entry (track 18, sector 1, first slot) --------------------
192
+ const dir = offsetOf(DIR_TRACK, DIR_START_SECTOR);
193
+ img[dir + 0] = 0x00; // next dir track = 0 (only one dir sector)
194
+ img[dir + 1] = 0xff; // next dir sector = 0xff (last in chain)
195
+ // entry: file type (0x82 = closed PRG), then first (track,sector)
196
+ img[dir + 2] = 0x82;
197
+ img[dir + 3] = chain[0][0];
198
+ img[dir + 4] = chain[0][1];
199
+ img.set(petsciiName(fileName, 16), dir + 5);
200
+ // bytes 0x15-0x1d: REL side info / unused for PRG → 0
201
+ img[dir + 0x1e] = fileBlocks & 0xff; // block count low
202
+ img[dir + 0x1f] = (fileBlocks >> 8) & 0xff; // block count high
203
+
204
+ return img;
205
+ }
206
+
207
+ /**
208
+ * Read the directory of a `.d64` image.
209
+ * @param {Uint8Array|Buffer} d64
210
+ * @returns {Array<{name:string, type:string, track:number, sector:number, blocks:number}>}
211
+ */
212
+ export function readDirectory(d64) {
213
+ const img = d64 instanceof Uint8Array ? d64 : new Uint8Array(d64);
214
+ const TYPES = ["DEL", "SEQ", "PRG", "USR", "REL"];
215
+ const out = [];
216
+ let t = DIR_TRACK, s = DIR_START_SECTOR;
217
+ const seen = new Set();
218
+ while (t !== 0 && !seen.has(`${t},${s}`)) {
219
+ seen.add(`${t},${s}`);
220
+ const base = offsetOf(t, s);
221
+ const nextT = img[base + 0];
222
+ const nextS = img[base + 1];
223
+ // 8 entries per sector, 32 bytes each, first entry at +2 then every +32.
224
+ for (let e = 0; e < 8; e++) {
225
+ const entryBase = base + (e === 0 ? 2 : 2 + e * 32);
226
+ const typeByte = img[entryBase + 0];
227
+ if ((typeByte & 0x0f) === 0 && typeByte === 0) continue; // empty slot
228
+ const ft = img[entryBase + 0];
229
+ const ftrack = img[entryBase + 1];
230
+ const fsector = img[entryBase + 2];
231
+ const nameBytes = img.subarray(entryBase + 3, entryBase + 3 + 16);
232
+ const name = asciiFromPetscii(nameBytes);
233
+ if (!name) continue;
234
+ const blocks = img[entryBase + 0x1c] | (img[entryBase + 0x1d] << 8);
235
+ out.push({ name, type: TYPES[ft & 0x07] || "DEL", track: ftrack, sector: fsector, blocks });
236
+ }
237
+ t = nextT; s = nextS;
238
+ }
239
+ return out;
240
+ }
241
+
242
+ /**
243
+ * Extract a file's raw bytes (including its 2-byte load address for PRG) from a
244
+ * `.d64`, by following its sector chain. If `name` is omitted, the first file
245
+ * is returned.
246
+ * @param {Uint8Array|Buffer} d64
247
+ * @param {string} [name]
248
+ * @returns {Uint8Array|null} file bytes, or null if not found
249
+ */
250
+ export function extractFile(d64, name) {
251
+ const img = d64 instanceof Uint8Array ? d64 : new Uint8Array(d64);
252
+ const dir = readDirectory(img);
253
+ const entry = name
254
+ ? dir.find((d) => d.name.toUpperCase() === String(name).toUpperCase())
255
+ : dir[0];
256
+ if (!entry) return null;
257
+
258
+ const bytes = [];
259
+ let t = entry.track, s = entry.sector;
260
+ const seen = new Set();
261
+ while (t !== 0 && !seen.has(`${t},${s}`)) {
262
+ seen.add(`${t},${s}`);
263
+ const base = offsetOf(t, s);
264
+ const nextT = img[base + 0];
265
+ const nextS = img[base + 1];
266
+ if (nextT === 0) {
267
+ // last sector: nextS is the index of the last valid byte (+1 over data)
268
+ const used = nextS; // bytes 2..used hold data
269
+ for (let i = 2; i <= used && base + i < img.length; i++) bytes.push(img[base + i]);
270
+ break;
271
+ } else {
272
+ for (let i = 2; i < SECTOR_SIZE; i++) bytes.push(img[base + i]);
273
+ }
274
+ t = nextT; s = nextS;
275
+ }
276
+ return new Uint8Array(bytes);
277
+ }
278
+
279
+ export const D64_IMAGE_SIZE = IMAGE_SIZE;
280
+ export const D64_DISK_EXTENSIONS = [".d64", ".d71", ".d81", ".g64", ".t64", ".tap", ".crt", ".p00"];
@@ -29,8 +29,6 @@
29
29
  // $D41B voice 3 OSC3 readback
30
30
  // $D41C voice 3 ENV3 readback
31
31
 
32
- const WAVEFORMS = ["none", "triangle", "sawtooth", "tri+saw", "pulse", "tri+pulse", "saw+pulse", "tri+saw+pulse", "noise"];
33
-
34
32
  function decodeControl(byte) {
35
33
  // Decode the waveform field (top 4 bits) as a name where possible.
36
34
  const wfBits = (byte >> 4) & 0x0F;
@@ -203,7 +203,7 @@ export async function gbAdapter(host, platform) {
203
203
  // palette line (CRAM entries 16-31). Tile data base from VDP reg 6.
204
204
  // =====================================================================
205
205
  export async function smsAdapter(host, platform) {
206
- const { decodeSmsTile, decodeSmsVdpRegs, decodeSmsCram, decodeGgCram, snapshotPalette } = await import("../sms/vdp.js");
206
+ const { decodeSmsTile, decodeSmsVdpRegs, snapshotPalette } = await import("../sms/vdp.js");
207
207
  const vramRegion = platform === "gg" ? "gg_vram" : "sms_vram";
208
208
  const vram = host.readMemory(vramRegion, 0, 0x4000);
209
209
  const regs = host.readMemory("sms_vdp_regs", 0, 16);
@@ -75,7 +75,7 @@ static u16 ${v}_draw(u16 firstSlot, s16 x, s16 y, u16 baseTile) {
75
75
  }
76
76
 
77
77
  // ---- SNES (PVSnesLib oamSet-style) ----
78
- function emitSnes(v, layout, tiles, palette) {
78
+ function emitSnes(v, layout, tiles, _palette) {
79
79
  const pieces = layout.pieces.map((p) => {
80
80
  // PVSnesLib oamSet: size 0=8x8/16x16 small/large per OBSEL — we expose
81
81
  // wPx/hPx and let the user pick the OBSEL pair; flip bits in attr.
@@ -99,7 +99,7 @@ const unsigned short ${v}_piece_count = ${layout.pieces.length};
99
99
  }
100
100
 
101
101
  // ---- NES (shadow-OAM bytes) ----
102
- function emitNes(v, layout, tiles, palette) {
102
+ function emitNes(v, layout, tiles, _palette) {
103
103
  // NES draw = write 4 OAM bytes per cell (y, tile, attr, x). We emit pieces
104
104
  // as (x,y,tile,attr) so the user copies them into shadow OAM at their base.
105
105
  const cells = [];
@@ -127,7 +127,7 @@ const unsigned char ${v}_cell_count = ${cells.length};
127
127
  }
128
128
 
129
129
  // ---- GB/GBC (shadow-OAM bytes) ----
130
- function emitGb(v, layout, tiles, palette) {
130
+ function emitGb(v, layout, tiles, _palette) {
131
131
  const cells = [];
132
132
  for (const p of layout.pieces) {
133
133
  for (let r = 0; r < p.hTiles; r++) {
@@ -235,14 +235,16 @@ export const ATARI7800_REGISTERS = {
235
235
  0x2E: "P3C1", 0x2F: "P3C2", 0x30: "P3C3",
236
236
  0x32: "P4C1", 0x33: "P4C2", 0x34: "P4C3",
237
237
  0x36: "P5C1", 0x37: "P5C2", 0x38: "P5C3",
238
- 0x3A: "P6C1", 0x3B: "P6C2", 0x3C: "P6C3",
238
+ 0x3A: "P6C1", 0x3B: "P6C2",
239
+ // $3C is BOTH the MARIA control reg (CTRL) and P6C3 depending on the
240
+ // reference's naming convention — a JS object holds one value per key, so
241
+ // name it for both rather than silently dropping one.
242
+ 0x3C: "CTRL/P6C3",
239
243
  0x3E: "P7C1", 0x3F: "P7C2", // P7C3 lives at $40 in some refs
240
244
  // DPP (display-list pointer) lives at $84/$85
241
245
  0x84: "DPPH", 0x85: "DPPL",
242
246
  0x87: "CHARBASE",
243
247
  0x88: "OFFSET",
244
- // MARIA control reg
245
- 0x3C: "CTRL", // overlaps with P6C3 — convention varies; tag both
246
248
  // RIOT (6532) regs at $280
247
249
  0x0280: "SWCHA", 0x0281: "SWACNT", 0x0282: "SWCHB", 0x0283: "SWBCNT",
248
250
  0x0284: "INTIM",
@@ -4,6 +4,16 @@ One page. Read once before you write your first game. The
4
4
  TROUBLESHOOTING.md alongside this file is for when something's broken;
5
5
  this is the "what's going on" version.
6
6
 
7
+ ## Blank screen? Verify rendering first (no vision needed)
8
+
9
+ Compiles clean but nothing on screen? Call **`frame({op:'verify', frames:60})`** —
10
+ one call fuses a framebuffer pixel scan with the live LCDC and returns
11
+ `{verified:true|false|null, issues[]}`. `renderDisabled` = LCD off (LCDC.7 clear);
12
+ `blankScreen`/`nearlyBlank` with LCD on = nothing in the BG map / OAM / palette
13
+ (check the footguns below + read `memory({op:'read', region:'gb_vram'})`);
14
+ `verified:null` = step a frame first. Zero image tokens, frame-0-guarded — use it
15
+ as the first move when a change "did nothing."
16
+
7
17
  ## Five silent-failure footguns to know before you start (R26 + R27)
8
18
 
9
19
  If your ROM compiles cleanly but doesn't render — or sprites land in
@@ -1,5 +1,11 @@
1
1
  # Game Boy / Game Boy Color — symptom → fix
2
2
 
3
+ > **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
4
+ > `issues: [{file, line, col, severity, message, stage}]` — the structured error
5
+ > list. It almost always names the exact line to fix. Read that before matching a
6
+ > symptom below or touching your source. Fall back to the raw `log` only if
7
+ > `issues[]` is empty but `ok:false`.
8
+
3
9
  Stuck? Find your symptom below; each entry has the 1-line diagnosis and
4
10
  the MCP tool call that confirms it. **Run the diagnosis BEFORE you start
5
11
  bisecting the C source** — most "GB doesn't render" bugs are one of these
@@ -127,10 +127,10 @@ void oam_dma_init_hram(void) {
127
127
  0x20, 0xFD, /* jr nz, -3 ─┘ spin while a != 0 */
128
128
  0xC9, /* ret */
129
129
  };
130
- uint8_t i;
131
- for (i = 0; i < sizeof(stub); i++) {
132
- HRAM_DMA_STUB[i] = stub[i];
133
- }
130
+ /* Use the pointer-walk memcpy_vram (not an indexed dst[i]=src[i] loop):
131
+ * SDCC sm83 miscompiles the indexed form into a high-pointer like
132
+ * HRAM_DMA_STUB ($FF80). memcpy_vram does *d++=*s++, which is safe. */
133
+ memcpy_vram(HRAM_DMA_STUB, stub, sizeof(stub));
134
134
  }
135
135
 
136
136
  /* OAM DMA — copy 160 bytes from `src` to OAM ($FE00-$FE9F) via the
@@ -1,5 +1,11 @@
1
1
  # Game Boy Advance — troubleshooting
2
2
 
3
+ > **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
4
+ > `issues: [{file, line, col, severity, message, stage}]` — the structured error
5
+ > list. It almost always names the exact line to fix. Read that before matching a
6
+ > symptom below or touching your source. Fall back to the raw `log` only if
7
+ > `issues[]` is empty but `ok:false`.
8
+
3
9
  When something's broken. Read MENTAL_MODEL.md first
4
10
  (via `platform({op:'doc', platform:"gba", name:"mental_model"})`).
5
11
 
@@ -1,5 +1,11 @@
1
1
  # Game Boy Color — troubleshooting
2
2
 
3
+ > **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
4
+ > `issues: [{file, line, col, severity, message, stage}]` — the structured error
5
+ > list. It almost always names the exact line to fix. Read that before matching a
6
+ > symptom below or touching your source. Fall back to the raw `log` only if
7
+ > `issues[]` is empty but `ok:false`.
8
+
3
9
  Read MENTAL_MODEL.md first (`platform({op:'doc', platform:"gbc",
4
10
  name:"mental_model"})`). Most DMG-era troubleshooting from GB applies
5
11
  unchanged — including the **two SDCC sm83 codegen footguns below**, which are
@@ -127,10 +127,10 @@ void oam_dma_init_hram(void) {
127
127
  0x20, 0xFD, /* jr nz, -3 ─┘ spin while a != 0 */
128
128
  0xC9, /* ret */
129
129
  };
130
- uint8_t i;
131
- for (i = 0; i < sizeof(stub); i++) {
132
- HRAM_DMA_STUB[i] = stub[i];
133
- }
130
+ /* Use the pointer-walk memcpy_vram (not an indexed dst[i]=src[i] loop):
131
+ * SDCC sm83 miscompiles the indexed form into a high-pointer like
132
+ * HRAM_DMA_STUB ($FF80). memcpy_vram does *d++=*s++, which is safe. */
133
+ memcpy_vram(HRAM_DMA_STUB, stub, sizeof(stub));
134
134
  }
135
135
 
136
136
  /* OAM DMA — copy 160 bytes from `src` to OAM ($FE00-$FE9F) via the
@@ -1,5 +1,11 @@
1
1
  # Sega Genesis / Mega Drive — troubleshooting
2
2
 
3
+ > **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
4
+ > `issues: [{file, line, col, severity, message, stage}]` — the structured error
5
+ > list. It almost always names the exact line to fix. Read that before matching a
6
+ > symptom below or touching your source. Fall back to the raw `log` only if
7
+ > `issues[]` is empty but `ok:false`.
8
+
3
9
  When something's broken. Read MENTAL_MODEL.md first for the "what's
4
10
  going on" version (via `platform({op:'doc', platform:"genesis", name:"mental_model"})`).
5
11
 
@@ -1,5 +1,11 @@
1
1
  # Game Gear — troubleshooting
2
2
 
3
+ > **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
4
+ > `issues: [{file, line, col, severity, message, stage}]` — the structured error
5
+ > list. It almost always names the exact line to fix. Read that before matching a
6
+ > symptom below or touching your source. Fall back to the raw `log` only if
7
+ > `issues[]` is empty but `ok:false`.
8
+
3
9
  When something's broken. Read MENTAL_MODEL.md first
4
10
  (`platform({op:'doc', platform:"gg", name:"mental_model"})`).
5
11
 
@@ -1,5 +1,11 @@
1
1
  # Atari Lynx — troubleshooting
2
2
 
3
+ > **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
4
+ > `issues: [{file, line, col, severity, message, stage}]` — the structured error
5
+ > list. It almost always names the exact line to fix. Read that before matching a
6
+ > symptom below or touching your source. Fall back to the raw `log` only if
7
+ > `issues[]` is empty but `ok:false`.
8
+
3
9
  Read MENTAL_MODEL.md first (`platform({op:'doc', platform:"lynx",
4
10
  name:"mental_model"})`).
5
11
 
@@ -12,12 +12,16 @@ romdev ships a **hardware helper library** (`src/platforms/msx/lib/c/`:
12
12
  `msx_psg_tone()` in plain C. It uses DIRECT Z80 I/O ports (the reliable path —
13
13
  NOT fragile inline-asm BIOS wrappers).
14
14
 
15
- The fastest way to a working game: **`scaffold({op:'project', platform: "msx", template:
16
- "sprite_move"})`** (also `music_sfx`, `catch_game`). It drops a complete,
17
- *building* project a verified playable example + the helper lib + the cart
18
- crt0 + docs. Read the example's `main.c`, then change it. Examples live in
19
- `examples/msx/`. **Gotcha:** read joystick **port 1** (`msx_read_joystick(1)`)
20
- port 0 is the keyboard, which an emulator's gamepad doesn't drive.
15
+ The fastest way to a working game: **`scaffold({op:'game', platform: "msx", genre:
16
+ "shmup"})`** or any of `platformer` / `puzzle` / `sports` / `racing`, the full
17
+ genre set. For a smaller starting point use **`scaffold({op:'project', platform:
18
+ "msx", template: "sprite_move"})`** (also `music_sfx`, `catch_game`). Either drops
19
+ a complete, *building* project a verified playable example + the helper lib +
20
+ the cart crt0 + docs. Read the example's `main.c`, then change it. Examples live in
21
+ `examples/msx/`. The `platformer` scaffold column-streams the SCREEN 2 name table
22
+ for a tile-by-tile side-scroll. **Gotcha:** read joystick **port 1**
23
+ (`msx_read_joystick(1)`) — port 0 is the keyboard, which an emulator's gamepad
24
+ doesn't drive.
21
25
 
22
26
  ## CPU — Z80 (16-bit address space, paged in 16 KB slots)
23
27
 
@@ -1,5 +1,11 @@
1
1
  # MSX — troubleshooting (symptom → cause → fix)
2
2
 
3
+ > **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
4
+ > `issues: [{file, line, col, severity, message, stage}]` — the structured error
5
+ > list. It almost always names the exact line to fix. Read that before matching a
6
+ > symptom below or touching your source. Fall back to the raw `log` only if
7
+ > `issues[]` is empty but `ok:false`.
8
+
3
9
  Read this when something's broken. For the "how it works" overview, read
4
10
  MENTAL_MODEL.md first.
5
11