romdevtools 0.21.0 → 0.22.1

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 (127) hide show
  1. package/AGENTS.md +15 -4
  2. package/CHANGELOG.md +66 -0
  3. package/examples/atari7800/templates/hello_sprite.c +48 -4
  4. package/examples/atari7800/templates/music_demo.c +47 -2
  5. package/examples/c64/templates/tile_engine.c +77 -27
  6. package/examples/gb/templates/hello_sprite.c +15 -6
  7. package/examples/gb/templates/music_demo.c +36 -0
  8. package/examples/gb/templates/platformer.c +3 -2
  9. package/examples/gb/templates/puzzle.c +3 -2
  10. package/examples/gb/templates/racing.c +3 -2
  11. package/examples/gb/templates/shmup.c +3 -2
  12. package/examples/gb/templates/sports.c +3 -2
  13. package/examples/gb/templates/tile_engine.c +3 -2
  14. package/examples/gba/templates/maxmod_demo.c +36 -2
  15. package/examples/gba/templates/platformer.c +3 -1
  16. package/examples/gba/templates/tonc_hello_sprite.c +35 -1
  17. package/examples/gbc/templates/hello_sprite.c +12 -3
  18. package/examples/gbc/templates/music_demo.c +56 -12
  19. package/examples/gbc/templates/platformer.c +3 -2
  20. package/examples/gbc/templates/puzzle.c +3 -2
  21. package/examples/gbc/templates/racing.c +3 -2
  22. package/examples/gbc/templates/shmup.c +3 -2
  23. package/examples/gbc/templates/sports.c +3 -2
  24. package/examples/gbc/templates/tile_engine.c +3 -2
  25. package/examples/genesis/main.s +53 -1
  26. package/examples/genesis/templates/hello_sprite.c +25 -3
  27. package/examples/genesis/templates/shmup_2p.c +31 -0
  28. package/examples/genesis/templates/xgm2_demo.c +20 -0
  29. package/examples/gg/templates/hello_sprite.c +25 -2
  30. package/examples/gg/templates/music_demo.c +24 -2
  31. package/examples/gg/templates/racing.c +7 -4
  32. package/examples/gg/templates/sports.c +11 -13
  33. package/examples/gg/templates/tile_engine.c +12 -6
  34. package/examples/lynx/templates/hello_sprite.c +15 -1
  35. package/examples/lynx/templates/music_demo.c +13 -1
  36. package/examples/nes/templates/hello_sprite.c +35 -0
  37. package/examples/nes/templates/music_demo.c +40 -0
  38. package/examples/pce/catch_game/main.c +22 -3
  39. package/examples/pce/music_sfx/main.c +28 -1
  40. package/examples/pce/sprite_move/main.c +7 -2
  41. package/examples/sms/templates/hello_sprite.c +29 -3
  42. package/examples/sms/templates/music_demo.c +18 -4
  43. package/examples/sms/templates/shmup_2p.c +24 -1
  44. package/examples/sms/templates/sports.c +4 -2
  45. package/examples/snes/main.asm +108 -17
  46. package/examples/snes/templates/c-hello-data.asm +23 -0
  47. package/examples/snes/templates/c-hello.c +18 -1
  48. package/examples/snes/templates/hello_sprite-data.asm +23 -0
  49. package/examples/snes/templates/hello_sprite.c +17 -1
  50. package/examples/snes/templates/music_demo-data.asm +23 -0
  51. package/examples/snes/templates/music_demo.c +22 -4
  52. package/examples/snes/templates/platformer.c +4 -1
  53. package/examples/snes/templates/puzzle.c +4 -1
  54. package/package.json +1 -1
  55. package/src/cheats/gamegenie.js +0 -1
  56. package/src/cli/smoke.js +1 -3
  57. package/src/host/LibretroHost.js +69 -15
  58. package/src/host/chafa-render.js +2 -0
  59. package/src/host/dsp-state.js +2 -2
  60. package/src/host/gpgx-state.js +4 -0
  61. package/src/http/routes.js +1 -1
  62. package/src/mcp/server.js +1 -1
  63. package/src/mcp/state.js +36 -0
  64. package/src/mcp/tools/address-to-symbol.js +0 -1
  65. package/src/mcp/tools/art-loaders.js +1 -1
  66. package/src/mcp/tools/cart-parts.js +0 -1
  67. package/src/mcp/tools/classify-region.js +1 -1
  68. package/src/mcp/tools/diff-roms.js +1 -1
  69. package/src/mcp/tools/disasm-rebuild.js +1 -1
  70. package/src/mcp/tools/disasm.js +2 -3
  71. package/src/mcp/tools/find-references.js +1 -2
  72. package/src/mcp/tools/font-map.js +1 -1
  73. package/src/mcp/tools/index.js +0 -49
  74. package/src/mcp/tools/input-layout.js +0 -1
  75. package/src/mcp/tools/input.js +33 -3
  76. package/src/mcp/tools/lifecycle.js +14 -2
  77. package/src/mcp/tools/lospec.js +0 -19
  78. package/src/mcp/tools/platform-docs.js +1 -1
  79. package/src/mcp/tools/platform-tools.js +4 -4
  80. package/src/mcp/tools/project.js +0 -2
  81. package/src/mcp/tools/reinject.js +0 -1
  82. package/src/mcp/tools/rom-id.js +2 -2
  83. package/src/mcp/tools/snippets.js +2 -2
  84. package/src/mcp/tools/sprite-pipeline.js +1 -2
  85. package/src/mcp/tools/tile-inspect.js +1 -1
  86. package/src/mcp/tools/toolchain.js +29 -9
  87. package/src/mcp/tools/watch-memory.js +13 -3
  88. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
  89. package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
  90. package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
  91. package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
  92. package/src/platforms/c64/d64.js +0 -1
  93. package/src/platforms/c64/sid.js +0 -2
  94. package/src/platforms/common/metasprite-adapters.js +1 -1
  95. package/src/platforms/common/metasprite-codegen.js +3 -3
  96. package/src/platforms/common/registers.js +5 -3
  97. package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
  98. package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
  99. package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
  100. package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
  101. package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
  102. package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
  103. package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
  104. package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
  105. package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
  106. package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
  107. package/src/platforms/nes/image-to-tilemap.js +3 -0
  108. package/src/platforms/nes/lib/asm/famitone2.s +5 -1
  109. package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
  110. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  111. package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
  112. package/src/platforms/snes/brr.js +0 -2
  113. package/src/playtest/playtest.js +0 -7
  114. package/src/toolchains/asar/asar.js +0 -9
  115. package/src/toolchains/assemble-snippet.js +30 -12
  116. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
  117. package/src/toolchains/common/reassemble.js +0 -1
  118. package/src/toolchains/common/sdk-cache.js +1 -1
  119. package/src/toolchains/genesis-c/genesis-c.js +5 -3
  120. package/src/toolchains/index.js +27 -3
  121. package/src/toolchains/parse-errors.js +78 -1
  122. package/src/toolchains/sdcc/preflight-lint.js +5 -1
  123. package/src/toolchains/sdcc/sdcc.js +1 -1
  124. package/src/toolchains/sjasm/sjasm.js +1 -1
  125. package/src/toolchains/snes-c/snes-c.js +2 -2
  126. package/src/toolchains/vasm68k/vasm68k.js +2 -4
  127. package/src/toolchains/wladx/wladx.js +1 -1
