romdevtools 0.71.1 → 0.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/AGENTS.md +19 -16
  2. package/CHANGELOG.md +175 -0
  3. package/README.md +5 -5
  4. package/examples/README.md +1 -0
  5. package/examples/gametank/templates/gt_draw.h +91 -0
  6. package/examples/gametank/templates/gt_hud.h +89 -0
  7. package/examples/gametank/templates/gt_palette.h +59 -0
  8. package/examples/gametank/templates/gt_sound.h +90 -0
  9. package/examples/gametank/templates/gt_sprites.h +53 -0
  10. package/examples/gametank/templates/platformer.c +318 -0
  11. package/examples/gametank/templates/puzzle.c +295 -0
  12. package/examples/gametank/templates/racing.c +139 -0
  13. package/examples/gametank/templates/shmup.c +277 -0
  14. package/examples/gametank/templates/sports.c +119 -0
  15. package/package.json +23 -22
  16. package/src/cores/capabilities.js +24 -0
  17. package/src/cores/registry.js +7 -1
  18. package/src/host/LibretroHost.js +90 -17
  19. package/src/host/callbacks.js +20 -2
  20. package/src/host/cpu-state.js +17 -0
  21. package/src/host/gametank-acp-state.js +53 -0
  22. package/src/http/skill-doc.js +10 -6
  23. package/src/mcp/tools/audio.js +1 -1
  24. package/src/mcp/tools/cart-parts.js +76 -0
  25. package/src/mcp/tools/lifecycle.js +1 -1
  26. package/src/mcp/tools/platform-tools.js +9 -1
  27. package/src/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
  28. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
  29. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
  30. package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
  31. package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
  32. package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
  33. package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
  34. package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
  35. package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
  36. package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
  37. package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
  38. package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
  39. package/src/platforms/gametank/lib/gt/banking.c +29 -0
  40. package/src/platforms/gametank/lib/gt/banking.h +8 -0
  41. package/src/platforms/gametank/lib/gt/banking2.s +41 -0
  42. package/src/platforms/gametank/lib/gt/crt0.s +102 -0
  43. package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
  44. package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
  45. package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
  46. package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
  47. package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
  48. package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
  49. package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
  50. package/src/platforms/gametank/lib/gt/gametank.c +12 -0
  51. package/src/platforms/gametank/lib/gt/gametank.h +80 -0
  52. package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
  53. package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
  54. package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
  55. package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
  56. package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
  57. package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
  58. package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
  59. package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
  60. package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
  61. package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
  62. package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
  63. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
  64. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
  65. package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
  66. package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
  67. package/src/platforms/gametank/lib/gt/init.c +9 -0
  68. package/src/platforms/gametank/lib/gt/input.c +26 -0
  69. package/src/platforms/gametank/lib/gt/input.h +20 -0
  70. package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
  71. package/src/platforms/gametank/lib/gt/vectors.s +14 -0
  72. package/src/platforms/gametank/lib/gt/wait.s +53 -0
  73. package/src/playtest/playtest.js +174 -26
  74. package/src/playtest/resampler/build.sh +19 -0
  75. package/src/playtest/resampler/index.mjs +75 -0
  76. package/src/playtest/resampler/resampler.c +129 -0
  77. package/src/playtest/resampler/resampler.mjs +2 -0
  78. package/src/playtest/resampler/resampler.wasm +0 -0
  79. package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
  80. package/src/toolchains/asar/asar.js +10 -15
  81. package/src/toolchains/cc65/cc65.js +82 -92
  82. package/src/toolchains/cc65/da65.js +12 -17
  83. package/src/toolchains/cc65/preset-resolver.js +13 -2
  84. package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
  85. package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
  86. package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
  87. package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
  88. package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
  89. package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
  90. package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
  91. package/src/toolchains/common/c-build.js +109 -0
  92. package/src/toolchains/common/gcc-toolchain.js +164 -0
  93. package/src/toolchains/common/wasm-tool.js +101 -0
  94. package/src/toolchains/dasm/dasm.js +12 -18
  95. package/src/toolchains/gba-c/gba-c.js +253 -305
  96. package/src/toolchains/genesis-c/genesis-c.js +196 -225
  97. package/src/toolchains/index.js +58 -4
  98. package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
  99. package/src/toolchains/mips-c/mips-c.js +68 -78
  100. package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
  101. package/src/toolchains/rgbds/rgbds.js +7 -19
  102. package/src/toolchains/sdcc/sdcc.js +35 -26
  103. package/src/toolchains/sh-c/sh-c.js +44 -52
  104. package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
  105. package/src/toolchains/sjasm/sjasm.js +10 -14
  106. package/src/toolchains/snes-c/snes-c.js +125 -150
  107. package/src/toolchains/tcc816/tcc816.js +10 -15
  108. package/src/toolchains/vasm68k/vasm68k.js +11 -16
  109. package/src/toolchains/wladx/wladx.js +5 -17
  110. package/src/toolchains/z80/binutils.js +5 -11
