romdevtools 0.13.0 → 0.15.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 +21 -14
- package/CHANGELOG.md +125 -1
- package/README.md +13 -8
- package/examples/atari2600/main.asm +1 -1
- package/examples/atari2600/templates/default.asm +1 -1
- package/examples/atari2600/templates/paddle.asm +59 -47
- package/examples/atari7800/main.c +1 -1
- package/examples/atari7800/templates/default.c +1 -1
- package/examples/atari7800/templates/music_demo.c +1 -1
- package/examples/c64/main.c +1 -1
- package/examples/c64/templates/platformer.c +2 -2
- package/examples/c64/templates/puzzle.c +1 -1
- package/examples/c64/templates/racing.c +3 -3
- package/examples/c64/templates/shmup.c +6 -5
- package/examples/c64/templates/sports.c +4 -4
- package/examples/gb/main.asm +1 -1
- package/examples/gb/main.c +1 -1
- package/examples/gb/templates/puzzle.c +1 -1
- package/examples/gb/templates/racing.c +1 -1
- package/examples/gb/templates/shmup.c +1 -1
- package/examples/gba/templates/gba_hello.c +1 -1
- package/examples/gba/templates/maxmod_demo.c +1 -1
- package/examples/gba/templates/puzzle.c +17 -3
- package/examples/gba/templates/racing.c +16 -2
- package/examples/gba/templates/shmup.c +23 -4
- package/examples/gba/templates/tonc_hello.c +6 -4
- package/examples/gbc/main.asm +1 -1
- package/examples/gbc/templates/puzzle.c +1 -1
- package/examples/gbc/templates/racing.c +1 -1
- package/examples/gbc/templates/shmup.c +1 -1
- package/examples/genesis/main.s +1 -1
- package/examples/genesis/templates/puzzle.c +1 -1
- package/examples/genesis/templates/racing.c +45 -1
- package/examples/genesis/templates/shmup.c +12 -3
- package/examples/genesis/templates/shmup_2p.c +2 -2
- package/examples/genesis/templates/sports.c +39 -0
- package/examples/gg/templates/hello_sprite.c +38 -23
- package/examples/gg/templates/music_demo.c +11 -8
- package/examples/gg/templates/platformer.c +37 -15
- package/examples/gg/templates/racing.c +25 -12
- package/examples/gg/templates/shmup.c +12 -6
- package/examples/gg/templates/sports.c +30 -16
- package/examples/gg/templates/tile_engine.c +24 -10
- package/examples/lynx/templates/platformer.c +7 -1
- package/examples/lynx/templates/puzzle.c +8 -2
- package/examples/lynx/templates/racing.c +7 -1
- package/examples/lynx/templates/sports.c +7 -1
- package/examples/nes/main.c +2 -2
- package/examples/nes/space-shooter/nes_runtime.h +1 -1
- package/examples/nes/templates/default.c +4 -1
- package/examples/nes/templates/racing.c +50 -1
- package/examples/pce/main.c +1 -1
- package/examples/sms/templates/hello_sprite.c +1 -1
- package/examples/sms/templates/music_demo.c +1 -1
- package/examples/sms/templates/puzzle.c +1 -1
- package/examples/sms/templates/racing.c +1 -1
- package/examples/sms/templates/shmup.c +1 -1
- package/examples/sms/templates/shmup_2p.c +2 -2
- package/examples/snes/main.asm +1 -1
- package/examples/snes/templates/c-hello-data.asm +309 -14
- package/examples/snes/templates/c-hello.c +13 -2
- package/examples/snes/templates/default.c +1 -1
- package/examples/snes/templates/hello_sprite-data.asm +300 -2
- package/examples/snes/templates/hello_sprite.c +10 -1
- package/examples/snes/templates/music_demo-data.asm +300 -2
- package/examples/snes/templates/music_demo.c +10 -1
- package/examples/snes/templates/platformer-data.asm +300 -2
- package/examples/snes/templates/platformer.c +10 -1
- package/examples/snes/templates/puzzle-data.asm +300 -2
- package/examples/snes/templates/puzzle.c +11 -1
- package/examples/snes/templates/racing-data.asm +300 -2
- package/examples/snes/templates/racing.c +40 -4
- package/examples/snes/templates/shmup-data.asm +299 -6
- package/examples/snes/templates/shmup.c +11 -7
- package/examples/snes/templates/sports-data.asm +300 -2
- package/examples/snes/templates/sports.c +40 -5
- package/package.json +1 -1
- package/src/cheats/lookup.js +39 -18
- package/src/http/routes.js +58 -33
- package/src/http/skill-doc.js +10 -9
- package/src/http/swagger.js +1 -1
- package/src/http/tool-registry.js +72 -5
- package/src/mcp/server.js +6 -5
- package/src/mcp/state.js +8 -6
- package/src/mcp/tool-manifest.js +7 -7
- package/src/mcp/tools/cheats.js +4 -3
- package/src/mcp/tools/index.js +18 -2
- package/src/mcp/tools/playtest.js +48 -35
- package/src/mcp/tools/project.js +39 -73
- package/src/mcp/tools/rom-id.js +49 -4
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +183 -19
- package/src/mcp/tools/trace-vram-source.js +3 -3
- package/src/mcp/tools/watch-memory.js +27 -46
- package/src/observer/livestream.html +41 -5
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +5 -5
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +1 -1
- package/src/platforms/gb/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gb/lib/c/README.md +2 -2
- package/src/platforms/gb/lib/c/SDCC_GOTCHAS.md +1 -1
- package/src/platforms/gbc/MENTAL_MODEL.md +3 -3
- package/src/platforms/gbc/TROUBLESHOOTING.md +5 -5
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +2 -2
- package/src/platforms/gbc/lib/c/README.md +2 -2
- package/src/platforms/gbc/lib/c/SDCC_GOTCHAS.md +1 -1
- package/src/platforms/gg/MENTAL_MODEL.md +14 -13
- package/src/platforms/gg/lib/c/vdp_init.c +10 -8
- package/src/platforms/msx/MENTAL_MODEL.md +1 -1
- package/src/platforms/nes/TROUBLESHOOTING.md +1 -1
- package/src/platforms/nes/lib/c/nes_runtime.c +28 -6
- package/src/platforms/pce/MENTAL_MODEL.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +1 -0
- package/src/platforms/pce/lib/c/pce_video.c +26 -0
- package/src/platforms/sms/MENTAL_MODEL.md +12 -12
- package/src/platforms/sms/lib/c/vdp_init.c +10 -8
- package/src/platforms/sms/lib/vdp_init.s +1 -1
- package/src/playtest/playtest.js +25 -0
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.crt0.s +1 -1
- package/src/toolchains/genesis-c/README.md +1 -1
- package/src/toolchains/sdcc/preflight-lint.js +47 -7
- package/src/toolchains/snes-c/snes-c.js +3 -7
|
@@ -7,6 +7,7 @@ import { writeFile } from "node:fs/promises";
|
|
|
7
7
|
|
|
8
8
|
import { getHost, getHostOrNull } from "../state.js";
|
|
9
9
|
import { imageContent, jsonContent, safeTool, textContent } from "../util.js";
|
|
10
|
+
import { log } from "../log.js";
|
|
10
11
|
|
|
11
12
|
// Playtest windows are PER SESSION: the MCP server is multi-session (one server
|
|
12
13
|
// serves several agents at once), and the same user can have 2-3 different games
|
|
@@ -104,14 +105,13 @@ export function isPlaytestRunning(sessionKey) {
|
|
|
104
105
|
export function registerPlaytestTools(server, z, sessionKey) {
|
|
105
106
|
// op:'open' — open (or reuse) the SDL window for this session.
|
|
106
107
|
async function ptOpen({ scale = 3, title, aspect = "tv" }) {
|
|
107
|
-
// No preflight display checks. We just attempt to open the SDL window and
|
|
108
|
-
// report whatever SDL says — env-var guessing (DISPLAY/WAYLAND_DISPLAY)
|
|
109
|
-
// is Linux-only and wrong on macOS/Windows, where those vars are never
|
|
110
|
-
// set even with a full GUI session. SDL's createWindow already knows
|
|
111
|
-
// whether it can draw on any platform; the try/catch below surfaces the
|
|
112
|
-
// real error.
|
|
113
108
|
const host = getHost(sessionKey);
|
|
114
109
|
const loadedMediaPath = host.status?.mediaPath ?? null;
|
|
110
|
+
// No env-var preflight here — the GROUND-TRUTH "is there a real display?"
|
|
111
|
+
// check lives in loadSdl() (it asks SDL which video driver it selected and
|
|
112
|
+
// throws sdlKind:"no-display" if it's offscreen/dummy). That's cross-
|
|
113
|
+
// platform and doesn't false-bark on valid offscreen setups like Xvfb.
|
|
114
|
+
// The try/catch below surfaces it (and the binary errors) uniformly.
|
|
115
115
|
if (reconcileSession(sessionKey)) {
|
|
116
116
|
// THIS session already has a window open. We don't open a second one for
|
|
117
117
|
// the same session — it shares this session's live host — so report the
|
|
@@ -153,44 +153,55 @@ export function registerPlaytestTools(server, z, sessionKey) {
|
|
|
153
153
|
"stepFrames / pressButton) still works against the live ROM — only " +
|
|
154
154
|
"the interactive window is affected.";
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
// A failed window-open is a REAL FAILURE — THROW it, don't return a soft
|
|
157
|
+
// {opened:false} object. Returning success-shaped JSON made the failure
|
|
158
|
+
// invisible on the REST/skill surface (HTTP 200 = "it worked"), so an
|
|
159
|
+
// agent driving the routes would report "window's up!" while no window
|
|
160
|
+
// exists. Thrown → safeTool tags isError → runTool maps it to HTTP 400
|
|
161
|
+
// (REST) and a tool error (MCP). We also log to the server console so a
|
|
162
|
+
// human watching the terminal sees it even if the agent buries the error.
|
|
163
|
+
let reason, message;
|
|
164
|
+
if (kind === "no-display") {
|
|
165
|
+
// GROUND TRUTH: SDL came up on the offscreen/dummy driver — there is no
|
|
166
|
+
// physical screen to show the window on (it would render + play audio
|
|
167
|
+
// but be invisible). loadSdl()'s message already says exactly this + the
|
|
168
|
+
// fix; pass it straight through.
|
|
169
|
+
reason = "no-display";
|
|
170
|
+
message = (e?.message ?? String(e)) + headlessNote;
|
|
171
|
+
} else if (kind === "missing-binary" || kind === "install-failed") {
|
|
157
172
|
// Native-addon problem, NOT a display problem.
|
|
158
173
|
const fix = e?.fixCmd
|
|
159
174
|
? `Run: ${e.fixCmd} (then restart the server). `
|
|
160
175
|
: "Reinstall @kmamal/sdl so its prebuilt binary is fetched. ";
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// A genuine SDL init / display failure (e.g. no video device, no
|
|
179
|
-
// desktop session). NOW the desktop-session advice is the right call.
|
|
180
|
-
return jsonContent({
|
|
181
|
-
opened: false,
|
|
182
|
-
reason: "sdl-error",
|
|
183
|
-
platform: process.platform,
|
|
184
|
-
message:
|
|
176
|
+
reason = "sdl-binary-missing";
|
|
177
|
+
message =
|
|
178
|
+
"The playtest window couldn't open because the @kmamal/sdl native " +
|
|
179
|
+
"binary isn't installed: " + (e?.message ?? String(e)) + ". " +
|
|
180
|
+
(kind === "install-failed"
|
|
181
|
+
? "An automatic install was attempted but failed (often a network/proxy block on the GitHub release download). "
|
|
182
|
+
: "(This is common under `npx romdevtools` — npm skips @kmamal/sdl's install script that fetches the binary; the server tried to self-heal but the binary is still absent.) ") +
|
|
183
|
+
fix + "This is a one-time native-addon fix, NOT a display/desktop " +
|
|
184
|
+
"issue." + headlessNote;
|
|
185
|
+
} else {
|
|
186
|
+
// A genuine SDL init / display failure (no video device / no desktop
|
|
187
|
+
// session). The desktop-session advice is the right call here.
|
|
188
|
+
reason = "sdl-error";
|
|
189
|
+
message =
|
|
185
190
|
"Couldn't open the SDL playtest window: " + (e?.message ?? String(e)) +
|
|
186
191
|
". SDL initialized but couldn't get a display. This usually means the " +
|
|
187
192
|
"server has no access to a logged-in desktop session — e.g. it was " +
|
|
188
193
|
"spawned as an MCP subprocess by your agent host, or runs over plain " +
|
|
189
194
|
"SSH/headless. The reliable fix: run the server yourself in a terminal " +
|
|
190
195
|
"inside your desktop session, then connect your agent to it." +
|
|
191
|
-
headlessNote + " You can also open the built ROM in any standalone emulator."
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
headlessNote + " You can also open the built ROM in any standalone emulator.";
|
|
197
|
+
}
|
|
198
|
+
// Server-console breadcrumb (stderr) so a human at the terminal sees the
|
|
199
|
+
// failure regardless of whether the agent relays the tool error.
|
|
200
|
+
log.error(`playtest: window failed to open (${reason}) — ${e?.fixCmd ? "fix: " + e.fixCmd : message.slice(0, 120)}`);
|
|
201
|
+
const err = new Error(message);
|
|
202
|
+
err.reason = reason;
|
|
203
|
+
if (e?.fixCmd) err.fixCommand = e.fixCmd;
|
|
204
|
+
throw err;
|
|
194
205
|
}
|
|
195
206
|
// Detach so process doesn't hang on the closed promise. Only clear THIS
|
|
196
207
|
// session's slot, and only if it still points at this same session (a
|
|
@@ -284,7 +295,9 @@ export function registerPlaytestTools(server, z, sessionKey) {
|
|
|
284
295
|
});
|
|
285
296
|
}
|
|
286
297
|
if (!inline && !outPath) {
|
|
287
|
-
|
|
298
|
+
// Usage error → throw so REST returns 400 (not a 200 with ok:false the
|
|
299
|
+
// caller might ignore).
|
|
300
|
+
throw new Error("playtest framebuffer: pass `path` (where to write the PNG) or `inline:true`.");
|
|
288
301
|
}
|
|
289
302
|
const frame = sessions.get(sessionKey).captureFrame();
|
|
290
303
|
if (!frame) {
|
package/src/mcp/tools/project.js
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
// Policy (2026-05-25): no auto-injection at build time. createProject copies
|
|
4
4
|
// every file the template depends on (runtime, headers, crt0, linker .cfg)
|
|
5
5
|
// into the project directory. The project is then self-contained — any
|
|
6
|
-
// `
|
|
6
|
+
// `build({output:'run'})` call points at the project's own files via
|
|
7
7
|
// sources/sourcesPaths/includePaths/crt0/linkerConfig args. If you take
|
|
8
8
|
// the project elsewhere and rebuild with cc65/sdcc directly, every byte
|
|
9
9
|
// that compiles is in the directory.
|
|
10
10
|
|
|
11
11
|
import { readFile, writeFile } from "node:fs/promises";
|
|
12
|
-
import { patchGbHeader } from "../../platforms/gb/lib/c/patch-header.js";
|
|
13
12
|
import { jsonContent, safeTool } from "../util.js";
|
|
14
13
|
import { starterSnippetsCore, copyStarterSnippetsCore } from "./snippets.js";
|
|
15
14
|
|
|
@@ -320,7 +319,7 @@ TEMPLATES.gbc = {
|
|
|
320
319
|
default: {
|
|
321
320
|
main: "templates/default.c", runtime: GBC_RUNTIME,
|
|
322
321
|
lang: GBC_LANG, ext: ".gbc",
|
|
323
|
-
describe: "Minimal GBC starter. Same shape as the GB default but ROM extension .gbc —
|
|
322
|
+
describe: "Minimal GBC starter. Same shape as the GB default but ROM extension .gbc — the GB-header patch sets $0143=$80 so gambatte boots in CGB mode.",
|
|
324
323
|
},
|
|
325
324
|
hello_sprite: {
|
|
326
325
|
main: "templates/hello_sprite.c", runtime: GBC_RUNTIME,
|
|
@@ -1087,7 +1086,7 @@ TEMPLATES.gba = {
|
|
|
1087
1086
|
runtimeDirs: GBA_LIBTONC_RUNTIME_DIRS,
|
|
1088
1087
|
lang: GBA_TONC_LANG,
|
|
1089
1088
|
ext: ".gba",
|
|
1090
|
-
describe: "Idiomatic Tonc-tutorial GBA C starter. #include <tonc.h>, TTE (Tonc Text Engine) draws 'Hello, Tonc!' on BG0 in MODE_0. Matches what every published GBA C tutorial at gbadev.net teaches. libtonc is compiled from its vendored source by the build (a fast prebuilt seed by default; pass rebuildSdk:true if you edit the SDK source) — the project gets the headers + gba_crt0 + linker script. Build with
|
|
1089
|
+
describe: "Idiomatic Tonc-tutorial GBA C starter. #include <tonc.h>, TTE (Tonc Text Engine) draws 'Hello, Tonc!' on BG0 in MODE_0. Matches what every published GBA C tutorial at gbadev.net teaches. libtonc is compiled from its vendored source by the build (a fast prebuilt seed by default; pass rebuildSdk:true if you edit the SDK source) — the project gets the headers + gba_crt0 + linker script. Build with build({output:'run', platform:'gba', language:'c'}) — defaults to runtime:'libtonc'.",
|
|
1091
1090
|
},
|
|
1092
1091
|
tonc_hello_sprite: {
|
|
1093
1092
|
main: "templates/tonc_hello_sprite.c",
|
|
@@ -1145,10 +1144,10 @@ TEMPLATES.gba = {
|
|
|
1145
1144
|
runtimeDirs: GBA_LIBGBA_RUNTIME_DIRS,
|
|
1146
1145
|
lang: GBA_LIBGBA_LANG,
|
|
1147
1146
|
ext: ".gba",
|
|
1148
|
-
describe: "Alternate GBA C starter using devkitPro's libgba SDK. MODE_3 framebuffer + red pixel. Pass runtime:'libgba' to
|
|
1147
|
+
describe: "Alternate GBA C starter using devkitPro's libgba SDK. MODE_3 framebuffer + red pixel. Pass runtime:'libgba' to build({output:'run'}) — or just use the Tonc path (gba_hello_tonc) which is better aligned with what published tutorials teach.",
|
|
1149
1148
|
},
|
|
1150
1149
|
// R34: maxmod music demo. Ships a hand-authored CC0 chiptune.xm +
|
|
1151
|
-
// its pre-built soundbank.bin.
|
|
1150
|
+
// its pre-built soundbank.bin. build({output:'run'}) must be called with
|
|
1152
1151
|
// `maxmod: true` AND binaryIncludes:{ "soundbank.bin": <bytes> } —
|
|
1153
1152
|
// the buildGbaC layer auto-emits a `.incbin "soundbank.bin"` asm
|
|
1154
1153
|
// stub exposing the soundbank under the global symbol soundbank_bin.
|
|
@@ -1174,7 +1173,7 @@ TEMPLATES.gba = {
|
|
|
1174
1173
|
ext: ".gba",
|
|
1175
1174
|
maxmod: true,
|
|
1176
1175
|
binaryIncludes: ["soundbank.bin"],
|
|
1177
|
-
describe: "Maxmod music demo (Tonc + libmm). Plays a CC0 chiptune.xm soundbank via mmInitDefault + mmStart + mmFrame, with START toggling pause. Pass `maxmod:true` AND `binaryIncludes:{\"soundbank.bin\": <bytes>}` to
|
|
1176
|
+
describe: "Maxmod music demo (Tonc + libmm). Plays a CC0 chiptune.xm soundbank via mmInitDefault + mmStart + mmFrame, with START toggling pause. Pass `maxmod:true` AND `binaryIncludes:{\"soundbank.bin\": <bytes>}` to build({output:'run'}). The .xm source + generator script + pre-built soundbank.bin all ship in the project — edit and re-run mmutil to swap the tune.",
|
|
1178
1177
|
},
|
|
1179
1178
|
};
|
|
1180
1179
|
|
|
@@ -1559,11 +1558,11 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1559
1558
|
const incLines = runtimeHeaders.length > 0
|
|
1560
1559
|
? runtimeHeaders.map((h) => ` "${h.dst}": "${h.dst}",`).join("\n")
|
|
1561
1560
|
: "";
|
|
1562
|
-
buildBlock = "```js\
|
|
1561
|
+
buildBlock = "```js\nbuild({\n output: \"run\",\n platform: \"" + platform + "\",\n sourcesPaths: {\n" + srcLines + "\n },\n" + (incLines ? " includePaths: {\n" + incLines + "\n },\n" : "") + " linkerConfig: /* contents of " + tmpl.linkerConfig.dst + " */,\n frames: 240,\n})\n```";
|
|
1563
1562
|
} else if (isSdccSm83) {
|
|
1564
|
-
// GB / GBC (SDCC sm83).
|
|
1565
|
-
// call AND auto-fixes the cartridge header (Nintendo logo, header +
|
|
1566
|
-
// global checksums, CGB flag on .gbc) — no manual
|
|
1563
|
+
// GB / GBC (SDCC sm83). build({output:'run'}) BUILDS + RUNS + SCREENSHOTS
|
|
1564
|
+
// in one call AND auto-fixes the cartridge header (Nintendo logo, header +
|
|
1565
|
+
// global checksums, CGB flag on .gbc) — no manual header-patch step.
|
|
1567
1566
|
// Derive sources/includes from the template's runtime list so extra
|
|
1568
1567
|
// .c files (e.g. music_demo's hUGEDriver) are listed too.
|
|
1569
1568
|
const runtimeCs = (tmpl?.runtime ?? []).filter((r) => /\.c$/i.test(r.dst));
|
|
@@ -1574,7 +1573,7 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1574
1573
|
? runtimeHeaders.map((h) => ` "${h.dst}": "${h.dst}",`).join("\n")
|
|
1575
1574
|
: "";
|
|
1576
1575
|
buildBlock =
|
|
1577
|
-
"```js\
|
|
1576
|
+
"```js\nbuild({\n output: \"run\",\n" +
|
|
1578
1577
|
" platform: \"" + platform + "\",\n" +
|
|
1579
1578
|
" sourcesPaths: {\n" + srcLines + "\n },\n" +
|
|
1580
1579
|
(incLines ? " includePaths: {\n" + incLines + "\n },\n" : "") +
|
|
@@ -1582,20 +1581,20 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1582
1581
|
" codeLoc: 0x150,\n" +
|
|
1583
1582
|
" frames: 60,\n" +
|
|
1584
1583
|
"})\n```\n\n" +
|
|
1585
|
-
"`
|
|
1586
|
-
"CGB flag) — you do **not** call
|
|
1587
|
-
"ROM. Use `
|
|
1588
|
-
"disk or to override header fields (title, cart type, ROM/RAM size).";
|
|
1584
|
+
"`build({output:\"run\"})` auto-fixes the GB/GBC cartridge header (logo, checksums, " +
|
|
1585
|
+
"CGB flag) — you do **not** call a header patch for a freshly built " +
|
|
1586
|
+
"ROM. Use `romPatch({op:'gbHeader'})` only to fix up an existing/external " +
|
|
1587
|
+
"ROM on disk or to override header fields (title, cart type, ROM/RAM size).";
|
|
1589
1588
|
} else if (isSdccZ80) {
|
|
1590
1589
|
const inc = runtimeHeaders.length > 0
|
|
1591
1590
|
? `\n includePaths: { ${runtimeHeaders.map((h) => `"${h.dst}": "${h.dst}"`).join(", ")} },`
|
|
1592
1591
|
: "";
|
|
1593
|
-
buildBlock = "```js\
|
|
1592
|
+
buildBlock = "```js\nbuild({\n output: \"run\",\n platform: \"" + platform + "\",\n sourcePath: \"" + mainFilename + "\"," + inc + "\n frames: 240,\n})\n```";
|
|
1594
1593
|
} else if (platform === "c64") {
|
|
1595
1594
|
const inc = runtimeHeaders.length > 0
|
|
1596
1595
|
? `\n includePaths: { ${runtimeHeaders.map((h) => `"${h.dst}": "${h.dst}"`).join(", ")} },`
|
|
1597
1596
|
: "";
|
|
1598
|
-
buildBlock = "```js\
|
|
1597
|
+
buildBlock = "```js\nbuild({\n output: \"run\",\n platform: \"c64\",\n sourcePath: \"" + mainFilename + "\"," + inc + "\n frames: 240,\n})\n```";
|
|
1599
1598
|
} else if (platform === "snes" && /\.c$/i.test(mainFilename)) {
|
|
1600
1599
|
// R19b: SNES C-mode template (PVSnesLib runtime auto-linked). Multi-file
|
|
1601
1600
|
// build with sibling .asm providing data symbols (tilfont/palfont).
|
|
@@ -1632,7 +1631,7 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1632
1631
|
.join("\n");
|
|
1633
1632
|
|
|
1634
1633
|
buildBlock =
|
|
1635
|
-
"```js\
|
|
1634
|
+
"```js\nbuild({\n output: \"run\",\n" +
|
|
1636
1635
|
" platform: \"snes\",\n" +
|
|
1637
1636
|
" language: \"c\",\n" +
|
|
1638
1637
|
" sourcesPaths: {\n" + sourceLines + "\n },\n" +
|
|
@@ -1673,12 +1672,12 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1673
1672
|
const incLine = runtimeHs.length > 0
|
|
1674
1673
|
? `\n includePaths: {\n${runtimeHs.map((r) => ` "${r.dst}": "${r.dst}",`).join("\n")}\n },`
|
|
1675
1674
|
: "";
|
|
1676
|
-
buildBlock = "```js\
|
|
1675
|
+
buildBlock = "```js\nbuild({\n output: \"run\",\n platform: \"" + platform + "\",\n language: \"c\",\n sourcesPaths: {\n" + srcLines + "\n }," + incLine + "\n frames: 240,\n})\n```";
|
|
1677
1676
|
} else {
|
|
1678
1677
|
const inc = runtimeAsmIncludes.length > 0
|
|
1679
1678
|
? `\n includePaths: { ${runtimeAsmIncludes.map((r) => `"${r.dst}": "${r.dst}"`).join(", ")} },`
|
|
1680
1679
|
: "";
|
|
1681
|
-
buildBlock = "```js\
|
|
1680
|
+
buildBlock = "```js\nbuild({\n output: \"run\",\n platform: \"" + platform + "\",\n sourcePath: \"" + mainFilename + "\"," + inc + "\n frames: 240,\n})\n```";
|
|
1682
1681
|
}
|
|
1683
1682
|
} else if (platform === "gba") {
|
|
1684
1683
|
// GBA libtonc / libgba runtimes ship gba_sfx.{h,c} as a tiny DMG-APU
|
|
@@ -1693,12 +1692,12 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1693
1692
|
const incLine = runtimeHs.length > 0
|
|
1694
1693
|
? `\n includePaths: {\n${runtimeHs.map((r) => ` "${r.dst}": "${r.dst}",`).join("\n")}\n },`
|
|
1695
1694
|
: "";
|
|
1696
|
-
buildBlock = "```js\
|
|
1695
|
+
buildBlock = "```js\nbuild({\n output: \"run\",\n platform: \"gba\",\n language: \"c\",\n sourcesPaths: {\n" + srcLines + "\n }," + incLine + "\n frames: 240,\n})\n```";
|
|
1697
1696
|
} else {
|
|
1698
|
-
buildBlock = "```js\
|
|
1697
|
+
buildBlock = "```js\nbuild({ output: \"run\", platform: \"gba\", language: \"c\", sourcePath: \"" + mainFilename + "\", frames: 240 })\n```";
|
|
1699
1698
|
}
|
|
1700
1699
|
} else {
|
|
1701
|
-
buildBlock = "```js\
|
|
1700
|
+
buildBlock = "```js\nbuild({ output: \"run\", platform: \"" + platform + "\", sourcePath: \"" + mainFilename + "\", frames: 240 })\n```";
|
|
1702
1701
|
}
|
|
1703
1702
|
|
|
1704
1703
|
let filesSection = `## Files\n\n- \`${mainFilename}\` — your game's entry point.\n`;
|
|
@@ -1715,7 +1714,7 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1715
1714
|
}
|
|
1716
1715
|
filesSection += `\nEvery byte that compiles into your ROM is in this directory. If you move the repo somewhere else, you don't need to install anything from romdev to rebuild it — the compiler binaries are the only external dependency.\n\n`;
|
|
1717
1716
|
|
|
1718
|
-
const readme = `# ${title ?? name}\n\nA ${lang} project for ${platform}, scaffolded by romdev.\n\n${tmpl?.describe ? tmpl.describe + "\n\n" : ""}${filesSection}${c89Note}## Build + run with romdev\n\n${buildBlock}\n\n## Iterating\n\n- Edit \`${mainFilename}\` (or any of the runtime / crt0 / cfg files — they're yours).\n- Call \`
|
|
1717
|
+
const readme = `# ${title ?? name}\n\nA ${lang} project for ${platform}, scaffolded by romdev.\n\n${tmpl?.describe ? tmpl.describe + "\n\n" : ""}${filesSection}${c89Note}## Build + run with romdev\n\n${buildBlock}\n\n## Iterating\n\n- Edit \`${mainFilename}\` (or any of the runtime / crt0 / cfg files — they're yours).\n- Call \`build({output:"run", ...})\` to see your changes. It builds + loads + runs + screenshots in one round trip.\n- Inspect at byte level: \`memory({op:"read"})\`, \`sprites({op:"inspect"})\`, \`palette({source:"live"})\`, \`background({view:"rendered"})\`.\n- Open a playtest window for human eyes: \`playtest({op:"open"})\` — returns immediately, the window follows your rebuilds, and the emulator stays live for every other tool.\n`;
|
|
1719
1718
|
await fs.writeFile(path.join(projPath, "README.md"), readme, "utf-8");
|
|
1720
1719
|
writtenFiles.push("README.md");
|
|
1721
1720
|
|
|
@@ -1784,7 +1783,7 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1784
1783
|
snippetsCopied: withSnippets ? snippetFiles : null,
|
|
1785
1784
|
sourceFile: path.join(projPath, mainFilename),
|
|
1786
1785
|
toolchain: lang,
|
|
1787
|
-
nextStep: `Edit ${path.join(projPath, mainFilename)} and call
|
|
1786
|
+
nextStep: `Edit ${path.join(projPath, mainFilename)} and call build({output:"run", ...}) with sourcesPaths/includePaths pointing at the project's files (see the README's "Build + run" block for the exact call). Everything you need is in the directory — nothing is hidden.`,
|
|
1788
1787
|
};
|
|
1789
1788
|
}
|
|
1790
1789
|
|
|
@@ -1811,10 +1810,21 @@ async function createGameCore({ platform, genre, name, path: projPath, title, ov
|
|
|
1811
1810
|
? CANONICAL_GENRES.filter((g) => platformTemplates[g])
|
|
1812
1811
|
: [];
|
|
1813
1812
|
if (availableGenres.length === 0) {
|
|
1813
|
+
// Point at the real, working project templates each genre-less
|
|
1814
|
+
// platform actually ships, so the agent has a concrete next step
|
|
1815
|
+
// instead of a bare "default".
|
|
1816
|
+
const PROJECT_TEMPLATE_HINTS = {
|
|
1817
|
+
msx: "default, sprite_move, music_sfx, catch_game",
|
|
1818
|
+
pce: "default, sprite_move, music_sfx, catch_game",
|
|
1819
|
+
atari2600: "default, single_screen, paddle, mini_invaders, music_demo",
|
|
1820
|
+
};
|
|
1821
|
+
const hint = PROJECT_TEMPLATE_HINTS[platform];
|
|
1814
1822
|
throw new Error(
|
|
1815
1823
|
`createGame: no genre scaffolds for platform '${platform}' yet. ` +
|
|
1816
1824
|
`Supported platforms: ${genrePlatforms.join(", ") || "(none)"}. ` +
|
|
1817
|
-
|
|
1825
|
+
(hint
|
|
1826
|
+
? `For ${platform}, use createProject({platform:"${platform}", template:"..."}) with one of: ${hint}.`
|
|
1827
|
+
: `For other platforms, use createProject({platform, template:"default"}) and build up from there.`)
|
|
1818
1828
|
);
|
|
1819
1829
|
}
|
|
1820
1830
|
if (!availableGenres.includes(genre)) {
|
|
@@ -1890,50 +1900,6 @@ export function registerProjectTools(server, z) {
|
|
|
1890
1900
|
}),
|
|
1891
1901
|
);
|
|
1892
1902
|
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
"Use this to write a complete, valid GB/GBC cartridge header into a ROM: Nintendo boot logo, EVERY " +
|
|
1896
|
-
"header byte ($0134-$014C — title, CGB flag, cart type, ROM/RAM size, etc.) with ROM-only defaults, " +
|
|
1897
|
-
"plus the header + global checksums. SDCC-path equivalent of `rgbfix -v -p 0`. Fills ALL bytes " +
|
|
1898
|
-
"deliberately: leaving the CGB flag as the linker's $FF pad makes gambatte enter CGB mode and ignore " +
|
|
1899
|
-
"DMG palette writes → white screen. Also shipped as `patch-header.js` in every GB/GBC project for use " +
|
|
1900
|
-
"outside MCP.",
|
|
1901
|
-
{
|
|
1902
|
-
path: z.string().describe("Absolute path to the .gb / .gbc ROM file. Patched in place unless outputPath is given."),
|
|
1903
|
-
outputPath: z.string().optional().describe("If given, write the patched ROM here instead of overwriting."),
|
|
1904
|
-
cgb: z.boolean().optional().describe("If true, sets the CGB flag at $0143 to $80 (CGB-aware + DMG-compatible). If omitted, auto-detects from .gbc extension; default for plain .gb is false (DMG-only)."),
|
|
1905
|
-
title: z.string().optional().describe("Cartridge title, up to 11 chars at $0134..$013E. Uppercased + zero-padded. Default = zero-fill."),
|
|
1906
|
-
cartType: z.number().int().min(0).max(0xFF).optional().describe("Cart-type byte at $0147. Default $00 (ROM-only). Common alternatives: $01=MBC1, $03=MBC1+RAM+BAT, $11=MBC3, $13=MBC3+RAM+BAT, $19=MBC5."),
|
|
1907
|
-
romSize: z.number().int().min(0).max(0xFF).optional().describe("ROM-size byte at $0148. Default $00 (32 KB / 2 banks). 1=64KB, 2=128KB, 3=256KB, 4=512KB, 5=1MB, 6=2MB, 7=4MB."),
|
|
1908
|
-
ramSize: z.number().int().min(0).max(0xFF).optional().describe("RAM-size byte at $0149. Default $00 (none). $02=8KB, $03=32KB. Only meaningful with battery-backed MBC."),
|
|
1909
|
-
destination: z.number().int().min(0).max(0xFF).optional().describe("Destination at $014A. Default $01 (non-Japan). $00 = Japan."),
|
|
1910
|
-
},
|
|
1911
|
-
safeTool(async ({ path: inPath, outputPath, cgb, title, cartType, romSize, ramSize, destination }) => {
|
|
1912
|
-
const rom = new Uint8Array(await readFile(inPath));
|
|
1913
|
-
const cgbFlag = cgb ?? (/\.gbc$/i.test(inPath) || (outputPath && /\.gbc$/i.test(outputPath)));
|
|
1914
|
-
patchGbHeader(rom, { cgb: cgbFlag, title, cartType, romSize, ramSize, destination });
|
|
1915
|
-
const outPath = outputPath ?? inPath;
|
|
1916
|
-
await writeFile(outPath, rom);
|
|
1917
|
-
return jsonContent({
|
|
1918
|
-
path: outPath,
|
|
1919
|
-
bytes: rom.length,
|
|
1920
|
-
cgb: !!cgbFlag,
|
|
1921
|
-
patched: [
|
|
1922
|
-
"nintendo_logo@$0104..$0133",
|
|
1923
|
-
"title@$0134..$013E",
|
|
1924
|
-
`cgb_flag@$0143=${cgbFlag ? "$80" : "$00"}`,
|
|
1925
|
-
"licensee@$0144..$0145=$00$00",
|
|
1926
|
-
"sgb_flag@$0146=$00",
|
|
1927
|
-
`cart_type@$0147=$${(cartType ?? 0).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
1928
|
-
`rom_size@$0148=$${(romSize ?? 0).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
1929
|
-
`ram_size@$0149=$${(ramSize ?? 0).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
1930
|
-
`destination@$014A=$${(destination ?? 1).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
1931
|
-
"old_licensee@$014B=$33",
|
|
1932
|
-
"rom_version@$014C=$00",
|
|
1933
|
-
"header_checksum@$014D",
|
|
1934
|
-
"global_checksum@$014E..$014F",
|
|
1935
|
-
],
|
|
1936
|
-
});
|
|
1937
|
-
}),
|
|
1938
|
-
);
|
|
1903
|
+
// patchGbHeader was folded into romPatch({op:'gbHeader'}) (rom-id.js) — it's a
|
|
1904
|
+
// ROM-file patch op, same family as romPatch's other ops, not a scaffold tool.
|
|
1939
1905
|
}
|
package/src/mcp/tools/rom-id.js
CHANGED
|
@@ -30,6 +30,41 @@ export async function patchRomCore({ input, output, writes, allowExpand }) {
|
|
|
30
30
|
return await patchRomFile({ input, output, writes, allowExpand });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// romPatch({op:'gbHeader'}) — write a complete valid GB/GBC cartridge header
|
|
34
|
+
// (logo + every header byte + header/global checksums) into a ROM file. Folded
|
|
35
|
+
// in from the old standalone patchGbHeader tool. Also shipped as patch-header.js
|
|
36
|
+
// in every GB/GBC project for use outside romdev.
|
|
37
|
+
export async function gbHeaderCore({ path: inPath, outputPath, cgb, title, cartType, romSize, ramSize, destination }) {
|
|
38
|
+
if (!inPath) throw new Error("romPatch({op:'gbHeader'}): `path` (the .gb/.gbc ROM) is required.");
|
|
39
|
+
const { readFile, writeFile } = await import("node:fs/promises");
|
|
40
|
+
const { patchGbHeader } = await import("../../platforms/gb/lib/c/patch-header.js");
|
|
41
|
+
const rom = new Uint8Array(await readFile(inPath));
|
|
42
|
+
const cgbFlag = cgb ?? (/\.gbc$/i.test(inPath) || (outputPath && /\.gbc$/i.test(outputPath)));
|
|
43
|
+
patchGbHeader(rom, { cgb: cgbFlag, title, cartType, romSize, ramSize, destination });
|
|
44
|
+
const outPath = outputPath ?? inPath;
|
|
45
|
+
await writeFile(outPath, rom);
|
|
46
|
+
return {
|
|
47
|
+
path: outPath,
|
|
48
|
+
bytes: rom.length,
|
|
49
|
+
cgb: !!cgbFlag,
|
|
50
|
+
patched: [
|
|
51
|
+
"nintendo_logo@$0104..$0133",
|
|
52
|
+
"title@$0134..$013E",
|
|
53
|
+
`cgb_flag@$0143=${cgbFlag ? "$80" : "$00"}`,
|
|
54
|
+
"licensee@$0144..$0145=$00$00",
|
|
55
|
+
"sgb_flag@$0146=$00",
|
|
56
|
+
`cart_type@$0147=$${(cartType ?? 0).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
57
|
+
`rom_size@$0148=$${(romSize ?? 0).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
58
|
+
`ram_size@$0149=$${(ramSize ?? 0).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
59
|
+
`destination@$014A=$${(destination ?? 1).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
60
|
+
"old_licensee@$014B=$33",
|
|
61
|
+
"rom_version@$014C=$00",
|
|
62
|
+
"header_checksum@$014D",
|
|
63
|
+
"global_checksum@$014E..$014F",
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
export function registerRomIdTools(server, z, sessionKey) {
|
|
34
69
|
// identifyRom folded into `cart`; patchFile/patchRom/spliceCHR/relocate/etc.
|
|
35
70
|
// folded into the `romPatch` tool (router below).
|
|
@@ -48,7 +83,8 @@ export function registerRomIdTools(server, z, sessionKey) {
|
|
|
48
83
|
"makeStored → {rawHex|rawBytes, format, interleave?}; " +
|
|
49
84
|
"findFree → {minLength, fillBytes?, start?, end?, maxRunsReturned?}; " +
|
|
50
85
|
"findPointer → {romOffset, mapper?, widths?, suppressShadows?, maxHitsReturned?}; " +
|
|
51
|
-
"diff → {a, b, maxChangesReturned?}
|
|
86
|
+
"diff → {a, b, maxChangesReturned?}; " +
|
|
87
|
+
"gbHeader → {path, outputPath?, cgb?, title?, cartType?, romSize?, ramSize?, destination?}.\n" +
|
|
52
88
|
"• op:'write' — write N bytes into any binary file at `offset` (the generic splicer: PRG patches, CHR splices, SNES tile/sample injection). `allowExpand` grows the file — default OFF; most hacks must NOT change size or headers/mapper break. `outputPath` else writes in place.\n" +
|
|
53
89
|
"• op:'writeMany' — apply a LIST of {offset, hex|base64} `writes` from `input` ROM to `output`.\n" +
|
|
54
90
|
"• op:'spliceCHR' — inject a PNG's tiles into a CHR region.\n" +
|
|
@@ -56,10 +92,11 @@ export function registerRomIdTools(server, z, sessionKey) {
|
|
|
56
92
|
"• op:'makeStored' — wrap raw bytes so the game's OWN decompressor expands them VERBATIM (edit tiles → makeStored → write, no compressor needed). `format` (raw/lz77-literal/lz2-direct/sega-rle/konami-rle/packbits/kosinski-literal; invalid → returns the platform's list). ALWAYS verify via cpu({op:'call'}) on the game's decompressor.\n" +
|
|
57
93
|
"• op:'findFree' — find a run of free space to relocate into (`fillBytes` defaults to [0xFF, 0x00]).\n" +
|
|
58
94
|
"• op:'findPointer' — find every pointer in the ROM that references `romOffset` (platform-correct encoding), the missing piece for redirecting a loader. `mapper` overrides SNES detection. On wide systems (Genesis/GBA) a 32-bit hit's low bytes also match the narrower form one byte over — those tail SHADOWS are suppressed by default (count in `shadowsSuppressed`); pass `suppressShadows:false` for raw, or `widths:[4]` to search only 32-bit forms. On banked 8-bit systems a 16-bit pointer is page-ambiguous — correlate with the bank-set instruction.\n" +
|
|
59
|
-
"• op:'diff' — diff two ROMs (`a`, `b`) → the changed byte ranges
|
|
95
|
+
"• op:'diff' — diff two ROMs (`a`, `b`) → the changed byte ranges.\n" +
|
|
96
|
+
"• op:'gbHeader' — GAME BOY / GBC ONLY: write a complete, valid GB/GBC cartridge header into a ROM at `path` — Nintendo boot logo, every header byte ($0134-$014C: title, CGB flag, cart type, ROM/RAM size, …) with ROM-only defaults, plus the header + global checksums. The SDCC-path equivalent of `rgbfix -v -p 0`, for fixing up an externally built / hand-assembled GB ROM. (A normal build({output:'rom'/'run'}) already does this — you do NOT call gbHeader on a freshly built ROM.) Leaving the CGB flag as the linker's $FF pad makes gambatte enter CGB mode and white-screen, so this fills it deliberately.",
|
|
60
97
|
{
|
|
61
|
-
op: z.enum(["write", "writeMany", "spliceCHR", "relocate", "makeStored", "findFree", "findPointer", "diff"])
|
|
62
|
-
.describe("write=N bytes at an offset; writeMany=a list of writes; spliceCHR=PNG tiles into CHR; relocate=write a block to free space + repoint; makeStored=wrap bytes for the game's decompressor; findFree=find free space; findPointer=find pointers to an offset; diff=diff two ROMs."),
|
|
98
|
+
op: z.enum(["write", "writeMany", "spliceCHR", "relocate", "makeStored", "findFree", "findPointer", "diff", "gbHeader"])
|
|
99
|
+
.describe("write=N bytes at an offset; writeMany=a list of writes; spliceCHR=PNG tiles into CHR; relocate=write a block to free space + repoint; makeStored=wrap bytes for the game's decompressor; findFree=find free space; findPointer=find pointers to an offset; diff=diff two ROMs; gbHeader=write a valid GB/GBC cartridge header + checksums."),
|
|
63
100
|
path: z.string().optional().describe("op:write/spliceCHR/relocate/findFree/findPointer — absolute path to the ROM/file."),
|
|
64
101
|
platform: z.enum(PLATFORMS).optional().describe("op:findPointer/relocate/makeStored/spliceCHR/diff — platform (inferred from extension except makeStored, which requires it)."),
|
|
65
102
|
offset: z.number().int().min(0).optional().describe("op:write — file offset to write at (NOT a CPU address)."),
|
|
@@ -112,6 +149,13 @@ export function registerRomIdTools(server, z, sessionKey) {
|
|
|
112
149
|
a: z.string().optional().describe("op:diff — path to ROM A."),
|
|
113
150
|
b: z.string().optional().describe("op:diff — path to ROM B."),
|
|
114
151
|
maxChangesReturned: z.number().int().min(1).max(2048).default(256).describe("op:diff — cap the change ranges returned."),
|
|
152
|
+
// gbHeader (path + outputPath reuse the spine fields above)
|
|
153
|
+
cgb: z.boolean().optional().describe("op:gbHeader — if true, sets the CGB flag at $0143 to $80 (CGB-aware + DMG-compatible). If omitted, auto-detects from a .gbc extension; default for plain .gb is false (DMG-only)."),
|
|
154
|
+
title: z.string().optional().describe("op:gbHeader — cartridge title, up to 11 chars at $0134..$013E. Uppercased + zero-padded. Default = zero-fill."),
|
|
155
|
+
cartType: z.number().int().min(0).max(0xFF).optional().describe("op:gbHeader — cart-type byte at $0147. Default $00 (ROM-only). Common: $01=MBC1, $03=MBC1+RAM+BAT, $11=MBC3, $13=MBC3+RAM+BAT, $19=MBC5."),
|
|
156
|
+
romSize: z.number().int().min(0).max(0xFF).optional().describe("op:gbHeader — ROM-size byte at $0148. Default $00 (32 KB / 2 banks). 1=64KB, 2=128KB, 3=256KB, 4=512KB, 5=1MB, 6=2MB, 7=4MB."),
|
|
157
|
+
ramSize: z.number().int().min(0).max(0xFF).optional().describe("op:gbHeader — RAM-size byte at $0149. Default $00 (none). $02=8KB, $03=32KB. Only meaningful with battery-backed MBC."),
|
|
158
|
+
destination: z.number().int().min(0).max(0xFF).optional().describe("op:gbHeader — destination at $014A. Default $01 (non-Japan). $00 = Japan."),
|
|
115
159
|
},
|
|
116
160
|
safeTool(async (args) => {
|
|
117
161
|
switch (args.op) {
|
|
@@ -135,6 +179,7 @@ export function registerRomIdTools(server, z, sessionKey) {
|
|
|
135
179
|
if (!args.a || !args.b) throw new Error("romPatch({op:'diff'}): `a` and `b` (the two ROM paths) are required.");
|
|
136
180
|
return jsonContent(await diffRomsCore({ ...args, aPath: args.a, bPath: args.b }));
|
|
137
181
|
}
|
|
182
|
+
case "gbHeader": return jsonContent(await gbHeaderCore(args));
|
|
138
183
|
default: throw new Error(`romPatch: unknown op '${args.op}'`);
|
|
139
184
|
}
|
|
140
185
|
}),
|
|
@@ -254,7 +254,7 @@ export function registerTileInspectTools(server, z, sessionKey) {
|
|
|
254
254
|
tilePath: z.string().optional().describe("op:preview — path to a tile dump (raw) or iNES ROM (NES auto-locates CHR)."),
|
|
255
255
|
fromEmulator: z.boolean().optional().describe("op:preview — read tiles from the running emulator's live VRAM (tileStart/tileCount pick the range). Genesis byte-swap handled. Mutually exclusive with tileBytes/tilePath."),
|
|
256
256
|
tileStart: z.number().int().min(0).optional().describe("op:preview — starting tile index in the source."),
|
|
257
|
-
byteOffset: z.number().int().min(0).optional().describe("op:preview — start at a raw BYTE offset instead of a tile index (pass a
|
|
257
|
+
byteOffset: z.number().int().min(0).optional().describe("op:preview — start at a raw BYTE offset instead of a tile index (pass a watch({on:'dma'}) / disasm-references source directly). WARNS on misalignment. Takes precedence over tileStart."),
|
|
258
258
|
palette: z.array(z.any()).optional().describe("op:preview — explicit palette (NES: 4 master indices; others: RGB triples or indices)."),
|
|
259
259
|
palettePath: z.string().optional().describe("op:preview — raw palette dump from disk."),
|
|
260
260
|
// shared output
|