@@ -2,17 +2,36 @@ import { mkdir, mkdtemp, writeFile, readdir, readFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { TOOLCHAINS } from "../../toolchains/registry.js";
5
- import { buildForPlatform } from "../../toolchains/index.js";
5
+ import { buildForPlatform, rankIssues } from "../../toolchains/index.js";
6
6
  import { resolveLinkerConfig } from "../../toolchains/cc65/preset-resolver.js";
7
+ import { parseBuildLog } from "../../toolchains/parse-errors.js";
7
8
  import { inesHeaderSource, charsSource, nromFlatCfg } from "../../toolchains/cc65/ines.js";
8
9
  import { resolveCore } from "../../cores/registry.js";
9
10
  import { resetHost, getDisclosure } from "../state.js";
10
11
  import { PLATFORM_VIRTUAL_EXT } from "../../host/LibretroHost.js";
11
- import { imageContent, jsonContent, safeTool, textContent } from "../util.js";
12
+ import { imageContent, jsonContent, safeTool } from "../util.js";
12
13
  import { isPlaytestRunning } from "./playtest.js";
13
14
  import { buildSourceWithDebugCore } from "./symbols.js";
14
15
  import { log as serverLog } from "../log.js";
15
16
 
17
+ // crt0 (the per-platform startup stub) is assembled BEHIND the user's build.
18
+ // When it fails the agent gets a raw assembler log — route it through the same
19
+ // parser build() uses so the error leads with the first file:line: message
20
+ // (the issues[]-style surfacing), full log appended for fallback.
21
+ function crt0AssemblyError(log) {
22
+ const issues = parseBuildLog(log ?? "");
23
+ const first = issues.find((i) => i.severity === "error") ?? issues[0];
24
+ const headline = first
25
+ ? `${first.file ? first.file + ":" : ""}${first.line ? first.line + ": " : ""}${first.message}`
26
+ : "no structured diagnostic found";
27
+ return new Error(
28
+ `crt0 (startup stub) assembly failed: ${headline}` +
29
+ (issues.length > 1 ? ` (+${issues.length - 1} more)` : "") +
30
+ `\nThis is the bundled startup code, not your source — if it's the only error, ` +
31
+ `report it. Full assembler log:\n${log ?? ""}`,
32
+ );
33
+ }
34
+
16
35
  // Record a build outcome into the /log ring buffer so a failed build's stage +
17
36
  // error tail are diagnosable later (the request was already traced; this adds
18
37
  // the RESULT). On failure we keep a chunk of the build log; on success just the
@@ -318,7 +337,7 @@ export function registerToolchainTools(server, z, sessionKey) {
318
337
  const { runSdasgb, runSdasz80 } = await import("../../toolchains/sdcc/sdcc.js");
319
338
  const asm = isSm83 ? await runSdasgb({ source: crt0 }) : await runSdasz80({ source: crt0 });
320
339
  if (!asm.rel) {
321
- throw new Error(`crt0 assembly failed:\n${asm.log}`);
340
+ throw crt0AssemblyError(asm.log);
322
341
  }
323
342
  crt0Rel = asm.rel;
324
343
  }
@@ -400,7 +419,7 @@ export function registerToolchainTools(server, z, sessionKey) {
400
419
  ...(result.stage ? { stage: result.stage } : {}),
401
420
  ...(result.sdkEditIgnored ? { sdkEditIgnored: result.sdkEditIgnored } : {}),
402
421
  ...(await logField(result.log, inline, logSibling, result.ok)),
403
- issues: result.issues ?? [],
422
+ issues: rankIssues(result.issues ?? []),
404
423
  ...(showHint ? { hint: showHint } : {}),
405
424
  };
406
425
  // When a build failed on a specific TU (multi-source SDCC build),
@@ -538,7 +557,7 @@ export function registerToolchainTools(server, z, sessionKey) {
538
557
  const { runSdasgb, runSdasz80 } = await import("../../toolchains/sdcc/sdcc.js");
539
558
  const asm = isSm83 ? await runSdasgb({ source: crt0 }) : await runSdasz80({ source: crt0 });
540
559
  if (!asm.rel) {
541
- throw new Error(`crt0 assembly failed:\n${asm.log}`);
560
+ throw crt0AssemblyError(asm.log);
542
561
  }
543
562
  crt0Rel2 = asm.rel;
544
563
  }
@@ -569,7 +588,7 @@ export function registerToolchainTools(server, z, sessionKey) {
569
588
  toolchain: build.toolchain,
570
589
  exitCode: build.exitCode,
571
590
  ...(await logField(build.log, false, null)),
572
- issues: build.issues ?? [],
591
+ issues: rankIssues(build.issues ?? []),
573
592
  });