@@ -1,216 +1,67 @@
1
1
  // arm-none-eabi-gcc — WASM toolchain wrappers for GBA C builds.
2
2
  //
3
- // Mirrors src/toolchains/m68k-elf-gcc/gcc.js. The GBA target uses
4
- // ARM7TDMI in Thumb-interwork mode by default (matches libgba's
5
- // expectations).
3
+ // The full pipeline:
4
+ // runCc1arm({source, headers, options}) ARM assembly text (.s)
5
+ // runArmAs({source, includes}).o ELF object
6
+ // runArmLd({objects, linkScript}) → linked .elf (+ map)
7
+ // runArmObjcopy({elf}) → raw .gba ROM
6
8
  //
7
- // Pipeline:
8
- // runCc1arm({source, headers, options}) → ARM assembly text (.s)
9
- // runArmAs({asmSource, includes}) → .o ELF object
10
- // runArmLd({objects, linkScript}) → linked .elf
11
- // runArmObjcopy({elf}) → raw .gba ROM
9
+ // 0.81.0: the 4 stages come from the shared makeGccToolchain() factory
10
+ // (common/gcc-toolchain.js); this file is just the ARM config + thin re-exports
11
+ // so existing call sites keep their runArm*/runCc1arm names.
12
12
  //
13
- // Each step runs as a WASM module through the worker pool (R12
14
- // crash isolation). The WASM tools live under wasm/ (cc1-arm.{mjs,wasm}
15
- // etc.). emcc emits ESM (EXPORT_ES6=1) so we use .mjs extensions.
16
-
13
+ // The WASM glue ships in romdev-platform-gba (155MB, incl. the 135MB cc1-arm);
14
+ // resolution is lazy + memoized, so booting never loads it unless a GBA C ROM
15
+ // is actually built. ARMv4T (arm7tdmi), thumb-interwork.
17
16
  import { fileURLToPath } from "node:url";
18
- import { existsSync } from "node:fs";
19
17
  import path from "node:path";
20
18
 
21
- import { runIsolated, textFile, binaryFile, getOutputBytes, getOutputText } from "../_worker/run.js";
19
+ import { makeGccToolchain } from "../common/gcc-toolchain.js";
22
20
 
23
- const __filename = fileURLToPath(import.meta.url);
24
- const __dirname = path.dirname(__filename);
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
22
 
26
- // arm-none-eabi-gcc's WASM ships in romdev-platform-gba (the GBA platform is
27
- // the only consumer). Resolve each tool's glue from that package; fall back to
28
- // a local copy under src/ if present (transition / dev). emcc emits ESM
29
- // (EXPORT_ES6=1) so the glue uses .mjs extensions. The package is a hard dep
30
- // of romdev.
31
- function resolveArmGlue(file) {
32
- try {
33
- const u = import.meta.resolve("romdev-platform-gba");
34
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
35
- if (existsSync(p)) return p;
36
- } catch { /* package not resolvable — fall through to local */ }
37
- const local = path.join(__dirname, "wasm", file);
38
- if (existsSync(local)) return local;
39
- throw new Error(`arm-none-eabi-gcc WASM (${file}) not found — install romdev-platform-gba`);
40
- }
23
+ const ARM_FLAGS = ["-mcpu=arm7tdmi", "-mthumb-interwork"];
41
24
 
