romdevtools 0.27.0 → 0.29.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 +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -177
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -180
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -156
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +19 -6
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +64 -19
package/src/mcp/tools/rom-id.js
CHANGED
|
@@ -305,13 +305,17 @@ export async function extractSpriteSheetCore({ platform, path: romPath, offset,
|
|
|
305
305
|
const path = await import("node:path");
|
|
306
306
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
307
307
|
await writeFile(outputPath, png);
|
|
308
|
-
|
|
308
|
+
// Livestream sideband: show the rendered sheet even though the agent
|
|
309
|
+
// only gets the path.
|
|
310
|
+
const out = jsonContent({
|
|
309
311
|
path: outputPath,
|
|
310
312
|
intent: d.intent,
|
|
311
313
|
bytes: png.length,
|
|
312
314
|
paletteSource,
|
|
313
315
|
note,
|
|
314
316
|
});
|
|
317
|
+
out._observerImages = [{ kind: "image", mimeType: "image/png", base64: png.toString("base64") }];
|
|
318
|
+
return out;
|
|
315
319
|
}
|
|
316
320
|
return {
|
|
317
321
|
content: [
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { getHost } from "../state.js";
|
|
12
12
|
import { jsonContent, safeTool } from "../util.js";
|
|
13
|
+
import { attachObserverFrame } from "./watch-memory.js";
|
|
13
14
|
|
|
14
15
|
export function registerRunUntilTools(server, z, sessionKey) {
|
|
15
16
|
const memoryCondition = z.object({
|
|
@@ -75,11 +76,12 @@ export function registerRunUntilTools(server, z, sessionKey) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
// Livestream: the frame where the condition was met (or where we gave up).
|
|
80
|
+
return attachObserverFrame(jsonContent({
|
|
79
81
|
conditionMet: met,
|
|
80
82
|
framesStepped,
|
|
81
83
|
finalValue,
|
|
82
|
-
});
|
|
84
|
+
}), host, met ? "runUntil: condition met" : "runUntil: gave up");
|
|
83
85
|
}),
|
|
84
86
|
);
|
|
85
87
|
}
|
|
@@ -97,11 +97,11 @@ export function registerSnippetTools(_server, _z) {
|
|
|
97
97
|
platform, languages, snippets: filtered,
|
|
98
98
|
note: filtered.length === 0
|
|
99
99
|
? `No snippets matched for '${platform}'${language ? ` (language=${language})` : ""}.`
|
|
100
|
-
: `Fetch one with
|
|
100
|
+
: `Fetch one with examples({ op:'snippets', platform, mode:'get', snippetName${language ? ", language" : ""} }), or all with mode:'getAll'.`,
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
async function snippetsGet(platform, name, language) {
|
|
104
|
-
if (!name) throw new Error("
|
|
104
|
+
if (!name) throw new Error("examples({op:'snippets'}) mode:'get' requires `snippetName`.");
|
|
105
105
|
if (name.includes("..") || (name.includes("/") && !/^[a-z]+\/[\w.-]+$/i.test(name))) {
|
|
106
106
|
throw new Error("snippet name must not contain '..' or arbitrary path separators");
|
|
107
107
|
}
|
|
@@ -124,7 +124,7 @@ export function registerSnippetTools(_server, _z) {
|
|
|
124
124
|
return textContent(`No snippets available for '${platform}'${language ? ` (language=${language})` : ""}.`);
|
|
125
125
|
}
|
|
126
126
|
if (!inline && !outputPath) {
|
|
127
|
-
throw new Error("
|
|
127
|
+
throw new Error("examples({op:'snippets'}) mode:'getAll': pass outputPath (write the joined snippets to disk, returns {path}) or inline:true (return `combined` in the response). Or use examples({op:'copySnippets'}) to write each snippet as its own file.");
|
|
128
128
|
}
|
|
129
129
|
const parts = [];
|
|
130
130
|
for (const s of filtered) {
|
|
@@ -161,7 +161,7 @@ export function registerSnippetTools(_server, _z) {
|
|
|
161
161
|
if (filtered.length === 0) {
|
|
162
162
|
const langPart = language ? ` (language=${language})` : "";
|
|
163
163
|
const includePart = include ? ` (include=${JSON.stringify(include)})` : "";
|
|
164
|
-
throw new Error(`
|
|
164
|
+
throw new Error(`examples({op:'copySnippets'}): no snippets matched for platform '${platform}'${langPart}${includePart}.`);
|
|
165
165
|
}
|
|
166
166
|
await mkdir(destinationDir, { recursive: true });
|
|
167
167
|
const written = [];
|
|
@@ -201,9 +201,9 @@ export function registerSnippetTools(_server, _z) {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
// starterSnippets/copyStarterSnippets folded into the `
|
|
204
|
+
// starterSnippets/copyStarterSnippets folded into the `examples` tool. The cores
|
|
205
205
|
// are assigned inside registerSnippetTools (they close over the local helpers);
|
|
206
|
-
//
|
|
206
|
+
// examples imports these and calls them. registerSnippetTools registers NO tools
|
|
207
207
|
// now — it just wires the cores.
|
|
208
208
|
export let starterSnippetsCore = async () => { throw new Error("snippet cores not initialized — registerSnippetTools must run first"); };
|
|
209
209
|
export let copyStarterSnippetsCore = async () => { throw new Error("snippet cores not initialized — registerSnippetTools must run first"); };
|
|
@@ -143,6 +143,7 @@ async function cropSpriteSheetImpl({ path, tileX, tileY, tileW, tileH, tileSize
|
|
|
143
143
|
}
|
|
144
144
|
await writeFile(outputPath, outBuf);
|
|
145
145
|
return {
|
|
146
|
+
_observerImages: [{ kind: "image", mimeType: "image/png", base64: outBuf.toString("base64") }],
|
|
146
147
|
path: outputPath,
|
|
147
148
|
intent: d.intent,
|
|
148
149
|
width: pxW,
|
|
@@ -157,6 +158,16 @@ async function cropSpriteSheetImpl({ path, tileX, tileY, tileW, tileH, tileSize
|
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
// Lift a plain core result's livestream sideband OUT of the JSON body and
|
|
162
|
+
// onto the MCP result object (it must never serialize into agent-visible text).
|
|
163
|
+
function liftObserverImages(r) {
|
|
164
|
+
const sideband = r._observerImages;
|
|
165
|
+
delete r._observerImages;
|
|
166
|
+
const out = jsonContent(r);
|
|
167
|
+
if (sideband) out._observerImages = sideband;
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
// ── quantizePngForPlatform ──────────────────────────────────────────
|
|
161
172
|
|
|
162
173
|
/**
|
|
@@ -247,6 +258,7 @@ async function quantizePngForPlatformImpl({ path, platform, outputPath, intent,
|
|
|
247
258
|
const outBuf = writeIndexedPng(png.width, png.height, indices, palette);
|
|
248
259
|
await writeFile(outputPath, outBuf);
|
|
249
260
|
return {
|
|
261
|
+
_observerImages: [{ kind: "image", mimeType: "image/png", base64: outBuf.toString("base64") }],
|
|
250
262
|
path: outputPath,
|
|
251
263
|
intent: d.intent,
|
|
252
264
|
width: png.width,
|
|
@@ -596,12 +608,12 @@ export function registerSpritePipelineTools(server, z, _sessionKey) {
|
|
|
596
608
|
case "quantize": {
|
|
597
609
|
if (!args.platform) throw new Error("encodeArt({stage:'quantize'}): `platform` is required.");
|
|
598
610
|
if (!args.path || !args.outputPath) throw new Error("encodeArt({stage:'quantize'}): `path` and `outputPath` are required.");
|
|
599
|
-
return
|
|
611
|
+
return liftObserverImages(await quantizePngForPlatformImpl(args));
|
|
600
612
|
}
|
|
601
613
|
case "crop": {
|
|
602
614
|
if (!args.path || !args.outputPath) throw new Error("encodeArt({stage:'crop'}): `path` and `outputPath` are required.");
|
|
603
615
|
if (args.tileX == null || args.tileY == null || args.tileW == null || args.tileH == null) throw new Error("encodeArt({stage:'crop'}): `tileX`, `tileY`, `tileW`, `tileH` are required.");
|
|
604
|
-
return
|
|
616
|
+
return liftObserverImages(await cropSpriteSheetImpl(args));
|
|
605
617
|
}
|
|
606
618
|
case "tiles": {
|
|
607
619
|
if (!args.platform) throw new Error("encodeArt({stage:'tiles'}): `platform` is required.");
|
package/src/mcp/tools/state.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getHost } from "../state.js";
|
|
4
|
+
import { attachObserverFrame } from "./watch-memory.js";
|
|
4
5
|
import { jsonContent, safeTool } from "../util.js";
|
|
5
6
|
|
|
6
7
|
// Resolve a state-file `path`. An ABSOLUTE path is used as-is. A RELATIVE path
|
|
@@ -342,7 +343,7 @@ export function registerStateTools(server, z, sessionKey) {
|
|
|
342
343
|
safeTool(async (args) => {
|
|
343
344
|
switch (args.op) {
|
|
344
345
|
case "save": return jsonContent(await saveStateCore(args, sessionKey));
|
|
345
|
-
case "load": return jsonContent(await loadStateCore(args, sessionKey));
|
|
346
|
+
case "load": return attachObserverFrame(jsonContent(await loadStateCore(args, sessionKey)), getHost(sessionKey), `state load ${args.name ?? args.path ?? ""}`.trim());
|
|
346
347
|
case "list": return jsonContent(listStatesCore(args, sessionKey));
|
|
347
348
|
case "export": {
|
|
348
349
|
if (!args.fromSlot) throw new Error("state({op:'export'}): `fromSlot` is required.");
|
|
@@ -284,7 +284,14 @@ export function registerTileInspectTools(server, z, sessionKey) {
|
|
|
284
284
|
if (r.pngBase64) {
|
|
285
285
|
return { content: [imageContent(r.pngBase64), textContent(JSON.stringify({ ...r, pngBase64: undefined }))] };
|
|
286
286
|
}
|
|
287
|
-
|
|
287
|
+
// Lift the livestream sideband OUT of the core's plain result before
|
|
288
|
+
// it's serialized — it must ride on the MCP result object, never in
|
|
289
|
+
// the agent-visible JSON text.
|
|
290
|
+
const sideband = r._observerImages;
|
|
291
|
+
delete r._observerImages;
|
|
292
|
+
const out = jsonContent(r);
|
|
293
|
+
if (sideband) out._observerImages = sideband;
|
|
294
|
+
return out;
|
|
288
295
|
}
|
|
289
296
|
default: throw new Error(`tiles: unknown op '${args.op}'`);
|
|
290
297
|
}
|
|
@@ -266,7 +266,7 @@ export function installToolchainCore({ id }) {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
export function registerToolchainTools(server, z, sessionKey) {
|
|
269
|
-
async function buildSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, crt0, crt0Path, codeLoc, dataLoc, options, linkerConfig, inesHeader, outputPath, inline = false, includeSymbols = false, lint = "advisory", runtime, maxmod, rebuildSdk }) {
|
|
269
|
+
async function buildSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, crt0, crt0Path, codeLoc, dataLoc, options, linkerConfig, linkerConfigPath, inesHeader, outputPath, inline = false, includeSymbols = false, lint = "advisory", runtime, maxmod, rebuildSdk }) {
|
|
270
270
|
// Reject conflicting inline vs path args — fail loud, not silent.
|
|
271
271
|
if (source != null && sourcePath != null) {
|
|
272
272
|
throw new Error("build({output:'rom'}): pass either `source` OR `sourcePath`, not both.");
|
|
@@ -281,6 +281,12 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
281
281
|
if (crt0Path) {
|
|
282
282
|
crt0 = await readFile(crt0Path, "utf-8");
|
|
283
283
|
}
|
|
284
|
+
// linkerConfigPath: read the .cfg from disk so a large multi-bank
|
|
285
|
+
// config (e.g. a disasm'd mapper-2 rebuild) isn't re-streamed through
|
|
286
|
+
// context on every build (0.27.0 feedback #2).
|
|
287
|
+
if (linkerConfigPath && linkerConfig == null) {
|
|
288
|
+
linkerConfig = await readFile(linkerConfigPath, "utf-8");
|
|
289
|
+
}
|
|
284
290
|
// Auto-inject the bundled crt0 for SMS/GG when caller didn't pass
|
|
285
291
|
// one. Stock SDCC crt0 doesn't boot these targets; without this,
|
|
286
292
|
// user main() is never called → black screen. See AUTO_CRT0_PLATFORMS.
|
|
@@ -464,7 +470,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
464
470
|
return jsonContent(payload);
|
|
465
471
|
}
|
|
466
472
|
|
|
467
|
-
async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, inesHeader, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
|
|
473
|
+
async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, linkerConfigPath, inesHeader, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
|
|
468
474
|
const { buildForPlatform } = await import("../../toolchains/index.js");
|
|
469
475
|
const resolved = resolveCore(platform);
|
|
470
476
|
if (!resolved) throw new Error(`no core available for platform '${platform}'`);
|
|
@@ -480,6 +486,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
480
486
|
binaryIncludes = { ...(binaryIncludes ?? {}), ...r.binaryIncludes };
|
|
481
487
|
if (r.crt0 != null) crt0 = r.crt0;
|
|
482
488
|
if (r.codeLoc != null) codeLoc = r.codeLoc;
|
|
489
|
+
if (r.dataLoc != null && dataLoc == null) dataLoc = r.dataLoc;
|
|
483
490
|
if (r.linkerConfig != null && linkerConfig == null) linkerConfig = r.linkerConfig;
|
|
484
491
|
if (r.runtime != null && runtime == null) runtime = r.runtime;
|
|
485
492
|
if (r.maxmod != null && maxmod == null) maxmod = r.maxmod;
|
|
@@ -510,6 +517,9 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
510
517
|
if (crt0Path) {
|
|
511
518
|
crt0 = await readFile(crt0Path, "utf-8");
|
|
512
519
|
}
|
|
520
|
+
if (linkerConfigPath && linkerConfig == null) {
|
|
521
|
+
linkerConfig = await readFile(linkerConfigPath, "utf-8");
|
|
522
|
+
}
|
|
513
523
|
// Auto-inject bundled crt0 for SMS/GG when caller didn't pass one
|
|
514
524
|
// (stock SDCC crt0 doesn't boot these targets — see buildSource).
|
|
515
525
|
if (crt0 == null) {
|
|
@@ -693,7 +703,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
693
703
|
"• 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" +
|
|
694
704
|
"• 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`). **`resolveSymbols:['grid','score']` folds those names' addresses ({resolvedSymbols:{grid:{address,hex,region?,ramOffset?}}}) straight into the result — the cheap way to a WRAM variable's address without loading the whole map (or round-tripping it through `symbols`).**\n" +
|
|
695
705
|
"• 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" +
|
|
696
|
-
"• output:'project' — build a project DIRECTORY (`path`) without re-passing the file manifest each call. Entry point is `main.c` (C/SGDK Genesis, GBA, cc65/SDCC C) OR `main.s`/`main.asm` (asm). Every `.c`/`.s`/`.asm` in the dir is a translation unit (linked together), every `.h`/`.inc` an include, and `.bin/.chr/.pcm/.brr/.vgm/...` become binaryIncludes (for `.incbin`). Iterate an on-disk project by re-calling with just `{path, platform}`. **This is the no-boilerplate path for
|
|
706
|
+
"• output:'project' — build a project DIRECTORY (`path`) without re-passing the file manifest each call. Entry point is `main.c` (C/SGDK Genesis, GBA, cc65/SDCC C) OR `main.s`/`main.asm` (asm). Every `.c`/`.s`/`.asm` in the dir is a translation unit (linked together), every `.h`/`.inc` an include, and `.bin/.chr/.pcm/.brr/.vgm/...` become binaryIncludes (for `.incbin`). Iterate an on-disk project by re-calling with just `{path, platform}`. **This is the no-boilerplate path for an examples({op:'fork'}) dir: the per-platform recipe auto-supplies the crt0 + load address — GB/GBC default `gb_crt0.s` + `codeLoc:0x150` (don't hand-pass them!), MSX routes `msx_crt0.s` + `codeLoc:0x4010`, SMS/GG auto-inject their bundled crt0, NES applies the chr-ram-runtime preset. PREFER this over re-passing `crt0Path`/`codeLoc` to output:'rom' for a forked project.**",
|
|
697
707
|
{
|
|
698
708
|
output: z.enum(["rom", "romWithDebug", "run", "project"])
|
|
699
709
|
.describe("rom=produce a ROM (default); romWithDebug=ROM + .dbg/.map debug files; run=build+load+run+screenshot; project=build a project directory."),
|
|
@@ -714,6 +724,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
714
724
|
dataLoc: z.coerce.number().int().optional().describe("SDCC — _DATA (WRAM) load address (default $C000 on Z80). NOT read by output:'romWithDebug'."),
|
|
715
725
|
options: z.array(z.string()).optional().describe("output:'rom' — extra toolchain CLI options."),
|
|
716
726
|
linkerConfig: z.string().optional().describe("ld65 linker config (cc65). NES presets: 'chr-ram-runtime' (RECOMMENDED for homebrew C — full crt0 + iNES header + NMI w/ OAM DMA + `_shadow_oam` at $0200), 'chr-ram' (bare nmi:rti stub), 'chr-rom' (cc65-C with FIXED CHR-ROM art — segment split + CHARS segment; supply CHR via binaryIncludePaths into a CHARS source + the header via `inesHeader`). Or full .cfg contents. Preset NAMES only resolve on output:'rom'/'run'; output:'romWithDebug' takes raw .cfg contents only. **For rebuilding a commercial NROM game from its disassembly, prefer `inesHeader` over a raw .cfg.**"),
|
|
727
|
+
linkerConfigPath: z.string().optional().describe("Path-based `linkerConfig`: absolute path to a .cfg file on disk (the server reads it — the cfg never enters your context; e.g. the multi-bank cfg a banked-NES disasm project ships). Ignored when `linkerConfig` is passed inline."),
|
|
717
728
|
inesHeader: z.object({
|
|
718
729
|
prgBanks: z.coerce.number().int().min(1).max(255).describe("16KB PRG-ROM banks (1 = NROM-128, 2 = NROM-256)."),
|
|
719
730
|
chrBanks: z.coerce.number().int().min(0).max(255).optional().describe("8KB CHR-ROM banks (0 = CHR-RAM, no CHARS segment). Default 0."),
|
|
@@ -785,8 +796,8 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
785
796
|
*/
|
|
786
797
|
export function projectBuildRecipe(platform, names) {
|
|
787
798
|
const has = (n) => names.includes(n);
|
|
788
|
-
/** @type {{crt0File:string|null, codeLoc:number|undefined, linkerConfig:string|undefined, runtime:string|undefined, maxmod:boolean|undefined, skip:Set<string>, includeAsC:Set<string>}} */
|
|
789
|
-
const r = { crt0File: null, codeLoc: undefined, linkerConfig: undefined, runtime: undefined, maxmod: undefined, skip: new Set(), includeAsC: new Set() };
|
|
799
|
+
/** @type {{crt0File:string|null, codeLoc:number|undefined, dataLoc:number|undefined, linkerConfig:string|undefined, runtime:string|undefined, maxmod:boolean|undefined, skip:Set<string>, includeAsC:Set<string>}} */
|
|
800
|
+
const r = { crt0File: null, codeLoc: undefined, dataLoc: undefined, linkerConfig: undefined, runtime: undefined, maxmod: undefined, skip: new Set(), includeAsC: new Set() };
|
|
790
801
|
|
|
791
802
|
// Reference/upstream sources ship for grepping, not compiling (e.g. GB
|
|
792
803
|
// music_demo's hUGEDriver.upstream.asm — the .c port is what builds). Skip
|
|
@@ -796,7 +807,11 @@ export function projectBuildRecipe(platform, names) {
|
|
|
796
807
|
if (platform === "gb" || platform === "gbc") {
|
|
797
808
|
// GB/GBC ship gb_crt0.s — it MUST go via crt0+codeLoc:0x150, never as a
|
|
798
809
|
// source (SDCC emits its own gsinit → "Multiple definition of gsinit").
|
|
799
|
-
|
|
810
|
+
// dataLoc 0xC200: statics start ABOVE shadow_oam ($C100-$C19F, fixed by
|
|
811
|
+
// the runtime). The sdld default of $C000 let any project with >256 bytes
|
|
812
|
+
// of statics silently overlap the OAM shadow — oam_clear() then zeroed
|
|
813
|
+
// game state (grid/RNG seed). 512 bytes of 8KB WRAM is cheap insurance.
|
|
814
|
+
if (has("gb_crt0.s")) { r.crt0File = "gb_crt0.s"; r.codeLoc = 0x150; r.dataLoc = 0xC200; }
|
|
800
815
|
} else if (platform === "nes") {
|
|
801
816
|
// A SCAFFOLDED NES project ships nes_runtime.c + a crt0 + a .cfg and needs
|
|
802
817
|
// the chr-ram-runtime preset (it defines the OAM/CHARS segments + a NMI with
|
|
@@ -823,10 +838,29 @@ export function projectBuildRecipe(platform, names) {
|
|
|
823
838
|
// commercial ROMs boot in the same host; only our scaffolds failed). Routing
|
|
824
839
|
// msx_crt0.s through crt0 makes ITS header + init the cartridge entry.
|
|
825
840
|
if (has("msx_crt0.s")) { r.crt0File = "msx_crt0.s"; r.codeLoc = 0x4010; }
|
|
841
|
+
} else if (platform === "pce") {
|
|
842
|
+
// PCE example projects (they ship pce_hw.h) build on the 'rom32k' preset:
|
|
843
|
+
// a 32KB HuCard with bank 0 (STARTUP/VECTORS) FIRST in the file at $E000
|
|
844
|
+
// and banks 1-3 (CODE/RODATA) at $8000-$DFFF, where cc65's pce crt0 TAMs
|
|
845
|
+
// them before main(). cc65's stock pce.cfg is an 8KB boot bank — too small
|
|
846
|
+
// for a complete example game — and its documented 32K variant places the
|
|
847
|
+
// vectors in the LAST file bank, which a HuCard never maps at reset
|
|
848
|
+
// (verified black screen on geargrafx). An 8KB-sized program still links
|
|
849
|
+
// and boots identically under this preset, so it's safe for every
|
|
850
|
+
// pce_hw.h-style project. Bare hand-rolled dirs are left alone.
|
|
851
|
+
if (has("pce_hw.h")) r.linkerConfig = "rom32k";
|
|
826
852
|
} else if (platform === "sms" || platform === "gg") {
|
|
827
|
-
// SMS/GG
|
|
828
|
-
//
|
|
829
|
-
|
|
853
|
+
// SMS/GG: route the project's *_crt0.s through the crt0 channel (like
|
|
854
|
+
// GB/MSX), NOT as a plain source TU. The OLD recipe skipped it on the
|
|
855
|
+
// belief that "buildForPlatform auto-injects the bundled crt0" — IT DOES
|
|
856
|
+
// NOT (only the output:'rom'/'run' MCP handlers auto-inject). So every
|
|
857
|
+
// output:'project' SMS/GG build linked SDCC's STOCK z80 crt0, whose boot
|
|
858
|
+
// is `ld a,#2 / rst $08 / halt` — main() never ran and every scaffold
|
|
859
|
+
// booted to a BLACK SCREEN (the RetroDECK "all broken" report; our
|
|
860
|
+
// output:'run' verifications were false-green via the other path).
|
|
861
|
+
// readProjectDir falls back to the bundled crt0 when the dir has none.
|
|
862
|
+
const crt0Name = names.find((n) => /_crt0\.s$/i.test(n));
|
|
863
|
+
if (crt0Name) r.crt0File = crt0Name;
|
|
830
864
|
} else if (platform === "genesis" || platform === "megadrive" || platform === "md") {
|
|
831
865
|
// SGDK supplies sega startup + rom header. The scaffold dir may contain
|
|
832
866
|
// generated intermediates (sega.s, sega.preprocessed.s, rom_header.*, and an
|
|
@@ -919,6 +953,15 @@ export async function readProjectDir(projPath, platform) {
|
|
|
919
953
|
}
|
|
920
954
|
}
|
|
921
955
|
|
|
956
|
+
// SMS/GG with no crt0 file in the dir → fall back to the bundled crt0,
|
|
957
|
+
// exactly like the output:'rom'/'run' handlers do. Without this the link
|
|
958
|
+
// silently uses SDCC's stock z80 crt0, which never calls main() (black
|
|
959
|
+
// screen at boot). The SMS scaffold historically shipped without a crt0
|
|
960
|
+
// file, so this fallback is load-bearing for existing project dirs.
|
|
961
|
+
if (crt0 == null && (platform === "sms" || platform === "gg")) {
|
|
962
|
+
crt0 = await resolveAutoCrt0(platform);
|
|
963
|
+
}
|
|
964
|
+
|
|
922
965
|
// GBA runtime refinement: libgba if the entry includes <gba.h>, else the
|
|
923
966
|
// libtonc default the recipe set.
|
|
924
967
|
let runtime = recipe.runtime;
|
|
@@ -926,11 +969,11 @@ export async function readProjectDir(projPath, platform) {
|
|
|
926
969
|
runtime = "libgba";
|
|
927
970
|
}
|
|
928
971
|
|
|
929
|
-
return { sources, includes, binaryIncludes, crt0, codeLoc: recipe.codeLoc, linkerConfig: recipe.linkerConfig, runtime, maxmod: recipe.maxmod };
|
|
972
|
+
return { sources, includes, binaryIncludes, crt0, codeLoc: recipe.codeLoc, dataLoc: recipe.dataLoc, linkerConfig: recipe.linkerConfig, runtime, maxmod: recipe.maxmod };
|
|
930
973
|
}
|
|
931
974
|
|
|
932
975
|
export async function buildProjectCore({ path: projPath, platform, outputPath }) {
|
|
933
|
-
const { sources, includes, binaryIncludes, crt0, codeLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform);
|
|
976
|
+
const { sources, includes, binaryIncludes, crt0, codeLoc, dataLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform);
|
|
934
977
|
|
|
935
978
|
// Linker preset: the recipe names it (e.g. NES 'chr-ram-runtime', which ships
|
|
936
979
|
// the OAM/CHARS segments + its own crt0). resolveLinkerConfig also returns any
|
|
@@ -968,6 +1011,7 @@ export async function buildProjectCore({ path: projPath, platform, outputPath })
|
|
|
968
1011
|
linkerConfig: resolvedLinkerConfig,
|
|
969
1012
|
crt0: crt0Rel,
|
|
970
1013
|
codeLoc,
|
|
1014
|
+
dataLoc,
|
|
971
1015
|
});
|
|
972
1016
|
if (outputPath && result.binary) {
|
|
973
1017
|
await mkdir(path.dirname(outputPath), { recursive: true });
|