574
593
  }
575
594
 
@@ -625,7 +644,7 @@ export function registerToolchainTools(server, z, sessionKey) {
625
644
  // Surface lint/build issues even on successful runs so agents see
626
645
  // linter warnings BEFORE the next iteration (was: runSource silently
627
646
  // ran with warnings, agent missed them, hit the crash 100 functions later).
628
- ...((build.issues ?? []).length > 0 ? { issues: build.issues } : {}),
647
+ ...((build.issues ?? []).length > 0 ? { issues: rankIssues(build.issues) } : {}),
629
648
  ...(hint ? { hint } : {}),
630
649
  };
631
650
 
@@ -670,6 +689,7 @@ export function registerToolchainTools(server, z, sessionKey) {
670
689
  server.tool(
671
690
  "build",
672
691
  "Compile/assemble source for a target platform; one tool keyed by `output`.\n" +
692
+ "ON FAILURE (ok:false): READ `issues[]` FIRST — it's the structured error list ({file,line,col,severity,message,stage}) and usually names the exact line to fix. Only fall back to the raw `log` if `issues[]` is empty. Don't guess or rebuild blindly before reading it.\n" +
673
693
  "• output:'rom' (default) — assemble or compile `source` (single) / `sources` ({name:contents}) / `sourcePath` / `sourcesPaths`. Returns the ROM (path by default; `inline:true` for binaryBase64) + build log. **`binaryIncludes`/`binaryIncludePaths` (base64/path CHR-ROM, music blobs for `.incbin`) — WITHOUT them no game with external assets builds.** `includes`/`includePaths` for `.include`d text. `linkerConfig` (cc65; NES preset 'chr-ram-runtime' RECOMMENDED). `crt0`/`crt0Path`/`codeLoc`/`dataLoc` (SDCC). `runtime`/`maxmod`/`rebuildSdk` (GBA/Genesis SDK). **`lint:'strict'` fails the build (stage:'lint', no binary) if the pre-flight SDCC crash-pattern scan flags anything (e.g. the uint8 loop-bound trap); 'advisory' (default) just lists hits in issues[].** **`includeSymbols:true` returns the .map text inline on a PLAIN rom build — distinct from output:'romWithDebug' which writes .dbg/.map FILES.** Language is inferred from extension/content — usually OMIT `language`.\n" +
674
694
  "• output:'romWithDebug' — like 'rom' but also emits linker debug info for the `symbols` tool: cc65 → `.dbg`, SDCC → sdld `.map`, Genesis m68k → GNU ld map (find where a RAM var landed). DEFAULT writes ROM + debug file + log to disk (`outputPath` required unless `inline:true`).\n" +
675
695
  "• output:'run' — BUILD + LOAD + RUN + SCREENSHOT in one round trip — the fastest iteration loop. Same build args; runs `frames` frames and returns the screenshot INLINE. `holdInputs` holds controller state; `screenshotPath` writes the PNG to disk instead; `projectName` titles the playtest window.\n" +
@@ -925,7 +945,7 @@ export async function buildProjectCore({ path: projPath, platform, outputPath })
925
945
  const isSm83 = platform === "gb" || platform === "gbc";
926
946
  const { runSdasgb, runSdasz80 } = await import("../../toolchains/sdcc/sdcc.js");
927
947
  const asm = isSm83 ? await runSdasgb({ source: crt0 }) : await runSdasz80({ source: crt0 });
928
- if (!asm.rel) throw new Error(`crt0 assembly failed:\n${asm.log}`);
948
+ if (!asm.rel) throw crt0AssemblyError(asm.log);
929
949
  crt0Rel = asm.rel;
930
950
  }
931
951
 
@@ -964,6 +984,6 @@ export async function buildProjectCore({ path: projPath, platform, outputPath })
964
984
  romLayout: describeRomLayout(platform, result.binary),
965
985
  ...(result.stage ? { stage: result.stage } : {}),
966
986
  ...(await logField(result.log, false, logSibling, result.ok)),
967
- issues: result.issues ?? [],
987
+ issues: rankIssues(result.issues ?? []),
968
988
  });