42
- // Lazy + memoized per tool: resolve only on the first GBA-C build that uses
43
- // each tool, not at module load — so booting the server never touches this
44
- // (155MB, incl. the 135MB cc1-arm) package unless a GBA C ROM is actually built.
45
- const _glue = {};
46
- const armGlue = (file) => (_glue[file] ??= resolveArmGlue(file));
25
+ const { runCc1, runAs, runLd, runObjcopy } = makeGccToolchain({
26
+ pkg: "romdev-platform-gba",
27
+ localDir: __dirname,
28
+ label: "arm-none-eabi-gcc",
29
+ glue: {
30
+ cc1: "cc1-arm.mjs",
31
+ as: "arm-none-eabi-as.mjs",
32
+ ld: "arm-none-eabi-ld.mjs",
33
+ objcopy: "arm-none-eabi-objcopy.mjs",
34
+ },
35
+ cc1Flags: ARM_FLAGS,
36
+ asFlags: ARM_FLAGS,
37
+ ldScriptName: "gba.ld",
38
+ outputName: "main.gba",
39
+ });
47
40
 
48
41
  /**
49
42
  * Compile a C source to ARM assembly via cc1.
50
- *
51
- * @param {Object} args
52
- * @param {string} args.source main C source text
53
- * @param {Record<string, string>} [args.headers] virtual headers visible via -I/work
54
- * @param {string[]} [args.options] extra cc1 flags
43
+ * @param {{source:string, headers?:Record<string,string>, options?:string[]}} args
55
44
  * @returns {Promise<{log:string, exitCode:number, asmSource:string|null, crash?:any}>}
56
45
  */
57
- export async function runCc1arm(args) {
58
- const { source, options = [] } = args;
59
- const headers = args.headers ?? {};
60
- /** @type {import("../_worker/run.js").InputFile[]} */
61
- const inputFiles = [textFile("/work/main.c", source)];
62
- for (const [name, content] of Object.entries(headers)) {
63
- inputFiles.push(textFile("/work/" + name, content));
64
- }
65
- const argv = [
66
- // GBA = ARM7TDMI. Thumb-interwork allows mixed ARM/Thumb code.
67
- "-mcpu=arm7tdmi",
68
- "-mthumb-interwork",
69
- "-iquote", "/work",
70
- "-I", "/work",
71
- ...options,
72
- "/work/main.c",
73
- "-o", "/work/main.s",
74
- ];
75
- const r = await runIsolated({
76
- gluePath: armGlue("cc1-arm.mjs"),
77
- argv,
78
- inputFiles,
79
- outputFiles: [{ vfsPath: "/work/main.s", encoding: "utf8" }],
80
- });
81
- return {
82
- log: r.log,
83
- exitCode: r.exitCode,
84
- asmSource: getOutputText(r, "/work/main.s") || null,
85
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
86
- };
87
- }
46
+ export const runCc1arm = runCc1;
88
47
 
89
48
  /**
90
49
  * Assemble ARM assembly with arm-none-eabi-as.
91
- *
92
- * @param {Object} args
93
- * @param {string} args.source assembly source text
94
- * @param {Record<string, string>} [args.includes]
95
- * @param {Record<string, Uint8Array>} [args.binaryIncludes]
96
- * @param {string[]} [args.options]
50
+ * @param {{source:string, includes?:Record<string,string>, binaryIncludes?:Record<string,Uint8Array>, options?:string[]}} args
97
51
  * @returns {Promise<{log:string, exitCode:number, object:Uint8Array|null, crash?:any}>}
98
52
  */
99
- export async function runArmAs(args) {
100
- const { source, options = [] } = args;
101
- const includes = args.includes ?? {};
102
- const binaryIncludes = args.binaryIncludes ?? {};
103
- /** @type {import("../_worker/run.js").InputFile[]} */
104
- const inputFiles = [textFile("/work/main.s", source)];
105
- for (const [name, content] of Object.entries(includes)) {
106
- inputFiles.push(textFile("/work/" + name, content));
107
- }
108
- for (const [name, bytes] of Object.entries(binaryIncludes)) {
109
- inputFiles.push(binaryFile("/work/" + name, bytes));
110
- }
111
- const argv = [
112
- "-mcpu=arm7tdmi",
113
- "-mthumb-interwork",
114
- "-I", "/work",
115
- ...options,
116
- "/work/main.s",
117
- "-o", "/work/main.o",
118
- ];
119
- const r = await runIsolated({
120
- gluePath: armGlue("arm-none-eabi-as.mjs"),
121
- argv,
122
- inputFiles,
123
- outputFiles: [{ vfsPath: "/work/main.o", encoding: "base64" }],
124
- });
125
- return {
126
- log: r.log,
127
- exitCode: r.exitCode,
128
- object: getOutputBytes(r, "/work/main.o"),
129
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
130
- };
131
- }
53
+ export const runArmAs = runAs;
132
54
 
