romdevtools 0.43.0 → 0.56.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/CHANGELOG.md +322 -0
- package/examples/n64/platformer/main.c +158 -0
- package/examples/n64/puzzle/main.c +117 -0
- package/examples/n64/racing/main.c +147 -0
- package/examples/n64/shmup/main.c +122 -0
- package/examples/n64/sports/main.c +127 -0
- package/examples/ps1/platformer/main.c +158 -0
- package/examples/ps1/puzzle/main.c +125 -0
- package/examples/ps1/racing/main.c +147 -0
- package/examples/ps1/shmup/main.c +192 -0
- package/examples/ps1/sports/main.c +127 -0
- package/examples/ps1/sprite_move/main.c +38 -0
- package/package.json +9 -2
- package/src/analysis/analyze.js +169 -29
- package/src/analysis/decompile.js +4 -0
- package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
- package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
- package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
- package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
- package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
- package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
- package/src/analysis/rizin.js +22 -1
- package/src/cores/capabilities.js +90 -2
- package/src/cores/registry.js +12 -0
- package/src/host/LibretroGL.js +270 -0
- package/src/host/LibretroGLBridge.js +836 -0
- package/src/host/LibretroHost.js +144 -3
- package/src/host/callbacks.js +18 -2
- package/src/host/coreLoader.js +49 -2
- package/src/host/cpu-state.js +27 -0
- package/src/host/framebuffer.js +14 -1
- package/src/host/glOptionalDep.js +60 -0
- package/src/host/n64-ai-state.js +43 -0
- package/src/host/ps1-spu-state.js +65 -0
- package/src/host/retroConstants.js +14 -0
- package/src/mcp/tools/cheats.js +73 -4
- package/src/mcp/tools/disasm.js +2 -0
- package/src/mcp/tools/frame.js +18 -9
- package/src/mcp/tools/index.js +12 -2
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +20 -4
- package/src/mcp/tools/platforms.js +38 -4
- package/src/mcp/tools/project.js +100 -0
- package/src/mcp/tools/rendering-context.js +2 -1
- package/src/mcp/tools/toolchain.js +1 -1
- package/src/mcp/tools/watch-memory.js +93 -20
- package/src/platforms/n64/lib/c/n64.c +196 -0
- package/src/platforms/n64/lib/c/n64.h +68 -0
- package/src/platforms/ps1/lib/c/psx.c +200 -0
- package/src/platforms/ps1/lib/c/psx.h +83 -0
- package/src/toolchains/cc65/cc65.js +11 -0
- package/src/toolchains/index.js +35 -0
- package/src/toolchains/mips-c/lib/be/libc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libm.a +0 -0
- package/src/toolchains/mips-c/lib/el/libc.a +0 -0
- package/src/toolchains/mips-c/lib/el/libm.a +0 -0
- package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
- package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
- package/src/toolchains/mips-c/lib/n64.ld +15 -0
- package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
- package/src/toolchains/mips-c/lib/ps1.ld +15 -0
- package/src/toolchains/mips-c/lib/softint.c +37 -0
- package/src/toolchains/mips-c/mips-c.js +155 -0
- package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
package/src/mcp/tools/cheats.js
CHANGED
|
@@ -204,18 +204,38 @@ export async function cheatsApplyCore({ code, desc, path: romPath, index, enable
|
|
|
204
204
|
const { code: codeToApply, appliedAs, reencodedFrom } = resolveCheatCodeForApply(rawCode, plat);
|
|
205
205
|
|
|
206
206
|
const unencodable = appliedAs === "rom-unencodable";
|
|
207
|
-
|
|
207
|
+
// Slot selection: an explicit `index` wins. Otherwise, if an active cheat
|
|
208
|
+
// already targets the SAME address, REUSE its slot — applying `005C:03` over
|
|
209
|
+
// an active `005C:02` should REPLACE the freeze, not stack a second one that
|
|
210
|
+
// fights for the same byte (v0.41.0 feedback 213831 #3). Only append a new
|
|
211
|
+
// slot when nothing targets this address yet.
|
|
212
|
+
const newAddr = cheatAddress(codeToApply);
|
|
213
|
+
let replacedSlot = null;
|
|
214
|
+
let slot;
|
|
215
|
+
if (index != null) {
|
|
216
|
+
slot = index;
|
|
217
|
+
} else if (newAddr != null) {
|
|
218
|
+
const existing = host.listActiveCheats().find((c) => cheatAddress(c.code) === newAddr);
|
|
219
|
+
if (existing) { slot = existing.index; replacedSlot = existing.index; }
|
|
220
|
+
else slot = host.listActiveCheats().length;
|
|
221
|
+
} else {
|
|
222
|
+
slot = host.listActiveCheats().length;
|
|
223
|
+
}
|
|
208
224
|
// Still install it (the raw poke may be what the user wants), but warn.
|
|
209
225
|
host.setCheat(slot, codeToApply, enabled);
|
|
210
226
|
return {
|
|
211
227
|
applied: enabled,
|
|
212
228
|
slot,
|
|
229
|
+
...(replacedSlot != null ? { replacedSameAddress: true } : {}),
|
|
213
230
|
code: codeToApply,
|
|
214
231
|
appliedAs,
|
|
215
232
|
...(reencodedFrom ? { reencodedFrom } : {}),
|
|
216
233
|
...(resolvedDesc ? { desc: resolvedDesc } : {}),
|
|
217
234
|
active: host.listActiveCheats(),
|
|
218
|
-
note: (
|
|
235
|
+
note: (replacedSlot != null
|
|
236
|
+
? `REPLACED the active cheat on the same address (slot ${replacedSlot}) — applying a new freeze on an address that already had one swaps it rather than stacking two that fight over the byte. To remove a single freeze without clearing the rest, use cheats({op:'remove', code|slot}). `
|
|
237
|
+
: "") +
|
|
238
|
+
(reencodedFrom
|
|
219
239
|
? `Raw code ${reencodedFrom} names a ROM address — re-encoded to the native ROM-patch device (${codeToApply}) so the core installs a read-intercept (a raw ADDR:VAL on a ROM address is treated as a RAM poke and would silently no-op). `
|
|
220
240
|
: unencodable
|
|
221
241
|
? `WARNING: this raw code names a ROM address but couldn't be re-encoded to a ROM-patch code (a ROM patch needs a COMPARE byte — pass ADDR:VAL:COMPARE). As applied it's a RAM poke and will likely NO-OP on this read-only ROM address. Read the current byte and supply it as the compare, or use makeCheat. `
|
|
@@ -226,6 +246,53 @@ export async function cheatsApplyCore({ code, desc, path: romPath, index, enable
|
|
|
226
246
|
};
|
|
227
247
|
}
|
|
228
248
|
|
|
249
|
+
/** The CPU/RAM address a cheat code targets, or null if undecodable. Used to
|
|
250
|
+
* detect same-address freezes (replace, not stack) + for op:'remove' by code. */
|
|
251
|
+
function cheatAddress(code) {
|
|
252
|
+
if (typeof code !== "string") return null;
|
|
253
|
+
// raw ADDR:VAL[:COMPARE] — the address is the first field.
|
|
254
|
+
if (code.includes(":")) {
|
|
255
|
+
const a = parseInt(code.split(":")[0], 16);
|
|
256
|
+
return Number.isNaN(a) ? null : (a & 0xffff);
|
|
257
|
+
}
|
|
258
|
+
// a device code (Game Genie / PAR / GameShark) — decode it platform-agnostically.
|
|
259
|
+
try {
|
|
260
|
+
const d = decodeCode(code, null);
|
|
261
|
+
return d && d.address != null ? (d.address & 0xffff) : null;
|
|
262
|
+
} catch { return null; }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** op:'remove' — disable ONE active cheat (by slot index or by code/address),
|
|
266
|
+
* leaving the rest in place. The single-slot complement to op:'clear' (nuke-all).
|
|
267
|
+
* v0.41.0 feedback 213831 #3. */
|
|
268
|
+
export async function cheatsRemoveCore({ slot, code }, sessionKey) {
|
|
269
|
+
const host = getHost(sessionKey);
|
|
270
|
+
if (!host.listActiveCheats) return { removed: false, reason: "no cheat interface on this core", active: [] };
|
|
271
|
+
const active = host.listActiveCheats();
|
|
272
|
+
let target = null;
|
|
273
|
+
if (slot != null) {
|
|
274
|
+
target = active.find((c) => c.index === slot);
|
|
275
|
+
} else if (code != null) {
|
|
276
|
+
// match by exact code OR by the address the code targets (so a user can
|
|
277
|
+
// remove '005C:02' by passing '005C:03', '$5C', or the device code).
|
|
278
|
+
const addr = cheatAddress(code);
|
|
279
|
+
target = active.find((c) => c.code === code) || (addr != null ? active.find((c) => cheatAddress(c.code) === addr) : null);
|
|
280
|
+
} else {
|
|
281
|
+
throw new Error("cheats({op:'remove'}): provide `slot` (index) or `code` (the cheat to remove).");
|
|
282
|
+
}
|
|
283
|
+
if (!target) {
|
|
284
|
+
return { removed: false, reason: `no active cheat matching ${slot != null ? `slot ${slot}` : `code '${code}'`}`, active };
|
|
285
|
+
}
|
|
286
|
+
host.setCheat(target.index, target.code, false); // disable that slot only
|
|
287
|
+
return {
|
|
288
|
+
removed: true,
|
|
289
|
+
slot: target.index,
|
|
290
|
+
code: target.code,
|
|
291
|
+
active: host.listActiveCheats(),
|
|
292
|
+
note: "Disabled that one cheat (volatile); the rest stay active. Use op:'clear' to remove ALL, op:'apply' to add/swap.",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
229
296
|
/** op:'clear' — remove ALL active cheats (volatile core-side reset). */
|
|
230
297
|
export async function cheatsClearCore(_args, sessionKey) {
|
|
231
298
|
const host = getHost(sessionKey);
|
|
@@ -319,7 +386,7 @@ export function registerCheatTools(server, z, sessionKey) {
|
|
|
319
386
|
"round-trip `verified`. RAM cheat = address+value; ROM/code cheat = also `compare` (the byte currently there — " +
|
|
320
387
|
"read it first).",
|
|
321
388
|
{
|
|
322
|
-
op: z.enum(["lookup", "search", "apply", "clear", "make"]).describe("lookup THIS game's DB cheats; search the DB by game name; apply a cheat live; clear
|
|
389
|
+
op: z.enum(["lookup", "search", "apply", "remove", "clear", "make"]).describe("lookup THIS game's DB cheats; search the DB by game name; apply a cheat live (replaces an active cheat on the SAME address); remove ONE active cheat (by slot/code); clear ALL cheats; make a new code."),
|
|
323
390
|
// lookup
|
|
324
391
|
path: z.string().optional().describe("op=lookup/apply(+desc): absolute ROM path. lookup sniffs platform+name from it."),
|
|
325
392
|
filter: z.string().optional().describe("op=lookup: case-insensitive substring filter on cheat descriptions."),
|
|
@@ -330,8 +397,9 @@ export function registerCheatTools(server, z, sessionKey) {
|
|
|
330
397
|
// apply
|
|
331
398
|
code: z.string().optional().describe("op=apply: raw cheat code (ADDR:VAL or a Game Genie code). Provide code OR desc."),
|
|
332
399
|
desc: z.string().optional().describe("op=apply: description of a matched cheat (requires `path`). First substring match is applied."),
|
|
333
|
-
index: z.number().int().min(0).optional().describe("op=apply: cheat slot (default: next free
|
|
400
|
+
index: z.number().int().min(0).optional().describe("op=apply: cheat slot (default: reuse the slot of an active cheat on the same address, else next free). Reuse a slot to replace it."),
|
|
334
401
|
enabled: z.boolean().default(true).describe("op=apply: false disables the slot instead of enabling."),
|
|
402
|
+
slot: z.number().int().min(0).optional().describe("op=remove: the active-cheat slot to disable (from apply's `slot` / active[].index). Provide slot OR code."),
|
|
335
403
|
// make / search / lookup share `platform`
|
|
336
404
|
platform: z.enum([...MAKE_CHEAT_PLATFORMS]).optional().describe("op=lookup: override platform detection. op=search: OPTIONAL — omit to search ALL platforms (each match returns its own `platform`); pass one only to scope the search. op=make: REQUIRED — the target platform (all 14 tier-1)."),
|
|
337
405
|
address: z.number().int().min(0).optional().describe("op=make: address to cheat (RAM addr, or the ROM addr to patch)."),
|
|
@@ -345,6 +413,7 @@ export function registerCheatTools(server, z, sessionKey) {
|
|
|
345
413
|
case "lookup": return jsonContent(await cheatsLookupCore(args));
|
|
346
414
|
case "search": return jsonContent(await cheatsSearchCore(args));
|
|
347
415
|
case "apply": return attachObserverFrame(jsonContent(await cheatsApplyCore(args, sessionKey)), getHost(sessionKey), "cheat applied");
|
|
416
|
+
case "remove": return jsonContent(await cheatsRemoveCore(args, sessionKey));
|
|
348
417
|
case "clear": return jsonContent(await cheatsClearCore(args, sessionKey));
|
|
349
418
|
case "make": return jsonContent(await cheatsMakeCore(args));
|
|
350
419
|
default: throw new Error(`cheats: unknown op '${args.op}'`);
|
package/src/mcp/tools/disasm.js
CHANGED
|
@@ -1609,6 +1609,8 @@ function sniffPlatformFromPath(p) {
|
|
|
1609
1609
|
if (/\.prg$/i.test(p)) return "c64";
|
|
1610
1610
|
if (/\.(lnx|lyx)$/i.test(p)) return "lynx";
|
|
1611
1611
|
if (/\.gba$/i.test(p)) return "gba";
|
|
1612
|
+
if (/\.(z64|n64|v64)$/i.test(p)) return "n64";
|
|
1613
|
+
if (/\.(psexe|psx)$/i.test(p)) return "ps1"; // .exe/.bin are ambiguous — pass platform explicitly
|
|
1612
1614
|
if (/\.(gen|md|bin)$/i.test(p)) return "genesis";
|
|
1613
1615
|
return null;
|
|
1614
1616
|
}
|
package/src/mcp/tools/frame.js
CHANGED
|
@@ -389,10 +389,14 @@ export function registerFrameTools(server, z, sessionKey) {
|
|
|
389
389
|
async function shootAscii({ cols, rows, symbols, colors, path: outPath, inline }) {
|
|
390
390
|
const host = getHost(sessionKey);
|
|
391
391
|
const { width, height, rgba } = host.screenshotRgba();
|
|
392
|
-
// Default
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
392
|
+
// Default to ONE cell per 8×8 tile (so a 256×224 NES frame → 32×28, legible
|
|
393
|
+
// game state). The old /16 default (16×14 for NES) was too coarse to read
|
|
394
|
+
// anything — feedback 0.44.0 #1. The terminal symbol's own subcell shape adds
|
|
395
|
+
// detail back, so this stays cheap.
|
|
396
|
+
if (cols == null) cols = Math.max(8, Math.floor(width / 8));
|
|
397
|
+
if (rows == null) rows = Math.max(8, Math.floor(height / 8));
|
|
398
|
+
// Warn when the caller forced a grid so coarse it can't show game state.
|
|
399
|
+
const tooCoarse = cols < Math.floor(width / 8) / 2 || rows < Math.floor(height / 8) / 2;
|
|
396
400
|
const { renderRgbaToAnsi } = await import("../../host/chafa-render.js");
|
|
397
401
|
const ansi = await renderRgbaToAnsi(rgba, width, height, { cols, rows, symbols, colors });
|
|
398
402
|
// Livestream sidebands: the human sees BOTH the real PNG and the ANSI.
|
|
@@ -401,15 +405,20 @@ export function registerFrameTools(server, z, sessionKey) {
|
|
|
401
405
|
_observerImages: [{ kind: "image", mimeType: "image/png", base64: shot.pngBase64 }],
|
|
402
406
|
_observerAnsi: ansi,
|
|
403
407
|
};
|
|
408
|
+
const coarseNote = tooCoarse
|
|
409
|
+
? `NOTE: ${cols}x${rows} is too coarse to read game state from this ${width}x${height} frame. `
|
|
410
|
+
+ `For a pass/fail check ("are we in gameplay?"), a memory({op:'read'}) byte assertion is cheaper and exact — `
|
|
411
|
+
+ `ascii is for a rough visual, not state.`
|
|
412
|
+
: null;
|
|
404
413
|
let result;
|
|
405
414
|
if (!inline) {
|
|
406
415
|
await writeFile(outPath, ansi, "utf-8");
|
|
407
|
-
result = jsonContent({ path: outPath, framebuffer: { width, height }, terminal: { cols, rows, symbols, colors }, ansiBytes: Buffer.byteLength(ansi, "utf-8") });
|
|
416
|
+
result = jsonContent({ path: outPath, framebuffer: { width, height }, terminal: { cols, rows, symbols, colors }, ansiBytes: Buffer.byteLength(ansi, "utf-8"), ...(coarseNote ? { note: coarseNote } : {}) });
|
|
408
417
|
} else {
|
|
409
418
|
result = {
|
|
410
419
|
content: [
|
|
411
420
|
{ type: "text", text: ansi },
|
|
412
|
-
{ type: "text", text: `framebuffer ${width}x${height} → terminal ${cols}x${rows} (${symbols}/${colors}, ${Buffer.byteLength(ansi, "utf-8")}B)` },
|
|
421
|
+
{ type: "text", text: `framebuffer ${width}x${height} → terminal ${cols}x${rows} (${symbols}/${colors}, ${Buffer.byteLength(ansi, "utf-8")}B)` + (coarseNote ? `\n${coarseNote}` : "") },
|
|
413
422
|
],
|
|
414
423
|
};
|
|
415
424
|
}
|
|
@@ -857,10 +866,10 @@ export function registerFrameTools(server, z, sessionKey) {
|
|
|
857
866
|
inline: z.boolean().default(false).describe("op=screenshot/stepAndShot: return the image in the response instead of writing to disk."),
|
|
858
867
|
overlayBoxes: z.boolean().default(false).describe("op=screenshot png: draw a colored bounding box per visible sprite (SNES+NES only)."),
|
|
859
868
|
scale: z.number().gt(0).max(16).refine((s) => s <= 1 || Number.isInteger(s), { message: "scale must be 0<scale≤1 (downscale) or an integer ≥2 (upscale)" }).optional().describe("op=screenshot png: nearest-neighbor resample factor. DEFAULT (unset/1) = NATIVE resolution — perfect pixels, the accurate representation; use this. 0<scale<1 DOWNscales (0.5 ≈ 75% fewer image tokens — useful for cheap 'did it change?' checks). integer scale≥2 UPscales by pixel-duplication (e.g. scale:4 → GB 160x144 → 640x576): it adds NO information (same pixels enlarged), costs MORE image tokens, and since VLM encoders resize to their own fixed resolution it may not change what the model sees and can slightly degrade it. Only for clients that render tiny images too small to use and can't zoom."),
|
|
860
|
-
cols: z.number().int().min(4).max(640).optional().describe("op=screenshot ascii: terminal columns (default fb_width/
|
|
861
|
-
rows: z.number().int().min(4).max(480).optional().describe("op=screenshot ascii: terminal rows (default fb_height/
|
|
869
|
+
cols: z.number().int().min(4).max(640).optional().describe("op=screenshot ascii: terminal columns (default fb_width/8 — one cell per 8×8 tile, legible game state)."),
|
|
870
|
+
rows: z.number().int().min(4).max(480).optional().describe("op=screenshot ascii: terminal rows (default fb_height/8)."),
|
|
862
871
|
symbols: z.enum(["ascii", "halfblock", "block", "quad", "sextant"]).default("ascii").describe("op=screenshot ascii: chafa symbol set."),
|
|
863
|
-
colors: z.enum(["true", "256", "16", "fgbg"]).default("
|
|
872
|
+
colors: z.enum(["true", "256", "16", "fgbg"]).default("256").describe("op=screenshot ascii: color depth. Default '256' (indexed) — far fewer ANSI escape bytes than 'true' (truecolor per cell) for a near-identical read. Use 'true' only when exact color matters."),
|
|
864
873
|
},
|
|
865
874
|
safeTool(async (args) => {
|
|
866
875
|
switch (args.op) {
|
package/src/mcp/tools/index.js
CHANGED
|
@@ -59,6 +59,7 @@ import { createDisclosure } from "../disclosure.js";
|
|
|
59
59
|
import { jsonContent, safeTool, withClearToolErrors } from "../util.js";
|
|
60
60
|
import { getHostOrNull, setDisclosure } from "../state.js";
|
|
61
61
|
import { da65Available } from "../../toolchains/cc65/da65.js";
|
|
62
|
+
import { cc65Available } from "../../toolchains/cc65/cc65.js";
|
|
62
63
|
import { readFileSync } from "node:fs";
|
|
63
64
|
import { fileURLToPath } from "node:url";
|
|
64
65
|
import { dirname, join } from "node:path";
|
|
@@ -75,7 +76,16 @@ import { dirname, join } from "node:path";
|
|
|
75
76
|
*/
|
|
76
77
|
function hostCapabilities(host) {
|
|
77
78
|
const da65 = da65Available();
|
|
78
|
-
|
|
79
|
+
const cc65 = cc65Available();
|
|
80
|
+
// Toolchain caps don't need a loaded host — report them either way so an agent
|
|
81
|
+
// can check build/disasm availability before loading a ROM.
|
|
82
|
+
const toolchains = {
|
|
83
|
+
da65Disasm: da65, // disasm({target:'rom'/'references'/'cfg'/'xrefs'/'functions'/'decompile'}) — all da65-backed
|
|
84
|
+
cc65Build: cc65, // build({platform:'nes'/'c64'/'atari7800'/'lynx'}) — cc65/ca65
|
|
85
|
+
ld65Link: cc65, // the ld65 linker (ships with cc65 in the same package)
|
|
86
|
+
da65Toolchain: da65, // legacy alias for da65Disasm (kept for back-compat)
|
|
87
|
+
};
|
|
88
|
+
if (!host) return toolchains;
|
|
79
89
|
const has = (m) => { try { return !!host[m]?.(); } catch { return false; } };
|
|
80
90
|
return {
|
|
81
91
|
pcBreakpoint: has("pcBreakSupported"), // breakpoint({on:'pc'})
|
|
@@ -87,7 +97,7 @@ function hostCapabilities(host) {
|
|
|
87
97
|
cheats: has("cheatsSupported"), // cheats({op:'apply'}) via retro_cheat_set
|
|
88
98
|
diskImage: has("diskImageSupported"), // C64 .d64 loadMedia
|
|
89
99
|
keyboard: has("keyboardSupported"), // keyboard input (C64/MSX)
|
|
90
|
-
|
|
100
|
+
...toolchains,
|
|
91
101
|
};
|
|
92
102
|
}
|
|
93
103
|
|
|
@@ -21,7 +21,7 @@ export function registerLifecycleTools(server, z, sessionKey) {
|
|
|
21
21
|
if (path && base64) throw new Error("loadMedia: provide `path` OR `base64`, not both.");
|
|
22
22
|
const slotB = slot === "b";
|
|
23
23
|
const host = slotB ? resetHostB(sessionKey) : resetHost(sessionKey);
|
|
24
|
-
await host.loadCore(resolved.jsPath, resolved.wasmPath);
|
|
24
|
+
await host.loadCore(resolved.jsPath, resolved.wasmPath, { hwRender: resolved.hwRender });
|
|
25
25
|
const bytes = base64 ? new Uint8Array(Buffer.from(base64, "base64")) : undefined;
|
|
26
26
|
await host.loadMedia({
|
|
27
27
|
platform,
|
|
@@ -6,6 +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 { naReason } from "../../cores/capabilities.js";
|
|
9
10
|
import { imageContent, jsonContent, unsupported } from "../util.js";
|
|
10
11
|
|
|
11
12
|
// Consolidation: several handlers in this big shared file are extracted as
|
|
@@ -39,6 +40,8 @@ import { decodeGbApu, decodeGbaApu } from "../../host/gb-apu-state.js";
|
|
|
39
40
|
import { decodeC64Sid } from "../../host/c64-sid-state.js";
|
|
40
41
|
import { decodeLynxMikey, decodeLynxPalette } from "../../host/lynx-mikey-state.js";
|
|
41
42
|
import { getPcePsgState } from "../../host/pce-psg-state.js";
|
|
43
|
+
import { decodePs1Spu } from "../../host/ps1-spu-state.js";
|
|
44
|
+
import { decodeN64Ai } from "../../host/n64-ai-state.js";
|
|
42
45
|
import { getMsxAyState } from "../../host/msx-ay-state.js";
|
|
43
46
|
import { decodeGbaSprites, decodeGbaPalette } from "../../host/gba-video-state.js";
|
|
44
47
|
|
|
@@ -339,7 +342,7 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
unsupported(p, "inspectPalette", {
|
|
342
|
-
reason: "no palette decoder for this platform",
|
|
345
|
+
reason: naReason(p, "inspectPalette") ?? "no palette decoder for this platform",
|
|
343
346
|
alternative: "platform({op:'capabilities'}) to see what's wired",
|
|
344
347
|
});
|
|
345
348
|
};
|
|
@@ -424,7 +427,20 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
424
427
|
if (!ay) throw new Error("getAudioState chip:'ay8910' — no PSG region (load an MSX ROM into the patched blueMSX core).");
|
|
425
428
|
return { platform: "msx", ...ay };
|
|
426
429
|
}
|
|
427
|
-
|
|
430
|
+
if (chip === "spu") {
|
|
431
|
+
// PS1 SPU — 24 ADPCM voices (volume/pitch/ADSR + key-on/off).
|
|
432
|
+
const regs = host.getSpuRegs?.();
|
|
433
|
+
if (!regs) throw new Error("getAudioState chip:'spu' — no SPU region (load a PS1 program into the rebuilt pcsx_rearmed core).");
|
|
434
|
+
return { platform: "ps1", ...decodePs1Spu(regs) };
|
|
435
|
+
}
|
|
436
|
+
if (chip === "ai") {
|
|
437
|
+
// N64 AI — the audio OUTPUT state (sample rate + playing + DMA source). N64
|
|
438
|
+
// audio is RSP-mixed, so there are no per-voice registers to decode.
|
|
439
|
+
const regs = host.getAiRegs?.();
|
|
440
|
+
if (!regs) throw new Error("getAudioState chip:'ai' — no AI region (load an N64 ROM into the rebuilt parallel_n64 core).");
|
|
441
|
+
return { platform: "n64", ...decodeN64Ai(regs) };
|
|
442
|
+
}
|
|
443
|
+
throw new Error(`getAudioState: unknown chip '${chip}'. Use 'nes' (NES 2A03), 'gb' (Game Boy/GBC), 'gba' (GBA), 'dsp' (SNES), 'psg' (Genesis/SMS/GG SN76489), 'ym2612' (Genesis FM), 'sid' (C64), 'mikey' (Lynx), 'pce', 'ay8910' (MSX), or 'spu' (PS1).`);
|
|
428
444
|
}
|
|
429
445
|
|
|
430
446
|
getAudioStateCore = async ({ chip }, callerSessionKey) => jsonContent(readAudioChip(chip, callerSessionKey));
|
|
@@ -701,7 +717,7 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
701
717
|
}
|
|
702
718
|
|
|
703
719
|
unsupported(p, "inspectSprites", {
|
|
704
|
-
reason: "no sprite decoder for this platform",
|
|
720
|
+
reason: naReason(p, "inspectSprites") ?? "no sprite decoder for this platform",
|
|
705
721
|
alternative: "memory({op:'read'}) the raw OAM/sprite-attribute region, or platform({op:'capabilities'}) to see what's wired",
|
|
706
722
|
});
|
|
707
723
|
};
|
|
@@ -813,7 +829,7 @@ export function registerPlatformTools(server, z, sessionKey) {
|
|
|
813
829
|
return emitImage(r.png, `SNES BG map composite (${r.width}×${r.height}, ${r.mapWidth}×${r.mapHeight} tiles, ${r.bpp}bpp, tilemap@0x${tilemapBaseByte.toString(16)}, tiles@0x${tileBaseByte.toString(16)}). ${r.note}`);
|
|
814
830
|
}
|
|
815
831
|
unsupported(p, "inspectBackground", {
|
|
816
|
-
reason: "no background-map snapshotter for this platform",
|
|
832
|
+
reason: naReason(p, "inspectBackground") ?? "no background-map snapshotter for this platform",
|
|
817
833
|
alternative: "background({view:'renderState'}) for the register-level context, or memory({op:'read'}) the raw VRAM/nametable region",
|
|
818
834
|
});
|
|
819
835
|
};
|
|
@@ -2,7 +2,7 @@ import { CORES, listAvailableCores, resolveCore } from "../../cores/registry.js"
|
|
|
2
2
|
import { TOOLCHAINS } from "../../toolchains/registry.js";
|
|
3
3
|
import { getLanguageOptions } from "../../toolchains/index.js";
|
|
4
4
|
import { jsonContent, safeTool } from "../util.js";
|
|
5
|
-
import { CAPABILITIES,
|
|
5
|
+
import { CAPABILITIES, capabilitiesFor, OP_KEYS, naReason } from "../../cores/capabilities.js";
|
|
6
6
|
import { listToolchainsCore, installToolchainCore } from "./toolchain.js";
|
|
7
7
|
import { listPlatformDocsCore, getPlatformDocCore } from "./platform-docs.js";
|
|
8
8
|
|
|
@@ -66,6 +66,30 @@ const PLATFORM_QUIRKS = {
|
|
|
66
66
|
],
|
|
67
67
|
starterSnippets: ["hello_msx.c", "msx_crt0.s"],
|
|
68
68
|
},
|
|
69
|
+
ps1: {
|
|
70
|
+
multiBank: false,
|
|
71
|
+
maxRomBytesPerBank: 0, // disc/EXE based, not a fixed cart
|
|
72
|
+
headerLocation: "PS-EXE: 'PS-X EXE' magic at 0, entry (pc0) at +0x10, load addr (t_addr) at +0x18; 2048-byte header then code",
|
|
73
|
+
notes: [
|
|
74
|
+
"32-bit MIPS R3000 (little-endian), framebuffer GPU. Runs via PCSX-ReARMed with its BUILT-IN HLE BIOS — no firmware file needed, software-rendered. Load a .exe (PS-EXE), or a disc image if you have one.",
|
|
75
|
+
"WORKS NOW: build (buildSource language:'c' → mips-elf-gcc → a PS-EXE the HLE BIOS runs; write GPU/SPU registers directly, no SDK yet), run, frame({op:'screenshot'}), cpu({op:'read'}) (live R3000 registers), cheats, readMemory/writeMemory (system_ram = 2MB main RAM), disasm + decompile (Ghidra MIPS C), the live-debug tools breakpoint({on:'pc'/'write'/'read'}) + watch({on:'range'}), AND getAudioState({chip:'spu'}) (24-voice SPU). MIPS is little-endian here.",
|
|
76
|
+
"The bare build path: a minimal crt0 sets the stack + clears .bss + calls main(); code loads at 0x80010000. No PSn00bSDK runtime yet — drive the GPU at ports 0x1F801810/0x1F801814. The framebuffer renderer has no tile/sprite inspectors — use screenshot + memory.",
|
|
77
|
+
"For higher fidelity (GL hardware renderer) a real PS1 BIOS + the beetle_psx_hw core is the alternative, but the HLE pcsx_rearmed path ships clean with zero firmware.",
|
|
78
|
+
],
|
|
79
|
+
starterSnippets: [],
|
|
80
|
+
},
|
|
81
|
+
n64: {
|
|
82
|
+
multiBank: false,
|
|
83
|
+
maxRomBytesPerBank: 0,
|
|
84
|
+
headerLocation: ".z64 (big-endian) magic 80 37 12 40 at 0; entry point (big-endian word) at +0x08; 0x1000-byte IPL3 bootcode then game code. .v64/.n64 byte orders are auto-normalized.",
|
|
85
|
+
notes: [
|
|
86
|
+
"32-bit MIPS R4300 (big-endian), 3D RDP/RSP. Runs via ParaLLEl-N64 with the glide64 GL renderer — HW-rendered through the OPTIONAL native GL stack (native-gles + webgl-node). Those are optionalDependencies: install them for N64; the other platforms don't need them.",
|
|
87
|
+
"WORKS NOW: build (buildSource language:'c' → mips-elf-gcc → a big-endian MIPS image; bare crt0, no libdragon yet so it's logic-only for now), run, frame({op:'screenshot'}) (real 3D frames, headless via FBO readback), cpu({op:'read'}) (live R4300 registers), cheats, readMemory/writeMemory (system_ram = 8MB RDRAM, 0x80xxxxxx maps to offset 0), disasm + decompile (Ghidra MIPS C), breakpoint + watch (live-debug instrumentation), AND getAudioState({chip:'ai'}) (audio output: sample rate + playing). MIPS is big-endian here.",
|
|
88
|
+
"build caveat: a fully BOOTABLE N64 ROM needs the IPL3 bootcode + a libdragon-style header (libdragon SDK forthcoming). The bare build compiles+links your C to a flat image — great for logic/RE, not yet a self-booting cart. 3D renderer has no tile/sprite inspectors — use screenshot + memory.",
|
|
89
|
+
"If frame() errors with an install hint, the optional GL module isn't installed: `npm install native-gles webgl-node`.",
|
|
90
|
+
],
|
|
91
|
+
starterSnippets: [],
|
|
92
|
+
},
|
|
69
93
|
};
|
|
70
94
|
|
|
71
95
|
/** op:'list' — every platform with core/toolchains/languages/quirks. */
|
|
@@ -129,10 +153,20 @@ export function registerPlatformTools(server, z) {
|
|
|
129
153
|
case "capabilities": {
|
|
130
154
|
if (args.platform) {
|
|
131
155
|
const cap = capabilitiesFor(args.platform);
|
|
132
|
-
if (!cap) throw new Error(`platform({op:'capabilities'}): unknown platform '${args.platform}'. Known: ${
|
|
133
|
-
|
|
156
|
+
if (!cap) throw new Error(`platform({op:'capabilities'}): unknown platform '${args.platform}'. Known: ${Object.keys(CAPABILITIES).join(", ")}.`);
|
|
157
|
+
// Call out WHY each unsupported op is N/A by hardware (a framebuffer/3D
|
|
158
|
+
// renderer has no tile/sprite tables; a disc system has no cart) so an
|
|
159
|
+
// agent reading the manifest sees "can't, because hardware" — not a
|
|
160
|
+
// bare `false` it might mistake for "not built yet".
|
|
161
|
+
const naReasons = {};
|
|
162
|
+
for (const op of OP_KEYS) {
|
|
163
|
+
if (!cap.ops[op]) { const r = naReason(args.platform, op); if (r) naReasons[op] = r; }
|
|
164
|
+
}
|
|
165
|
+
return jsonContent({ platform: args.platform, ...cap, ...(Object.keys(naReasons).length ? { naReasons } : {}) });
|
|
134
166
|
}
|
|
135
|
-
|
|
167
|
+
// All platforms incl. the partial MIPS tier (ps1/n64) — so an agent can
|
|
168
|
+
// discover their capability map and see which ops are live vs not-yet.
|
|
169
|
+
return jsonContent({ platforms: Object.keys(CAPABILITIES), capabilities: CAPABILITIES });
|
|
136
170
|
}
|
|
137
171
|
case "resolve": {
|
|
138
172
|
if (!args.platform) throw new Error("platform({op:'resolve'}): `platform` is required.");
|
package/src/mcp/tools/project.js
CHANGED
|
@@ -546,6 +546,106 @@ const TEMPLATES = {
|
|
|
546
546
|
},
|
|
547
547
|
};
|
|
548
548
|
})(),
|
|
549
|
+
|
|
550
|
+
// ── PlayStation (mips-elf-gcc R3000) — software 3D engine helper lib + examples ──
|
|
551
|
+
// The PS1 is a 3D machine: the helper lib (psx.{h,c}) is a real software 3D
|
|
552
|
+
// pipeline (fixed-point transform + perspective + cull). 4 examples are 3D; the
|
|
553
|
+
// puzzle is 2D (a flat grid is the right idiom even on 3D hardware).
|
|
554
|
+
ps1: (() => {
|
|
555
|
+
const PS1_RUNTIME = [
|
|
556
|
+
{ src: "lib/c/psx.h", dst: "psx.h" },
|
|
557
|
+
{ src: "lib/c/psx.c", dst: "psx.c" },
|
|
558
|
+
];
|
|
559
|
+
const mk = (name, describe) => ({ main: `${name}/main.c`, runtime: PS1_RUNTIME, lang: "C (mips-elf-gcc)", ext: ".exe", describe });
|
|
560
|
+
return {
|
|
561
|
+
default: mk("shmup", "STARFALL — the canonical PS1 3D starter: a vertical shooter where enemies fly in from depth and grow under perspective projection. Exercises the whole 3D helper lib (camera, model transform, culled cubes) + SIO pad + HUD. Same as 'shmup'."),
|
|
562
|
+
shmup: {
|
|
563
|
+
...mk("shmup", "STARFALL — a 3D PlayStation vertical shooter. The playfield recedes into the screen; enemy cubes fly in from the far distance and scale up under perspective as they approach. Stream bullets into Z, AABB collision, xorshift wave spawner, parallax starfield, title/play/game-over with score + lives. Built on the software 3D pipeline (psx_camera/psx_model/psx_quad3d, 16.16 fixed point)."),
|
|
564
|
+
players: "1 (PS1 has two pad ports; a 2P round can hook port 2 later)",
|
|
565
|
+
sram: "none in this starter — PS1 saves go to a Memory Card via the BIOS; the helper lib keeps hi-score in-session (stated in-file).",
|
|
566
|
+
mechanics: ["3D perspective playfield", "depth-scaled enemies", "projectile pools", "wave spawner", "AABB collision", "lives + score", "title/play/game-over state machine"],
|
|
567
|
+
techniques: ["software 3D: fixed-point camera + model transform", "perspective projection + back-face cull", "GPU flat-shaded quads (GP0 0x20/0x60)", "SIO controller polling", "blocky HUD number font"],
|
|
568
|
+
},
|
|
569
|
+
racing: {
|
|
570
|
+
...mk("racing", "POLE BENDER — a 3D PlayStation racer. The road is a ribbon of perspective quads receding to the horizon and bending with a sine curve; you steer a car between the verges as the world scrolls toward you, rival cubes growing as you close on them. The PS1 signature: a real 3D track, not pseudo-3D scaling. Distance score, collision spin-out, title/race/results."),
|
|
571
|
+
players: "1",
|
|
572
|
+
sram: "none (in-session best time/score; Memory Card path is the real-hardware save).",
|
|
573
|
+
mechanics: ["3D curved road (perspective quads)", "throttle/brake/steer physics", "rival traffic + collision", "distance scoring", "title/race/results state machine"],
|
|
574
|
+
techniques: ["receding road segments with depth-driven centerline curve", "no-cull ground-plane quads (psx_quad3d_nc)", "chase camera with downward tilt", "16.16 fixed-point world scroll"],
|
|
575
|
+
},
|
|
576
|
+
platformer: {
|
|
577
|
+
...mk("platformer", "BLOCK HOP — a 3D PlayStation platformer. A cube hero runs and jumps across floating platforms drawn in perspective; gravity + jump physics in 16.16 fixed point, AABB landing on platform tops, coins to collect, a lethal pit, a chase camera that follows the hero. Title/play/game-over, score + lives."),
|
|
578
|
+
players: "1",
|
|
579
|
+
sram: "none (in-session hi-score; Memory Card is the real save path).",
|
|
580
|
+
mechanics: ["gravity + jump physics", "platform-top AABB landing", "coin pickups", "lethal pit + lives", "follow camera", "title/play/game-over"],
|
|
581
|
+
techniques: ["3D platforms as no-cull flat-topped boxes", "culled hero/coin cubes", "smooth-follow camera (psx_camera per frame)", "fixed-point physics"],
|
|
582
|
+
},
|
|
583
|
+
sports: {
|
|
584
|
+
...mk("sports", "SLAM COURT — a 3D PlayStation sports game (air-hockey / pong). A perspective court seen down its length; you control the near paddle, the CPU the far one, and the ball bounces in 3D (X across, Z into the screen), its size changing with depth. First to 7. Title/match/game-over."),
|
|
585
|
+
players: "1 (vs CPU; a 2P split-paddle round can hook port 2 later)",
|
|
586
|
+
sram: "none (match score is in-session).",
|
|
587
|
+
mechanics: ["3D ball physics (X/Z)", "player + CPU paddles", "wall bounces + scoring", "tracking CPU AI", "first-to-7 match", "title/match/game-over"],
|
|
588
|
+
techniques: ["perspective court floor (no-cull quad)", "depth-scaled ball", "paddle/ball cubes at world positions", "capped CPU tracking"],
|
|
589
|
+
},
|
|
590
|
+
puzzle: {
|
|
591
|
+
...mk("puzzle", "DROP GRID — the one 2D game in the PS1 set (a flat grid is the right idiom even on 3D hardware). A falling-block puzzle: colored blocks drop down a well, full rows clear and score, speed ramps up, stack the top = game over. Drawn with the GPU's 2D rectangle primitive. Title/play/game-over."),
|
|
592
|
+
players: "1",
|
|
593
|
+
sram: "none (in-session hi-score).",
|
|
594
|
+
mechanics: ["integer grid model", "falling block move/drop", "full-row clear + scoring", "ramping fall speed", "stack-out game over", "title/play/game-over"],
|
|
595
|
+
techniques: ["2D GPU rectangles (GP0 0x60 variable-size rect)", "integer board logic (no 3D needed)", "SIO pad edge detection", "row-shift clear"],
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
})(),
|
|
599
|
+
|
|
600
|
+
// ── Nintendo 64 (mips-elf-gcc R4300) — software 3D engine helper lib + examples ──
|
|
601
|
+
// The N64 was a 3D-first machine, so ALL 5 examples are 3D (even the puzzle —
|
|
602
|
+
// rendered as a 3D well of cubes). Same software-3D lib as PS1 (n64.{h,c}), a
|
|
603
|
+
// framebuffer backend the headless-angrylion core scans out.
|
|
604
|
+
n64: (() => {
|
|
605
|
+
const N64_RUNTIME = [
|
|
606
|
+
{ src: "lib/c/n64.h", dst: "n64.h" },
|
|
607
|
+
{ src: "lib/c/n64.c", dst: "n64.c" },
|
|
608
|
+
];
|
|
609
|
+
const mk = (name, describe) => ({ main: `${name}/main.c`, runtime: N64_RUNTIME, lang: "C (mips-elf-gcc)", ext: ".z64", describe });
|
|
610
|
+
return {
|
|
611
|
+
default: mk("shmup", "STARFALL 64 — the canonical N64 3D starter: a vertical shooter where enemies fly in from depth and scale up under perspective. Exercises the whole software-3D lib (camera, transform, culled cubes) + SI/PIF pad + HUD, presented through angrylion's VI scanout. Same as 'shmup'."),
|
|
612
|
+
shmup: {
|
|
613
|
+
...mk("shmup", "STARFALL 64 — a 3D Nintendo 64 vertical shooter (the N64 twin of the PS1 STARFALL). Enemy cubes fly in from the far distance and grow under perspective; stream bullets into Z, AABB collision, xorshift wave spawner, starfield, title/play/game-over with score + lives. Software 3D pipeline (n64_camera/n64_model/n64_quad3d) rendered to an RDRAM framebuffer the headless-angrylion core scans out."),
|
|
614
|
+
players: "1 (N64 has 4 controller ports; 2-4P can hook the other ports later)",
|
|
615
|
+
sram: "none in this starter — N64 saves go to Controller Pak / EEPROM / SRAM via the PI; the lib keeps hi-score in-session (stated in-file).",
|
|
616
|
+
mechanics: ["3D perspective playfield", "depth-scaled enemies", "projectile pools", "wave spawner", "AABB collision", "lives + score", "title/play/game-over state machine"],
|
|
617
|
+
techniques: ["software 3D: fixed-point camera + model transform", "perspective projection + back-face cull", "software triangle rasterizer → RDRAM framebuffer", "VI scanout (correct VI register setup)", "SI/PIF controller poll"],
|
|
618
|
+
},
|
|
619
|
+
racing: {
|
|
620
|
+
...mk("racing", "POLE BENDER 64 — a 3D Nintendo 64 racer. A perspective road ribbon recedes to the horizon and bends with a sine curve; steer between the verges as the world scrolls toward you, rival cubes growing as you close. Distance score, collision spin-out, title/race/results. The N64's 3D heritage on display."),
|
|
621
|
+
players: "1",
|
|
622
|
+
sram: "none (in-session best; Controller Pak/EEPROM is the real-hardware save).",
|
|
623
|
+
mechanics: ["3D curved road (perspective quads)", "throttle/brake/steer", "rival traffic + collision", "distance scoring", "title/race/results"],
|
|
624
|
+
techniques: ["receding road segments with depth-driven curve", "no-cull ground-plane quads", "chase camera", "16.16 fixed-point world scroll"],
|
|
625
|
+
},
|
|
626
|
+
platformer: {
|
|
627
|
+
...mk("platformer", "BLOCK HOP 64 — a 3D Nintendo 64 platformer. A cube hero runs and jumps across floating platforms in perspective; gravity + jump physics (16.16 fixed point), AABB landing on platform tops, coins, a lethal pit, a follow camera. Title/play/game-over, score + lives."),
|
|
628
|
+
players: "1",
|
|
629
|
+
sram: "none (in-session hi-score).",
|
|
630
|
+
mechanics: ["gravity + jump physics", "platform-top AABB landing", "coin pickups", "lethal pit + lives", "follow camera", "title/play/game-over"],
|
|
631
|
+
techniques: ["3D platforms as flat-topped boxes", "culled hero/coin cubes", "smooth-follow camera", "fixed-point physics"],
|
|
632
|
+
},
|
|
633
|
+
sports: {
|
|
634
|
+
...mk("sports", "SLAM COURT 64 — a 3D Nintendo 64 sports game (air-hockey / pong). A perspective court down its length; you control the near paddle, the CPU the far, the ball bounces in 3D (X across, Z into the screen) and scales with depth. First to 7. Title/match/game-over."),
|
|
635
|
+
players: "1 (vs CPU; the N64's extra ports can host 2-4P later)",
|
|
636
|
+
sram: "none (match score is in-session).",
|
|
637
|
+
mechanics: ["3D ball physics (X/Z)", "player + CPU paddles", "wall bounces + scoring", "tracking CPU AI", "first-to-7", "title/match/game-over"],
|
|
638
|
+
techniques: ["perspective court floor (no-cull quad)", "depth-scaled ball", "paddle/ball cubes", "capped CPU tracking"],
|
|
639
|
+
},
|
|
640
|
+
puzzle: {
|
|
641
|
+
...mk("puzzle", "DROP GRID 64 — a 3D Nintendo 64 falling-block puzzle. Unlike the flat-2D PS1 puzzle, this is rendered in 3D (the N64 was a 3D-first machine): the well is a perspective box of cube walls and the blocks are shaded cubes you watch fall in depth. Move/drop, full-row clear + scoring, ramping speed, stack-out = game over. Title/play/game-over."),
|
|
642
|
+
players: "1",
|
|
643
|
+
sram: "none (in-session hi-score).",
|
|
644
|
+
mechanics: ["integer grid model", "falling block move/drop", "full-row clear + scoring", "ramping fall speed", "stack-out game over", "title/play/game-over"],
|
|
645
|
+
techniques: ["3D-rendered well (cube walls + floor)", "blocks as shaded 3D cubes", "tilted camera into the well", "integer board logic", "SI/PIF pad edge detection"],
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
})(),
|
|
549
649
|
};
|
|
550
650
|
// R37: GBC has its own scaffold tree at examples/gbc/templates/ +
|
|
551
651
|
// src/platforms/gbc/lib/c/. Same runtime files as GB (the APU + Z80 +
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { getHost } from "../state.js";
|
|
12
12
|
import { jsonContent, safeTool, unsupported } from "../util.js";
|
|
13
|
+
import { naReason } from "../../cores/capabilities.js";
|
|
13
14
|
import { inspectBackgroundMapCore } from "./platform-tools.js";
|
|
14
15
|
import { whichTilesAreRenderedCore } from "./which-tiles.js";
|
|
15
16
|
|
|
@@ -533,7 +534,7 @@ export async function getRenderingContextCore({ platform, area = "all", sessionK
|
|
|
533
534
|
case "msx": return msxContext(host, area);
|
|
534
535
|
default:
|
|
535
536
|
unsupported(p, "renderingContext", {
|
|
536
|
-
reason: "no rendering-context decoder for this platform",
|
|
537
|
+
reason: naReason(p, "renderingContext") ?? "no rendering-context decoder for this platform",
|
|
537
538
|
alternative: "platform({op:'capabilities'}) to see what's wired",
|
|
538
539
|
});
|
|
539
540
|
}
|
|
@@ -603,7 +603,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
603
603
|
}
|
|
604
604
|
|
|
605
605
|
const host = resetHost(sessionKey);
|
|
606
|
-
await host.loadCore(resolved.jsPath, resolved.wasmPath);
|
|
606
|
+
await host.loadCore(resolved.jsPath, resolved.wasmPath, { hwRender: resolved.hwRender });
|
|
607
607
|
// Pass projectName as a virtualName so the playtest window titles itself
|
|
608
608
|
// with the game name (the SDL window reads host.status.mediaPath). Keep
|
|
609
609
|
// a platform-correct extension so shared cores still resolve the system.
|