969
989
  }
@@ -97,9 +97,18 @@ export function makePressDriver(host, presses) {
97
97
  let applied = 0; // how many scheduled presses actually got a frame
98
98
  let lastSet = null; // last setInput payload we pushed (to avoid churn)
99
99
  const platform = host.status?.platform;
100
+ // When NO pressDuring schedule is given, the driver must NOT touch input at
101
+ // all — it leaves whatever persistent state input({op:'set'}) established in
102
+ // place, so a watch/breakpoint inherits the held pad exactly like
103
+ // frame({op:'step'}) does. (Previously applyForFrame(0) pushed an empty
104
+ // [{},{}] payload on the first frame, silently neutralizing a held Right+A —
105
+ // the v0.16.0 movement-analysis bug.) A non-empty schedule still OWNS the
106
+ // pad (deterministic capture): it drives the buttons and releases on finish.
107
+ const driven = presses.length > 0;
100
108
  return {
101
109
  applied: () => applied,
102
110
  applyForFrame(i) {
111
+ if (!driven) return; // inherit persistent input({op:'set'}) state
103
112
  // Buttons whose [frame, frame+holdFrames) window covers frame i.
104
113
  const held = presses.filter((p) => i >= p.frame && i < p.frame + (p.holdFrames ?? 2));
105
114
  // Build a 2-port setInput payload from the held buttons.
@@ -114,6 +123,7 @@ export function makePressDriver(host, presses) {
114
123
  for (const p of held) { if (p.frame === i) applied++; }
115
124
  },
116
125
  finish() {
126
+ if (!driven) return; // we never touched input; leave it as the caller set it
117
127
  if (lastSet !== null && lastSet !== "[{},{}]") host.setInput({ ports: [{}, {}] });
118
128
  },
119
129
  };
@@ -134,7 +144,7 @@ const MEMORY_REGIONS = /** @type {[string, ...string[]]} */ (Object.keys(MemoryR
134
144
  // with WHY, instead of burning all maxFrames on a meaningless miss.
135
145
  function makeAbortGuard(host, abortIf) {
136
146
  const specs = Array.isArray(abortIf) ? abortIf : [];
137
- const watched = specs.map((s, i) => {
147
+ const watched = specs.map((s, _i) => {
138
148
  const region = s.region ?? "system_ram";
139
149
  const offset = s.offset ?? 0;
140
150
  let before;
@@ -751,7 +761,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
751
761
  button: z.string(),
752
762
  port: z.number().int().min(0).max(3).default(0),
753
763
  holdFrames: z.number().int().min(1).default(2),
754
- })).optional().describe("Schedule input while waiting (drive the game to the state that triggers the condition)."),
764
+ })).optional().describe("Schedule input while waiting (drive the game to the state that triggers the condition). If OMITTED, this run inherits whatever input({op:'set'}) last held — same as frame({op:'step'}). If GIVEN, the schedule OWNS the pad for the whole run (a prior input({op:'set'}) is ignored); use it to drive the watched window itself. Entries with OVERLAPPING windows on the same port are OR'd into a chord (e.g. b+right held while a fires mid-window), not overwritten."),
755
765
  abortIf: z.array(z.object({
756
766
  region: z.enum(MEMORY_REGIONS).optional().describe("memory region (default system_ram)"),
757
767
  offset: z.number().int().min(0).describe("byte offset within the region"),
@@ -1030,7 +1040,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
1030
1040
  button: z.string(),
1031
1041
  port: z.number().int().min(0).max(3).default(0),
1032
1042
  holdFrames: z.number().int().min(1).default(2),
1033
- })).optional().describe("Schedule input while watching (drive the game to the state that touches the watched bytes/range, or uploads the graphic for on:'dma')."),
1043
+ })).optional().describe("Schedule input while watching (drive the game to the state that touches the watched bytes/range, or uploads the graphic for on:'dma'). If OMITTED, this run inherits whatever input({op:'set'}) last held — same as frame({op:'step'}). If GIVEN, the schedule OWNS the pad for the whole run (a prior input({op:'set'}) is ignored). Entries with OVERLAPPING windows on the same port are OR'd into a chord (e.g. b+right held while a fires mid-window), not overwritten."),
1034
1044
  fromState: z.string().optional().describe("on:'range'/'pc' — restore an in-memory savestate SLOT (from state({op:'save', name})) BEFORE tracing, so the log runs from a known moment (jump to the boss fight, then see what writes HP). Deterministic + repeatable."),