133
55
  /**
134
- * Link ARM object files into an ELF executable.
135
- *
136
- * @param {Object} args
137
- * @param {Record<string, Uint8Array>} args.objects
138
- * @param {string} args.linkScript linker script contents (lnkscript / gba.ld)
139
- * @param {string[]} [args.libraryPaths]
140
- * @param {string[]} [args.libraries]
141
- * @param {Record<string, Uint8Array>} [args.archives]
142
- * @param {string[]} [args.options]
143
- * @returns {Promise<{log:string, exitCode:number, elf:Uint8Array|null, crash?:any}>}
56
+ * Link ARM object files into an ELF executable (+ linker map).
57
+ * @param {{objects:Record<string,Uint8Array>, linkScript:string, libraries?:string[], libraryPaths?:string[], archives?:Record<string,Uint8Array>, options?:string[]}} args
58
+ * @returns {Promise<{log:string, exitCode:number, elf:Uint8Array|null, map:string|null, crash?:any}>}
144
59
  */
145
- export async function runArmLd(args) {
146
- const { objects, linkScript, libraries = [], libraryPaths = [], options = [] } = args;
147
- const archives = args.archives ?? {};
148
- /** @type {import("../_worker/run.js").InputFile[]} */
149
- const inputFiles = [textFile("/work/gba.ld", linkScript)];
150
- for (const [name, bytes] of Object.entries(objects)) {
151
- inputFiles.push(binaryFile("/work/" + name, bytes));
152
- }
153
- for (const [name, bytes] of Object.entries(archives)) {
154
- inputFiles.push(binaryFile("/work/" + name, bytes));
155
- }
156
- const argv = [
157
- "-T", "/work/gba.ld",
158
- "-o", "/work/main.elf",
159
- // Emit a GNU ld map so symbols({op:'resolve'/'lookup'/'list'/'map'}) can turn
160
- // a C global's name into an address (parsed by gnu-ld-map.js) — same as the
161
- // m68k/Genesis path. Without this the GBA build returns no symbol table.
162
- "-Map=/work/main.map",
163
- ...libraryPaths.flatMap((p) => ["-L", p]),
164
- ...Object.keys(objects).map((n) => "/work/" + n),
165
- ...libraries.map((l) => `-l${l}`),
166
- ...options,
167
- ];
168
- const r = await runIsolated({
169
- gluePath: armGlue("arm-none-eabi-ld.mjs"),
170
- argv,
171
- inputFiles,
172
- outputFiles: [
173
- { vfsPath: "/work/main.elf", encoding: "base64" },
174
- { vfsPath: "/work/main.map", encoding: "utf8" },
175
- ],
176
- });
177
- return {
178
- log: r.log,
179
- exitCode: r.exitCode,
180
- elf: getOutputBytes(r, "/work/main.elf"),
181
- map: getOutputText(r, "/work/main.map") || null,
182
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
183
- };
184
- }
60
+ export const runArmLd = runLd;
185
61
 
186
62
  /**
187
63
  * Strip an ELF down to a raw .gba ROM.
188
- *
189
- * @param {Object} args
190
- * @param {Uint8Array} args.elf
191
- * @param {string[]} [args.options]
64
+ * @param {{elf:Uint8Array, options?:string[]}} args
192
65
  * @returns {Promise<{log:string, exitCode:number, binary:Uint8Array|null, crash?:any}>}
193
66
  */
194
- export async function runArmObjcopy(args) {
195
- const { elf, options = [] } = args;
196
- /** @type {import("../_worker/run.js").InputFile[]} */
197
- const inputFiles = [binaryFile("/work/main.elf", elf)];
198
- const argv = [
199
- "-O", "binary",
200
- ...options,
201
- "/work/main.elf",
202
- "/work/main.gba",
203
- ];
204
- const r = await runIsolated({
205
- gluePath: armGlue("arm-none-eabi-objcopy.mjs"),
206
- argv,
207
- inputFiles,
208
- outputFiles: [{ vfsPath: "/work/main.gba", encoding: "base64" }],
209
- });
210
- return {
211
- log: r.log,
212
- exitCode: r.exitCode,
213
- binary: getOutputBytes(r, "/work/main.gba"),
214
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
215
- };
216
- }
67
+ export const runArmObjcopy = runObjcopy;
@@ -5,30 +5,25 @@
5
5
  // directives.
