romdevtools 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +15 -4
- package/CHANGELOG.md +66 -0
- package/examples/atari7800/templates/hello_sprite.c +48 -4
- package/examples/atari7800/templates/music_demo.c +47 -2
- package/examples/c64/templates/tile_engine.c +77 -27
- package/examples/gb/templates/hello_sprite.c +15 -6
- package/examples/gb/templates/music_demo.c +36 -0
- package/examples/gb/templates/platformer.c +3 -2
- package/examples/gb/templates/puzzle.c +3 -2
- package/examples/gb/templates/racing.c +3 -2
- package/examples/gb/templates/shmup.c +3 -2
- package/examples/gb/templates/sports.c +3 -2
- package/examples/gb/templates/tile_engine.c +3 -2
- package/examples/gba/templates/maxmod_demo.c +36 -2
- package/examples/gba/templates/platformer.c +3 -1
- package/examples/gba/templates/tonc_hello_sprite.c +35 -1
- package/examples/gbc/templates/hello_sprite.c +12 -3
- package/examples/gbc/templates/music_demo.c +56 -12
- package/examples/gbc/templates/platformer.c +3 -2
- package/examples/gbc/templates/puzzle.c +3 -2
- package/examples/gbc/templates/racing.c +3 -2
- package/examples/gbc/templates/shmup.c +3 -2
- package/examples/gbc/templates/sports.c +3 -2
- package/examples/gbc/templates/tile_engine.c +3 -2
- package/examples/genesis/main.s +53 -1
- package/examples/genesis/templates/hello_sprite.c +25 -3
- package/examples/genesis/templates/shmup_2p.c +31 -0
- package/examples/genesis/templates/xgm2_demo.c +20 -0
- package/examples/gg/templates/hello_sprite.c +25 -2
- package/examples/gg/templates/music_demo.c +24 -2
- package/examples/gg/templates/racing.c +7 -4
- package/examples/gg/templates/sports.c +11 -13
- package/examples/gg/templates/tile_engine.c +12 -6
- package/examples/lynx/templates/hello_sprite.c +15 -1
- package/examples/lynx/templates/music_demo.c +13 -1
- package/examples/nes/templates/hello_sprite.c +35 -0
- package/examples/nes/templates/music_demo.c +40 -0
- package/examples/pce/catch_game/main.c +22 -3
- package/examples/pce/music_sfx/main.c +28 -1
- package/examples/pce/sprite_move/main.c +7 -2
- package/examples/sms/templates/hello_sprite.c +29 -3
- package/examples/sms/templates/music_demo.c +18 -4
- package/examples/sms/templates/shmup_2p.c +24 -1
- package/examples/sms/templates/sports.c +4 -2
- package/examples/snes/main.asm +108 -17
- package/examples/snes/templates/c-hello-data.asm +23 -0
- package/examples/snes/templates/c-hello.c +18 -1
- package/examples/snes/templates/hello_sprite-data.asm +23 -0
- package/examples/snes/templates/hello_sprite.c +17 -1
- package/examples/snes/templates/music_demo-data.asm +23 -0
- package/examples/snes/templates/music_demo.c +22 -4
- package/examples/snes/templates/platformer.c +4 -1
- package/examples/snes/templates/puzzle.c +4 -1
- package/package.json +1 -1
- package/src/cheats/gamegenie.js +0 -1
- package/src/cli/smoke.js +1 -3
- package/src/host/LibretroHost.js +69 -15
- package/src/host/chafa-render.js +2 -0
- package/src/host/dsp-state.js +2 -2
- package/src/host/gpgx-state.js +4 -0
- package/src/http/routes.js +1 -1
- package/src/mcp/server.js +1 -1
- package/src/mcp/state.js +36 -0
- package/src/mcp/tools/address-to-symbol.js +0 -1
- package/src/mcp/tools/art-loaders.js +1 -1
- package/src/mcp/tools/cart-parts.js +0 -1
- package/src/mcp/tools/classify-region.js +1 -1
- package/src/mcp/tools/diff-roms.js +1 -1
- package/src/mcp/tools/disasm-rebuild.js +1 -1
- package/src/mcp/tools/disasm.js +2 -3
- package/src/mcp/tools/find-references.js +1 -2
- package/src/mcp/tools/font-map.js +1 -1
- package/src/mcp/tools/index.js +0 -49
- package/src/mcp/tools/input-layout.js +0 -1
- package/src/mcp/tools/input.js +33 -3
- package/src/mcp/tools/lifecycle.js +14 -2
- package/src/mcp/tools/lospec.js +0 -19
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/platform-tools.js +4 -4
- package/src/mcp/tools/project.js +0 -2
- package/src/mcp/tools/reinject.js +0 -1
- package/src/mcp/tools/rom-id.js +2 -2
- package/src/mcp/tools/snippets.js +2 -2
- package/src/mcp/tools/sprite-pipeline.js +1 -2
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +29 -9
- package/src/mcp/tools/watch-memory.js +13 -3
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
- package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
- package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/d64.js +0 -1
- package/src/platforms/c64/sid.js +0 -2
- package/src/platforms/common/metasprite-adapters.js +1 -1
- package/src/platforms/common/metasprite-codegen.js +3 -3
- package/src/platforms/common/registers.js +5 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
- package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
- package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/image-to-tilemap.js +3 -0
- package/src/platforms/nes/lib/asm/famitone2.s +5 -1
- package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
- package/src/platforms/snes/brr.js +0 -2
- package/src/playtest/playtest.js +0 -7
- package/src/toolchains/asar/asar.js +0 -9
- package/src/toolchains/assemble-snippet.js +30 -12
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
- package/src/toolchains/common/reassemble.js +0 -1
- package/src/toolchains/common/sdk-cache.js +1 -1
- package/src/toolchains/genesis-c/genesis-c.js +5 -3
- package/src/toolchains/index.js +27 -3
- package/src/toolchains/parse-errors.js +78 -1
- package/src/toolchains/sdcc/preflight-lint.js +5 -1
- package/src/toolchains/sdcc/sdcc.js +1 -1
- package/src/toolchains/sjasm/sjasm.js +1 -1
- package/src/toolchains/snes-c/snes-c.js +2 -2
- package/src/toolchains/vasm68k/vasm68k.js +2 -4
- package/src/toolchains/wladx/wladx.js +1 -1
package/src/host/chafa-render.js
CHANGED
|
@@ -24,6 +24,7 @@ let lastSettings = "";
|
|
|
24
24
|
// I had BLOCK=1, ASCII=2; real BLOCK=0x8, ASCII=0x4000. The bad
|
|
25
25
|
// values silently picked an unrelated tag, which is why "ascii"
|
|
26
26
|
// mode was still rendering Unicode block glyphs.
|
|
27
|
+
/* eslint-disable no-unused-vars -- the full chafa tag enum is kept for reference; not all are used. */
|
|
27
28
|
const TAG_SPACE = 0x1;
|
|
28
29
|
const TAG_SOLID = 0x2;
|
|
29
30
|
const TAG_STIPPLE = 0x4;
|
|
@@ -35,6 +36,7 @@ const TAG_BRAILLE = 0x800;
|
|
|
35
36
|
const TAG_ASCII = 0x4000;
|
|
36
37
|
const TAG_SEXTANT = 0x400000;
|
|
37
38
|
const TAG_OCTANT = 0x4000000;
|
|
39
|
+
/* eslint-enable no-unused-vars */
|
|
38
40
|
|
|
39
41
|
const SYMBOL_TAGS = {
|
|
40
42
|
// Pure ASCII glyphs (space + printable 7-bit) — most text-shaped,
|
package/src/host/dsp-state.js
CHANGED
|
@@ -102,8 +102,8 @@ export function decodeSnes9xDSP(state) {
|
|
|
102
102
|
bufLastSamples.push(u >= 0x8000 ? u - 0x10000 : u);
|
|
103
103
|
}
|
|
104
104
|
p += 24;
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
p += 2; // interpPos (decoded field, not surfaced)
|
|
106
|
+
p += 2; // brrAddr (decoded field, not surfaced)
|
|
107
107
|
const env = state[p] | (state[p + 1] << 8); p += 2;
|
|
108
108
|
const hiddenEnvU = state[p] | (state[p + 1] << 8); p += 2;
|
|
109
109
|
const hiddenEnv = hiddenEnvU >= 0x8000 ? hiddenEnvU - 0x10000 : hiddenEnvU;
|
package/src/host/gpgx-state.js
CHANGED
|
@@ -17,7 +17,11 @@ const formatCpuState = (s) => s;
|
|
|
17
17
|
// entry is 5 × 4 = 20 bytes, so memory_map takes 256 × 20 = 5120 bytes.
|
|
18
18
|
// After that come the fields we want.
|
|
19
19
|
const M68K_BASE = 5120; // start of cpu_idle_t poll
|
|
20
|
+
// M68K_POLL / M68K_CYCLES document the struct layout (consumed implicitly by the
|
|
21
|
+
// next offset) — keep them named even though nothing reads them directly.
|
|
22
|
+
// eslint-disable-next-line no-unused-vars
|
|
20
23
|
const M68K_POLL = M68K_BASE + 0; // 12 bytes
|
|
24
|
+
// eslint-disable-next-line no-unused-vars
|
|
21
25
|
const M68K_CYCLES = M68K_BASE + 12; // 12 bytes (cycles + refresh_cycles + cycle_end)
|
|
22
26
|
const M68K_DAR = M68K_BASE + 24; // uint dar[16] — D0..D7 then A0..A7
|
|
23
27
|
const M68K_PC = M68K_DAR + 64; // uint pc
|
package/src/http/routes.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// (the app already mounts localhostHostValidation()).
|
|
19
19
|
|
|
20
20
|
import { buildToolRegistry, runTool, toolJsonSchema } from "./tool-registry.js";
|
|
21
|
-
import {
|
|
21
|
+
import { buildSkillDoc } from "./skill-doc.js";
|
|
22
22
|
import { swaggerHtml, swaggerAsset } from "./swagger.js";
|
|
23
23
|
import { observer } from "../observer/bus.js";
|
|
24
24
|
import { log } from "../mcp/log.js";
|
package/src/mcp/server.js
CHANGED
|
@@ -140,7 +140,7 @@ async function main() {
|
|
|
140
140
|
// in verbose mode (log.debug handles that split). This keeps the console
|
|
141
141
|
// quiet in prod while /log stays rich enough to diagnose what an agent did.
|
|
142
142
|
if (req.method === "POST" && req.body) {
|
|
143
|
-
const { method,
|
|
143
|
+
const { method, params } = req.body;
|
|
144
144
|
if (method === "tools/call") {
|
|
145
145
|
const argKeys = params?.arguments ? Object.keys(params.arguments) : [];
|
|
146
146
|
const summary = argKeys.map((k) => {
|
package/src/mcp/state.js
CHANGED
|
@@ -14,6 +14,24 @@ import { LibretroHost } from "../host/index.js";
|
|
|
14
14
|
/** @type {Map<string, LibretroHost>} */
|
|
15
15
|
const hosts = new Map();
|
|
16
16
|
|
|
17
|
+
// What this session last loaded, kept OUTSIDE the host map so it SURVIVES a
|
|
18
|
+
// host eviction (server restart / session reconnect / unload). The host itself
|
|
19
|
+
// is gone in those cases, so the "No ROM loaded" error has nothing to read —
|
|
20
|
+
// this is the breadcrumb that lets the error tell the agent exactly how to
|
|
21
|
+
// recover ("you last loaded <X>; re-run loadMedia to pick back up") instead of
|
|
22
|
+
// a generic wipe. Set by loadMedia on success; never cleared on eviction.
|
|
23
|
+
/** @type {Map<string, {platform?: string, path?: string, fromBase64?: boolean}>} */
|
|
24
|
+
const lastMedia = new Map();
|
|
25
|
+
|
|
26
|
+
/** Record the media a session last loaded (for recovery hints). @param {string} sessionKey */
|
|
27
|
+
export function rememberLastMedia(sessionKey, info) {
|
|
28
|
+
lastMedia.set(sessionKey, info);
|
|
29
|
+
}
|
|
30
|
+
/** @param {string} sessionKey */
|
|
31
|
+
export function getLastMedia(sessionKey) {
|
|
32
|
+
return lastMedia.get(sessionKey) ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* @param {string} sessionKey
|
|
19
37
|
* @returns {LibretroHost}
|
|
@@ -21,6 +39,24 @@ const hosts = new Map();
|
|
|
21
39
|
export function getHost(sessionKey) {
|
|
22
40
|
const host = hosts.get(sessionKey);
|
|
23
41
|
if (!host) {
|
|
42
|
+
// If THIS session loaded media before, the host was evicted (restart /
|
|
43
|
+
// reconnect / unload) — lead with the exact recovery call instead of the
|
|
44
|
+
// generic "you're in the wrong session" guidance, which doesn't apply here.
|
|
45
|
+
const prev = lastMedia.get(sessionKey);
|
|
46
|
+
if (prev && (prev.path || prev.fromBase64)) {
|
|
47
|
+
const recall = prev.path
|
|
48
|
+
? `loadMedia({ platform: "${prev.platform}", path: "${prev.path}" })`
|
|
49
|
+
: `loadMedia({ platform: "${prev.platform}", base64: ... }) (your ROM came from base64 — re-supply the bytes)`;
|
|
50
|
+
throw new Error(
|
|
51
|
+
"No ROM loaded in this session — the host was evicted (the server restarted, " +
|
|
52
|
+
"your session reconnected, or the media was unloaded). Emulator state lives in " +
|
|
53
|
+
"server memory only, so it did not survive. RECOVER by re-running your last load:\n " +
|
|
54
|
+
recall +
|
|
55
|
+
"\nThen replay any boot/navigate steps to get back to where you were. " +
|
|
56
|
+
"(If instead you expected a DIFFERENT session, you may be sending an inconsistent " +
|
|
57
|
+
"`x-romdev-session` header — reuse one stable id on every call.)",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
24
60
|
throw new Error(
|
|
25
61
|
"No ROM loaded in this session — call loadMedia({path}) first. " +
|
|
26
62
|
"If you DID loadMedia and still see this, your calls are landing in DIFFERENT " +
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
// - Future: vasm listings, asar symbol tables
|
|
19
19
|
|
|
20
20
|
import { readFile } from "node:fs/promises";
|
|
21
|
-
import { jsonContent, safeTool } from "../util.js";
|
|
22
21
|
import { parseGnuLdMap, isGnuLdMap } from "../../toolchains/gnu-ld-map.js";
|
|
23
22
|
|
|
24
23
|
function parseSdldStyle(text) {
|
|
@@ -407,7 +407,7 @@ function aseCelToRgba(cel, colorDepth, palette) {
|
|
|
407
407
|
return rgba;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
async function loadAsepriteSheetImpl({ path: asePath, platform,
|
|
410
|
+
async function loadAsepriteSheetImpl({ path: asePath, platform, _tile_size = 8, outputDir, slice_strategy = "slices", emit = "raw", emitDefines = false }) {
|
|
411
411
|
const buf = await readFile(asePath);
|
|
412
412
|
const ase = new Aseprite(buf, path.basename(asePath));
|
|
413
413
|
ase.parse();
|
|
@@ -131,7 +131,6 @@ function extractNes(data) {
|
|
|
131
131
|
*/
|
|
132
132
|
function extractSnes(data) {
|
|
133
133
|
const copierOff = (data.length % 0x8000 === 0x200) ? 0x200 : 0;
|
|
134
|
-
const loMapper = data[copierOff + 0x7FC0 + 0x15];
|
|
135
134
|
const hiMapper = data[copierOff + 0xFFC0 + 0x15];
|
|
136
135
|
const isLo = !(hiMapper === 0x21 || hiMapper === 0x31);
|
|
137
136
|
const internalHeaderBase = copierOff + (isLo ? 0x7FC0 : 0xFFC0);
|
|
@@ -51,7 +51,7 @@ function zeroRatio(bytes) {
|
|
|
51
51
|
* @param {boolean} [opts.bigEndian] platform endianness (for pointer-table guess)
|
|
52
52
|
* @returns {{ looksLike: string, printableRatio:number, entropy:number, zeroRatio:number, longestAsciiRun:number, asciiPreview:string|null, confidence:string, note:string }}
|
|
53
53
|
*/
|
|
54
|
-
export function classifyBytes(bytes,
|
|
54
|
+
export function classifyBytes(bytes, _opts = {}) {
|
|
55
55
|
const pr = printableRatio(bytes);
|
|
56
56
|
const ent = entropy(bytes);
|
|
57
57
|
const zr = zeroRatio(bytes);
|
|
@@ -49,7 +49,7 @@ function regionMapForRom(platform, data) {
|
|
|
49
49
|
* live anywhere the developer wants. We tag the header so diffs there
|
|
50
50
|
* are obvious.
|
|
51
51
|
*/
|
|
52
|
-
function regionMapSms(data,
|
|
52
|
+
function regionMapSms(data, _platform) {
|
|
53
53
|
const regions = [];
|
|
54
54
|
// Header lives at $7FF0 IF the file is at least 32 KB. Some homebrew
|
|
55
55
|
// skips the header entirely (just runs from $0000) — flag with kind
|
|
@@ -135,7 +135,7 @@ const PLANNERS = {
|
|
|
135
135
|
// a one-call build() rebuild. planRegions strips the 2-byte load address
|
|
136
136
|
// (fileOffset 2); we re-emit it via a synthesized LOADADDR segment + a custom
|
|
137
137
|
// 2-area .cfg (LOADADDR then the body). PROVEN byte-identical via build().
|
|
138
|
-
c64(data,
|
|
138
|
+
c64(data, _regions) {
|
|
139
139
|
const loadAddr = data[0] | (data[1] << 8);
|
|
140
140
|
const bodyLen = data.length - 2;
|
|
141
141
|
const loadaddrSrc =
|
package/src/mcp/tools/disasm.js
CHANGED
|
@@ -208,7 +208,6 @@ function nesVectors(data) {
|
|
|
208
208
|
*/
|
|
209
209
|
function snesVectors(data) {
|
|
210
210
|
const copierOff = (data.length % 0x8000 === 0x200) ? 0x200 : 0;
|
|
211
|
-
const loMapper = data[copierOff + 0x7FC0 + 0x15];
|
|
212
211
|
const hiMapper = data[copierOff + 0xFFC0 + 0x15];
|
|
213
212
|
const isLo = !(hiMapper === 0x21 || hiMapper === 0x31);
|
|
214
213
|
const headerBase = copierOff + (isLo ? 0x7FC0 : 0xFFC0);
|
|
@@ -513,7 +512,7 @@ export function mapGenesisAddress(data, cpuAddr, length) {
|
|
|
513
512
|
* 48 KB: $4000-$FFFF (rare)
|
|
514
513
|
* 144 KB SuperGame: bank-switched at $8000-$BFFF + fixed at $C000
|
|
515
514
|
*/
|
|
516
|
-
export function mapAtari7800Address(data, cpuAddr, length,
|
|
515
|
+
export function mapAtari7800Address(data, cpuAddr, length, _bank = 0) {
|
|
517
516
|
// Detect header. "ATARI7800" magic at offset 1.
|
|
518
517
|
const hasHeader =
|
|
519
518
|
data.length > 128 &&
|
|
@@ -557,7 +556,7 @@ export function mapAtari7800Address(data, cpuAddr, length, bank = 0) {
|
|
|
557
556
|
* docs); not implemented here — pass `bank` instead and call with the raw
|
|
558
557
|
* binary if you're hand-mapping.
|
|
559
558
|
*/
|
|
560
|
-
export function mapC64Address(data, cpuAddr, length,
|
|
559
|
+
export function mapC64Address(data, cpuAddr, length, _bank = 0) {
|
|
561
560
|
// Detect .prg by reading the load address and seeing if it makes sense.
|
|
562
561
|
// (Anything is a valid load addr in theory, so we just trust the first
|
|
563
562
|
// 2 bytes here.)
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
// though they're not "instructions" per se.
|
|
8
8
|
|
|
9
9
|
import { readFile } from "node:fs/promises";
|
|
10
|
-
import {
|
|
11
|
-
import { mapNesAddress, mapSnesAddress, mapAtari2600Address, mapAtari7800Address, mapC64Address } from "./disasm.js";
|
|
10
|
+
import { mapSnesAddress, mapAtari2600Address, mapAtari7800Address, mapC64Address } from "./disasm.js";
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Classify a referring instruction by its mnemonic.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// in findEncodedText (NES/GB/GBC bank-aware, Genesis flat; SNES is mapper-
|
|
17
17
|
// dependent and left to fileOffset).
|
|
18
18
|
|
|
19
|
-
import { readFile
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
20
20
|
import { jsonContent, safeTool } from "../util.js";
|
|
21
21
|
import { getHost } from "../state.js";
|
|
22
22
|
|
package/src/mcp/tools/index.js
CHANGED
|
@@ -304,52 +304,3 @@ export function registerTools(server, z, sessionKey) {
|
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
// ---- helper: which category owns a tool name? ----
|
|
308
|
-
// Used so describeTool's error message can name the category to load.
|
|
309
|
-
// Maintained by hand for now; if categories explode we can derive this
|
|
310
|
-
// by registering each category into a dummy server and comparing diffs.
|
|
311
|
-
const TOOL_OWNER = {
|
|
312
|
-
// platforms category
|
|
313
|
-
platform: "platforms",
|
|
314
|
-
// run category
|
|
315
|
-
loadMedia: "run", host: "run",
|
|
316
|
-
frame: "run",
|
|
317
|
-
// input category
|
|
318
|
-
input: "input",
|
|
319
|
-
// state category
|
|
320
|
-
state: "state",
|
|
321
|
-
// memory category
|
|
322
|
-
memory: "memory",
|
|
323
|
-
// debug category
|
|
324
|
-
tiles: "debug", sprites: "debug",
|
|
325
|
-
background: "debug", encodeArt: "assets",
|
|
326
|
-
cpu: "debug", audioDebug: "debug",
|
|
327
|
-
symbols: "debug",
|
|
328
|
-
disasm: "debug",
|
|
329
|
-
cheats: "debug",
|
|
330
|
-
inspectTile: "debug",
|
|
331
|
-
// assets category
|
|
332
|
-
encodeAudio: "assets",
|
|
333
|
-
cart: "assets",
|
|
334
|
-
listRoms: "assets", romPatch: "assets", validateRom: "assets",
|
|
335
|
-
assembleSnippet: "assets",
|
|
336
|
-
|
|
337
|
-
text: "assets",
|
|
338
|
-
importArt: "assets",
|
|
339
|
-
palette: "debug",
|
|
340
|
-
// project category
|
|
341
|
-
scaffold: "project",
|
|
342
|
-
// show category (was: advanced)
|
|
343
|
-
playtest: "show",
|
|
344
|
-
// advanced category
|
|
345
|
-
runUntil: "advanced",
|
|
346
|
-
watch: "advanced", breakpoint: "advanced",
|
|
347
|
-
recordSession: "advanced",
|
|
348
|
-
// entry tier itself
|
|
349
|
-
catalog: "entry",
|
|
350
|
-
build: "entry", listRunnableFormats: "entry",
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
function ownerCategoryOf(name) {
|
|
354
|
-
return TOOL_OWNER[name] ?? null;
|
|
355
|
-
}
|
package/src/mcp/tools/input.js
CHANGED
|
@@ -65,18 +65,45 @@ function buttonShape(z) {
|
|
|
65
65
|
start: z.boolean().optional(),
|
|
66
66
|
select: z.boolean().optional(),
|
|
67
67
|
})
|
|
68
|
+
// passthrough (not the zod default of stripping) so a TYPO'd button name
|
|
69
|
+
// ({jump:true}, {aa:true}) survives into the handler and can be reported as
|
|
70
|
+
// ignored — instead of being silently dropped, leaving the agent believing
|
|
71
|
+
// it pressed something it didn't.
|
|
72
|
+
.passthrough()
|
|
68
73
|
.describe(
|
|
69
74
|
"Per-port controller state. Prefer the spatial face-button names (north/east/south/west) for cross-platform code — they map to the physical button in that compass position on each platform's controller (e.g. on NES east=A, on SNES east=A, on Genesis east=C). Raw libretro names (a/b/x/y/l/r/...) also work if you need direct control. Omitted buttons are released.",
|
|
70
75
|
);
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
// Every button key portInputToMask + the spatial resolver actually honor.
|
|
79
|
+
// Anything else in a `ports` object is a typo and gets reported, not pressed.
|
|
80
|
+
const KNOWN_BUTTONS = new Set([
|
|
81
|
+
"up", "down", "left", "right",
|
|
82
|
+
"north", "east", "south", "west",
|
|
83
|
+
"a", "b", "x", "y", "l", "r", "l2", "r2", "l3", "r3", "start", "select",
|
|
84
|
+
]);
|
|
85
|
+
|
|
73
86
|
// ── *Core functions for the `input` tool ──
|
|
74
87
|
|
|
75
88
|
/** op:'set' — set held controller state (persists until changed). */
|
|
76
89
|
function inputSetCore({ ports }, sessionKey) {
|
|
90
|
+
// Flag any key that isn't a real button BEFORE we set input — a typo
|
|
91
|
+
// ({jump:true}) would otherwise resolve to nothing and press silently.
|
|
92
|
+
const ignoredButtons = [];
|
|
93
|
+
ports.forEach((p, port) => {
|
|
94
|
+
for (const k of Object.keys(p)) {
|
|
95
|
+
if (p[k] === true && !KNOWN_BUTTONS.has(k)) ignoredButtons.push({ port, name: k });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
77
98
|
getHost(sessionKey).setInput({ ports });
|
|
78
|
-
const requested = ports.map((p) => Object.keys(p).filter((k) => p[k] === true));
|
|
79
|
-
return {
|
|
99
|
+
const requested = ports.map((p) => Object.keys(p).filter((k) => p[k] === true && KNOWN_BUTTONS.has(k)));
|
|
100
|
+
return {
|
|
101
|
+
inputSet: true,
|
|
102
|
+
requested,
|
|
103
|
+
...(ignoredButtons.length
|
|
104
|
+
? { ignoredButtons, ignoredNote: `Ignored ${ignoredButtons.length} unknown button name(s) — not pressed. Valid: ${[...KNOWN_BUTTONS].join(", ")}.` }
|
|
105
|
+
: {}),
|
|
106
|
+
};
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
/** op:'press' — press one named button N frames then release (port 0 default). */
|
|
@@ -168,7 +195,10 @@ export function registerInputTools(server, z, sessionKey) {
|
|
|
168
195
|
server.tool(
|
|
169
196
|
"input",
|
|
170
197
|
"Drive the controller. `op`: 'set' | 'press' | 'sequence' | 'navigate' | 'layout'.\n" +
|
|
171
|
-
"'set': hold controller state (persists until changed) via `ports:[{a:true,...},{...}]
|
|
198
|
+
"'set': hold controller state (persists until changed) via `ports:[{a:true,...},{...}]`. " +
|
|
199
|
+
"The held state is honored by frame({op:'step'}) AND by watch/breakpoint runs that have NO `pressDuring` " +
|
|
200
|
+
"schedule (they inherit it). If a watch/breakpoint IS given `pressDuring`, that schedule OWNS the pad for " +
|
|
201
|
+
"the run and this set state is ignored — so drive a watched window with `pressDuring`, not a prior `set`.\n" +
|
|
172
202
|
"'press': press one named `button` for `frames` then release (port 0 default).\n" +
|
|
173
203
|
"'sequence': scripted frame-by-frame `steps:[{input:{ports}, frames}]` for replays/tests.\n" +
|
|
174
204
|
"'navigate': walk a menu by advancing on SCREEN CHANGE — `steps:[{button, holdFrames?, maxWaitFrames?, " +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveCore } from "../../cores/registry.js";
|
|
2
|
-
import { clearHost, getHost, getHostOrNull, resetHost } from "../state.js";
|
|
2
|
+
import { clearHost, getHost, getHostOrNull, rememberLastMedia, resetHost } from "../state.js";
|
|
3
3
|
import { jsonContent, safeTool, textContent } from "../util.js";
|
|
4
4
|
import { resolveCheatCodeForApply } from "./cheats.js";
|
|
5
5
|
|
|
@@ -44,6 +44,13 @@ export function registerLifecycleTools(server, z, sessionKey) {
|
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
+
// Remember what we loaded so a later host eviction (restart/reconnect) can
|
|
48
|
+
// tell the agent the exact loadMedia call to recover with. Survives reset.
|
|
49
|
+
rememberLastMedia(sessionKey, {
|
|
50
|
+
platform,
|
|
51
|
+
...(bytes ? { fromBase64: true } : { path: host.status.mediaPath ?? path }),
|
|
52
|
+
});
|
|
53
|
+
|
|
47
54
|
// Framebuffer dimensions are NOT known until the core has run at least one
|
|
48
55
|
// frame — before that, fbWidth/fbHeight hold a pre-boot default (e.g.
|
|
49
56
|
// 256×192 on Genesis) that does NOT match the real output resolution
|
|
@@ -103,7 +110,12 @@ export function registerLifecycleTools(server, z, sessionKey) {
|
|
|
103
110
|
switch (op) {
|
|
104
111
|
case "unload": {
|
|
105
112
|
const host = getHostOrNull(sessionKey);
|
|
106
|
-
if (host
|
|
113
|
+
if (!host || !host.status.loaded) {
|
|
114
|
+
// Don't claim success when there was nothing loaded — that masks a
|
|
115
|
+
// session/state mix-up (the agent thinks it unloaded media it never had).
|
|
116
|
+
return textContent("nothing to unload — no media is loaded in this session");
|
|
117
|
+
}
|
|
118
|
+
host.unloadMedia();
|
|
107
119
|
return textContent("unloaded");
|
|
108
120
|
}
|
|
109
121
|
case "shutdown":
|
package/src/mcp/tools/lospec.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// per-palette overrides.
|
|
5
5
|
|
|
6
6
|
import { jsonContent, safeTool } from "../util.js";
|
|
7
|
-
import { resolveIntent } from "../../platforms/common/intent.js";
|
|
8
7
|
import { inspectPaletteCore, getPlatformMasterPaletteCore } from "./platform-tools.js";
|
|
9
8
|
|
|
10
9
|
/** lospec.com hosts each palette at https://lospec.com/palette-list/<id>.json. */
|
|
@@ -61,24 +60,6 @@ async function fetchLospecPalette(id) {
|
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
/**
|
|
65
|
-
* Snap each color in `palette` to the nearest entry in `master`.
|
|
66
|
-
* Used when the caller wants a lospec palette but the platform has a
|
|
67
|
-
* fixed hardware palette (NES 2C02 master, etc.).
|
|
68
|
-
*/
|
|
69
|
-
function snapToMaster(palette, master) {
|
|
70
|
-
return palette.map(([r, g, b]) => {
|
|
71
|
-
let best = master[0];
|
|
72
|
-
let bestD = Infinity;
|
|
73
|
-
for (const [mr, mg, mb] of master) {
|
|
74
|
-
const dr = r - mr, dg = g - mg, db = b - mb;
|
|
75
|
-
const d = dr * dr + dg * dg + db * db;
|
|
76
|
-
if (d < bestD) { bestD = d; best = [mr, mg, mb]; }
|
|
77
|
-
}
|
|
78
|
-
return best;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
63
|
/**
|
|
83
64
|
* Programmatic equivalent of the MCP tool. Exported for tests.
|
|
84
65
|
*
|
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { readFile, stat } from "node:fs/promises";
|
|
18
18
|
|
|
19
|
-
import { jsonContent
|
|
19
|
+
import { jsonContent } from "../util.js";
|
|
20
20
|
|
|
21
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
22
|
const __dirname = path.dirname(__filename);
|
|
@@ -6,7 +6,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { PNG } from "pngjs";
|
|
8
8
|
import { getHost } from "../state.js";
|
|
9
|
-
import { imageContent, jsonContent
|
|
9
|
+
import { imageContent, jsonContent } from "../util.js";
|
|
10
10
|
|
|
11
11
|
// Consolidation: several handlers in this big shared file are extracted as
|
|
12
12
|
// *Core functions that the consolidated domain tools (palette/tiles/background/
|
|
@@ -37,10 +37,10 @@ import { getNesApuState } from "../../host/nes-apu-state.js";
|
|
|
37
37
|
import { decodeGenesisPSG, decodeGenesisYM2612 } from "../../host/gpgx-state.js";
|
|
38
38
|
import { decodeGbApu, decodeGbaApu } from "../../host/gb-apu-state.js";
|
|
39
39
|
import { decodeC64Sid } from "../../host/c64-sid-state.js";
|
|
40
|
-
import { decodeLynxMikey, decodeLynxPalette
|
|
40
|
+
import { decodeLynxMikey, decodeLynxPalette } from "../../host/lynx-mikey-state.js";
|
|
41
41
|
import { getPcePsgState } from "../../host/pce-psg-state.js";
|
|
42
42
|
import { getMsxAyState } from "../../host/msx-ay-state.js";
|
|
43
|
-
import { decodeGbaSprites, decodeGbaPalette
|
|
43
|
+
import { decodeGbaSprites, decodeGbaPalette } from "../../host/gba-video-state.js";
|
|
44
44
|
|
|
45
45
|
/** Resolve the platform to inspect: explicit arg → currently loaded host. */
|
|
46
46
|
function resolvePlatform(host, requested) {
|
|
@@ -119,7 +119,7 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
119
119
|
// SMS tiles live in VRAM at runtime — the cart has no fixed CHR
|
|
120
120
|
// region. Render all 448 tiles (the entire 16KB VRAM mapped to
|
|
121
121
|
// tiles), using the live first-BG-palette so colors look right.
|
|
122
|
-
const {
|
|
122
|
+
const { snapshotPalette } = await import("../../platforms/sms/vdp.js");
|
|
123
123
|
const { colors } = snapshotPalette(host, p);
|
|
124
124
|
// Use BG palette (entries 0..15) for rendering.
|
|
125
125
|
const bgPal = colors.slice(0, 16).map((c) => [c.r, c.g, c.b]);
|
package/src/mcp/tools/project.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
// the project elsewhere and rebuild with cc65/sdcc directly, every byte
|
|
9
9
|
// that compiles is in the directory.
|
|
10
10
|
|
|
11
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
12
11
|
import { jsonContent, safeTool } from "../util.js";
|
|
13
12
|
import { starterSnippetsCore, copyStarterSnippetsCore } from "./snippets.js";
|
|
14
13
|
|
|
@@ -1776,7 +1775,6 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1776
1775
|
// runtime tmpl.runtime list typically includes them) are skipped.
|
|
1777
1776
|
const snippetFiles = [];
|
|
1778
1777
|
if (withSnippets) {
|
|
1779
|
-
const { listSnippetsForPlatform } = await import("./snippets.js").catch(() => ({}));
|
|
1780
1778
|
// Inline minimal duplicate of listSnippetsForPlatform since snippets.js
|
|
1781
1779
|
// doesn't export it. Keep this in sync with snippets.js.
|
|
1782
1780
|
const LIB_DIR = path.join(PLATFORM_LIB_DIR, "lib");
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// dev wiki (citations inline).
|
|
17
17
|
|
|
18
18
|
import { readFile, writeFile } from "node:fs/promises";
|
|
19
|
-
import { jsonContent, safeTool } from "../util.js";
|
|
20
19
|
|
|
21
20
|
// ───────────────────────────────────────────────────────────────────────────
|
|
22
21
|
// Pointer encoding per platform.
|
package/src/mcp/tools/rom-id.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { imageContent, jsonContent, safeTool } from "../util.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveIntent } from "../../platforms/common/intent.js";
|
|
3
3
|
import { getDefaultPalette, DEFAULT_PALETTES } from "../../platforms/common/default-palette.js";
|
|
4
4
|
import { spliceChrCore } from "./splice-chr.js";
|
|
5
5
|
import { relocateBlockCore, makeStoredBlockCore, findPointerToCore, PLATFORM_REGISTRY } from "./reinject.js";
|
|
@@ -65,7 +65,7 @@ export async function gbHeaderCore({ path: inPath, outputPath, cgb, title, cartT
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export function registerRomIdTools(server, z,
|
|
68
|
+
export function registerRomIdTools(server, z, _sessionKey) {
|
|
69
69
|
// identifyRom folded into `cart`; patchFile/patchRom/spliceCHR/relocate/etc.
|
|
70
70
|
// folded into the `romPatch` tool (router below).
|
|
71
71
|
const PLATFORMS = Object.keys(PLATFORM_REGISTRY);
|
|
@@ -13,7 +13,7 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { readdir, readFile, stat, mkdir, writeFile } from "node:fs/promises";
|
|
15
15
|
|
|
16
|
-
import { jsonContent,
|
|
16
|
+
import { jsonContent, textContent, writeOutput } from "../util.js";
|
|
17
17
|
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
const __dirname = path.dirname(__filename);
|
|
@@ -87,7 +87,7 @@ async function listSnippetsForPlatform(platform) {
|
|
|
87
87
|
return out;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export function registerSnippetTools(
|
|
90
|
+
export function registerSnippetTools(_server, _z) {
|
|
91
91
|
// ── Shared implementations for the three snippet modes ──────────
|
|
92
92
|
async function snippetsList(platform, language) {
|
|
93
93
|
const all = await listSnippetsForPlatform(platform);
|
|
@@ -15,7 +15,6 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
15
15
|
import { PNG } from "pngjs";
|
|
16
16
|
import { jsonContent, safeTool } from "../util.js";
|
|
17
17
|
import { intentZod, resolveIntent, intentError } from "../../platforms/common/intent.js";
|
|
18
|
-
import { getDefaultPalette } from "../../platforms/common/default-palette.js";
|
|
19
18
|
import { convertImageToTilesCore, imageToTilemapCore } from "./platform-tools.js";
|
|
20
19
|
import { validateGenesisTilesCore } from "./metasprite-tools.js";
|
|
21
20
|
|
|
@@ -542,7 +541,7 @@ async function crossPlatformSpriteImportImpl(args) {
|
|
|
542
541
|
|
|
543
542
|
// ── Register all three tools ─────────────────────────────────────────
|
|
544
543
|
|
|
545
|
-
export function registerSpritePipelineTools(server, z,
|
|
544
|
+
export function registerSpritePipelineTools(server, z, _sessionKey) {
|
|
546
545
|
server.tool(
|
|
547
546
|
"encodeArt",
|
|
548
547
|
"Encode a PNG into a platform's native art format, one tool keyed by `stage` — the PNG→tiles pipeline. " +
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// always reports `source: "file" | "emulator"` so the caller knows which.
|
|
9
9
|
|
|
10
10
|
import { readFile } from "node:fs/promises";
|
|
11
|
-
import {
|
|
11
|
+
import { getHost } from "../state.js";
|
|
12
12
|
import { jsonContent, imageContent, textContent, safeTool } from "../util.js";
|
|
13
13
|
import { inspectPatternTilesCore } from "./platform-tools.js";
|
|
14
14
|
import { extractSpriteSheetCore } from "./rom-id.js";
|