1035
1045
  fromStatePath: z.string().optional().describe("on:'range'/'pc' — like fromState but restore from a savestate FILE on disk (state({op:'save', path})). Relative path resolves against the loaded ROM's dir."),
1036
1046
  },
@@ -218,6 +218,40 @@ whole attract sequence each time. `input({op:'set'})`'s `requested` echo is what
218
218
  not proof the pad saw it — verify via the held-buttons RAM byte or a state
219
219
  transition.
220
220
 
221
+ ## 6b. Iterative measurement — boot to gameplay ONCE, reload per run
222
+
223
+ When you run many measurements on the same starting state (frame-by-frame
224
+ velocity tables, per-input timing, A/B trials), do NOT replay the
225
+ `loadMedia → step to title → press start → step into the level` preamble every
226
+ time — that intro is often hundreds of frames and 4+ calls, and you pay it on
227
+ every iteration. Boot once, snapshot, and reload the snapshot per run:
228
+
229
+ ```
230
+ loadMedia({platform, path, cheats}) // cheats survive the save state
231
+ frame({op:'step', frames:180}) // title renders
232
+ input({op:'press', button:'start'})
233
+ frame({op:'step', frames:300}) // into gameplay
234
+ state({op:'save', name:'ready'}) // <-- the reusable starting point
235
+
236
+ // then per measurement:
237
+ state({op:'load', name:'ready'}) // 1 call instead of the whole boot
238
+ ... drive + watch ...
239
+ ```
240
+
241
+ A save state captures applied cheats and the exact RAM/PPU state, so every run
242
+ starts byte-identical — *more* repeatable than re-booting, not just cheaper. A
243
+ named slot (`name`) lives in memory for the session; a `path` persists to disk
244
+ across sessions. If a task says "restart before each run", a state reload
245
+ satisfies that intent far cheaper than a fresh `loadMedia`.
246
+
247
+ **Driving input through a watched run:** a `watch`/`breakpoint` with NO
248
+ `pressDuring` inherits whatever `input({op:'set'})` last held (same as
249
+ `frame({op:'step'})`). But if you pass `pressDuring`, that schedule OWNS the pad
250
+ for the whole run and a prior `input({op:'set'})` is ignored — so to hold a
251
+ button *through* a watched window, put it in `pressDuring`, not a preceding
252
+ `set`. (This is the documented contract; the schemas of `watch`/`breakpoint`
253
+ and `input({op:'set'})` state it too.)
254
+
221
255
  ---