6
6
 
7
7
  import { fileURLToPath } from "node:url";
8
- import { existsSync } from "node:fs";
9
8
  import path from "node:path";
10
9
 
11
10
  import { runIsolated, textFile, binaryFile, getOutputBytes, getOutputText } from "../_worker/run.js";
11
+ import { resolveGlueFile } from "../common/wasm-tool.js";
12
12
 
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = path.dirname(__filename);
15
15
 
16
16
  // asar's WASM ships in romdev-platform-snes (the SNES platform is the only
17
- // consumer). Resolve from that package; fall back to a local copy under src/
18
- // if present (transition / dev). The package is a hard dep of romdev.
19
- function resolveAsarGlue() {
20
- try {
21
- const u = import.meta.resolve("romdev-platform-snes");
22
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", "asar.js");
23
- if (existsSync(p)) return p;
24
- } catch { /* package not resolvable — fall through to local */ }
25
- const local = path.join(__dirname, "wasm", "asar.js");
26
- if (existsSync(local)) return local;
27
- throw new Error("asar WASM not found — install romdev-platform-snes");
28
- }
29
- // Lazy + memoized: resolve only on the first asar (SNES asm) build, not at boot.
17
+ // consumer); a local src/ copy is the dev fallback. Lazy + memoized: resolve
18
+ // only on the first asar (SNES asm) build, not at boot.
30
19
  let _gluePath;
31
- const gluePath = () => (_gluePath ??= resolveAsarGlue());
20
+ const gluePath = () =>
21
+ (_gluePath ??= resolveGlueFile({
22
+ pkg: "romdev-platform-snes",
23
+ file: "asar.js",
24
+ localDir: __dirname,
25
+ label: "asar",
26
+ }));
32
27
 
33
28
  /**
34
29
  * Assemble an asar source.
@@ -15,6 +15,9 @@ import { fileURLToPath } from "node:url";
15
15
  import { existsSync } from "node:fs";
16
16
  import path from "node:path";
17
17
 
18
+ import { resolveToolBaseDir } from "../common/wasm-tool.js";
19
+ import { CBuild, BuildError } from "../common/c-build.js";
20
+
18
21
  const __filename = fileURLToPath(import.meta.url);
19
22
  const __dirname = path.dirname(__filename);
20
23
 
@@ -23,21 +26,18 @@ const __dirname = path.dirname(__filename);
23
26
  // needs to link a target, so they must come from the same place as the WASM.
24
27
  // Resolve the package's base dir once; fall back to a local copy under src/
25
28
  // if present (transition / dev). The package is a hard dep of romdev.
26
- function resolveCc65BaseDir() {
27
- try {
28
- const u = import.meta.resolve("romdev-toolchain-cc65");
29
- const dir = path.dirname(fileURLToPath(u));
30
- if (existsSync(path.join(dir, "wasm", "cc65.js"))) return dir;
31
- } catch { /* package not resolvable — fall through to local */ }
32
- if (existsSync(path.join(__dirname, "wasm", "cc65.js"))) return __dirname;
33
- throw new Error("cc65 WASM not found — install romdev-toolchain-cc65");
34
- }
35
29
  // Lazy + memoized: resolve (and possibly throw "not installed") only on the
36
30
  // first cc65 build (NES/C64/Atari7800/Lynx), not at module load — so booting
37
31
  // the server never touches this package unless cc65 is actually used. Resolve
38
32
  // the base dir once; derive the wasm + share dirs from it on demand.
39
33
  let _cc65Base;
40
- const cc65Base = () => (_cc65Base ??= resolveCc65BaseDir());
34
+ const cc65Base = () =>
35
+ (_cc65Base ??= resolveToolBaseDir({
36
+ pkg: "romdev-toolchain-cc65",
37
+ sentinel: "wasm/cc65.js",
38
+ localDir: __dirname,
39
+ label: "cc65",
40
+ }));
41
41
 
