romdevtools 0.16.0 → 0.22.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 +75 -16
- package/CHANGELOG.md +316 -0
- package/examples/README.md +2 -0
- package/examples/atari2600/templates/platformer.asm +460 -0
- package/examples/atari2600/templates/racing.asm +463 -0
- package/examples/atari2600/templates/shmup.asm +386 -0
- package/examples/atari2600/templates/sports.asm +362 -0
- package/examples/atari7800/templates/default.c +49 -5
- package/examples/atari7800/templates/hello_sprite.c +48 -4
- package/examples/atari7800/templates/music_demo.c +47 -2
- package/examples/atari7800/templates/platformer.c +43 -4
- package/examples/atari7800/templates/puzzle.c +39 -4
- package/examples/atari7800/templates/racing.c +39 -4
- package/examples/atari7800/templates/shmup.c +40 -2
- package/examples/atari7800/templates/sports.c +36 -5
- package/examples/c64/templates/platformer.c +19 -5
- package/examples/c64/templates/puzzle.c +32 -2
- package/examples/c64/templates/shmup.c +28 -2
- package/examples/c64/templates/sports.c +30 -2
- package/examples/c64/templates/tile_engine.c +77 -27
- package/examples/gb/templates/default.c +110 -16
- package/examples/gb/templates/hello_sprite.c +15 -6
- package/examples/gb/templates/music_demo.c +36 -0
- package/examples/gb/templates/platformer.c +28 -6
- package/examples/gb/templates/puzzle.c +35 -4
- package/examples/gb/templates/racing.c +75 -10
- package/examples/gb/templates/shmup.c +41 -3
- package/examples/gb/templates/sports.c +51 -3
- package/examples/gb/templates/tile_engine.c +3 -2
- package/examples/gba/templates/gba_hello.c +29 -11
- package/examples/gba/templates/maxmod_demo.c +36 -2
- package/examples/gba/templates/platformer.c +3 -1
- package/examples/gba/templates/puzzle.c +15 -3
- package/examples/gba/templates/racing.c +65 -3
- package/examples/gba/templates/shmup.c +41 -4
- package/examples/gba/templates/sports.c +36 -2
- package/examples/gba/templates/tonc_hello.c +41 -5
- package/examples/gba/templates/tonc_hello_sprite.c +35 -1
- package/examples/gbc/templates/default.c +103 -26
- package/examples/gbc/templates/hello_sprite.c +12 -3
- package/examples/gbc/templates/music_demo.c +56 -12
- package/examples/gbc/templates/platformer.c +28 -6
- package/examples/gbc/templates/puzzle.c +35 -4
- package/examples/gbc/templates/racing.c +88 -21
- package/examples/gbc/templates/shmup.c +37 -3
- package/examples/gbc/templates/sports.c +48 -3
- 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/puzzle.c +37 -3
- package/examples/genesis/templates/racing.c +44 -11
- package/examples/genesis/templates/sgdk_hello.c +34 -1
- package/examples/genesis/templates/shmup.c +31 -1
- package/examples/genesis/templates/shmup_2p.c +31 -0
- package/examples/genesis/templates/xgm2_demo.c +20 -0
- package/examples/gg/templates/default.c +56 -18
- package/examples/gg/templates/hello_sprite.c +25 -2
- package/examples/gg/templates/music_demo.c +24 -2
- package/examples/gg/templates/platformer.c +18 -12
- package/examples/gg/templates/puzzle.c +38 -7
- package/examples/gg/templates/racing.c +58 -9
- package/examples/gg/templates/shmup.c +47 -3
- package/examples/gg/templates/sports.c +57 -16
- package/examples/gg/templates/tile_engine.c +12 -6
- package/examples/lynx/templates/default.c +39 -8
- package/examples/lynx/templates/hello_sprite.c +15 -1
- package/examples/lynx/templates/music_demo.c +13 -1
- package/examples/lynx/templates/puzzle.c +28 -1
- package/examples/lynx/templates/racing.c +34 -7
- package/examples/lynx/templates/shmup.c +42 -3
- package/examples/lynx/templates/sports.c +29 -2
- package/examples/msx/platformer/main.c +213 -0
- package/examples/msx/puzzle/main.c +250 -0
- package/examples/msx/racing/main.c +249 -0
- package/examples/msx/shmup/main.c +288 -0
- package/examples/msx/sports/main.c +182 -0
- package/examples/nes/templates/default.c +67 -19
- package/examples/nes/templates/hello_sprite.c +35 -0
- package/examples/nes/templates/music_demo.c +40 -0
- package/examples/nes/templates/platformer.c +65 -6
- package/examples/nes/templates/puzzle.c +67 -6
- package/examples/nes/templates/racing.c +45 -13
- package/examples/nes/templates/shmup.c +51 -2
- package/examples/nes/templates/sports.c +51 -6
- package/examples/pce/catch_game/main.c +22 -3
- package/examples/pce/music_sfx/main.c +28 -1
- package/examples/pce/platformer/main.c +283 -0
- package/examples/pce/puzzle/main.c +304 -0
- package/examples/pce/racing/main.c +304 -0
- package/examples/pce/shmup/main.c +346 -0
- package/examples/pce/sports/main.c +254 -0
- package/examples/pce/sprite_move/main.c +7 -2
- package/examples/sms/main.c +35 -6
- package/examples/sms/templates/hello_sprite.c +29 -3
- package/examples/sms/templates/music_demo.c +18 -4
- package/examples/sms/templates/puzzle.c +34 -5
- package/examples/sms/templates/racing.c +39 -2
- package/examples/sms/templates/shmup.c +41 -2
- package/examples/sms/templates/shmup_2p.c +24 -1
- package/examples/sms/templates/sports.c +47 -4
- 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/default.c +50 -28
- 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-data.asm +22 -0
- package/examples/snes/templates/platformer.c +20 -2
- package/examples/snes/templates/puzzle-data.asm +22 -0
- package/examples/snes/templates/puzzle.c +21 -2
- package/examples/snes/templates/racing-data.asm +22 -0
- package/examples/snes/templates/racing.c +17 -1
- package/examples/snes/templates/shmup-data.asm +22 -0
- package/examples/snes/templates/shmup.c +20 -1
- package/examples/snes/templates/sports-data.asm +22 -0
- package/examples/snes/templates/sports.c +16 -1
- package/package.json +1 -1
- package/src/cheats/gamegenie.js +0 -1
- package/src/cli/smoke.js +1 -3
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +191 -16
- package/src/host/callbacks.js +9 -1
- 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/host/types.js +15 -8
- package/src/http/routes.js +1 -1
- package/src/http/tool-registry.js +26 -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 +75 -4
- 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 +507 -0
- package/src/mcp/tools/disasm.js +97 -9
- package/src/mcp/tools/find-references.js +1 -2
- package/src/mcp/tools/font-map.js +1 -1
- package/src/mcp/tools/frame.js +168 -3
- 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 +18 -4
- 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 +54 -11
- 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/state.js +201 -14
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +105 -12
- package/src/mcp/tools/watch-memory.js +137 -16
- 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/MENTAL_MODEL.md +45 -1
- package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/d64.js +280 -0
- 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/MENTAL_MODEL.md +10 -0
- 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/MENTAL_MODEL.md +10 -6
- package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/MENTAL_MODEL.md +63 -2
- 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/MENTAL_MODEL.md +9 -4
- package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
- package/src/platforms/pce/lib/c/pce_video.c +1 -1
- 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/rom-id/identifier.js +15 -0
- package/src/toolchains/asar/asar.js +0 -9
- package/src/toolchains/assemble-snippet.js +30 -12
- package/src/toolchains/cc65/ines.js +145 -0
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
- package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
- package/src/toolchains/common/reassemble.js +10 -3
- 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/LibretroHost.js
CHANGED
|
@@ -43,6 +43,21 @@ const PLATFORM_CORE_OPTIONS = {
|
|
|
43
43
|
// `… - C-BIOS` machine tree ships in romdev-core-bluemsx/bios and is mirrored
|
|
44
44
|
// into the wasm FS as the system dir (see loadMedia + resolveSystemDir).
|
|
45
45
|
msx: { bluemsx_msxtype: "MSX2+ - C-BIOS" },
|
|
46
|
+
// VICE mounts a .d64/.tap/.crt but, with autostart off, just sits at the BASIC
|
|
47
|
+
// `READY.` prompt — the agent would see a blue boot screen, not the game. Force
|
|
48
|
+
// autostart so a disk/tape image runs the first program automatically (same as
|
|
49
|
+
// typing LOAD"*",8,1 : RUN). warp-during-autostart skips the slow 1541 load so
|
|
50
|
+
// the game is up in a fraction of the wall-clock. A bare .prg is injected and
|
|
51
|
+
// run directly by the core regardless of this; it matters for disk/tape/cart.
|
|
52
|
+
c64: {
|
|
53
|
+
vice_autostart: "enabled",
|
|
54
|
+
vice_autoloadwarp: "enabled",
|
|
55
|
+
vice_warp_boost: "enabled",
|
|
56
|
+
// Leave the mounted disk WRITABLE so a save-capable game can write its save
|
|
57
|
+
// files back into the .d64 (VICE updates the in-FS image in place). Without
|
|
58
|
+
// this a game's SAVE silently fails / errors — defeating disk-save support.
|
|
59
|
+
vice_floppy_write_protection: "disabled",
|
|
60
|
+
},
|
|
46
61
|
};
|
|
47
62
|
|
|
48
63
|
/**
|
|
@@ -188,7 +203,11 @@ export class LibretroHost {
|
|
|
188
203
|
async loadMedia(args) {
|
|
189
204
|
const mod = this._needMod();
|
|
190
205
|
const { platform } = args;
|
|
191
|
-
|
|
206
|
+
// Derive the kind from the file/virtual extension when the caller didn't say
|
|
207
|
+
// — so a C64 .d64 reports mediaKind:"disk" (writable save target) vs a .prg
|
|
208
|
+
// "program". For an in-memory load, the virtualName carries the ext.
|
|
209
|
+
const kindExt = path.extname(args.path || args.virtualName || "");
|
|
210
|
+
const mediaKind = args.mediaKind ?? defaultMediaKind(platform, kindExt);
|
|
192
211
|
|
|
193
212
|
// Apply per-platform core option defaults BEFORE retro_load_game.
|
|
194
213
|
// Most cores work with their option defaults; a few need explicit
|
|
@@ -282,7 +301,20 @@ export class LibretroHost {
|
|
|
282
301
|
mod._free(infoPtr);
|
|
283
302
|
|
|
284
303
|
if (!ok) {
|
|
285
|
-
|
|
304
|
+
// This is the failure path for EVERY bad/wrong-platform/corrupt/
|
|
305
|
+
// unsupported-mapper image — the most common loadMedia failure. The core
|
|
306
|
+
// returns a bare false, so name the likely causes + the exact checks
|
|
307
|
+
// rather than leaving the agent with "failed".
|
|
308
|
+
throw new Error(
|
|
309
|
+
`The '${platform}' core REFUSED this ${mediaKind || "media"} ` +
|
|
310
|
+
`(${data.length} bytes${ext ? `, ${ext}` : ""}, path ${mediaPath}). ` +
|
|
311
|
+
`retro_load_game returned false — the bytes reached the core but it would not accept them. ` +
|
|
312
|
+
`Common causes: (1) wrong platform for this file (a GB ROM loaded as 'nes', etc.) — ` +
|
|
313
|
+
`confirm the platform matches the file; (2) a corrupt or TRUNCATED image — re-check the byte length; ` +
|
|
314
|
+
`(3) an unsupported mapper/board or a missing/!bad header. ` +
|
|
315
|
+
`Inspect the file with cart({op:'identify'}) to see what platform/mapper it really is, ` +
|
|
316
|
+
`then load with the matching platform.`,
|
|
317
|
+
);
|
|
286
318
|
}
|
|
287
319
|
|
|
288
320
|
this.status.platform = platform;
|
|
@@ -417,7 +449,7 @@ export class LibretroHost {
|
|
|
417
449
|
*/
|
|
418
450
|
stepFrames(n) {
|
|
419
451
|
const mod = this._needMod();
|
|
420
|
-
|
|
452
|
+
this._needMedia();
|
|
421
453
|
if (this.status.paused) return 0;
|
|
422
454
|
for (let i = 0; i < n; i++) {
|
|
423
455
|
mod._retro_run();
|
|
@@ -436,7 +468,7 @@ export class LibretroHost {
|
|
|
436
468
|
* Returns the frame count after. */
|
|
437
469
|
renderOneFrame() {
|
|
438
470
|
const mod = this._needMod();
|
|
439
|
-
|
|
471
|
+
this._needMedia();
|
|
440
472
|
mod._retro_run();
|
|
441
473
|
this.status.frameCount++;
|
|
442
474
|
if (this.state.lastFrame) {
|
|
@@ -575,7 +607,7 @@ export class LibretroHost {
|
|
|
575
607
|
/** @param {string} name */
|
|
576
608
|
loadState(name) {
|
|
577
609
|
const snapshot = this.namedStates.get(name);
|
|
578
|
-
if (!snapshot) throw new Error(
|
|
610
|
+
if (!snapshot) throw new Error(this._noStateError(name));
|
|
579
611
|
return this.unserializeState(snapshot); // returns # cheats cleared
|
|
580
612
|
}
|
|
581
613
|
|
|
@@ -622,10 +654,21 @@ export class LibretroHost {
|
|
|
622
654
|
* disturbing the live host). Throws if the slot doesn't exist. */
|
|
623
655
|
getStateBlob(name) {
|
|
624
656
|
const blob = this.namedStates.get(name);
|
|
625
|
-
if (!blob) throw new Error(
|
|
657
|
+
if (!blob) throw new Error(this._noStateError(name));
|
|
626
658
|
return blob;
|
|
627
659
|
}
|
|
628
660
|
|
|
661
|
+
/** Build a "no save state named X" error that lists the slots that DO exist
|
|
662
|
+
* (or says there are none) and names the op to create one. */
|
|
663
|
+
_noStateError(name) {
|
|
664
|
+
const names = [...this.namedStates.keys()];
|
|
665
|
+
return names.length
|
|
666
|
+
? `No save state named '${name}'. Existing in-memory slots: ${names.map((n) => `'${n}'`).join(", ")}. ` +
|
|
667
|
+
`(List them with state({op:'list'}); create one with state({op:'save', name}).)`
|
|
668
|
+
: `No save state named '${name}' — this session has NO in-memory save slots yet. ` +
|
|
669
|
+
`Create one with state({op:'save', name:'${name}'}) first (or load from disk with state({op:'load', path})).`;
|
|
670
|
+
}
|
|
671
|
+
|
|
629
672
|
/**
|
|
630
673
|
* @param {import("./types.js").MemoryRegion} region
|
|
631
674
|
* @param {number} offset
|
|
@@ -648,7 +691,7 @@ export class LibretroHost {
|
|
|
648
691
|
readMemory(region, offset, length) {
|
|
649
692
|
const mod = this._needMod();
|
|
650
693
|
const id = MemoryRegionToRetro[region];
|
|
651
|
-
if (id === undefined) throw new Error(
|
|
694
|
+
if (id === undefined) throw new Error(this._unknownRegionError(region));
|
|
652
695
|
const ptr = mod._retro_get_memory_data(id);
|
|
653
696
|
const size = mod._retro_get_memory_size(id);
|
|
654
697
|
if (!ptr || !size) throw new Error(this._emptyRegionError(region));
|
|
@@ -666,7 +709,7 @@ export class LibretroHost {
|
|
|
666
709
|
writeMemory(region, offset, bytes) {
|
|
667
710
|
const mod = this._needMod();
|
|
668
711
|
const id = MemoryRegionToRetro[region];
|
|
669
|
-
if (id === undefined) throw new Error(
|
|
712
|
+
if (id === undefined) throw new Error(this._unknownRegionError(region));
|
|
670
713
|
const ptr = mod._retro_get_memory_data(id);
|
|
671
714
|
const size = mod._retro_get_memory_size(id);
|
|
672
715
|
if (!ptr || !size) throw new Error(this._emptyRegionError(region));
|
|
@@ -676,6 +719,89 @@ export class LibretroHost {
|
|
|
676
719
|
mod.HEAPU8.set(bytes, ptr + offset);
|
|
677
720
|
}
|
|
678
721
|
|
|
722
|
+
// ── C64 disk image read/write (VICE) ───────────────────────────────────────
|
|
723
|
+
// The VICE WASM core takes content in-memory and exposes no disk memory region,
|
|
724
|
+
// so these go through dedicated core exports that read/write the LIVE mounted
|
|
725
|
+
// 1541 disk_image_t directly (see scripts/patches/vice-romdev-memory-regions.patch).
|
|
726
|
+
// Only the standard 35-track 1541 .d64 (174848 bytes) is supported. unit 8.
|
|
727
|
+
|
|
728
|
+
/** True if the loaded core exposes the romdev disk read/write exports. */
|
|
729
|
+
diskImageSupported() {
|
|
730
|
+
const mod = this.mod;
|
|
731
|
+
return !!(mod && typeof mod._romdev_disk_export === "function"
|
|
732
|
+
&& typeof mod._romdev_disk_import === "function");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Read the LIVE mounted disk image out as a flat .d64.
|
|
737
|
+
* @param {number} [unit] drive unit (default 8)
|
|
738
|
+
* @returns {Uint8Array} the .d64 bytes (copied out of WASM memory)
|
|
739
|
+
*/
|
|
740
|
+
exportDiskImage(unit = 8) {
|
|
741
|
+
const mod = this._needMod();
|
|
742
|
+
if (typeof mod._romdev_disk_export !== "function") {
|
|
743
|
+
throw new Error("this core build does not expose disk export (C64/VICE only).");
|
|
744
|
+
}
|
|
745
|
+
const len = mod._romdev_disk_export(unit >>> 0, 0) >>> 0;
|
|
746
|
+
if (!len) {
|
|
747
|
+
throw new Error("no disk image mounted on this unit, or it is not a 35-track .d64. " +
|
|
748
|
+
"Load a .d64 with loadMedia({platform:'c64', path}) first.");
|
|
749
|
+
}
|
|
750
|
+
const ptr = mod._romdev_disk_ptr();
|
|
751
|
+
// copy out — the core buffer is reused on the next export
|
|
752
|
+
return mod.HEAPU8.slice(ptr, ptr + len);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Write a flat .d64 back into the LIVE mounted disk image (sector by sector).
|
|
757
|
+
* @param {Uint8Array} bytes a 174848-byte .d64
|
|
758
|
+
* @param {number} [unit] drive unit (default 8)
|
|
759
|
+
* @returns {number} bytes written
|
|
760
|
+
*/
|
|
761
|
+
importDiskImage(bytes, unit = 8) {
|
|
762
|
+
const mod = this._needMod();
|
|
763
|
+
if (typeof mod._romdev_disk_import !== "function") {
|
|
764
|
+
throw new Error("this core build does not expose disk import (C64/VICE only).");
|
|
765
|
+
}
|
|
766
|
+
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
767
|
+
const ptr = mod._malloc(data.length);
|
|
768
|
+
try {
|
|
769
|
+
mod.HEAPU8.set(data, ptr);
|
|
770
|
+
const n = mod._romdev_disk_import(ptr, data.length >>> 0, unit >>> 0, 0) >>> 0;
|
|
771
|
+
if (!n) throw new Error("disk import failed — no writable .d64 mounted on this unit.");
|
|
772
|
+
return n;
|
|
773
|
+
} finally {
|
|
774
|
+
mod._free(ptr);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Write ONE PRG file (name + bytes incl. its 2-byte load address) straight into
|
|
780
|
+
* the LIVE mounted disk via the vdrive — the "inject a save" primitive.
|
|
781
|
+
* @param {string} name file name (PETSCII, ≤16 chars)
|
|
782
|
+
* @param {Uint8Array} bytes the PRG file bytes (load address + body)
|
|
783
|
+
* @param {number} [unit] drive unit (default 8)
|
|
784
|
+
*/
|
|
785
|
+
putDiskFile(name, bytes, unit = 8) {
|
|
786
|
+
const mod = this._needMod();
|
|
787
|
+
if (typeof mod._romdev_disk_putfile !== "function") {
|
|
788
|
+
throw new Error("this core build does not expose disk putfile (C64/VICE only).");
|
|
789
|
+
}
|
|
790
|
+
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
791
|
+
const nameBytes = Buffer.from(String(name) + "\0", "latin1");
|
|
792
|
+
const namePtr = mod._malloc(nameBytes.length);
|
|
793
|
+
const dataPtr = mod._malloc(data.length || 1);
|
|
794
|
+
try {
|
|
795
|
+
mod.HEAPU8.set(nameBytes, namePtr);
|
|
796
|
+
mod.HEAPU8.set(data, dataPtr);
|
|
797
|
+
const rc = mod._romdev_disk_putfile(unit >>> 0, namePtr, dataPtr, data.length >>> 0);
|
|
798
|
+
if (rc !== 0) throw new Error(`disk putfile failed (rc=${rc}) — no writable .d64 mounted, or the disk is full.`);
|
|
799
|
+
} finally {
|
|
800
|
+
mod._free(namePtr);
|
|
801
|
+
mod._free(dataPtr);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
679
805
|
reset() {
|
|
680
806
|
const mod = this._needMod();
|
|
681
807
|
mod._retro_reset();
|
|
@@ -956,7 +1082,7 @@ export class LibretroHost {
|
|
|
956
1082
|
|
|
957
1083
|
runUntilPC(address, maxFrames = 600) {
|
|
958
1084
|
this._needMod();
|
|
959
|
-
|
|
1085
|
+
this._needMedia();
|
|
960
1086
|
if (!this.pcBreakSupported()) {
|
|
961
1087
|
throw new Error("PC breakpoint not supported by this core (Genesis today; other cores as patched).");
|
|
962
1088
|
}
|
|
@@ -988,7 +1114,7 @@ export class LibretroHost {
|
|
|
988
1114
|
*/
|
|
989
1115
|
runUntilRead(address, maxFrames = 600) {
|
|
990
1116
|
this._needMod();
|
|
991
|
-
|
|
1117
|
+
this._needMedia();
|
|
992
1118
|
if (!this.readWatchSupported()) {
|
|
993
1119
|
throw new Error("read watchpoint not supported by this core (Genesis today; other cores as patched).");
|
|
994
1120
|
}
|
|
@@ -1020,7 +1146,7 @@ export class LibretroHost {
|
|
|
1020
1146
|
*/
|
|
1021
1147
|
stepInstruction() {
|
|
1022
1148
|
this._needMod();
|
|
1023
|
-
|
|
1149
|
+
this._needMedia();
|
|
1024
1150
|
if (!this.pcBreakSupported()) {
|
|
1025
1151
|
throw new Error("single-step not supported by this core (Genesis today; other cores as patched).");
|
|
1026
1152
|
}
|
|
@@ -1088,8 +1214,8 @@ export class LibretroHost {
|
|
|
1088
1214
|
* @param {(host:LibretroHost)=>any} [a.capture] read result from core RAM BEFORE restore
|
|
1089
1215
|
*/
|
|
1090
1216
|
callSubroutine(a) {
|
|
1091
|
-
|
|
1092
|
-
|
|
1217
|
+
this._needMod();
|
|
1218
|
+
this._needMedia();
|
|
1093
1219
|
if (!this.setRegSupported()) {
|
|
1094
1220
|
throw new Error("cpu({op:'call'}) not supported by this core (rebuild with romdev_setreg/romdev_getreg).");
|
|
1095
1221
|
}
|
|
@@ -1325,7 +1451,7 @@ export class LibretroHost {
|
|
|
1325
1451
|
*/
|
|
1326
1452
|
watchRange(lo, hi, mode, frames) {
|
|
1327
1453
|
const mod = this._needMod();
|
|
1328
|
-
|
|
1454
|
+
this._needMedia();
|
|
1329
1455
|
if (!this.rangeWatchSupported()) throw new Error("range watch not supported by this core.");
|
|
1330
1456
|
const m = mode === "read" ? 1 : mode === "write" ? 2 : 3;
|
|
1331
1457
|
mod._romdev_range_set(lo >>> 0, hi >>> 0, m, 1);
|
|
@@ -1358,7 +1484,7 @@ export class LibretroHost {
|
|
|
1358
1484
|
*/
|
|
1359
1485
|
logPCRange(lo, hi, frames) {
|
|
1360
1486
|
const mod = this._needMod();
|
|
1361
|
-
|
|
1487
|
+
this._needMedia();
|
|
1362
1488
|
if (!this.rangeWatchSupported()) throw new Error("coverage trace not supported by this core.");
|
|
1363
1489
|
mod._romdev_cov_set(lo >>> 0, hi >>> 0, 1);
|
|
1364
1490
|
this._runFramesExclusive(() => false, frames);
|
|
@@ -1398,7 +1524,7 @@ export class LibretroHost {
|
|
|
1398
1524
|
*/
|
|
1399
1525
|
watchDma(frames) {
|
|
1400
1526
|
const mod = this._needMod();
|
|
1401
|
-
|
|
1527
|
+
this._needMedia();
|
|
1402
1528
|
if (!this.dmaWatchSupported()) throw new Error("VDP-DMA watch not supported by this core (Genesis only).");
|
|
1403
1529
|
mod._romdev_dmawatch_set(1);
|
|
1404
1530
|
this._runFramesExclusive(() => false, frames);
|
|
@@ -1436,6 +1562,21 @@ export class LibretroHost {
|
|
|
1436
1562
|
return this.mod;
|
|
1437
1563
|
}
|
|
1438
1564
|
|
|
1565
|
+
// Guard for every op that needs a loaded game. The bare "no media loaded"
|
|
1566
|
+
// left the agent guessing; this names the fix (loadMedia) and the gotcha
|
|
1567
|
+
// (emulator state is in-memory, so a reconnect/restart drops it). The richer
|
|
1568
|
+
// session-aware recovery (echoing the exact prior loadMedia call) lives at the
|
|
1569
|
+
// tool layer in state.js getHost(); this is the host-level twin.
|
|
1570
|
+
_needMedia() {
|
|
1571
|
+
if (!this.status.loaded) {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
"No media loaded — call loadMedia({platform, path}) before this op. " +
|
|
1574
|
+
"(If you DID load and hit this after a reconnect/restart, the host's in-memory " +
|
|
1575
|
+
"state didn't survive — re-run loadMedia with your ROM to pick back up.)",
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1439
1580
|
/**
|
|
1440
1581
|
* Build a friendly error message when a memory region is empty (the
|
|
1441
1582
|
* core didn't expose it). Includes per-platform suggestions when we
|
|
@@ -1446,8 +1587,42 @@ export class LibretroHost {
|
|
|
1446
1587
|
* "my VRAM writes are being optimized away" spiral — when in fact
|
|
1447
1588
|
* gambatte exposes VRAM as `gb_vram`, not the generic id.
|
|
1448
1589
|
*/
|
|
1590
|
+
_unknownRegionError(region) {
|
|
1591
|
+
// A bad region name should never leave the agent guessing — list the valid
|
|
1592
|
+
// ones (the single source of truth, MemoryRegionToRetro) so it can pick the
|
|
1593
|
+
// right one. The cross-platform names (system_ram / video_ram / save_ram)
|
|
1594
|
+
// exist everywhere; the rest are platform-specific.
|
|
1595
|
+
const valid = Object.keys(MemoryRegionToRetro).sort();
|
|
1596
|
+
const common = valid.filter((r) => ["system_ram", "video_ram", "save_ram", "rom"].includes(r));
|
|
1597
|
+
return (
|
|
1598
|
+
`Unknown memory region '${region}'. ` +
|
|
1599
|
+
`Common (most platforms): ${common.join(", ")}. ` +
|
|
1600
|
+
`All registered region names: ${valid.join(", ")}. ` +
|
|
1601
|
+
`(Region availability is per platform — some names only resolve on the platform that has that hardware.)`
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1449
1605
|
_emptyRegionError(region) {
|
|
1450
1606
|
const plat = this.status && this.status.platform;
|
|
1607
|
+
// SRAM gets an honest, specific answer: empty save_ram almost always means
|
|
1608
|
+
// "this cart/system has no battery save," NOT "the core is broken."
|
|
1609
|
+
if (region === "save_ram") {
|
|
1610
|
+
if (["atari2600", "atari7800", "lynx"].includes(plat)) {
|
|
1611
|
+
return `'${plat}' has no cartridge battery saves (the hardware never supported them) — ` +
|
|
1612
|
+
`save_ram is always empty here, there's no save file to read/write.`;
|
|
1613
|
+
}
|
|
1614
|
+
if (plat === "c64") {
|
|
1615
|
+
return `C64 has no cartridge battery SRAM — the C64 save medium is the FLOPPY (.d64), ` +
|
|
1616
|
+
`not save_ram (so save_ram is empty, as expected). A game's own KERNAL SAVE writes ` +
|
|
1617
|
+
`into the live disk; capture it with state({op:'exportDisk', path}) (the .d64 then ` +
|
|
1618
|
+
`includes the saved file, re-loadable to resume). Inject an outside save with ` +
|
|
1619
|
+
`state({op:'importDisk', path}) or state({op:'putDiskFile', path}).`;
|
|
1620
|
+
}
|
|
1621
|
+
return `save_ram is empty on platform '${plat}': this CART has no battery save ` +
|
|
1622
|
+
`(check cart({op:'identify'}).saveRam.hasBattery — many ROMs use passwords or no save). ` +
|
|
1623
|
+
`If you expected a save, confirm the cart header marks it battery-backed. ` +
|
|
1624
|
+
`For a full-machine snapshot regardless of SRAM, use state({op:'save'/'load', path}).`;
|
|
1625
|
+
}
|
|
1451
1626
|
const suggestions = {
|
|
1452
1627
|
// platform → { generic-region-name: "use this instead" }
|
|
1453
1628
|
gb: { video_ram: "gb_vram", save_ram: "save_ram (likely empty on cartless ROMs — try gb_oam / gb_io / gb_hram for non-VRAM state)" },
|
package/src/host/callbacks.js
CHANGED
|
@@ -310,10 +310,18 @@ function handleEnv(mod, state, rawCmd, dataPtr, log) {
|
|
|
310
310
|
const semi = desc.indexOf("; ");
|
|
311
311
|
if (semi >= 0) {
|
|
312
312
|
const options = desc.substring(semi + 2).split("|");
|
|
313
|
+
// PRESERVE a value the host already pre-seeded (e.g. via
|
|
314
|
+
// PLATFORM_CORE_OPTIONS, set before retro_load_game). The core
|
|
315
|
+
// registering its variables must NOT clobber that override back to
|
|
316
|
+
// its own default (options[0]) — that silently reset forced options
|
|
317
|
+
// like bluemsx's machine type / cart mapper. Keep the prior value if
|
|
318
|
+
// it's still a valid option; otherwise fall back to the default.
|
|
319
|
+
const prior = state.coreVariables.get(key);
|
|
320
|
+
const keep = prior && options.includes(prior.value) ? prior.value : options[0];
|
|
313
321
|
state.coreVariables.set(key, {
|
|
314
322
|
description: desc.substring(0, semi),
|
|
315
323
|
options,
|
|
316
|
-
value:
|
|
324
|
+
value: keep,
|
|
317
325
|
});
|
|
318
326
|
}
|
|
319
327
|
ptr += 8;
|
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/host/types.js
CHANGED
|
@@ -223,18 +223,25 @@ export const MemoryRegionToRetro = {
|
|
|
223
223
|
*/
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
* Default mediaKind for a platform when caller doesn't specify.
|
|
227
|
-
*
|
|
226
|
+
* Default mediaKind for a platform when caller doesn't specify. Consoles default
|
|
227
|
+
* to cartridge; C64 depends on the file kind — a `.d64`/`.g64`/`.d71`/`.d81` is a
|
|
228
|
+
* disk, a `.tap` is a tape, a `.crt` is a cartridge, and a bare `.prg`/`.p00` is
|
|
229
|
+
* a program injected directly. The extension is passed when known (loadMedia has
|
|
230
|
+
* it) so disk/tape images report honestly in status() and the agent knows a
|
|
231
|
+
* writable disk exists for saves.
|
|
228
232
|
* @param {string} platform
|
|
233
|
+
* @param {string} [ext] lower- or mixed-case file extension incl. dot, e.g. ".d64"
|
|
229
234
|
* @returns {MediaKind}
|
|
230
235
|
*/
|
|
231
|
-
export function defaultMediaKind(platform) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
236
|
+
export function defaultMediaKind(platform, ext) {
|
|
237
|
+
if (platform === "c64") {
|
|
238
|
+
const e = (ext || "").toLowerCase();
|
|
239
|
+
if (/\.(d64|g64|d71|d81|d80|d82|nib)$/.test(e)) return "disk";
|
|
240
|
+
if (/\.(tap|t64)$/.test(e)) return "tape";
|
|
241
|
+
if (/\.(crt|bin)$/.test(e)) return "cartridge";
|
|
242
|
+
return "program"; // .prg / .p00 / unknown → injected program
|
|
237
243
|
}
|
|
244
|
+
return "cartridge";
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
/**
|
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";
|
|
@@ -128,7 +128,32 @@ export async function runTool(tool, args, sessionKey) {
|
|
|
128
128
|
emit({ ok: false, error: text });
|
|
129
129
|
return { ok: false, error: text };
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
// Observer sidebands — identical to the MCP observer middleware (tool-wrap.js),
|
|
132
|
+
// because the HTTP path runs handlers directly and would otherwise drop them:
|
|
133
|
+
// • _observerImages — a frame the tool wrote to DISK instead of returning
|
|
134
|
+
// inline (e.g. screenshot({path:...})); surface it to /livestream anyway.
|
|
135
|
+
// • _observerFrameProvider — a DEFERRED framebuffer thunk (frame({op:'verify'}),
|
|
136
|
+
// watch/breakpoint tools): the tool advanced/looked at the emulator but
|
|
137
|
+
// returns JSON-only to the caller. We encode the PNG ASYNC (setImmediate,
|
|
138
|
+
// after the HTTP response goes out) and push it as a `call_frame` event so
|
|
139
|
+
// the human's livestream sees the frame at zero cost to the caller.
|
|
140
|
+
// Strip both from the caller-visible result before it's serialized.
|
|
141
|
+
let sidebandImages = [];
|
|
142
|
+
let frameProvider = null;
|
|
143
|
+
if (r && typeof r === "object") {
|
|
144
|
+
if (Array.isArray(r._observerImages)) { sidebandImages = r._observerImages; delete r._observerImages; }
|
|
145
|
+
if (typeof r._observerFrameProvider === "function") { frameProvider = r._observerFrameProvider; delete r._observerFrameProvider; }
|
|
146
|
+
}
|
|
147
|
+
if (frameProvider) {
|
|
148
|
+
setImmediate(() => {
|
|
149
|
+
try {
|
|
150
|
+
const img = frameProvider();
|
|
151
|
+
if (img) observer.push({ type: "call_frame", sessionKey: sessionKey ?? "http", ts: startedAt, tool: tool.name, images: [img] });
|
|
152
|
+
} catch { /* livestream is best-effort; never affects the caller */ }
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const inlineImages = extractImages(r);
|
|
156
|
+
const images = inlineImages.length > 0 ? inlineImages : sidebandImages;
|
|
132
157
|
const text = r?.content?.[0]?.text;
|
|
133
158
|
if (typeof text === "string") {
|
|
134
159
|
// most tools return jsonContent(...) → text is JSON; parse it back so the
|
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();
|