romdevtools 0.21.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.
- package/AGENTS.md +15 -4
- package/CHANGELOG.md +58 -0
- package/examples/atari7800/templates/hello_sprite.c +48 -4
- package/examples/atari7800/templates/music_demo.c +47 -2
- package/examples/c64/templates/tile_engine.c +77 -27
- package/examples/gb/templates/hello_sprite.c +15 -6
- package/examples/gb/templates/music_demo.c +36 -0
- package/examples/gb/templates/platformer.c +3 -2
- package/examples/gb/templates/puzzle.c +3 -2
- package/examples/gb/templates/racing.c +3 -2
- package/examples/gb/templates/shmup.c +3 -2
- package/examples/gb/templates/sports.c +3 -2
- package/examples/gb/templates/tile_engine.c +3 -2
- package/examples/gba/templates/maxmod_demo.c +36 -2
- package/examples/gba/templates/platformer.c +3 -1
- package/examples/gba/templates/tonc_hello_sprite.c +35 -1
- package/examples/gbc/templates/hello_sprite.c +12 -3
- package/examples/gbc/templates/music_demo.c +56 -12
- package/examples/gbc/templates/platformer.c +3 -2
- package/examples/gbc/templates/puzzle.c +3 -2
- package/examples/gbc/templates/racing.c +3 -2
- package/examples/gbc/templates/shmup.c +3 -2
- package/examples/gbc/templates/sports.c +3 -2
- package/examples/gbc/templates/tile_engine.c +3 -2
- package/examples/genesis/main.s +53 -1
- package/examples/genesis/templates/hello_sprite.c +25 -3
- package/examples/genesis/templates/shmup_2p.c +31 -0
- package/examples/genesis/templates/xgm2_demo.c +20 -0
- package/examples/gg/templates/hello_sprite.c +25 -2
- package/examples/gg/templates/music_demo.c +24 -2
- package/examples/gg/templates/racing.c +7 -4
- package/examples/gg/templates/sports.c +11 -13
- package/examples/gg/templates/tile_engine.c +12 -6
- package/examples/lynx/templates/hello_sprite.c +15 -1
- package/examples/lynx/templates/music_demo.c +13 -1
- package/examples/nes/templates/hello_sprite.c +35 -0
- package/examples/nes/templates/music_demo.c +40 -0
- package/examples/pce/catch_game/main.c +22 -3
- package/examples/pce/music_sfx/main.c +28 -1
- package/examples/pce/sprite_move/main.c +7 -2
- package/examples/sms/templates/hello_sprite.c +29 -3
- package/examples/sms/templates/music_demo.c +18 -4
- package/examples/sms/templates/shmup_2p.c +24 -1
- package/examples/sms/templates/sports.c +4 -2
- package/examples/snes/main.asm +108 -17
- package/examples/snes/templates/c-hello-data.asm +23 -0
- package/examples/snes/templates/c-hello.c +18 -1
- package/examples/snes/templates/hello_sprite-data.asm +23 -0
- package/examples/snes/templates/hello_sprite.c +17 -1
- package/examples/snes/templates/music_demo-data.asm +23 -0
- package/examples/snes/templates/music_demo.c +22 -4
- package/examples/snes/templates/platformer.c +4 -1
- package/examples/snes/templates/puzzle.c +4 -1
- package/package.json +1 -1
- package/src/cheats/gamegenie.js +0 -1
- package/src/cli/smoke.js +1 -3
- package/src/host/LibretroHost.js +69 -15
- package/src/host/chafa-render.js +2 -0
- package/src/host/dsp-state.js +2 -2
- package/src/host/gpgx-state.js +4 -0
- package/src/http/routes.js +1 -1
- package/src/mcp/server.js +1 -1
- package/src/mcp/state.js +36 -0
- package/src/mcp/tools/address-to-symbol.js +0 -1
- package/src/mcp/tools/art-loaders.js +1 -1
- package/src/mcp/tools/cart-parts.js +0 -1
- package/src/mcp/tools/classify-region.js +1 -1
- package/src/mcp/tools/diff-roms.js +1 -1
- package/src/mcp/tools/disasm-rebuild.js +1 -1
- package/src/mcp/tools/disasm.js +2 -3
- package/src/mcp/tools/find-references.js +1 -2
- package/src/mcp/tools/font-map.js +1 -1
- package/src/mcp/tools/index.js +0 -49
- package/src/mcp/tools/input-layout.js +0 -1
- package/src/mcp/tools/input.js +33 -3
- package/src/mcp/tools/lifecycle.js +14 -2
- package/src/mcp/tools/lospec.js +0 -19
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/platform-tools.js +4 -4
- package/src/mcp/tools/project.js +0 -2
- package/src/mcp/tools/reinject.js +0 -1
- package/src/mcp/tools/rom-id.js +2 -2
- package/src/mcp/tools/snippets.js +2 -2
- package/src/mcp/tools/sprite-pipeline.js +1 -2
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +29 -9
- package/src/mcp/tools/watch-memory.js +13 -3
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
- package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
- package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/d64.js +0 -1
- package/src/platforms/c64/sid.js +0 -2
- package/src/platforms/common/metasprite-adapters.js +1 -1
- package/src/platforms/common/metasprite-codegen.js +3 -3
- package/src/platforms/common/registers.js +5 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
- package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
- package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/image-to-tilemap.js +3 -0
- package/src/platforms/nes/lib/asm/famitone2.s +5 -1
- package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
- package/src/platforms/snes/brr.js +0 -2
- package/src/playtest/playtest.js +0 -7
- package/src/toolchains/asar/asar.js +0 -9
- package/src/toolchains/assemble-snippet.js +30 -12
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
- package/src/toolchains/common/reassemble.js +0 -1
- package/src/toolchains/common/sdk-cache.js +1 -1
- package/src/toolchains/genesis-c/genesis-c.js +5 -3
- package/src/toolchains/index.js +27 -3
- package/src/toolchains/parse-errors.js +78 -1
- package/src/toolchains/sdcc/preflight-lint.js +5 -1
- package/src/toolchains/sdcc/sdcc.js +1 -1
- package/src/toolchains/sjasm/sjasm.js +1 -1
- package/src/toolchains/snes-c/snes-c.js +2 -2
- package/src/toolchains/vasm68k/vasm68k.js +2 -4
- 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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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."),
|
|
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)."),
|
|
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
|
|
package/src/platforms/c64/d64.js
CHANGED
|
@@ -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
|
package/src/platforms/c64/sid.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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",
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 = $
|
|
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
|
package/src/playtest/playtest.js
CHANGED
|
@@ -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.
|