romdevtools 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -41
- package/CHANGELOG.md +46 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +1 -1
- package/src/host/LibretroHost.js +59 -1
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +4 -3
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
package/src/host/LibretroHost.js
CHANGED
|
@@ -43,6 +43,12 @@ 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
|
+
// geargrafx ships with the TurboTap disabled, which makes port-1 input
|
|
47
|
+
// unreachable in-game (every pad scan slot mirrors pad 0). Enabling it
|
|
48
|
+
// costs nothing for 1P games (slot 0 still reads pad 0) and routes the
|
|
49
|
+
// host's port-1 input to pad slot 2 — PCE 2P works (probed 2026-06-10
|
|
50
|
+
// during the ZENITH BARRAGE gold round).
|
|
51
|
+
pce: { geargrafx_turbotap: "Enabled" },
|
|
46
52
|
// VICE mounts a .d64/.tap/.crt but, with autostart off, just sits at the BASIC
|
|
47
53
|
// `READY.` prompt — the agent would see a blue boot screen, not the game. Force
|
|
48
54
|
// autostart so a disk/tape image runs the first program automatically (same as
|
|
@@ -57,6 +63,19 @@ const PLATFORM_CORE_OPTIONS = {
|
|
|
57
63
|
// files back into the .d64 (VICE updates the in-FS image in place). Without
|
|
58
64
|
// this a game's SAVE silently fails / errors — defeating disk-save support.
|
|
59
65
|
vice_floppy_write_protection: "disabled",
|
|
66
|
+
// TWO live C64 control ports so 2P games see player 2. The VICE core drives
|
|
67
|
+
// ONE control port per RetroPad by default (every retro port → cur_port);
|
|
68
|
+
// the per-port split only happens with the userport adapter, where the
|
|
69
|
+
// mapper does vice_port = cur_port + retro_port. With joyport=1 + a userport
|
|
70
|
+
// adapter that gives retro0→control-port-1, retro1→control-port-2 — BOTH
|
|
71
|
+
// standard ports live. Our games read P1 on control port 2 ($DC00) and P2
|
|
72
|
+
// on port 1 ($DC01); the host swaps the two retro ports below
|
|
73
|
+
// (portInputToMask C64 path) so host port 0 = P1 (control port 2) and host
|
|
74
|
+
// port 1 = P2 (control port 1), matching the universal "port 0 = player 1"
|
|
75
|
+
// convention. Verified: drives both paddles independently in 2P, 1P-vs-CPU
|
|
76
|
+
// still reachable. (Both options ship in the wasm — no core rebuild.)
|
|
77
|
+
vice_joyport: "1",
|
|
78
|
+
vice_userport_joytype: "HIT",
|
|
60
79
|
},
|
|
61
80
|
};
|
|
62
81
|
|
|
@@ -415,6 +434,9 @@ export class LibretroHost {
|
|
|
415
434
|
|
|
416
435
|
// Configure controller port 0 as joypad (some cores default to NONE).
|
|
417
436
|
mod._retro_set_controller_port_device(0, RETRO_DEVICE_JOYPAD);
|
|
437
|
+
// Port 1 too — needed for 2P. The C64/VICE 2P path (two live control ports)
|
|
438
|
+
// only reads RetroPad port 1 when it's registered as a joypad device.
|
|
439
|
+
mod._retro_set_controller_port_device(1, RETRO_DEVICE_JOYPAD);
|
|
418
440
|
|
|
419
441
|
// ---- Settle the framebuffer to the ROM's chosen geometry ----
|
|
420
442
|
//
|
|
@@ -623,7 +645,14 @@ export class LibretroHost {
|
|
|
623
645
|
this._applyC64ButtonKeys(input.ports[0] || {});
|
|
624
646
|
}
|
|
625
647
|
for (let port = 0; port < this.state.inputPorts.length; port++) {
|
|
626
|
-
|
|
648
|
+
// C64 2P port swap: with joyport=1 + userport the VICE mapper binds
|
|
649
|
+
// RetroPad 0 → C64 control port 1 and RetroPad 1 → control port 2. But
|
|
650
|
+
// our games read player 1 on control port 2 ($DC00) and player 2 on
|
|
651
|
+
// control port 1 ($DC01). So feed host port 0's input to RetroPad slot 1
|
|
652
|
+
// (→ control port 2 = P1) and host port 1's to slot 0 (→ port 1 = P2),
|
|
653
|
+
// restoring the universal "host port 0 = player 1" convention.
|
|
654
|
+
const srcPort = platform === "c64" ? (port ^ 1) : port;
|
|
655
|
+
const portInput = this._c64StripKeyButtons(input.ports[srcPort], platform);
|
|
627
656
|
this.state.inputPorts[port][0] = portInputToMask(portInput, platform);
|
|
628
657
|
}
|
|
629
658
|
}
|
|
@@ -1095,7 +1124,36 @@ export class LibretroHost {
|
|
|
1095
1124
|
this.reset();
|
|
1096
1125
|
return false;
|
|
1097
1126
|
}
|
|
1127
|
+
// Battery semantics: SAVE_RAM survives a power-cycle on a battery cart.
|
|
1128
|
+
// Carry it across the reload (the reload itself zeroes it).
|
|
1129
|
+
let sram = null;
|
|
1130
|
+
try {
|
|
1131
|
+
const size = this.regionSize("save_ram");
|
|
1132
|
+
if (size > 0) sram = Uint8Array.from(this.readMemory("save_ram", 0, size));
|
|
1133
|
+
} catch { /* no save_ram region on this core/cart — nothing to carry */ }
|
|
1098
1134
|
await this.loadMedia(this._loadArgs);
|
|
1135
|
+
if (sram && sram.some((b) => b !== 0)) {
|
|
1136
|
+
// Restore like a frontend restores the .srm: bytes in place BEFORE the
|
|
1137
|
+
// game's boot code reads them. Some cores size SAVE_RAM lazily (gpgx
|
|
1138
|
+
// scans for the last non-empty byte → size 0 on a fresh boot), so fall
|
|
1139
|
+
// back to the raw region pointer when the sized path refuses.
|
|
1140
|
+
let restored = false;
|
|
1141
|
+
try {
|
|
1142
|
+
if (this.regionSize("save_ram") >= sram.length) {
|
|
1143
|
+
this.writeMemory("save_ram", 0, sram);
|
|
1144
|
+
restored = true;
|
|
1145
|
+
}
|
|
1146
|
+
} catch { /* sized path unavailable */ }
|
|
1147
|
+
if (!restored) {
|
|
1148
|
+
try {
|
|
1149
|
+
const ptr = this.mod._retro_get_memory_data(0); // RETRO_MEMORY_SAVE_RAM
|
|
1150
|
+
if (ptr) { this.mod.HEAPU8.set(sram, ptr); restored = true; }
|
|
1151
|
+
} catch { /* no save buffer on this core/cart */ }
|
|
1152
|
+
}
|
|
1153
|
+
// loadMedia's settle frames may already have run the game's
|
|
1154
|
+
// hi-score load against empty SRAM — soft-reset so boot re-reads.
|
|
1155
|
+
if (restored) { try { this.reset(); } catch { /* keep the loaded state */ } }
|
|
1156
|
+
}
|
|
1099
1157
|
return true;
|
|
1100
1158
|
}
|
|
1101
1159
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { registerTools } from "../mcp/tools/index.js";
|
|
19
19
|
import { withClearToolErrors } from "../mcp/util.js";
|
|
20
|
-
import { observer, summarizeForLog, extractImages } from "../observer/bus.js";
|
|
20
|
+
import { observer, summarizeForLog, extractImages, pushObserverFrame } from "../observer/bus.js";
|
|
21
21
|
import { getHostOrNull } from "../mcp/state.js";
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -151,21 +151,21 @@ export async function runTool(tool, args, sessionKey) {
|
|
|
151
151
|
// Strip both from the caller-visible result before it's serialized.
|
|
152
152
|
let sidebandImages = [];
|
|
153
153
|
let frameProvider = null;
|
|
154
|
+
let frameCaption = null;
|
|
154
155
|
if (r && typeof r === "object") {
|
|
155
156
|
if (Array.isArray(r._observerImages)) { sidebandImages = r._observerImages; delete r._observerImages; }
|
|
156
157
|
if (typeof r._observerFrameProvider === "function") { frameProvider = r._observerFrameProvider; delete r._observerFrameProvider; }
|
|
158
|
+
if (typeof r._observerFrameCaption === "string") { frameCaption = r._observerFrameCaption; delete r._observerFrameCaption; }
|
|
157
159
|
}
|
|
158
160
|
if (frameProvider) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
} catch { /* livestream is best-effort; never affects the caller */ }
|
|
168
|
-
});
|
|
161
|
+
// Throttled to one per 2s per (session, tool), trailing-edge — same
|
|
162
|
+
// policy as the MCP path (bus.js pushObserverFrame). Platform is
|
|
163
|
+
// re-resolved at emit time (loadMedia sets it DURING the call).
|
|
164
|
+
pushObserverFrame({
|
|
165
|
+
sessionKey, tool: tool.name, ts: startedAt, platform,
|
|
166
|
+
resolvePlatform: () => { try { return getHostOrNull(sessionKey)?.status?.platform ?? platform; } catch { return platform; } },
|
|
167
|
+
...(frameCaption ? { caption: frameCaption } : {}),
|
|
168
|
+
}, frameProvider);
|
|
169
169
|
}
|
|
170
170
|
const inlineImages = extractImages(r);
|
|
171
171
|
const images = inlineImages.length > 0 ? inlineImages : sidebandImages;
|
package/src/mcp/tools/cheats.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { getHost } from "../state.js";
|
|
4
4
|
import { jsonContent, safeTool } from "../util.js";
|
|
5
|
+
import { attachObserverFrame } from "./watch-memory.js";
|
|
5
6
|
import { lookupCheats, searchCheatGames } from "../../cheats/lookup.js";
|
|
6
7
|
import { encodeForDevice, nativeDevicesFor, decodeCode } from "../../cheats/gamegenie.js";
|
|
7
8
|
|
|
@@ -334,7 +335,7 @@ export function registerCheatTools(server, z, sessionKey) {
|
|
|
334
335
|
switch (args.op) {
|
|
335
336
|
case "lookup": return jsonContent(await cheatsLookupCore(args));
|
|
336
337
|
case "search": return jsonContent(await cheatsSearchCore(args));
|
|
337
|
-
case "apply": return jsonContent(await cheatsApplyCore(args, sessionKey));
|
|
338
|
+
case "apply": return attachObserverFrame(jsonContent(await cheatsApplyCore(args, sessionKey)), getHost(sessionKey), "cheat applied");
|
|
338
339
|
case "clear": return jsonContent(await cheatsClearCore(args, sessionKey));
|
|
339
340
|
case "make": return jsonContent(await cheatsMakeCore(args));
|
|
340
341
|
default: throw new Error(`cheats: unknown op '${args.op}'`);
|
package/src/mcp/tools/frame.js
CHANGED
|
@@ -249,12 +249,13 @@ export function registerFrameTools(server, z, sessionKey) {
|
|
|
249
249
|
// actively playing in the playtest window means this step raced their
|
|
250
250
|
// real-time loop. Field only appears when the conflict is real.
|
|
251
251
|
const coDrive = humanCoDriveWarning(sessionKey);
|
|
252
|
-
|
|
252
|
+
// Livestream: the post-step frame (throttled to 1/2s per tool by the bus).
|
|
253
|
+
return attachObserverFrame(jsonContent({
|
|
253
254
|
framesRun: n,
|
|
254
255
|
frameCount: host.status.frameCount,
|
|
255
256
|
framebuffer: { width: host.status.fbWidth, height: host.status.fbHeight },
|
|
256
257
|
...(coDrive ? { humanCoDriveWarning: coDrive } : {}),
|
|
257
|
-
});
|
|
258
|
+
}), host, `step ×${n}`);
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
// Contract: an image goes to disk (path) OR comes back inline (inline:true).
|
package/src/mcp/tools/index.js
CHANGED
|
@@ -87,7 +87,7 @@ const CATEGORIES = [
|
|
|
87
87
|
{
|
|
88
88
|
name: "platforms",
|
|
89
89
|
description: "Discover supported platforms, their cores, toolchains, and language matrices.",
|
|
90
|
-
useWhen: ["
|
|
90
|
+
useWhen: ["before forking an example for a new game", "checking which platforms are available", "looking up a platform's default language"],
|
|
91
91
|
register: (s, z, k) => registerPlatformTools(s, z, k), // listPlatforms, resolvePlatform
|
|
92
92
|
},
|
|
93
93
|
{
|
|
@@ -138,8 +138,8 @@ const CATEGORIES = [
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
name: "project",
|
|
141
|
-
description: "
|
|
142
|
-
useWhen: ["starting a new game
|
|
141
|
+
description: "The example-game library (fork/list/show) + starter snippets per platform.",
|
|
142
|
+
useWhen: ["starting a new game (ALWAYS fork the nearest example — never a blank file)", "looking up canonical patterns like NMI handler, OAM DMA, joypad read"],
|
|
143
143
|
register: (s, z, k) => { registerProjectTools(s, z, k); registerSnippetTools(s, z, k); registerPlatformDocsTools(s, z); },
|
|
144
144
|
},
|
|
145
145
|
{
|
package/src/mcp/tools/input.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getHost } from "../state.js";
|
|
|
2
2
|
import { jsonContent, safeTool } from "../util.js";
|
|
3
3
|
import { getInputLayoutCore } from "./input-layout.js";
|
|
4
4
|
import { humanCoDriveWarning } from "./playtest.js";
|
|
5
|
+
import { attachObserverFrame } from "./watch-memory.js";
|
|
5
6
|
|
|
6
7
|
// Spreadable co-drive conflict marker for every input-driving op: while a
|
|
7
8
|
// human is actively playing in this session's playtest window, their input
|
|
@@ -257,21 +258,21 @@ export function registerInputTools(server, z, sessionKey) {
|
|
|
257
258
|
switch (args.op) {
|
|
258
259
|
case "set": {
|
|
259
260
|
if (!args.ports) throw new Error("input({op:'set'}): `ports` is required.");
|
|
260
|
-
return jsonContent(inputSetCore(args, sessionKey));
|
|
261
|
+
return attachObserverFrame(jsonContent(inputSetCore(args, sessionKey)), getHost(sessionKey), "input set");
|
|
261
262
|
}
|
|
262
263
|
case "press": {
|
|
263
264
|
if (!args.button) throw new Error("input({op:'press'}): `button` is required.");
|
|
264
|
-
return jsonContent(inputPressCore(args, sessionKey));
|
|
265
|
+
return attachObserverFrame(jsonContent(inputPressCore(args, sessionKey)), getHost(sessionKey), `press ${args.button}`);
|
|
265
266
|
}
|
|
266
267
|
case "sequence": {
|
|
267
268
|
if (!args.steps) throw new Error("input({op:'sequence'}): `steps` is required.");
|
|
268
|
-
return jsonContent(inputSequenceCore(args, sessionKey));
|
|
269
|
+
return attachObserverFrame(jsonContent(inputSequenceCore(args, sessionKey)), getHost(sessionKey), "input sequence");
|
|
269
270
|
}
|
|
270
271
|
case "navigate": {
|
|
271
272
|
if (!args.steps) throw new Error("input({op:'navigate'}): `steps` is required.");
|
|
272
273
|
// Fill per-step defaults the old navigate schema provided.
|
|
273
274
|
const steps = args.steps.map((s) => ({ holdFrames: 2, maxWaitFrames: 120, settleFrames: 2, ...s }));
|
|
274
|
-
return jsonContent(inputNavigateCore({ steps }, sessionKey));
|
|
275
|
+
return attachObserverFrame(jsonContent(inputNavigateCore({ steps }, sessionKey)), getHost(sessionKey), "navigate");
|
|
275
276
|
}
|
|
276
277
|
case "layout": {
|
|
277
278
|
if (!args.platform) throw new Error("input({op:'layout'}): `platform` is required.");
|
|
@@ -2,6 +2,7 @@ import { resolveCore } from "../../cores/registry.js";
|
|
|
2
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
|
+
import { attachObserverFrame } from "./watch-memory.js";
|
|
5
6
|
|
|
6
7
|
const MEDIA_KINDS = ["cartridge", "disk", "tape", "program"];
|
|
7
8
|
|
|
@@ -58,7 +59,8 @@ export function registerLifecycleTools(server, z, sessionKey) {
|
|
|
58
59
|
// on dimensions, so we omit it until a frame has been stepped and point the
|
|
59
60
|
// caller at stepFrames instead.
|
|
60
61
|
const framebufferKnown = host.status.frameCount > 0;
|
|
61
|
-
|
|
62
|
+
// Livestream: show what just loaded (the boot frame).
|
|
63
|
+
return attachObserverFrame(jsonContent({
|
|
62
64
|
loaded: true,
|
|
63
65
|
platform,
|
|
64
66
|
core: resolved.coreName,
|
|
@@ -68,7 +70,7 @@ export function registerLifecycleTools(server, z, sessionKey) {
|
|
|
68
70
|
? { framebuffer: { width: host.status.fbWidth, height: host.status.fbHeight } }
|
|
69
71
|
: { framebufferNote: "Framebuffer dimensions are unknown until the core runs — call stepFrames first, then getStatus (the pre-boot default does not match the real output resolution)." }),
|
|
70
72
|
...(appliedCheats ? { cheats: appliedCheats } : {}),
|
|
71
|
-
});
|
|
73
|
+
}), host, `loaded ${host.status.mediaPath ? host.status.mediaPath.split("/").pop() : platform}`);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
server.tool(
|
|
@@ -125,10 +127,10 @@ export function registerLifecycleTools(server, z, sessionKey) {
|
|
|
125
127
|
const host = getHost(sessionKey);
|
|
126
128
|
if (hard) {
|
|
127
129
|
const reloaded = await host.hardReset();
|
|
128
|
-
return textContent(reloaded ? "reset (hard / power-cycle — RAM cleared)" : "reset (soft — no cached ROM to reload for a hard reset)");
|
|
130
|
+
return attachObserverFrame(textContent(reloaded ? "reset (hard / power-cycle — RAM cleared)" : "reset (soft — no cached ROM to reload for a hard reset)"), host, "reset (hard)");
|
|
129
131
|
}
|
|
130
132
|
host.reset();
|
|
131
|
-
return textContent("reset (soft — RESET button; work RAM persists, use hard:true to clear it)");
|
|
133
|
+
return attachObserverFrame(textContent("reset (soft — RESET button; work RAM persists, use hard:true to clear it)"), host, "reset");
|
|
132
134
|
}
|
|
133
135
|
case "pause":
|
|
134
136
|
getHost(sessionKey).pause();
|
|
@@ -69,7 +69,7 @@ export async function listPlatformDocsCore({ platform }) {
|
|
|
69
69
|
platform,
|
|
70
70
|
docs,
|
|
71
71
|
note: docs.length === 0
|
|
72
|
-
? `No docs shipped for '${platform}' yet. Try a different platform or
|
|
72
|
+
? `No docs shipped for '${platform}' yet. Try a different platform, or fork an example game (examples({op:'fork'})) for boilerplate. (For RE/patching workflow, see platform({op:'doc', platform:'romhacking', name:'playbook'}).)`
|
|
73
73
|
: `Call platform({op:'doc', platform, name}) to read one. 'name' is 'mental_model' or 'troubleshooting'. For RE/patching workflow across platforms, see platform({op:'doc', platform:'romhacking', name:'playbook'}).`,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
@@ -409,7 +409,10 @@ export async function previewTileArtCore(args) {
|
|
|
409
409
|
|
|
410
410
|
if (outputPath) {
|
|
411
411
|
await writeFile(outputPath, png);
|
|
412
|
-
|
|
412
|
+
// Livestream sideband: the human sees the rendered sheet even though the
|
|
413
|
+
// agent only gets the path.
|
|
414
|
+
return { ...result, outputPath, note: `${png.length} bytes of PNG written to ${outputPath}.`,
|
|
415
|
+
_observerImages: [{ kind: "image", mimeType: "image/png", base64: png.toString("base64") }] };
|
|
413
416
|
}
|
|
414
417
|
return { ...result, pngBase64: png.toString("base64") };
|
|
415
418
|
}
|
|
@@ -467,7 +470,8 @@ async function previewMsxScreen2(args, d) {
|
|
|
467
470
|
};
|
|
468
471
|
if (outputPath) {
|
|
469
472
|
await writeFile(outputPath, buf);
|
|
470
|
-
return { ...result, outputPath
|
|
473
|
+
return { ...result, outputPath,
|
|
474
|
+
_observerImages: [{ kind: "image", mimeType: "image/png", base64: buf.toString("base64") }] };
|
|
471
475
|
}
|
|
472
476
|
return { ...result, pngBase64: buf.toString("base64") };
|
|
473
477
|
}
|