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.
- package/AGENTS.md +15 -4
- package/CHANGELOG.md +66 -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
|
@@ -25,11 +25,30 @@ import { runCa65, runLd65 } from "./cc65/cc65.js";
|
|
|
25
25
|
import { runAsar } from "./asar/asar.js";
|
|
26
26
|
import { runVasm68k } from "./vasm68k/vasm68k.js";
|
|
27
27
|
import { runSdasz80, runSdld, ihxToBin } from "./sdcc/sdcc.js";
|
|
28
|
-
import { runRgbasm, runRgblink
|
|
28
|
+
import { runRgbasm, runRgblink } from "./rgbds/rgbds.js";
|
|
29
|
+
import { parseBuildLog } from "./parse-errors.js";
|
|
29
30
|
|
|
30
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
32
|
const __dirname = path.dirname(__filename);
|
|
32
33
|
|
|
34
|
+
// Build a transparent snippet-assembly error: lead with the FIRST structured
|
|
35
|
+
// diagnostic (file:line: message) parsed out of the raw log — the same
|
|
36
|
+
// issues[]-style surfacing build() does — instead of dumping the unparsed
|
|
37
|
+
// assembler stdout and making the agent grep it. Full log still appended for
|
|
38
|
+
// fallback. `where` is e.g. "ca65" / "ld65 link" / "asar".
|
|
39
|
+
function asmError(where, log) {
|
|
40
|
+
const issues = parseBuildLog(log ?? "");
|
|
41
|
+
const first = issues.find((i) => i.severity === "error") ?? issues[0];
|
|
42
|
+
const headline = first
|
|
43
|
+
? `${first.file ? first.file + ":" : ""}${first.line ? first.line + ": " : ""}${first.message}`
|
|
44
|
+
: "no structured diagnostic found";
|
|
45
|
+
return new Error(
|
|
46
|
+
`assembleSnippet[${where}] failed: ${headline}` +
|
|
47
|
+
(issues.length > 1 ? ` (+${issues.length - 1} more issue(s))` : "") +
|
|
48
|
+
`\nFix the source line above. Full assembler log:\n${log ?? ""}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
/**
|
|
34
53
|
* CPU dialect → assembler dispatch. Keys are also the public API.
|
|
35
54
|
*/
|
|
@@ -102,7 +121,7 @@ async function assembleCa65({ origin, code }, cpu = "6502") {
|
|
|
102
121
|
|
|
103
122
|
const asm = await runCa65({ source });
|
|
104
123
|
if (!asm.object) {
|
|
105
|
-
throw
|
|
124
|
+
throw asmError("ca65", asm.log);
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
// Minimal linker config: one MEMORY block at `origin`, one SEGMENT
|
|
@@ -116,7 +135,7 @@ SEGMENTS { CODE: load = OUT, type = ro; }
|
|
|
116
135
|
linkerConfig: cfg,
|
|
117
136
|
});
|
|
118
137
|
if (!linked.binary) {
|
|
119
|
-
throw
|
|
138
|
+
throw asmError("ld65 link", linked.log);
|
|
120
139
|
}
|
|
121
140
|
return { bytes: linked.binary, log: (asm.log ?? "") + (linked.log ?? "") };
|
|
122
141
|
}
|
|
@@ -137,7 +156,7 @@ async function assembleAsar({ origin, code }) {
|
|
|
137
156
|
const source = `org ${hex24}\n${code}\n`;
|
|
138
157
|
const r = await runAsar({ source, baseRom, symbols: false });
|
|
139
158
|
if (!r.binary) {
|
|
140
|
-
throw
|
|
159
|
+
throw asmError("asar", r.log);
|
|
141
160
|
}
|
|
142
161
|
const bin = r.binary;
|
|
143
162
|
// Find first non-sentinel byte (asar may have written anywhere depending
|
|
@@ -148,7 +167,7 @@ async function assembleAsar({ origin, code }) {
|
|
|
148
167
|
if (bin[i] !== SENTINEL) { start = i; break; }
|
|
149
168
|
}
|
|
150
169
|
if (start < 0) {
|
|
151
|
-
throw new Error(`assembleSnippet[asar]: no bytes written (origin 0x${origin.toString(16)})
|
|
170
|
+
throw new Error(`assembleSnippet[asar]: no bytes written (origin 0x${origin.toString(16)}) — the source assembled but emitted nothing at this origin. Check the org address and that the code actually emits bytes.\nFull log:\n${r.log ?? ""}`);
|
|
152
171
|
}
|
|
153
172
|
let end = start + 1;
|
|
154
173
|
let sentinelRun = 0;
|
|
@@ -181,7 +200,7 @@ async function assembleVasm68k({ origin, code }) {
|
|
|
181
200
|
const source = `\torg $${origin.toString(16).toUpperCase()}\n${indented}\n`;
|
|
182
201
|
const r = await runVasm68k({ source, options: ["-Fbin"] });
|
|
183
202
|
if (!r.binary || (r.exitCode != null && r.exitCode !== 0)) {
|
|
184
|
-
throw
|
|
203
|
+
throw asmError("vasm68k", r.log);
|
|
185
204
|
}
|
|
186
205
|
return { bytes: r.binary, log: r.log ?? "" };
|
|
187
206
|
}
|
|
@@ -199,7 +218,7 @@ async function assembleSdcc({ origin, code }) {
|
|
|
199
218
|
const source = `\t.module snippet\n\t.area _CODE\n${indented}\n`;
|
|
200
219
|
const asm = await runSdasz80({ source });
|
|
201
220
|
if (!asm.rel) {
|
|
202
|
-
throw
|
|
221
|
+
throw asmError("sdasz80", asm.log);
|
|
203
222
|
}
|
|
204
223
|
// Minimal empty crt0 rel: just declares _HEADER0 (zero bytes) and is
|
|
205
224
|
// enough for sdld to satisfy its hardcoded crt0.rel dependency.
|
|
@@ -212,7 +231,7 @@ async function assembleSdcc({ origin, code }) {
|
|
|
212
231
|
libraries: [],
|
|
213
232
|
});
|
|
214
233
|
if (!linked.ihx) {
|
|
215
|
-
throw
|
|
234
|
+
throw asmError("sdld link", linked.log);
|
|
216
235
|
}
|
|
217
236
|
const padded = ihxToBin(linked.ihx, 0x10000, 0xFF);
|
|
218
237
|
// Find first and last non-FF byte from origin onwards. The snippet bytes
|
|
@@ -232,19 +251,18 @@ async function assembleSdcc({ origin, code }) {
|
|
|
232
251
|
async function assembleRgbds({ origin, code }) {
|
|
233
252
|
// rgbasm needs SECTION declarations to place code at an address.
|
|
234
253
|
const sectionAt = `$${origin.toString(16).toUpperCase()}`;
|
|
235
|
-
|
|
236
|
-
// Actually GB has no BANK[0] for fixed ROM — use ROM0 if origin < 0x4000.
|
|
254
|
+
// GB has no BANK[0] for fixed ROM — use ROM0 if origin < 0x4000, else ROMX BANK[1].
|
|
237
255
|
const useRom0 = origin < 0x4000;
|
|
238
256
|
const realSource = useRom0
|
|
239
257
|
? `SECTION "snippet", ROM0[${sectionAt}]\n${code}\n`
|
|
240
258
|
: `SECTION "snippet", ROMX[${sectionAt}], BANK[1]\n${code}\n`;
|
|
241
259
|
const asm = await runRgbasm({ source: realSource });
|
|
242
260
|
if (!asm.object) {
|
|
243
|
-
throw
|
|
261
|
+
throw asmError("rgbasm", asm.log);
|
|
244
262
|
}
|
|
245
263
|
const linked = await runRgblink({ objects: { "snippet.o": asm.object }, padValue: 0x00 });
|
|
246
264
|
if (!linked.binary) {
|
|
247
|
-
throw
|
|
265
|
+
throw asmError("rgblink link", linked.log);
|
|
248
266
|
}
|
|
249
267
|
// Slice from origin, find last non-zero byte.
|
|
250
268
|
const start = useRom0 ? origin : (origin - 0x4000 + 0x4000); // bank 1 lives at file 0x4000
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
# 3. Write CHR data from C at runtime: PPUADDR = 0x00; PPUDATA = byte; etc.
|
|
20
20
|
|
|
21
21
|
SYMBOLS {
|
|
22
|
-
|
|
22
|
+
# Stack is $0200 (512 B) so the top RAM page ($0700-$07FF) can be
|
|
23
|
+
# reserved below for a music driver's scratch RAM (FamiTone2 et al.),
|
|
24
|
+
# which needs a dedicated, page-aligned block that the C BSS/DATA
|
|
25
|
+
# region must NOT overlap. Tiny NROM scaffolds use far less than 512 B
|
|
26
|
+
# of stack, so this is safe; scaffolds with no music driver simply
|
|
27
|
+
# leave the reserved page unused.
|
|
28
|
+
__STACKSIZE__: type = weak, value = $0200;
|
|
23
29
|
}
|
|
24
30
|
MEMORY {
|
|
25
31
|
ZP: file = "", start = $0002, size = $001A, type = rw, define = yes;
|
|
@@ -37,6 +43,13 @@ MEMORY {
|
|
|
37
43
|
|
|
38
44
|
SRAM: file = "", start = $0500, size = __STACKSIZE__, define = yes;
|
|
39
45
|
|
|
46
|
+
# Reserved page for a sound-driver's RAM scratch ($0700-$07FF). The
|
|
47
|
+
# bundled FamiTone2 engine (music_demo scaffold) pins FT_BASE_ADR here
|
|
48
|
+
# so its ~90 bytes of channel/envelope state can't collide with the C
|
|
49
|
+
# BSS/DATA at $0300-$04FF — that collision silently clobbers the NMI's
|
|
50
|
+
# cached PPUCTRL and stalls rendering. Unused by non-music scaffolds.
|
|
51
|
+
SNDRAM: file = "", start = $0700, size = $0100, define = yes;
|
|
52
|
+
|
|
40
53
|
# BSS / DATA live in real RAM ($0300-$04FF, 512 bytes). NROM (mapper 0)
|
|
41
54
|
# with no battery has $6000-$7FFF UNMAPPED — reads return open bus.
|
|
42
55
|
# Putting BSS there means `int counter = 0;` reads garbage (intermittent
|
|
@@ -20,7 +20,6 @@ const hex2 = (b) => "$" + (b & 0xFF).toString(16).padStart(2, "0").toUpperCase()
|
|
|
20
20
|
// Label form across disassemblers: `L` + 4..8 hex digits (m68k targets can be
|
|
21
21
|
// 24-bit, e.g. an $E0FF00xx RAM mirror → LE0FF0080).
|
|
22
22
|
const LABEL_RE = /^(L[0-9A-Fa-f]{4,8}):\s*$/;
|
|
23
|
-
const LABEL_REF_RE = /\bL[0-9A-Fa-f]{4,8}\b/g;
|
|
24
23
|
|
|
25
24
|
/** Parse one disasm line into {label?, code?, bytes?[]}. */
|
|
26
25
|
function parseLine(line) {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// This module is platform-agnostic: each toolchain passes in how to hash its
|
|
18
18
|
// source, where its seed lives, and how to compile from source.
|
|
19
19
|
|
|
20
|
-
import { readFile, writeFile, mkdir
|
|
20
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
import os from "node:os";
|
|
@@ -127,7 +127,7 @@ async function compileSgdkRuntime(baseHeaders, cc1Options) {
|
|
|
127
127
|
for (const [k, v] of Object.entries(localHeaders)) {
|
|
128
128
|
if (/\.i80$/i.test(k)) z80Includes[path.basename(k)] = v;
|
|
129
129
|
}
|
|
130
|
-
async function loadI80From(dir,
|
|
130
|
+
async function loadI80From(dir, _rel = "") {
|
|
131
131
|
let ents;
|
|
132
132
|
try { ents = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
133
133
|
for (const e of ents) {
|
|
@@ -268,8 +268,10 @@ async function buildWithSgdk({ sources, headers, binaryIncludes, cc1Options, reb
|
|
|
268
268
|
// implicit decls, …) parsed into structured issues[]. The SGDK runtime is
|
|
269
269
|
// compiled WITHOUT these (sgdkCc1Options) — we can't fix SDK warnings and they'd
|
|
270
270
|
// bury the agent's own. -Wno-unused-parameter avoids the common `(void)hard`
|
|
271
|
-
// scaffold-param noise.
|
|
272
|
-
|
|
271
|
+
// scaffold-param noise. -Wno-main: SGDK MANDATES `int main(bool hardReset)`
|
|
272
|
+
// (sys.c declares it and calls main(TRUE)/main(FALSE)); GCC's -Wmain objects to
|
|
273
|
+
// the non-standard signature, but it's REQUIRED here, not a bug — so silence it.
|
|
274
|
+
const userCc1Options = [...sgdkCc1Options, "-Wall", "-Wextra", "-Wno-unused-parameter", "-Wno-main"];
|
|
273
275
|
|
|
274
276
|
// ── Stage A: gather SGDK headers (visible to tcc via tcc-style flat mount) ──
|
|
275
277
|
// cc1's -iquote /work picks up sibling files mounted alongside main.c.
|
package/src/toolchains/index.js
CHANGED
|
@@ -117,6 +117,27 @@ const PLATFORM_DEFAULT_LANGUAGE = {
|
|
|
117
117
|
msx: "c",
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Order build issues by DANGER so the agent reads the lethal ones first:
|
|
122
|
+
* critical (crash-class, e.g. the uint8 infinite-loop) → error → warning → info.
|
|
123
|
+
* Stable within a rank (preserves source order / file:line). Pure; no dedup.
|
|
124
|
+
* @param {Array<{severity?:string, critical?:boolean}>} issues
|
|
125
|
+
* @returns {Array} the same issues, ranked
|
|
126
|
+
*/
|
|
127
|
+
export function rankIssues(issues) {
|
|
128
|
+
const rank = (i) =>
|
|
129
|
+
i?.critical ? 0
|
|
130
|
+
: i?.severity === "error" ? 1
|
|
131
|
+
: i?.severity === "warning" ? 2
|
|
132
|
+
: 3;
|
|
133
|
+
// map→sort→unmap keeps it stable (Array.prototype.sort is stable in V8, but
|
|
134
|
+
// tie-break on original index to be explicit and portable).
|
|
135
|
+
return issues
|
|
136
|
+
.map((issue, idx) => ({ issue, idx }))
|
|
137
|
+
.sort((a, b) => rank(a.issue) - rank(b.issue) || a.idx - b.idx)
|
|
138
|
+
.map((x) => x.issue);
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
/**
|
|
121
142
|
* Public API for the platforms tool to discover the language matrix.
|
|
122
143
|
* Returns `{defaultLanguage, languages: [{language, toolchain, available, note?}]}`.
|
|
@@ -835,10 +856,13 @@ export async function buildForPlatform(args) {
|
|
|
835
856
|
r.log += "\n--- MSX ROM header present (\"AB\") ---";
|
|
836
857
|
}
|
|
837
858
|
}
|
|
838
|
-
// Combine lint warnings with parsed build log
|
|
839
|
-
//
|
|
859
|
+
// Combine lint warnings with parsed build log, then RANK so an agent
|
|
860
|
+
// triaging issues[] sees the dangerous ones first: crash-class (critical)
|
|
861
|
+
// → errors → plain warnings. Without this, a "WILL HANG" infinite-loop
|
|
862
|
+
// warning sits among unused-variable noise and gets skipped (the exact
|
|
863
|
+
// "agent missed the warning, hit the crash 100 functions later" failure).
|
|
840
864
|
const buildIssues = parseBuildLog(r.log);
|
|
841
|
-
const issues = [...lintIssues, ...buildIssues];
|
|
865
|
+
const issues = rankIssues([...lintIssues, ...buildIssues]);
|
|
842
866
|
return {
|
|
843
867
|
ok: r.exitCode === 0 && binary !== null,
|
|
844
868
|
binary,
|
|
@@ -32,6 +32,9 @@ export function parseBuildLog(log) {
|
|
|
32
32
|
const baseStage = stage.split(/\s|\(/)[0].toLowerCase();
|
|
33
33
|
if (/^cc65$|^ca65$|^ld65$/.test(baseStage)) {
|
|
34
34
|
issues.push(...parseCc65Like(text, baseStage));
|
|
35
|
+
// ld65's linker-level errors (segment-missing / memory-overflow) carry no
|
|
36
|
+
// file:line and slip past parseCc65Like — pick them up too.
|
|
37
|
+
if (baseStage === "ld65") issues.push(...parseLd65Linker(text, baseStage));
|
|
35
38
|
} else if (/^dasm$/.test(baseStage)) {
|
|
36
39
|
issues.push(...parseDasm(text));
|
|
37
40
|
} else if (/^asar$/.test(baseStage)) {
|
|
@@ -68,6 +71,7 @@ export function parseBuildLog(log) {
|
|
|
68
71
|
// failure). Include vasm + sdcc + wla, which the old fallback omitted.
|
|
69
72
|
const tag = baseStage || "unknown";
|
|
70
73
|
issues.push(...parseCc65Like(text, tag));
|
|
74
|
+
issues.push(...parseLd65Linker(text, tag));
|
|
71
75
|
issues.push(...parseSdcc(text, tag));
|
|
72
76
|
issues.push(...parseDasm(text));
|
|
73
77
|
issues.push(...parseAsar(text, tag));
|
|
@@ -109,6 +113,50 @@ function splitByStage(log) {
|
|
|
109
113
|
return stages;
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
// ld65 LINKER diagnostics have NO file:line — parseCc65Like (which requires a
|
|
117
|
+
// `file:line:` lead) misses them entirely, so a failed link returned issues[]
|
|
118
|
+
// EMPTY even though the real error sat in the log. The common ones on a
|
|
119
|
+
// mis-wired NES rebuild (wrong/absent linker config for the project's segments):
|
|
120
|
+
// ld65: Warning: Segment 'HEADER' does not exist
|
|
121
|
+
// ld65: Error: Memory area overflow in 'ROM0', segment 'CODE' (6 bytes)
|
|
122
|
+
// Error: Cannot generate most of the files due to memory area overflow
|
|
123
|
+
// They appear either bare or with an `ld65:`/`ld65.exe:` tool prefix. We add a
|
|
124
|
+
// short hint on the segment/overflow case (the linker config doesn't match the
|
|
125
|
+
// project's segments — the exact CHR-ROM-vs-CHR-RAM mismatch agents hit).
|
|
126
|
+
function parseLd65Linker(text, stage) {
|
|
127
|
+
const out = [];
|
|
128
|
+
const re = /^(?:ld65(?:\.exe)?:\s*)?(?<sev>Error|Warning):\s*(?<msg>.+)$/gm;
|
|
129
|
+
let m;
|
|
130
|
+
while ((m = re.exec(text))) {
|
|
131
|
+
const msg = m.groups.msg.trim().replace(/\x1b\[[0-9;]*m/g, "");
|
|
132
|
+
// Skip the file:line form — parseCc65Like already owns those (and a leading
|
|
133
|
+
// path would have been consumed as the message here, doubling the issue).
|
|
134
|
+
if (/^[^\s:]+:\d+:/.test(msg)) continue;
|
|
135
|
+
// Actionable hint in a SEPARATE field (matching GNU ld / sdld), not glued
|
|
136
|
+
// onto the message — two common ld65 link failures:
|
|
137
|
+
let hint;
|
|
138
|
+
if (/does not exist|overflow|Cannot generate/i.test(msg)) {
|
|
139
|
+
hint =
|
|
140
|
+
"The linker config's segments don't match the project. For an NROM " +
|
|
141
|
+
"rebuild use build({inesHeader:{...}}) or linkerConfig:'chr-rom'; otherwise " +
|
|
142
|
+
"check that your .cfg's SEGMENTS match the .segment names in your source.";
|
|
143
|
+
} else if (/unresolved external|undefined symbol/i.test(msg)) {
|
|
144
|
+
const sym = msg.match(/['"`]?(?<s>[A-Za-z_]\w*)['"`]?\s*$/)?.groups?.s;
|
|
145
|
+
hint =
|
|
146
|
+
`${sym ? `\`${sym}'` : "That symbol"} is referenced but never defined or linked. ` +
|
|
147
|
+
"Add the source/object that defines it (or the right cc65 lib for this platform) " +
|
|
148
|
+
"to your build, or check for a typo.";
|
|
149
|
+
}
|
|
150
|
+
out.push({
|
|
151
|
+
severity: m.groups.sev.toLowerCase() === "error" ? "error" : "warning",
|
|
152
|
+
message: msg,
|
|
153
|
+
stage: stage || "ld65",
|
|
154
|
+
...(hint ? { hint } : {}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
112
160
|
// cc65, ca65, ld65 all use the gcc-style `file:line:col?: severity: message`.
|
|
113
161
|
// Example:
|
|
114
162
|
// /work/main.s:12: Error: Cannot open include file 'longbranch.mac'
|
|
@@ -173,10 +221,25 @@ function parseSdcc(text, stage) {
|
|
|
173
221
|
// An "Undefined Global" is effectively an error even though ASlink labels it
|
|
174
222
|
// a warning — the ROM won't run. Promote it so the agent treats it as fatal.
|
|
175
223
|
const isUndef = /undefined\s+global/i.test(msg);
|
|
224
|
+
// Give the same actionable hint GNU ld's undefined-reference path gets, so
|
|
225
|
+
// the SDCC platforms (GB/GBC/SMS/GG/MSX) reach parity on the single most
|
|
226
|
+
// common link failure (forgot to include the source/runtime that defines it).
|
|
227
|
+
// SDCC mangles C symbols with a leading '_' — show the C name too.
|
|
228
|
+
let hint;
|
|
229
|
+
if (isUndef) {
|
|
230
|
+
const sym = msg.match(/global\s+['"]?(?<s>\w+)['"]?/i)?.groups?.s;
|
|
231
|
+
const cName = sym && sym.startsWith("_") ? sym.slice(1) : sym;
|
|
232
|
+
hint =
|
|
233
|
+
`${sym ? `\`${sym}'` : "That symbol"} is called but never defined or linked` +
|
|
234
|
+
`${cName && cName !== sym ? ` (the C name is \`${cName}'; SDCC prefixes an underscore)` : ""}. ` +
|
|
235
|
+
"Add the source file (or runtime/library) that defines it to your build's sources/includes, " +
|
|
236
|
+
"or check for a typo in the name.";
|
|
237
|
+
}
|
|
176
238
|
out.push({
|
|
177
239
|
severity: lm.groups.sev === "Error" || isUndef ? "error" : "warning",
|
|
178
240
|
message: "linker: " + msg,
|
|
179
241
|
stage: "sdld",
|
|
242
|
+
...(hint ? { hint } : {}),
|
|
180
243
|
});
|
|
181
244
|
}
|
|
182
245
|
return out;
|
|
@@ -268,12 +331,26 @@ function parseWla(text, stage = "wla") {
|
|
|
268
331
|
const reLink = /^(?<obj>\S+\.obj):\s*(?<file>[^\s:]+):(?<line>\d+):\s*(?<phase>[A-Z_]+):\s*(?<msg>.+)$/gm;
|
|
269
332
|
let m;
|
|
270
333
|
while ((m = reLink.exec(text))) {
|
|
334
|
+
const rawMsg = m.groups.msg.trim();
|
|
335
|
+
// An "unknown label" reference is SNES's undefined-symbol failure — give it
|
|
336
|
+
// the same actionable hint ld65/sdld/GNU ld carry, so SNES reaches parity.
|
|
337
|
+
let hint;
|
|
338
|
+
const lbl = /unknown label\s+["'`]?(?<l>[A-Za-z_]\w*)/i.exec(rawMsg)?.groups?.l;
|
|
339
|
+
if (lbl || /reference to an unknown/i.test(rawMsg)) {
|
|
340
|
+
hint =
|
|
341
|
+
`${lbl ? `\`${lbl}'` : "That label"} is referenced but never defined or linked. ` +
|
|
342
|
+
"Add the source/object that defines it (or the right PVSnesLib runtime) to your build, " +
|
|
343
|
+
"or check for a typo.";
|
|
344
|
+
}
|
|
271
345
|
out.push({
|
|
272
346
|
severity: "error",
|
|
273
347
|
file: m.groups.file,
|
|
274
348
|
line: parseInt(m.groups.line, 10),
|
|
275
|
-
|
|
349
|
+
// Keep the wla internal phase name (FIX_REFERENCES, etc.) out of the
|
|
350
|
+
// agent-facing message — it's noise; the message + hint say what to do.
|
|
351
|
+
message: rawMsg,
|
|
276
352
|
stage: stage === "wla" ? "wlalink" : stage,
|
|
353
|
+
...(hint ? { hint } : {}),
|
|
277
354
|
});
|
|
278
355
|
}
|
|
279
356
|
// wla-65816 assembler: "<file>:<line>: ERROR|WARNING: <message>"
|
|
@@ -136,10 +136,14 @@ export function lintSdccSource(source, file = "main.c", opts = {}) {
|
|
|
136
136
|
if (bound !== null && bound > 255) {
|
|
137
137
|
issues.push({
|
|
138
138
|
severity: "warning",
|
|
139
|
+
// CRASH-CLASS: not cosmetic — this loop never exits and hangs the
|
|
140
|
+
// game. `critical` lifts it above ordinary warnings so an agent
|
|
141
|
+
// triaging issues[] can't miss it among unused-variable noise.
|
|
142
|
+
critical: true,
|
|
139
143
|
file,
|
|
140
144
|
line: i + 1,
|
|
141
145
|
stage: "lint",
|
|
142
|
-
message: `uint8 loop counter '${counter}' with bound ${bound} (> 255) — infinite loop`,
|
|
146
|
+
message: `WILL HANG: uint8 loop counter '${counter}' with bound ${bound} (> 255) — infinite loop`,
|
|
143
147
|
details: `A u8/uint8_t/char counter can never reach ${bound}, so this loop never exits and all code after it is dead. ${portLabel} gives no warning. Declare '${counter}' as uint16_t. See GB TROUBLESHOOTING § uint8 loop-bound trap.`,
|
|
144
148
|
ref: "uint8-loop-bound",
|
|
145
149
|
});
|
|
@@ -56,7 +56,7 @@ export const SDCC_PORTS = {
|
|
|
56
56
|
msx: { marg: "z80", libDir: "z80" },
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
import { runIsolated, textFile,
|
|
59
|
+
import { runIsolated, textFile, getOutputText } from "../_worker/run.js";
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Tag the abort/crash log with the SDCC-flavored hint pointing at the
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// #include that generated .h. Built by scripts/build-sjasm.sh.
|
|
6
6
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { existsSync
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -288,7 +288,7 @@ async function buildWithPvSnesLib({ sources, headers, tccOptions, wlaOptions, bi
|
|
|
288
288
|
* Minimum-viable path (R16 original behavior). No PVSnesLib runtime;
|
|
289
289
|
* bundled original crt0.asm + hdr.asm support a bare `int main()`.
|
|
290
290
|
*/
|
|
291
|
-
async function buildMinimal({ sources, headers, tccOptions, wlaOptions,
|
|
291
|
+
async function buildMinimal({ sources, headers, tccOptions, wlaOptions, _binaryIncludes = {} }) {
|
|
292
292
|
let log = "";
|
|
293
293
|
const hdrAsm = await readFile(path.join(MINIMAL_LIB_DIR, "hdr.asm"), "utf-8");
|
|
294
294
|
const crt0Asm = await readFile(path.join(MINIMAL_LIB_DIR, "crt0.asm"), "utf-8");
|
|
@@ -380,7 +380,7 @@ async function buildMinimal({ sources, headers, tccOptions, wlaOptions, binaryIn
|
|
|
380
380
|
let _headerCache = null;
|
|
381
381
|
async function loadPvSnesLibHeaders() {
|
|
382
382
|
if (_headerCache) return _headerCache;
|
|
383
|
-
const { readdir
|
|
383
|
+
const { readdir } = await import("node:fs/promises");
|
|
384
384
|
const out = {};
|
|
385
385
|
/**
|
|
386
386
|
* @param {string} dir
|
|
@@ -49,10 +49,8 @@ function vasm68kPreflight(source) {
|
|
|
49
49
|
const issues = [];
|
|
50
50
|
|
|
51
51
|
// Check 1: source includes an org $00000000 or starts at $00 implicitly.
|
|
52
|
-
// vasm68k defaults to $00000000 if no org is given, so missing org is fine
|
|
53
|
-
//
|
|
54
|
-
// vector table, the cart is unbootable.
|
|
55
|
-
const hasOrg0 = /\borg\s+\$0+\b/i.test(source) || /\borg\s+0\b/i.test(source);
|
|
52
|
+
// vasm68k defaults to $00000000 if no org is given, so a missing org is fine;
|
|
53
|
+
// what actually breaks boot is a missing reset vector, which we check below.
|
|
56
54
|
const hasReset = /\bdc\.l\s+(\$00FF[E-F][0-9A-F]{3}|_reset|reset)\b/i.test(source);
|
|
57
55
|
if (!hasReset) {
|
|
58
56
|
issues.push(
|
|
@@ -15,7 +15,7 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
|
|
18
|
-
import { runIsolated, textFile, binaryFile, getOutputBytes
|
|
18
|
+
import { runIsolated, textFile, binaryFile, getOutputBytes } from "../_worker/run.js";
|
|
19
19
|
|
|
20
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
21
|
const __dirname = path.dirname(__filename);
|