42
42
  /** True if the cc65 build toolchain WASM (cc65 + ld65, in romdev-toolchain-cc65)
43
43
  * is installed/resolvable, without throwing — for the catalog(status) capability
@@ -45,7 +45,7 @@ const cc65Base = () => (_cc65Base ??= resolveCc65BaseDir());
45
45
  * reflects ld65 (the linker) availability. */
46
46
  export function cc65Available() {
47
47
  try {
48
- const dir = resolveCc65BaseDir();
48
+ const dir = cc65Base();
49
49
  return existsSync(path.join(dir, "wasm", "cc65.js")) && existsSync(path.join(dir, "wasm", "ld65.js"));
50
50
  } catch { return false; }
51
51
  }
@@ -275,63 +275,57 @@ export async function buildC(args) {
275
275
  // and these are pure upside. (NOTE: cc65 errors on an unknown -W name, so this
276
276
  // list is verified valid against the bundled cc65.)
277
277
  const ccWarn = ["-W", "unused-var,unused-func,unused-label,const-comparison,struct-param,pointer-sign"];
278
- const ccOpts = args.debug ? ["-g", ...ccWarn] : [...ccWarn];
279
- const caOpts = args.debug ? ["-g"] : [];
278
+ // ccOptions/caOptions = caller-supplied extra flags (e.g. GameTank's `--cpu 65c02`
279
+ // for the `-t none` path where the CPU isn't implied by a built-in cc65 target).
280
+ const ccOpts = [...(args.debug ? ["-g"] : []), ...ccWarn, ...(args.ccOptions ?? [])];
281
+ const caOpts = [...(args.debug ? ["-g"] : []), ...(args.caOptions ?? [])];
280
282
  const sources = normalizeSources(args, "main.c");
281
283
 
282
- let log = "";
283
- /** @type {Record<string, Uint8Array>} */
284
- const objects = {};
285
- for (const [name, src] of Object.entries(sources)) {
286
- const ext = path.extname(name).toLowerCase();
287
- let asmSource;
288
- let asmName;
289
- if (ext === ".s" || ext === ".asm") {
290
- asmSource = src;
291
- asmName = name;
292
- } else {
293
- const cc = await runCc65({
294
- source: src,
295
- headers: args.headers,
296
- target: args.target,
297
- options: ccOpts,
298
- });
299
- log += `--- cc65 (${name}) ---\n` + cc.log + "\n";
300
- if (cc.exitCode !== 0 || !cc.asmSource) {
301
- return { binary: null, log, exitCode: cc.exitCode || 1, stage: "cc65" };
284
+ const cb = new CBuild();
285
+ try {
286
+ /** @type {Record<string, Uint8Array>} */
287
+ const objects = {};
288
+ for (const [name, src] of Object.entries(sources)) {
289
+ const ext = path.extname(name).toLowerCase();
290
+ let asmSource;
291
+ let asmName;
292
+ if (ext === ".s" || ext === ".asm") {
293
+ asmSource = src;
294
+ asmName = name;
295
+ } else {
296
+ // failure stage is bare "cc65" (no name) but the log header carries the name.
297
+ const cc = await cb.stage("cc65",
298
+ () => runCc65({ source: src, headers: args.headers, target: args.target, options: ccOpts }),
299
+ (r) => r.asmSource, { logName: `cc65 (${name})` });
300
+ asmSource = cc.asmSource;
301
+ asmName = name.replace(/\.(c|h)$/i, ".s");
302
302
  }
303
- asmSource = cc.asmSource;
304
- asmName = name.replace(/\.(c|h)$/i, ".s");
303
+ const ca = await cb.stage("ca65",
304
+ () => runCa65({ source: asmSource, includes: args.asmIncludes, binaryIncludes: args.binaryIncludes, target: args.target, options: caOpts }),
305
+ (r) => r.object, { logName: `ca65 (${asmName})` });
306
+ objects[asmName.replace(/\.s$/, ".o")] = ca.object;
305
307
  }
306
- const ca = await runCa65({
307
- source: asmSource,
308
- includes: args.asmIncludes,
309
- binaryIncludes: args.binaryIncludes,
308
+ // ld65 is NOT throw-on-fail here: it returns its own success/failure inline with
309
+ // dbg + ramUsage, so keep it as a plain call (the cc65 contract has no `ok` field).
310
+ const ld = await runLd65({
311
+ objects,
310
312
  target: args.target,
311
- options: caOpts,
313
+ debug: args.debug,
314
+ linkerConfig: args.linkerConfig,
312
315
  });
313
- log += `--- ca65 (${asmName}) ---\n` + ca.log + "\n";
314
- if (ca.exitCode !== 0 || !ca.object) {
315
- return { binary: null, log, exitCode: ca.exitCode || 1, stage: "ca65" };
316
- }
317
- const objName = asmName.replace(/\.s$/, ".o");
318
- objects[objName] = ca.object;
316
+ cb.log += "--- ld65 ---\n" + ld.log; // exact: no trailing newline (matches pre-refactor)
317
+ return {
318
+ binary: ld.binary,
319
+ dbg: ld.dbg,
320
+ log: cb.log,
321
+ exitCode: ld.exitCode,
322
+ ramUsage: parseRamUsage(ld.map),
323
+ stage: ld.exitCode === 0 ? "done" : "ld65",
324
+ };
325
+ } catch (e) {
326
+ if (e instanceof BuildError) return e.fields(); // cc65 shape: no `ok` field
327
+ throw e;
319
328
  }
320
- const ld = await runLd65({
321
- objects,
322
- target: args.target,
323
- debug: args.debug,
324
- linkerConfig: args.linkerConfig,
325
- });
326
- log += "--- ld65 ---\n" + ld.log;
327
- return {
328
- binary: ld.binary,
329
- dbg: ld.dbg,
330
- log,
331
- exitCode: ld.exitCode,
332
- ramUsage: parseRamUsage(ld.map),
333
- stage: ld.exitCode === 0 ? "done" : "ld65",
334
- };
335
329
  }
336
330
 
337
331
  /**
@@ -351,42 +345,38 @@ export async function buildC(args) {
351
345
  * @param {string} [args.linkerConfig] custom ld65 .cfg
352
346
  */
353
347
  export async function buildAsm(args) {
354
- const caOpts = args.debug ? ["-g"] : [];
348
+ const caOpts = [...(args.debug ? ["-g"] : []), ...(args.caOptions ?? [])];
355
349
  const sources = normalizeSources(args, "main.s");
356
350
 
357
- let log = "";
358
- /** @type {Record<string, Uint8Array>} */
359
- const objects = {};
360
- for (const [name, src] of Object.entries(sources)) {
361
- const ca = await runCa65({
362
- source: src,
363
- includes: args.includes,
364
- binaryIncludes: args.binaryIncludes,
351
+ const cb = new CBuild();
352
+ try {
353
+ /** @type {Record<string, Uint8Array>} */
354
+ const objects = {};
355
+ for (const [name, src] of Object.entries(sources)) {
356
+ const ca = await cb.stage("ca65",
357
+ () => runCa65({ source: src, includes: args.includes, binaryIncludes: args.binaryIncludes, target: args.target, options: caOpts }),
358
+ (r) => r.object, { logName: `ca65 (${name})` });
359
+ objects[name.replace(/\.(s|asm)$/i, ".o")] = ca.object;
360
+ }
361
+ const ld = await runLd65({
362
+ objects,
365
363
  target: args.target,
366
- options: caOpts,
364
+ debug: args.debug,
365
+ linkerConfig: args.linkerConfig,
367
366
  });
368
- log += `--- ca65 (${name}) ---\n` + ca.log + "\n";
369
- if (ca.exitCode !== 0 || !ca.object) {
370
- return { binary: null, log, exitCode: ca.exitCode || 1, stage: "ca65" };
371
- }
372
- const objName = name.replace(/\.(s|asm)$/i, ".o");
373
- objects[objName] = ca.object;
367
+ cb.log += "--- ld65 ---\n" + ld.log;
368
+ return {
369
+ binary: ld.binary,
370
+ dbg: ld.dbg,
371
+ log: cb.log,
372
+ exitCode: ld.exitCode,
373
+ ramUsage: parseRamUsage(ld.map),
374
+ stage: ld.exitCode === 0 ? "done" : "ld65",
375
+ };
376
+ } catch (e) {
377
+ if (e instanceof BuildError) return e.fields(); // cc65 shape: no `ok` field
378
+ throw e;
374
379
  }
375
- const ld = await runLd65({
376
- objects,
377
- target: args.target,
378
- debug: args.debug,
379
- linkerConfig: args.linkerConfig,
380
- });
381
- log += "--- ld65 ---\n" + ld.log;
382
- return {
383
- binary: ld.binary,
384
- dbg: ld.dbg,
385
- log,
386
- exitCode: ld.exitCode,
387
- ramUsage: parseRamUsage(ld.map),
388
- stage: ld.exitCode === 0 ? "done" : "ld65",
389
- };
390
380
  }
391
381
 
392
382
  /**
@@ -3,36 +3,31 @@
3
3
  // Run with --cpu 6502 (default). For SNES/65816 use --cpu 65816.
4
4
 
5
5
  import { fileURLToPath } from "node:url";
6
- import { existsSync } from "node:fs";
7
6
  import path from "node:path";
8
7
 
9
8
  import { runIsolated, textFile, binaryFile } from "../_worker/run.js";
9
+ import { resolveGlueFile } from "../common/wasm-tool.js";
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = path.dirname(__filename);
13
13
 
14
- // da65's WASM ships in romdev-toolchain-cc65 (alongside cc65 / ca65 / ld65).
15
- // Resolve from that package; fall back to a local copy under src/ if present
16
- // (transition / dev). The package is a hard dep of romdev.
17
- function resolveDa65Glue() {
18
- try {
19
- const u = import.meta.resolve("romdev-toolchain-cc65");
20
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", "da65.js");
21
- if (existsSync(p)) return p;
22
- } catch { /* package not resolvable — fall through to local */ }
23
- const local = path.join(__dirname, "wasm", "da65.js");
24
- if (existsSync(local)) return local;
25
- throw new Error("da65 WASM not found — install romdev-toolchain-cc65");
26
- }
27
- // Lazy + memoized: resolve only on the first da65 disassembly, not at boot.
14
+ // da65's WASM ships in romdev-toolchain-cc65 (alongside cc65 / ca65 / ld65); a
15
+ // local src/ copy is the dev fallback. Lazy + memoized: resolve only on the
16
+ // first da65 disassembly, not at boot.
28
17
  let _glue;
29
- const glue = () => (_glue ??= resolveDa65Glue());
18
+ const glue = () =>
19
+ (_glue ??= resolveGlueFile({
20
+ pkg: "romdev-toolchain-cc65",
21
+ file: "da65.js",
22
+ localDir: __dirname,
23
+ label: "da65",
24
+ }));
30
25
 
31
26
  /** True if the da65 WASM (romdev-toolchain-cc65) is installed/resolvable, without
32
27
  * throwing — for capability probes (catalog status) that must not crash when the
33
28
  * toolchain is absent. */
34
29
  export function da65Available() {
35
- try { return !!resolveDa65Glue(); } catch { return false; }
30
+ try { return !!glue(); } catch { return false; }
36
31
  }
37
32
 
38
33
  /**
@@ -40,12 +40,23 @@ export async function resolveLinkerConfig(platform, arg) {
40
40
  );
41
41
  }
42
42
  const supportSources = {};
43
+ // Bundled headers a preset makes available to the C compile (e.g. GameTank's
44
+ // `gametank.h` register defs) — keyed by their REAL name so `#include "gametank.h"`
45
+ // resolves, NOT renamed like the `.s` support sources. A bundled header is named
46
+ // `<name>.h` (no `<preset>.` prefix needed); the `.s`/`.asm` files follow the
47
+ // support-source convention.
48
+ /** @type {Record<string,string>} */
49
+ const headers = {};
43
50
  try {
44
51
  const entries = await readdir(presetDir);
45
52
  const prefix = `${preset}.`;
46
53
  for (const f of entries) {
47
- if (!f.startsWith(prefix) || f === `${preset}.cfg`) continue;
48
54
  const ext = path.extname(f).toLowerCase();
55
+ if (ext === ".h") {
56
+ headers[f] = await readFile(path.join(presetDir, f), "utf-8");
57
+ continue;
58
+ }
59
+ if (!f.startsWith(prefix) || f === `${preset}.cfg`) continue;
49
60
  if (ext === ".s" || ext === ".asm") {
50
61
  const contents = await readFile(path.join(presetDir, f), "utf-8");
51
62
  const suffix = f.slice(prefix.length);
@@ -55,5 +66,5 @@ export async function resolveLinkerConfig(platform, arg) {
55
66
  } catch {
56
67
  // No support files.
57
68
  }
58
- return { cfg, supportSources };
69
+ return { cfg, supportSources, headers };
59
70
  }