222
256
 
223
257
  ## Quick reference
@@ -1,5 +1,11 @@
1
1
  # Atari 2600 / VCS — 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:"atari2600", name:"mental_model"})`).
5
11
 
@@ -1,5 +1,11 @@
1
1
  # Atari 7800 — 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:"atari7800", name:"mental_model"})`)
5
11
  for the "what's going on" version — the 7800 is the architectural outlier
@@ -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
 
@@ -222,7 +222,6 @@ export function readDirectory(d64) {
222
222
  const nextS = img[base + 1];
223
223
  // 8 entries per sector, 32 bytes each, first entry at +2 then every +32.
224
224
  for (let e = 0; e < 8; e++) {
225
- const eb = base + 2 + e * 32 - (e === 0 ? 0 : 0);
226
225
  const entryBase = base + (e === 0 ? 2 : 2 + e * 32);
227
226
  const typeByte = img[entryBase + 0];
228
227
  if ((typeByte & 0x0f) === 0 && typeByte === 0) continue; // empty slot
@@ -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",
@@ -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
 
@@ -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
 
@@ -1,5 +1,11 @@
1
1
  # NES — 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
  Find your symptom below; each entry has the 1-line diagnosis + the
4
10
  MCP tool call that confirms it. Run these BEFORE you start bisecting
5
11
  your C source.
@@ -275,6 +275,9 @@ export function nesImageToTilemap(args) {
275
275
  // unique 8×8 patterns exist naturally. Returning the unmerged result
276
276
  // gives the caller a chance to retry; just be aware nametable indices
277
277
  // > 255 will wrap on hardware.
278
+ // Permanently disabled (see note above) but kept as documentation of the
279
+ // rejected greedy-merge approach.
280
+ // eslint-disable-next-line no-constant-condition, no-constant-binary-expression
278
281
  if (false && dedup && tileList.length > maxTiles) {
279
282
  const tileDist = (a, b) => {
280
283
  let d = 0;
@@ -8,7 +8,11 @@
8
8
 
9
9
  ;settings, uncomment or put them into your main program; the latter makes possible updates easier
10
10
 
11
- FT_BASE_ADR = $0300 ;page in the RAM used for FT2 variables, should be $xx00
11
+ FT_BASE_ADR = $0700 ;page in the RAM used for FT2 variables, should be $xx00
12
+ ;romdev: pinned to $0700 (the SNDRAM page reserved in
13
+ ;chr-ram-runtime.cfg). $0300 — the cc65 default — overlaps
14
+ ;the C BSS/DATA region, so FT2's per-frame writes would
15
+ ;clobber _ppuctrl_value / NMI state and stall rendering.
12
16
  FT_TEMP = $fd ;3 bytes in zeropage used by the library as a scratchpad
13
17
  FT_DPCM_OFF = $fc00 ;$c000..$ffc0, 64-byte steps
14
18
  FT_SFX_STREAMS = 1 ;number of sound effects played at once, 1..4
@@ -1,5 +1,11 @@
1
1
  # PC Engine — 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
 
@@ -1,5 +1,11 @@
1
1
  # Sega Master System / 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 for the
4
10
  "what's going on" version (via `platform({op:'doc', platform:"sms", name:"mental_model"})`).
5
11
 
@@ -1,5 +1,11 @@
1
1
  # Super Nintendo / Super Famicom — 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:"snes", name:"mental_model"})`).
5
11
 
@@ -23,8 +23,6 @@
23
23
  // lowest error and use that. The first block of any sample MUST use
24
24
  // filter 0 (no other choice has valid p1/p2 history).
25
25
 
26
- const BRR_BUF_DECODE = 16; // samples per block
27
-
28
26
  /** snes9x CLAMP16: saturate to int16. */
29
27
  function clamp16(io) {
30
28
  // The macro: if int16(io) != io, io = (io >> 31) ^ 0x7FFF
@@ -17,9 +17,6 @@ import { createRequire } from "node:module";
17
17
  const execFileAsync = promisify(execFile);
18
18
  const require = createRequire(import.meta.url);
19
19
 
20
- // One-pixel solid-black RGBA buffer; we stretch it across the letterbox
21
- // bars each frame so they don't smear with leftover pixels.
22
- const BLACK_PIXEL = Buffer.from([0, 0, 0, 0xFF]);
23
20
 
24
21
  /**
25
22
  * Choose a default window title from the loaded host. Prefers the loaded
@@ -796,10 +793,6 @@ export async function playtest(args) {
796
793
  };
797
794
  }
798
795
 
799
- function sleep(ms) {
800
- return new Promise((r) => setTimeout(r, ms));
801
- }
802
-
803
796
  function bitToName(bit) {
804
797
  return ({
805
798
  0: "b", 1: "y", 2: "select", 3: "start",
@@ -260,15 +260,6 @@ function lorom(fileStart, fileEnd) {
260
260
  return `${fmt(bankStart, offStart)}..${fmt(bankEnd, offEnd)} (spans banks)`;
261
261
  }
262
262
 
263
- function ensureDir(FS, dir) {
264
- const parts = dir.split("/").filter(Boolean);
265
- let cur = "";
266
- for (const p of parts) {
267
- cur += "/" + p;
268
- try { FS.mkdir(cur); } catch {}
269
- }
270
- }
271
-
272
263
  // Static analyzer for known asar landmines. Runs before the WASM call so
273
264
  // we can return a helpful error instead of letting asar abort silently.
274
265
  // Returns null when source looks clean, or a string with the diagnostic.