romdevtools 0.24.0 → 0.25.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 +6 -4
- package/CHANGELOG.md +73 -0
- package/examples/atari7800/templates/sports.c +6 -2
- package/examples/sms/templates/shmup.c +5 -2
- package/package.json +1 -1
- package/src/host/LibretroHost.js +80 -0
- package/src/mcp/tools/index.js +4 -46
- package/src/mcp/tools/input.js +5 -0
- package/src/mcp/tools/project.js +39 -6
- package/src/mcp/tools/record.js +9 -3
- package/src/platforms/c64/MENTAL_MODEL.md +20 -0
- package/src/mcp/tool-manifest.js +0 -92
package/AGENTS.md
CHANGED
|
@@ -148,10 +148,12 @@ worry about ground truth:
|
|
|
148
148
|
|
|
149
149
|
1. **`scaffold({op:'project', platform, template, name, path})`** — drops a
|
|
150
150
|
complete, self-contained project tree on disk (main.c + the
|
|
151
|
-
runtime files it needs +
|
|
152
|
-
reference + README + .gitignore).
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
runtime files it needs + the vendored library source for
|
|
152
|
+
reference + README + .gitignore). The response lists only the files you EDIT
|
|
153
|
+
(`files`) + a `vendorFileCount`; pass `verbose:true` for the full manifest.
|
|
154
|
+
Build the whole dir in one call with `build({output:'project', path,
|
|
155
|
+
outputPath})` (toolchain/crt0/linker inferred — no manifest); the bundled
|
|
156
|
+
examples ARE the reference implementation.
|
|
155
157
|
2. **`scaffold({op:'game', platform, genre})`** — same but picks a known-good
|
|
156
158
|
genre scaffold (shmup / platformer / puzzle / sports / racing).
|
|
157
159
|
3. **`scaffold({op:'snippets', platform, mode})`** (mode `list`/`get`/`getAll`)
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,79 @@ All notable changes to `romdevtools`. Dates are release dates.
|
|
|
4
4
|
(Published as `romdev-mcp` through 0.11.0; renamed to `romdevtools` in 0.13.0 —
|
|
5
5
|
the `romdev-mcp` bin is kept as an alias.)
|
|
6
6
|
|
|
7
|
+
## 0.25.0
|
|
8
|
+
|
|
9
|
+
### Added — C64 input scripting + verification (RE startup-flow telemetry)
|
|
10
|
+
Follow-up to the 0.24.0 C64 keyboard work: an agent RE'ing C64 Uridium could now
|
|
11
|
+
press keys, but couldn't (a) script a keyboard+joystick startup TIMELINE in one
|
|
12
|
+
call, or (b) tell whether a non-responsive key reached VICE at all. Both added — no
|
|
13
|
+
core rebuild (the `c64_cia1_regs` region + key matrix already existed):
|
|
14
|
+
- **`recordSession` `inputScript[].keys`** — hold C64 keyboard keys from a frame
|
|
15
|
+
until the next entry, interleaved with joystick `ports`, in one deterministic
|
|
16
|
+
timeline (e.g. `{atFrame:0,keys:['f1']},{atFrame:30,ports:[{b:true}]},
|
|
17
|
+
{atFrame:60,keys:['run/stop']},{atFrame:90,keys:[]}`). `ports` is now optional
|
|
18
|
+
(a step may set just keys). Unknown keys are **rejected with a clear error**, not
|
|
19
|
+
silently ignored.
|
|
20
|
+
- **`input({op:'pressKey', verify:true})`** — also samples CIA1 **`$DC00`/`$DC01`**
|
|
21
|
+
(the keyboard/joystick scan ports the KERNAL reads) **before / during (key held)
|
|
22
|
+
/ after**, plus matrix coords + active joyport. Lets you distinguish "my key
|
|
23
|
+
never reached VICE" (`before==during`) from "VICE saw it but the game ignored it"
|
|
24
|
+
(they differ, no reaction) when a C64 game doesn't respond.
|
|
25
|
+
|
|
26
|
+
### Changed — `scaffold` no longer echoes the vendored toolchain manifest
|
|
27
|
+
`scaffold({op:'project'|'game'})` used to return a flat `files[]` of EVERY written
|
|
28
|
+
file — including the toolchain copies (35 of 44 entries on NES, **173 of 264 on
|
|
29
|
+
GBA**, ~270 on SGDK Genesis) that an agent never touches. Across a matrix run (one
|
|
30
|
+
game × every genre × every platform) that was ~100 KB of pure vendored-path lists
|
|
31
|
+
in context with zero decision value. Now the response is a compact receipt:
|
|
32
|
+
- `files` — only the project-**OWNED** files you edit (main source, runtime, crt0,
|
|
33
|
+
cfg, README).
|
|
34
|
+
- `fileCount` (total written) + `vendorFileCount` (the summarized vendored copies,
|
|
35
|
+
on disk if you ever need them).
|
|
36
|
+
- `verbose:true` restores the full flat list as `allFiles`.
|
|
37
|
+
|
|
38
|
+
"Owned" is classified by what a file **is**, not just a `vendor/` prefix — so it
|
|
39
|
+
correctly excludes the SDK header trees the GBA (libtonc `include/`+`sysinclude/`)
|
|
40
|
+
and Genesis (SGDK `include/`) toolchains drop OUTSIDE `vendor/`, plus prebuilt
|
|
41
|
+
`crt*.o` / `*.a` / `*.lib`. (The initial fix used a `vendor/`-prefix denylist and
|
|
42
|
+
missed exactly those two SDK platforms — caught + fixed via a 0.24.0 matrix-run
|
|
43
|
+
report. GBA dropped 173→9 owned, Genesis 82→13.) Mirrors the `inline`/`outputPath`
|
|
44
|
+
choose-your-payload pattern the snippets op already had.
|
|
45
|
+
|
|
46
|
+
### Changed — scaffold README + `nextStep` lead with `build({output:'project'})`
|
|
47
|
+
The generated project README and the scaffold's `nextStep` now lead with the
|
|
48
|
+
one-call **`build({output:'project', platform, path, outputPath})`** form (infers
|
|
49
|
+
toolchain/crt0/linker from the directory — no `sourcesPaths`/`includePaths`/
|
|
50
|
+
`linkerConfig` to hand-specify), and demote the verbose `output:'run'` + manifest
|
|
51
|
+
form to a collapsed "compiling edited loose source" alternative. The project-dir
|
|
52
|
+
build was already the easier path; now it's the one a fresh agent copies first.
|
|
53
|
+
|
|
54
|
+
### Fixed — SMS shmup + Atari 7800 sports scaffolds rendered with wrong colors
|
|
55
|
+
Both built and booted but looked broken (a 0.24.0 matrix report flagged them):
|
|
56
|
+
- **SMS shmup** rendered the starfield as blue/**GREEN** striped bands. The BG
|
|
57
|
+
palette had colour 1 = `0x08`, which in SMS 2-2-2 BGR is green (G bits), not the
|
|
58
|
+
"deep space blue" the comment claimed. Fixed to a pure-blue depth gradient
|
|
59
|
+
(`0x10/0x20/0x30`) — the bands now read as space, dominant colour went green
|
|
60
|
+
`#00aa00` → blue `#0000ad`.
|
|
61
|
+
- **Atari 7800 sports** rendered a near-black playfield that looked dead. Two MARIA
|
|
62
|
+
colour-byte bugs: the court walls used `0x48` (hue 4 = RED → **pink**, not the
|
|
63
|
+
intended blue) and the court floor was `0x00` (black, indistinguishable from a
|
|
64
|
+
blank screen). Fixed to blue walls (`0x8A`, hue 8) + a dark-green court floor
|
|
65
|
+
(`0xB4`) — now reads as an actual court (dominant black → green `#008221`).
|
|
66
|
+
|
|
67
|
+
Both verified by screenshot + `frame({op:'verify'})`. (These were colour-value
|
|
68
|
+
bugs in the scaffold templates, not the render pipeline.)
|
|
69
|
+
|
|
70
|
+
### Removed — `catalog({op:'whatsNew'})` + the old→new tool rename table
|
|
71
|
+
`whatsNew` returned a 125-entry map of pre-1.0 renamed tool names (plus, until now,
|
|
72
|
+
~1.4k tokens of inlined CHANGELOG prose) so an agent resuming an old handoff could
|
|
73
|
+
re-map a tool that had moved. The pre-1.0 consolidation is long settled — the old
|
|
74
|
+
names are git history, and no running agent carries them — so maintaining a
|
|
75
|
+
forever-growing rename record (and risking it landing in context) wasn't worth it.
|
|
76
|
+
Dropped the op, the `tool-manifest.js` map, and its tests. An agent that hits an
|
|
77
|
+
unknown tool name now just reads the current surface (`catalog({op:'categories'})`
|
|
78
|
+
or the tool list); full release notes remain in CHANGELOG.md for humans.
|
|
79
|
+
|
|
7
80
|
## 0.24.0
|
|
8
81
|
|
|
9
82
|
### Added — C64 keyboard + joyport input (VICE core patch)
|
|
@@ -180,11 +180,15 @@ void main(void) {
|
|
|
180
180
|
p1y = 110; p2y = 110;
|
|
181
181
|
serve_ball(0);
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
/* MARIA color byte = (hue<<4)|lum. Court = dark green so the play area
|
|
184
|
+
* reads as an actual field (0x00 black looked like a blank/dead screen);
|
|
185
|
+
* walls = blue. NB: 0x48 is hue 4 = RED-magenta (renders PINK), not blue —
|
|
186
|
+
* blue is hue 8/9, so the walls use 0x8A. */
|
|
187
|
+
BACKGRND = 0xB4; /* dark green court floor (hue 11) */
|
|
184
188
|
P0C1 = 0x0F; /* white paddles + ball */
|
|
185
189
|
P0C2 = 0x0F;
|
|
186
190
|
P0C3 = 0x0F;
|
|
187
|
-
P1C1 =
|
|
191
|
+
P1C1 = 0x8A; /* court walls — BLUE (hue 8); was 0x48 = pink */
|
|
188
192
|
CHARBASE = 0;
|
|
189
193
|
OFFSET = 0;
|
|
190
194
|
|
|
@@ -30,8 +30,11 @@ extern void sms_sat_upload(void);
|
|
|
30
30
|
#define T_ENEMY 2
|
|
31
31
|
|
|
32
32
|
static const uint8_t palette[32] = {
|
|
33
|
-
/* BG: 0 = backdrop, 1 = deep space blue, 2 = lighter space blue, 3 = star white
|
|
34
|
-
|
|
33
|
+
/* BG: 0 = backdrop, 1 = deep space blue, 2 = lighter space blue, 3 = star white.
|
|
34
|
+
* SMS CRAM is 2-2-2 BGR (bits --BB GGRR), so PURE blue uses only the B bits:
|
|
35
|
+
* 0x10=dark, 0x20=medium, 0x30=bright. (The old 0x08 for colour 1 was G=2 =
|
|
36
|
+
* GREEN, which made the alternating starfield bands render blue/GREEN striped.) */
|
|
37
|
+
0x10,0x20,0x30,0x3F, 0x00,0x00,0x00,0x00,
|
|
35
38
|
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
36
39
|
/* Sprite palette: white, yellow, red */
|
|
37
40
|
0x00,0x3F,0x0F,0x03, 0x00,0x00,0x00,0x00,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "romdevtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 14 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp/server.js",
|
package/src/host/LibretroHost.js
CHANGED
|
@@ -928,6 +928,86 @@ export class LibretroHost {
|
|
|
928
928
|
return { key: String(key).toLowerCase(), row, col, frames: Math.max(1, frames | 0) };
|
|
929
929
|
}
|
|
930
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Press a C64 key like pressC64Key, but sample the machine-visible input
|
|
933
|
+
* state (CIA1 $DC00/$DC01 — the keyboard/joystick scan ports) BEFORE,
|
|
934
|
+
* DURING (key held), and AFTER (released). Lets an RE agent tell apart
|
|
935
|
+
* "my key never reached VICE" from "VICE saw it but the game didn't scan
|
|
936
|
+
* it this frame". No core change — reads the already-exposed c64_cia1_regs
|
|
937
|
+
* region ($DC00..$DC0F).
|
|
938
|
+
* @param {string} key
|
|
939
|
+
* @param {number} frames frames to hold (sampled at the midpoint)
|
|
940
|
+
* @returns {object} matrix coords + held flag + per-phase CIA snapshots
|
|
941
|
+
*/
|
|
942
|
+
pressC64KeyVerify(key, frames = 4) {
|
|
943
|
+
const mod = this._needMod();
|
|
944
|
+
if (typeof mod._romdev_key_matrix !== "function") {
|
|
945
|
+
throw new Error("this core build does not expose C64 keyboard input (C64/VICE only).");
|
|
946
|
+
}
|
|
947
|
+
const pos = C64_KEY_MATRIX[String(key).toLowerCase()];
|
|
948
|
+
if (!pos) {
|
|
949
|
+
throw new Error(`unknown C64 key '${key}'. Known: ${Object.keys(C64_KEY_MATRIX).join(", ")}.`);
|
|
950
|
+
}
|
|
951
|
+
const [row, col] = pos;
|
|
952
|
+
const held = Math.max(1, frames | 0);
|
|
953
|
+
// $DC00 = CIA1 PRA (port A — joystick 2 + keyboard col select),
|
|
954
|
+
// $DC01 = CIA1 PRB (port B — joystick 1 + keyboard row read).
|
|
955
|
+
const cia = () => {
|
|
956
|
+
try {
|
|
957
|
+
const r = this.readMemory("c64_cia1_regs", 0, 2);
|
|
958
|
+
return { DC00: r[0], DC01: r[1] };
|
|
959
|
+
} catch { return null; }
|
|
960
|
+
};
|
|
961
|
+
const before = cia();
|
|
962
|
+
mod._romdev_key_matrix(row, col, 1); // press
|
|
963
|
+
this.stepFrames(Math.ceil(held / 2));
|
|
964
|
+
const during = cia(); // key still held
|
|
965
|
+
this.stepFrames(Math.max(1, held - Math.ceil(held / 2)));
|
|
966
|
+
mod._romdev_key_matrix(row, col, 0); // release
|
|
967
|
+
this.stepFrames(1);
|
|
968
|
+
const after = cia();
|
|
969
|
+
return {
|
|
970
|
+
key: String(key).toLowerCase(),
|
|
971
|
+
row, col,
|
|
972
|
+
frames: held,
|
|
973
|
+
joyport: this.getC64JoyPort?.() ?? null,
|
|
974
|
+
autoReleased: true,
|
|
975
|
+
cia1: { before, during, after },
|
|
976
|
+
note: "CIA1 $DC00 (port A) / $DC01 (port B) are the keyboard/joystick scan ports the KERNAL reads. `during` is sampled with the key held; if before==during the key never moved the matrix line (didn't reach VICE); if they differ but the game didn't react, it scanned a different key/port or that screen ignores it.",
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Set the SET of C64 keyboard keys held down (for scripted timelines like
|
|
982
|
+
* recordSession). Diffs against the currently-held set: presses newly-added
|
|
983
|
+
* keys' matrix lines, releases removed ones. Pass [] to release all. Does NOT
|
|
984
|
+
* step frames — the caller's loop owns stepping. Unknown keys throw.
|
|
985
|
+
* @param {string[]} keys
|
|
986
|
+
* @returns {{held: string[], matrix: Array<[number,number]>}}
|
|
987
|
+
*/
|
|
988
|
+
setC64HeldKeys(keys) {
|
|
989
|
+
const mod = this._needMod();
|
|
990
|
+
if (typeof mod._romdev_key_matrix !== "function") {
|
|
991
|
+
throw new Error("this core build does not expose C64 keyboard input (C64/VICE only).");
|
|
992
|
+
}
|
|
993
|
+
const want = new Set((keys ?? []).map((k) => String(k).toLowerCase()));
|
|
994
|
+
for (const k of want) {
|
|
995
|
+
if (!C64_KEY_MATRIX[k]) {
|
|
996
|
+
throw new Error(`unknown C64 key '${k}'. Known: ${Object.keys(C64_KEY_MATRIX).join(", ")}.`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const have = this._c64HeldKeys ?? (this._c64HeldKeys = new Set());
|
|
1000
|
+
// release keys no longer wanted
|
|
1001
|
+
for (const k of have) {
|
|
1002
|
+
if (!want.has(k)) { const [r, c] = C64_KEY_MATRIX[k]; mod._romdev_key_matrix(r, c, 0); have.delete(k); }
|
|
1003
|
+
}
|
|
1004
|
+
// press newly-added keys
|
|
1005
|
+
for (const k of want) {
|
|
1006
|
+
if (!have.has(k)) { const [r, c] = C64_KEY_MATRIX[k]; mod._romdev_key_matrix(r, c, 1); have.add(k); }
|
|
1007
|
+
}
|
|
1008
|
+
return { held: [...have], matrix: [...have].map((k) => C64_KEY_MATRIX[k]) };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
931
1011
|
/**
|
|
932
1012
|
* Feed a PETSCII string into the C64 kernal keyboard buffer (for typing
|
|
933
1013
|
* LOAD/RUN/filenames). `\r` (or `\n`) becomes RETURN. Non-blocking — the
|
package/src/mcp/tools/index.js
CHANGED
|
@@ -58,13 +58,11 @@ import { registerCheatTools } from "./cheats.js";
|
|
|
58
58
|
import { createDisclosure } from "../disclosure.js";
|
|
59
59
|
import { jsonContent, safeTool, withClearToolErrors } from "../util.js";
|
|
60
60
|
import { getHostOrNull, setDisclosure } from "../state.js";
|
|
61
|
-
import { MERGE_MAP } from "../tool-manifest.js";
|
|
62
|
-
import { readFile } from "node:fs/promises";
|
|
63
61
|
import { readFileSync } from "node:fs";
|
|
64
62
|
import { fileURLToPath } from "node:url";
|
|
65
63
|
import { dirname, join } from "node:path";
|
|
66
64
|
|
|
67
|
-
// Package version — surfaced by catalog({op:'status'
|
|
65
|
+
// Package version — surfaced by catalog({op:'status'}) so an agent can
|
|
68
66
|
// check the running romdev version with a plain TOOL CALL (works over MCP AND the
|
|
69
67
|
// HTTP/skill surface), e.g. to detect a saved skill is stale. (GET /healthz also
|
|
70
68
|
// reports it for non-tool HTTP clients.)
|
|
@@ -76,42 +74,6 @@ const PKG_VERSION = (() => {
|
|
|
76
74
|
}
|
|
77
75
|
})();
|
|
78
76
|
|
|
79
|
-
// catalog({op:'whatsNew'}): the recent CHANGELOG + an old→new RENAME TABLE
|
|
80
|
-
// derived from MERGE_MAP (the single source of truth for the consolidation), so
|
|
81
|
-
// an agent resuming a handoff written against an older server can re-map every
|
|
82
|
-
// renamed tool in ONE read. Pre-1.0 the surface is consolidated freely with NO
|
|
83
|
-
// deprecated aliases (see the consolidation), which is exactly why this exists.
|
|
84
|
-
async function buildWhatsNew() {
|
|
85
|
-
// old name → "newTool({axis:'oldOpName'})". The op value is best-effort: most
|
|
86
|
-
// absorbed tools become an op whose name is a shortened form, so we surface the
|
|
87
|
-
// axis + the new tool and let the tool's own description give the exact op enum.
|
|
88
|
-
const renames = {};
|
|
89
|
-
for (const [newTool, entry] of Object.entries(MERGE_MAP)) {
|
|
90
|
-
if (entry.unchanged) continue;
|
|
91
|
-
for (const old of entry.absorbs ?? []) {
|
|
92
|
-
renames[old] = { nowOn: newTool, axis: entry.axis };
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Recent CHANGELOG entries (top of the file = newest). Best-effort: if the file
|
|
96
|
-
// isn't packaged in some install, return the rename table alone.
|
|
97
|
-
let changelog = null;
|
|
98
|
-
try {
|
|
99
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
100
|
-
// src/mcp/tools/ → package root is three up.
|
|
101
|
-
const text = await readFile(join(here, "..", "..", "..", "CHANGELOG.md"), "utf8");
|
|
102
|
-
// Keep the two most recent version sections.
|
|
103
|
-
const sections = text.split(/^## /m);
|
|
104
|
-
changelog = sections.slice(0, 3).join("## ").trim();
|
|
105
|
-
} catch { /* changelog not present in this install */ }
|
|
106
|
-
return {
|
|
107
|
-
romdevVersion: PKG_VERSION,
|
|
108
|
-
note: "Pre-1.0 the tool surface is consolidated freely with NO deprecated aliases. If a tool name from an older handoff is missing, it's almost certainly now an `op` (or other axis) on a domain tool — find it below, then read that tool's description for the exact op enum and params.",
|
|
109
|
-
renameTable: renames,
|
|
110
|
-
axisLegend: "Every domain tool is keyed by ONE axis: op (most), output (build), on (breakpoint), target (disasm), view (background), source (palette), stage (encodeArt), from (importArt). The value names the operation, e.g. romPatch({op:'findPointer'}).",
|
|
111
|
-
...(changelog ? { changelog } : { changelogNote: "CHANGELOG.md not bundled in this install." }),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
77
|
/**
|
|
116
78
|
* Categories for progressive disclosure. Each entry's `register` is the
|
|
117
79
|
* existing per-module registration function — we don't rewrite the
|
|
@@ -225,16 +187,12 @@ export function registerTools(server, z, sessionKey) {
|
|
|
225
187
|
"catalog",
|
|
226
188
|
"Orient yourself, keyed by `op`.\n" +
|
|
227
189
|
"• op:'categories' (default) — the catalog of tool categories, each {name, description, useWhen[], loaded}. This server registers EVERY tool at session start, so this is just a map grouped by purpose for orientation, NOT a gate — you do NOT need to load anything before calling a tool.\n" +
|
|
228
|
-
"• op:'status' — a snapshot of the current session: which platform's core/ROM is in the running host (if any), current frame count, last-loaded media, loaded categories. Call this when you've lost context across many tool calls and want to re-ground
|
|
229
|
-
"• op:'whatsNew' — the recent CHANGELOG + an OLD→NEW tool RENAME TABLE. Call this FIRST if you're resuming work from a handoff written against an older server: pre-1.0 the surface is consolidated freely (no deprecated aliases), so a name you remember may now be an `op` on a domain tool. This maps them in one read instead of probing each tool.",
|
|
190
|
+
"• op:'status' — a snapshot of the current session: which platform's core/ROM is in the running host (if any), current frame count, last-loaded media, loaded categories. Call this when you've lost context across many tool calls and want to re-ground.",
|
|
230
191
|
{
|
|
231
|
-
op: z.enum(["categories", "status"
|
|
232
|
-
.describe("categories=tool-category catalog; status=live session snapshot (romdevVersion + host/platform/frameCount/media — call this to check the running version, e.g. is a saved skill stale)
|
|
192
|
+
op: z.enum(["categories", "status"]).default("categories")
|
|
193
|
+
.describe("categories=tool-category catalog; status=live session snapshot (romdevVersion + host/platform/frameCount/media — call this to check the running version, e.g. is a saved skill stale)."),
|
|
233
194
|
},
|
|
234
195
|
safeTool(async ({ op = "categories" }) => {
|
|
235
|
-
if (op === "whatsNew") {
|
|
236
|
-
return jsonContent(await buildWhatsNew());
|
|
237
|
-
}
|
|
238
196
|
if (op === "status") {
|
|
239
197
|
const host = getHostOrNull(sessionKey);
|
|
240
198
|
const cats = disclosure.listCategories();
|
package/src/mcp/tools/input.js
CHANGED
|
@@ -228,6 +228,7 @@ export function registerInputTools(server, z, sessionKey) {
|
|
|
228
228
|
key: z.string().optional().describe("op=pressKey (C64): key name — f1/f3/f5/f7, return, space, run/stop, a-z, 0-9, ctrl, cbm, home, down, right, lshift, rshift."),
|
|
229
229
|
text: z.string().optional().describe("op=typeText (C64): string fed into the keyboard buffer; \\r / \\n become RETURN. e.g. 'LOAD\"*\",8,1\\rRUN\\r'."),
|
|
230
230
|
joyport: z.number().int().min(1).max(2).optional().describe("op=joyport (C64): set the active joystick port (1 or 2). Omit to just GET the current port. Default is 2 (most C64 games)."),
|
|
231
|
+
verify: z.boolean().default(false).describe("op=pressKey (C64): also sample CIA1 $DC00/$DC01 (the keyboard/joystick scan ports the KERNAL reads) BEFORE / DURING (key held) / AFTER, plus matrix coords + active joyport. Use to tell apart 'my key never reached VICE' (before==during) from 'VICE saw it but the game ignored it' (they differ but no reaction) when a C64 game doesn't respond to a key."),
|
|
231
232
|
},
|
|
232
233
|
safeTool(async (args) => {
|
|
233
234
|
switch (args.op) {
|
|
@@ -256,6 +257,10 @@ export function registerInputTools(server, z, sessionKey) {
|
|
|
256
257
|
case "pressKey": {
|
|
257
258
|
if (!args.key) throw new Error("input({op:'pressKey'}): `key` is required (C64 keyboard key, e.g. 'f1', 'return', 'run/stop').");
|
|
258
259
|
const host = getHost(sessionKey);
|
|
260
|
+
if (args.verify) {
|
|
261
|
+
const v = host.pressC64KeyVerify(args.key, args.frames ?? 4);
|
|
262
|
+
return jsonContent({ pressedKey: v.key, matrix: [v.row, v.col], frames: v.frames, joyport: v.joyport, autoReleased: v.autoReleased, cia1: v.cia1, frameCount: host.status.frameCount, note: v.note });
|
|
263
|
+
}
|
|
259
264
|
const r = host.pressC64Key(args.key, args.frames ?? 4);
|
|
260
265
|
return jsonContent({ pressedKey: r.key, matrix: [r.row, r.col], frames: r.frames, frameCount: host.status.frameCount });
|
|
261
266
|
}
|
package/src/mcp/tools/project.js
CHANGED
|
@@ -1422,7 +1422,7 @@ async function copyDirRecursive(fs, path, srcDir, dstDir, writtenFiles, dstPrefi
|
|
|
1422
1422
|
* handler returns: {path, platform, template, files, sourceFile,
|
|
1423
1423
|
* toolchain, nextStep}.
|
|
1424
1424
|
*/
|
|
1425
|
-
export async function createProjectImpl({ platform, name, path: projPath, title, template, overwrite = false, withSnippets = false }) {
|
|
1425
|
+
export async function createProjectImpl({ platform, name, path: projPath, title, template, overwrite = false, withSnippets = false, verbose = false }) {
|
|
1426
1426
|
const fs = await import("node:fs/promises");
|
|
1427
1427
|
const path = await import("node:path");
|
|
1428
1428
|
const { fileURLToPath } = await import("node:url");
|
|
@@ -1768,7 +1768,12 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1768
1768
|
}
|
|
1769
1769
|
filesSection += `\nEvery byte that compiles into your ROM is in this directory. If you move the repo somewhere else, you don't need to install anything from romdev to rebuild it — the compiler binaries are the only external dependency.\n\n`;
|
|
1770
1770
|
|
|
1771
|
-
|
|
1771
|
+
// Lead with the project-dir build — ONE call, no manifest. The verbose
|
|
1772
|
+
// output:'run' + sourcesPaths form (buildBlock) is the "editing loose
|
|
1773
|
+
// source" variant, shown second.
|
|
1774
|
+
const projectBuildBlock =
|
|
1775
|
+
"```js\nbuild({\n output: \"project\",\n platform: \"" + platform + "\",\n path: \"" + projPath + "\",\n outputPath: \"" + name + romExt + "\",\n})\n```";
|
|
1776
|
+
const readme = `# ${title ?? name}\n\nA ${lang} project for ${platform}, scaffolded by romdev.\n\n${tmpl?.describe ? tmpl.describe + "\n\n" : ""}${filesSection}${c89Note}## Build + run with romdev\n\nThe whole project directory builds in ONE call — romdev infers the toolchain, crt0, and linker from the directory, so you don't pass a file manifest:\n\n${projectBuildBlock}\n\nAdd \`output:"run"\` instead of \`"project"\` to also load + run + screenshot in the same round trip. Re-run the exact same call after every edit.\n\n<details>\n<summary>Alternative: build from a hand-specified source manifest (when compiling edited loose source, not a project dir)</summary>\n\n${buildBlock}\n</details>\n\n## Iterating\n\n- Edit \`${mainFilename}\` (or any of the runtime / crt0 / cfg files — they're yours).\n- Re-run the \`build({output:"project"|"run", path})\` call above to see your changes — it builds + (for run) loads + runs + screenshots in one round trip.\n- Inspect at byte level: \`memory({op:"read"})\`, \`sprites({op:"inspect"})\`, \`palette({source:"live"})\`, \`background({view:"rendered"})\`.\n- Open a playtest window for human eyes: \`playtest({op:"open"})\` — returns immediately, the window follows your rebuilds, and the emulator stays live for every other tool.\n`;
|
|
1772
1777
|
await fs.writeFile(path.join(projPath, "README.md"), readme, "utf-8");
|
|
1773
1778
|
writtenFiles.push("README.md");
|
|
1774
1779
|
|
|
@@ -1828,19 +1833,46 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1828
1833
|
}
|
|
1829
1834
|
}
|
|
1830
1835
|
|
|
1836
|
+
// Split the manifest: project-OWNED files (main.c, runtime helpers, crt0,
|
|
1837
|
+
// cfg, README…) are the only ones an agent touches; the rest are internal
|
|
1838
|
+
// toolchain copies on disk that never enter a decision. Echoing all of
|
|
1839
|
+
// them — 35/44 on NES (vendor/cc65/libsrc/*), 173/264 on GBA (libtonc
|
|
1840
|
+
// include/+sysinclude/), ~270 on SGDK Genesis — was pure context noise
|
|
1841
|
+
// across a matrix run. Default to a compact receipt (owned list + a
|
|
1842
|
+
// not-owned COUNT); `verbose:true` restores the full flat list.
|
|
1843
|
+
//
|
|
1844
|
+
// Classify NON-owned by what it actually is, NOT just a `vendor/` prefix:
|
|
1845
|
+
// the cc65 path lands under vendor/, but the GBA/Genesis SDKs drop their
|
|
1846
|
+
// header trees at include/ + sysinclude/ (no vendor/ prefix) and prebuilt
|
|
1847
|
+
// crt objects/archives at the root — none of which an agent edits. (R: the
|
|
1848
|
+
// original `!startsWith('vendor/')` denylist missed exactly these two SDK
|
|
1849
|
+
// platforms — same bug class as the original fix, second location.)
|
|
1850
|
+
const isVendored = (f) =>
|
|
1851
|
+
f.startsWith("vendor/") || // cc65 libsrc, pvsneslib, sgdk src
|
|
1852
|
+
f.startsWith("include/") || // SDK header trees (libtonc/libgba/SGDK/maxmod)
|
|
1853
|
+
f.startsWith("sysinclude/") || // libgba/libtonc system headers
|
|
1854
|
+
/^crt[a-z0-9]*\.o$/i.test(f) || // prebuilt crt objects (crti/crtn/crtbegin/crtend)
|
|
1855
|
+
/\.(a|lib)$/i.test(f); // prebuilt static archives
|
|
1856
|
+
const ownedFiles = writtenFiles.filter((f) => !isVendored(f));
|
|
1857
|
+
const vendorFileCount = writtenFiles.length - ownedFiles.length;
|
|
1831
1858
|
return {
|
|
1832
1859
|
path: projPath,
|
|
1833
1860
|
platform,
|
|
1834
1861
|
template: hasTemplates ? (template ?? "default") : null,
|
|
1835
|
-
files
|
|
1862
|
+
// The files you actually edit. Vendored toolchain copies are summarized,
|
|
1863
|
+
// not listed — they're on disk under vendor/ if you ever need them.
|
|
1864
|
+
files: ownedFiles,
|
|
1865
|
+
fileCount: writtenFiles.length,
|
|
1866
|
+
vendorFileCount,
|
|
1867
|
+
...(verbose ? { allFiles: writtenFiles } : {}),
|
|
1836
1868
|
snippetsCopied: withSnippets ? snippetFiles : null,
|
|
1837
1869
|
sourceFile: path.join(projPath, mainFilename),
|
|
1838
1870
|
toolchain: lang,
|
|
1839
|
-
nextStep: `
|
|
1871
|
+
nextStep: `Build the scaffold AS-IS in one call: build({output:"project", platform:"${platform}", path:"${projPath}", outputPath:"<game>.<ext>"}) — it infers the toolchain/crt0/linker from the directory, no sourcesPaths/includePaths/linkerConfig needed. Then edit ${mainFilename} and re-run the same call. (build({output:"run", ...}) with a hand-specified sourcesPaths manifest is the alternative when you're compiling edited loose source instead of a project dir.)`,
|
|
1840
1872
|
};
|
|
1841
1873
|
}
|
|
1842
1874
|
|
|
1843
|
-
async function createGameCore({ platform, genre, name, path: projPath, title, overwrite }) {
|
|
1875
|
+
async function createGameCore({ platform, genre, name, path: projPath, title, overwrite, verbose = false }) {
|
|
1844
1876
|
// The five canonical genres. A genre is available on a platform iff
|
|
1845
1877
|
// TEMPLATES[platform] has a matching template entry — we DERIVE
|
|
1846
1878
|
// availability from TEMPLATES rather than maintain a parallel table,
|
|
@@ -1887,7 +1919,7 @@ async function createGameCore({ platform, genre, name, path: projPath, title, ov
|
|
|
1887
1919
|
// Genre id IS the template id (they're 1:1 by construction).
|
|
1888
1920
|
const templateId = genre;
|
|
1889
1921
|
const result = await createProjectImpl({
|
|
1890
|
-
platform, template: templateId, name, path: projPath, title, overwrite,
|
|
1922
|
+
platform, template: templateId, name, path: projPath, title, overwrite, verbose,
|
|
1891
1923
|
});
|
|
1892
1924
|
return { ...result, genre, template: templateId };
|
|
1893
1925
|
}
|
|
@@ -1917,6 +1949,7 @@ export function registerProjectTools(server, z) {
|
|
|
1917
1949
|
// project
|
|
1918
1950
|
template: z.string().optional().describe("op=project: template id ('default' | 'hello_sprite' | 'tile_engine' on NES/GB/GBC; 'default' elsewhere)."),
|
|
1919
1951
|
withSnippets: z.boolean().default(false).describe("op=project: also drop every vetted snippet alongside main (= scaffold copySnippets after)."),
|
|
1952
|
+
verbose: z.boolean().default(false).describe("op=project/game: echo the FULL flat file manifest (incl. vendor/** toolchain copies) as `allFiles`. Default false — the response lists only project-OWNED files you edit (`files`) plus a `vendorFileCount`, since the vendored toolchain copies are on disk and never need echoing (they're 35 of 44 entries on NES, ~270 on SGDK Genesis). Set true only if you specifically need every path in the response."),
|
|
1920
1953
|
// game
|
|
1921
1954
|
genre: z.string().optional().describe("op=game: 'shmup' | 'platformer' | 'puzzle' | 'sports' | 'racing'."),
|
|
1922
1955
|
// snippets
|
package/src/mcp/tools/record.js
CHANGED
|
@@ -44,11 +44,12 @@ export function registerRecordTools(server, z, sessionKey) {
|
|
|
44
44
|
.array(
|
|
45
45
|
z.object({
|
|
46
46
|
atFrame: z.number().int().min(0),
|
|
47
|
-
ports: z.array(inputShape).max(2),
|
|
47
|
+
ports: z.array(inputShape).max(2).optional(),
|
|
48
|
+
keys: z.array(z.string()).optional().describe("C64-ONLY: C64 keyboard keys held from this frame until the next entry (f1/f3/f5/f7, return, space, run/stop, a-z, 0-9, …). [] releases all held keys. Unknown keys are rejected with a clear error. Lets you script a keyboard+joystick startup timeline (e.g. {atFrame:0,keys:['f1']},{atFrame:30,ports:[{b:true}]},{atFrame:90,keys:['run/stop']}) in one call."),
|
|
48
49
|
}),
|
|
49
50
|
)
|
|
50
51
|
.optional()
|
|
51
|
-
.describe("Per-frame input changes. Each entry sets the input at `atFrame` and holds it until the next entry."),
|
|
52
|
+
.describe("Per-frame input changes. Each entry sets the input at `atFrame` (joystick `ports` and/or C64 `keys`) and holds it until the next entry. Either field is optional — a step may set just keys, just ports, or both."),
|
|
52
53
|
memorySamples: z
|
|
53
54
|
.array(
|
|
54
55
|
z.object({
|
|
@@ -99,7 +100,12 @@ export function registerRecordTools(server, z, sessionKey) {
|
|
|
99
100
|
while (elapsed < frames) {
|
|
100
101
|
// Apply any scripted inputs whose atFrame ≤ current frame.
|
|
101
102
|
while (scriptIdx < script.length && script[scriptIdx].atFrame <= elapsed) {
|
|
102
|
-
|
|
103
|
+
const entry = script[scriptIdx];
|
|
104
|
+
if (entry.ports) host.setInput({ ports: entry.ports });
|
|
105
|
+
// C64 keyboard keys held from this entry until the next. Pass [] to
|
|
106
|
+
// release all. Only valid on a C64/VICE host (setC64HeldKeys throws
|
|
107
|
+
// otherwise — surfaced as a clear error, not a silent no-op).
|
|
108
|
+
if (entry.keys !== undefined) host.setC64HeldKeys(entry.keys);
|
|
103
109
|
scriptIdx++;
|
|
104
110
|
}
|
|
105
111
|
const batch = Math.min(sampleEvery, frames - elapsed);
|
|
@@ -134,6 +134,26 @@ key:
|
|
|
134
134
|
A typical C64 RE startup: load → step to the title → `pressKey f1` (1 player) →
|
|
135
135
|
`set {b:true}` (fire to start) → step → you're in gameplay → `state({op:'save'})`.
|
|
136
136
|
|
|
137
|
+
**Script the whole startup in one call.** `recordSession`'s `inputScript` takes
|
|
138
|
+
C64 `keys` alongside joystick `ports` — held from each entry until the next — so a
|
|
139
|
+
key+joystick startup is one deterministic timeline instead of many calls:
|
|
140
|
+
```js
|
|
141
|
+
recordSession({ frames:600, sampleEvery:60, outputDir:'…', inputScript:[
|
|
142
|
+
{ atFrame:0, keys:['f1'] }, // 1 player
|
|
143
|
+
{ atFrame:30, ports:[{ b:true }] }, // fire (port 2)
|
|
144
|
+
{ atFrame:90, keys:['run/stop'] }, // start
|
|
145
|
+
{ atFrame:120, keys:[] } ] }) // release
|
|
146
|
+
```
|
|
147
|
+
A step may set just `keys`, just `ports`, or both; `keys:[]` releases all. An
|
|
148
|
+
unknown key is rejected with a clear error (not a silent no-op).
|
|
149
|
+
|
|
150
|
+
**When a key seems ignored, probe it.** `input({op:'pressKey', key:'run/stop',
|
|
151
|
+
verify:true})` returns the matrix coords, active joyport, and a CIA1 snapshot
|
|
152
|
+
(`$DC00`/`$DC01`) **before / during (held) / after**. `before==during` ⇒ the key
|
|
153
|
+
never moved the matrix line (didn't reach VICE); they differ but the game didn't
|
|
154
|
+
react ⇒ it scanned a different key/port, or that screen (crack/doc/trainer)
|
|
155
|
+
ignores it. This is how you tell a romdev problem from a game-flow problem.
|
|
156
|
+
|
|
137
157
|
**Controller-alone (playtest):** a human in `playtest` needs no keyboard — the
|
|
138
158
|
pad's spare buttons map to the C64 keys (X=Space, L2=Run/Stop, R2=Return,
|
|
139
159
|
right-stick=F1/F3/F5/F7, top face=F1; d-pad+Fire=joystick). The same mapping
|
package/src/mcp/tool-manifest.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// Tool manifest — the SINGLE SOURCE OF TRUTH for the consolidated tool surface.
|
|
2
|
-
//
|
|
3
|
-
// The 132→34 consolidation (see internal CONSOLIDATION_PLAN) merges narrow tools
|
|
4
|
-
// into domain tools with a typed operation axis. This manifest records, for each
|
|
5
|
-
// consolidated tool, the OLD tools it absorbs and the axis it routes on — so:
|
|
6
|
-
// 1. a coverage-gate test can assert every old tool name maps to exactly one
|
|
7
|
-
// new tool (no capability silently dropped, no dupe);
|
|
8
|
-
// 2. a tool-count budget test can fail the build if the surface regrows;
|
|
9
|
-
// 3. docs + the rename map for downstream agents derive from one place.
|
|
10
|
-
//
|
|
11
|
-
// GOVERNANCE: a new capability is a new PARAMETER/op-value on an existing tool by
|
|
12
|
-
// default — NOT a new top-level tool. Adding an entry here is a deliberate act the
|
|
13
|
-
// budget test surfaces at PR time.
|
|
14
|
-
//
|
|
15
|
-
// Each MERGE_MAP entry: newTool → { absorbs:[...oldNames], axis:'op'|'as'|... }.
|
|
16
|
-
// `absorbs: []` + `unchanged:true` means the tool kept its name (no merge).
|
|
17
|
-
// This map grows one domain at a time as each consolidated tool lands.
|
|
18
|
-
|
|
19
|
-
export const MERGE_MAP = {
|
|
20
|
-
// ── files (generic disk I/O) ──
|
|
21
|
-
files: { absorbs: ["writeAsset", "readAsset", "listAssets"], axis: "op" },
|
|
22
|
-
// ── cheats (DB lookup/search + apply/clear + make) ──
|
|
23
|
-
cheats: { absorbs: ["gameCheats", "searchCheats", "applyCheat", "clearCheats", "makeCheat"], axis: "op" },
|
|
24
|
-
// ── text (custom-font learn/encode/find for romhacking) ──
|
|
25
|
-
text: { absorbs: ["learnFontMap", "encodeTextForRom", "findEncodedText"], axis: "op" },
|
|
26
|
-
// ── symbols (name↔addr, memory map, PC→symbol). buildSourceWithDebug stays for `build`. ──
|
|
27
|
-
symbols: { absorbs: ["resolveSymbol", "lookupAddress", "getMemoryMap", "listSymbols", "addressToSymbol"], axis: "op" },
|
|
28
|
-
// ── disasm (raw bytes / ROM / project / references) ──
|
|
29
|
-
disasm: { absorbs: ["disassemble", "disassembleRom", "disassembleProject", "findReferences"], axis: "target" },
|
|
30
|
-
// ── state (save/load/list/export/dump/diff; diffState moved here from memory.js) ──
|
|
31
|
-
state: { absorbs: ["saveState", "loadState", "listStates", "exportState", "dumpState", "diffState"], axis: "op" },
|
|
32
|
-
// ── input (set/press/sequence/navigate/layout; getInputLayout folded in) ──
|
|
33
|
-
input: { absorbs: ["setInput", "pressButton", "inputSequence", "navigate", "getInputLayout"], axis: "op" },
|
|
34
|
-
// ── platform (list/resolve/toolchains/docs/doc; spans platforms.js+platform-docs.js+toolchain.js) ──
|
|
35
|
-
platform: { absorbs: ["listPlatforms", "resolvePlatform", "listToolchains", "installToolchain", "listPlatformDocs", "getPlatformDoc"], axis: "op" },
|
|
36
|
-
// ── host (unload/shutdown/reset/pause/resume FSM; loadMedia + getStatus stay separate) ──
|
|
37
|
-
host: { absorbs: ["unloadMedia", "shutdown", "reset", "pause", "resume"], axis: "op" },
|
|
38
|
-
// ── frame (step/screenshot/stepAndShot/stepInstruction; stepInstruction folded from watch-memory.js) ──
|
|
39
|
-
frame: { absorbs: ["stepFrames", "screenshot", "stepAndScreenshot", "stepInstruction"], axis: "op" },
|
|
40
|
-
// ── scaffold (project/game + snippets; patchGbHeader folded into romPatch op:'gbHeader') ──
|
|
41
|
-
scaffold: { absorbs: ["createProject", "createGame", "starterSnippets", "copyStarterSnippets"], axis: "op" },
|
|
42
|
-
// ── cart (identify/extract/wrap; identifyRom from rom-id.js, rest from cart-parts.js) ──
|
|
43
|
-
cart: { absorbs: ["identifyRom", "extractCart", "wrapRomFromParts"], axis: "op" },
|
|
44
|
-
// ── palette (live/platformMaster/lospec; spans platform-tools.js + lospec.js) ──
|
|
45
|
-
palette: { absorbs: ["inspectPalette", "getPlatformPalettePng", "getLospecPalette"], axis: "source" },
|
|
46
|
-
// ── audioDebug (inspect/record; getAudioState from platform-tools.js, recordAudio from audio.js; pcmToBrr/wavToXgm2Pcm stay) ──
|
|
47
|
-
audioDebug: { absorbs: ["getAudioState", "recordAudio"], axis: "op" },
|
|
48
|
-
// ── sprites (inspect OAM + meta-sprite pipeline; inspectSprites from platform-tools.js, rest from metasprite-tools.js; validateGenesisTiles stays for encodeArt) ──
|
|
49
|
-
sprites: { absorbs: ["inspectSprites", "groupVisibleSprites", "previewVisibleSprites", "captureMetaSprite", "renderMetaSpritePreview", "emitMetaSpriteRenderer", "extractSpriteFromScreenshot"], axis: "op" },
|
|
50
|
-
// ── background (tilemap/render-state; inspectBackgroundMap from platform-tools.js, getRenderingContext from rendering-context.js, whichTilesAreRendered from which-tiles.js) ──
|
|
51
|
-
background: { absorbs: ["inspectBackgroundMap", "getRenderingContext", "whichTilesAreRendered"], axis: "view" },
|
|
52
|
-
// ── tiles (decode/render tile bytes; inspectPatternTiles from platform-tools.js, getTile/tileFingerprints/tilesAscii from tile-inspect.js, extractSpriteSheet from rom-id.js, previewTileArt from preview-tile.js) ──
|
|
53
|
-
tiles: { absorbs: ["inspectPatternTiles", "getTile", "tileFingerprints", "tilesAscii", "extractSpriteSheet", "previewTileArt"], axis: "op" },
|
|
54
|
-
// ── encodeArt (PNG→native art; convertImageToTiles+imageToTilemap from platform-tools.js, quantizePngForPlatform+cropSpriteSheet from sprite-pipeline.js, validateGenesisTiles from metasprite-tools.js) ──
|
|
55
|
-
encodeArt: { absorbs: ["convertImageToTiles", "imageToTilemap", "quantizePngForPlatform", "cropSpriteSheet", "validateGenesisTiles"], axis: "stage" },
|
|
56
|
-
// ── importArt (editor-file/ROM → native tiles; load* from art-loaders.js, crossPlatformSpriteImport from sprite-pipeline.js as from:'rom') ──
|
|
57
|
-
importArt: { absorbs: ["loadAsepriteSheet", "loadGifAnimation", "loadSpriteSheet", "loadTilemap", "crossPlatformSpriteImport"], axis: "from" },
|
|
58
|
-
// ── memory (read/write/search; all 8 from memory.js) ──
|
|
59
|
-
memory: { absorbs: ["readMemory", "writeMemory", "readCartRom", "snapshotMemory", "diffMemory", "classifyRegion", "searchValue", "searchNext"], axis: "op" },
|
|
60
|
-
// ── cpu (read/drive; getCPUState from platform-tools.js, setRegister/callSubroutine/decompressWith from watch-memory.js) ──
|
|
61
|
-
cpu: { absorbs: ["getCPUState", "setRegister", "callSubroutine", "decompressWith"], axis: "op" },
|
|
62
|
-
// ── breakpoint (STOP-on-first; all 4 from watch-memory.js) ──
|
|
63
|
-
breakpoint: { absorbs: ["findWriter", "runUntilWrite", "runUntilPC", "runUntilRead"], axis: "on" },
|
|
64
|
-
// ── watch (LOG-ALL; watchMemory/watchRange/logPCRange + Genesis VDP-DMA trace
|
|
65
|
-
// on:'dma' from watchDma/traceVramSource — all from watch-memory.js +
|
|
66
|
-
// trace-vram-source.js. dmaTrace was folded in as watch({on:'dma'}).) ──
|
|
67
|
-
watch: { absorbs: ["watchMemory", "watchRange", "logPCRange", "watchDma", "traceVramSource"], axis: "on" },
|
|
68
|
-
// ── build (compile/run; buildSource/buildProject/runSource from toolchain.js, buildSourceWithDebug from symbols.js). ENTRY-TIER. ──
|
|
69
|
-
build: { absorbs: ["buildSource", "buildSourceWithDebug", "buildProject", "runSource"], axis: "output" },
|
|
70
|
-
// ── romPatch (9-op ROM-hack toolkit; patchFile/patchRom from rom-id.js, spliceCHR from splice-chr.js, relocateBlock/makeStoredBlock/findPointerTo from reinject.js, findFreeSpace from free-space.js, diffRoms from diff-roms.js, patchGbHeader as op:'gbHeader') ──
|
|
71
|
-
romPatch: { absorbs: ["patchFile", "patchRom", "spliceCHR", "relocateBlock", "makeStoredBlock", "findFreeSpace", "findPointerTo", "diffRoms", "patchGbHeader"], axis: "op" },
|
|
72
|
-
// ── catalog (orient; listCategories + getStatus, both entry-tier in index.js) ──
|
|
73
|
-
catalog: { absorbs: ["listCategories", "getStatus"], axis: "op" },
|
|
74
|
-
// ── playtest (show-a-human window FSM; all 4 from playtest.js). ENTRY-TIER. ──
|
|
75
|
-
playtest: { absorbs: ["playtestStop", "playtestStatus", "playtestFramebuffer"], axis: "op" },
|
|
76
|
-
// ── encodeAudio (external clip → native sample format; pcmToBrr + wavToXgm2Pcm from audio.js) ──
|
|
77
|
-
encodeAudio: { absorbs: ["pcmToBrr", "wavToXgm2Pcm"], axis: "target" },
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/** Every OLD tool name that the consolidation removes (absorbed into a new tool). */
|
|
81
|
-
export function absorbedToolNames() {
|
|
82
|
-
const names = [];
|
|
83
|
-
for (const entry of Object.values(MERGE_MAP)) {
|
|
84
|
-
if (entry.absorbs) names.push(...entry.absorbs);
|
|
85
|
-
}
|
|
86
|
-
return names;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The consolidated (new) tool names this manifest defines. */
|
|
90
|
-
export function consolidatedToolNames() {
|
|
91
|
-
return Object.keys(MERGE_MAP);
|
|
92
|
-
}
|