romdevtools 0.23.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 +145 -498
- package/CHANGELOG.md +114 -3
- package/examples/atari7800/templates/sports.c +6 -2
- package/examples/sms/templates/shmup.c +5 -2
- package/package.json +2 -2
- 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 +250 -1
- package/src/http/skill-doc.js +1 -1
- package/src/mcp/tools/index.js +4 -46
- package/src/mcp/tools/input-layout.js +10 -0
- package/src/mcp/tools/input.js +31 -2
- package/src/mcp/tools/playtest.js +17 -2
- package/src/mcp/tools/project.js +39 -6
- package/src/mcp/tools/record.js +9 -3
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +155 -6
- package/src/platforms/atari2600/MENTAL_MODEL.md +37 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +36 -0
- package/src/platforms/c64/MENTAL_MODEL.md +103 -6
- package/src/platforms/gb/MENTAL_MODEL.md +56 -0
- package/src/platforms/gba/MENTAL_MODEL.md +57 -3
- package/src/platforms/gba/lib/arm-archives/libc.a +0 -0
- package/src/platforms/gba/lib/arm-archives/libgcc.a +0 -0
- package/src/platforms/gba/lib/arm-archives/libnosys.a +0 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +21 -0
- package/src/platforms/genesis/MENTAL_MODEL.md +19 -0
- package/src/platforms/genesis/lib/c/libc.a +0 -0
- package/src/platforms/genesis/lib/c/libgcc.a +0 -0
- package/src/platforms/genesis/lib/c/libm.a +0 -0
- package/src/platforms/gg/MENTAL_MODEL.md +24 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +33 -7
- package/src/platforms/msx/MENTAL_MODEL.md +27 -0
- package/src/platforms/nes/MENTAL_MODEL.md +35 -0
- package/src/platforms/sms/MENTAL_MODEL.md +51 -0
- package/src/platforms/snes/MENTAL_MODEL.md +21 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +43 -0
- package/src/playtest/playtest.js +48 -0
- package/examples/msx/catch_game/_verify.mjs +0 -93
- package/examples/pce/catch_game/_verify.mjs +0 -75
- package/src/mcp/tool-manifest.js +0 -92
|
@@ -160,6 +160,30 @@ void main(void) {
|
|
|
160
160
|
}
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
## MCP debug & inspection tooling
|
|
164
|
+
|
|
165
|
+
Game Gear runs on the same genesis_plus_gx (gpgx, patched) core as SMS, so the
|
|
166
|
+
inspectors are identical. **The canonical reference lives in the SMS
|
|
167
|
+
MENTAL_MODEL** (`src/platforms/sms/MENTAL_MODEL.md`, "MCP debug & inspection
|
|
168
|
+
tooling" section): `sprites({op:'inspect'})`, `tiles({op:'png'})`,
|
|
169
|
+
`cpu({op:'read'})` (Z80), `background({view:'renderState'})`,
|
|
170
|
+
`audioDebug({op:'inspect', chip:'psg'})` (SN76489, the shared
|
|
171
|
+
SMS/GG/Genesis region), and the z80 `objdump` disasm pipeline all apply
|
|
172
|
+
unchanged.
|
|
173
|
+
|
|
174
|
+
Game-Gear-only deltas:
|
|
175
|
+
|
|
176
|
+
- **`palette({source:'live'})`** decodes **12-bit BGR (4-4-4)**, twice the
|
|
177
|
+
depth of SMS's 6-bit. CRAM is **64 bytes** (2 little-endian bytes per
|
|
178
|
+
entry) instead of 32.
|
|
179
|
+
- The Game-Gear memory regions are **`gg_vram`** and **`gg_cram`** (the
|
|
180
|
+
64-byte palette); use these instead of `sms_vram` / `sms_cram`. The
|
|
181
|
+
`sms_vdp_regs` / `sms_z80_regs` register regions are shared (same VDP and
|
|
182
|
+
Z80).
|
|
183
|
+
- `sprites({op:'inspect'})` X/Y fields are reported in **256×192 hardware
|
|
184
|
+
coordinates**, not the 160×144 visible window — match them with
|
|
185
|
+
hardware-coord arithmetic (see "Sprite coords are hardware-space" above).
|
|
186
|
+
|
|
163
187
|
## Differences from SMS — quick reference
|
|
164
188
|
|
|
165
189
|
- Visible 160×144 vs 256×192 — center content
|
|
@@ -35,13 +35,39 @@ Mikey handles:
|
|
|
35
35
|
- **Joystick** — read via SWITCHES register at `$FCB0`. cc65 provides
|
|
36
36
|
`joy_read(JOY_1)` + `JOY_LEFT/RIGHT/UP/DOWN/BTN_1/BTN_2` macros.
|
|
37
37
|
|
|
38
|
-
**Live debug:**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
**Live debug:** the MCP inspectors (`palette` / `cpu` / `audioDebug` /
|
|
39
|
+
`background` / `breakpoint`, and the SCB-list-head `sprites` special case) are
|
|
40
|
+
documented in "MCP debug & inspection tooling" below.
|
|
41
|
+
|
|
42
|
+
## MCP debug & inspection tooling
|
|
43
|
+
|
|
44
|
+
The Lynx runs on handy (patched). The inspectors read the *live* core state —
|
|
45
|
+
reach for them when a sprite or palette renders wrong and the source alone
|
|
46
|
+
doesn't explain it. Details and the per-tool facts:
|
|
47
|
+
|
|
48
|
+
- **`palette({source:'live'})`** — the **16-entry, 12-bit RGB** Mikey palette
|
|
49
|
+
(`$FDA0-$FDBF`) converted to RGB.
|
|
50
|
+
- **`cpu({op:'read'})`** — 65C02 dump: A / X / Y / P / SP / PC plus the
|
|
51
|
+
decoded flag bits.
|
|
52
|
+
- **`audioDebug({op:'inspect', chip:'mikey'})`** — the 4 Mikey voices: volume,
|
|
53
|
+
the timer→period→frequency→note chain, and the **12-bit LFSR** state.
|
|
54
|
+
- **`background({view:'renderState'})`** — decodes DISPCTL: the DMA-enable,
|
|
55
|
+
flip, and color-mode bits plus the display base address.
|
|
56
|
+
- **`sprites({op:'inspect'})` is the special case.** The Lynx has **no fixed
|
|
57
|
+
OAM** — sprites are **SCB (Sprite Control Block) linked lists in RAM** that
|
|
58
|
+
Suzy walks at blit time. So this tool can't return a sprite table; instead
|
|
59
|
+
it returns the **SCB list head (SCBNEXT, `$FC10`/`$FC11`)** plus
|
|
60
|
+
instructions to walk the chain yourself over `system_ram`.
|
|
61
|
+
|
|
62
|
+
### Memory regions (`memory({op:'read', region:…})`)
|
|
63
|
+
|
|
64
|
+
| Region | Address / size | Contents |
|
|
65
|
+
|-----------------|-----------------------|-------------------------------------------------------|
|
|
66
|
+
| `lynx_cpu_regs` | — | 65C02 register snapshot |
|
|
67
|
+
| `lynx_hw_regs` | $FC00-$FDFF window | the **Suzy + Mikey** register window — sprite-engine regs, LCD control, audio, palette |
|
|
68
|
+
| `system_ram` | 64 KB | full address space (also where the SCB chain lives) |
|
|
69
|
+
|
|
70
|
+
Pair these with `breakpoint({on:'write'})` for the full live-debug loop.
|
|
45
71
|
|
|
46
72
|
## Frame heartbeat (cc65 + tgi)
|
|
47
73
|
|
|
@@ -117,3 +117,30 @@ exactly this.
|
|
|
117
117
|
generator + the envelope (period + shape bits).
|
|
118
118
|
- `memory({op:'read'})` regions: `msx_vram`, `msx_vdp_regs`, `msx_vdp_status`,
|
|
119
119
|
`msx_palette`, `msx_cpu_regs`, `msx_psg_regs`, plus `system_ram` (work RAM).
|
|
120
|
+
|
|
121
|
+
## MCP debug & inspection tooling
|
|
122
|
+
|
|
123
|
+
MSX is a **Tier-1** platform with deep introspection — the full set of
|
|
124
|
+
inspectors and memory regions is listed under **"Debugging tools"** above
|
|
125
|
+
(`cpu` / `background` / `palette` / `sprites` / `symbols` / `audioDebug` for
|
|
126
|
+
the AY-3-8910 PSG, and the `msx_vram` / `msx_vdp_regs` / `msx_vdp_status` /
|
|
127
|
+
`msx_palette` / `msx_cpu_regs` / `msx_psg_regs` / `system_ram` regions). The
|
|
128
|
+
PSG-channel decode means `audioDebug({op:'inspect', chip:'ay8910'})` gives
|
|
129
|
+
you the 3 square-wave channels plus the shared noise generator and envelope
|
|
130
|
+
without poking at `msx_psg_regs` by hand.
|
|
131
|
+
|
|
132
|
+
### ColecoVision shares this core family — but is bring-up only
|
|
133
|
+
|
|
134
|
+
ColecoVision runs the same toolchain family and exposes only the **standard**
|
|
135
|
+
introspection: `system_ram` + `save_ram` + `video_ram`. It has **no deep
|
|
136
|
+
inspectors** (no `palette` / `sprites` / `background` / `audioDebug` decode)
|
|
137
|
+
and **no MENTAL_MODEL of its own** — treat it as a bring-up target, not a
|
|
138
|
+
finished Tier-1 platform.
|
|
139
|
+
|
|
140
|
+
### Extending introspection (for whoever adds a platform)
|
|
141
|
+
|
|
142
|
+
Deeper, decoded inspectors are not free — each is implemented by **patching
|
|
143
|
+
the emulator core** to expose the extra register/VRAM regions, then wiring a
|
|
144
|
+
decoder. To add deep introspection to ColecoVision (or any thin platform),
|
|
145
|
+
follow the existing core-patch pattern used for snes9x / gpgx / fceumm / vice
|
|
146
|
+
under **`scripts/patches/`**.
|
|
@@ -25,6 +25,13 @@ The cc65 runtime claims:
|
|
|
25
25
|
- ZP $1C+ available to your game (with our chr-ram crt0)
|
|
26
26
|
- `$0500-$07FF` (3 pages): cc65 C parameter stack
|
|
27
27
|
|
|
28
|
+
> **cc65 zero-page starts at $02, not $00 (applies to every cc65 platform —
|
|
29
|
+
> NES, C64, Atari, Lynx, …).** cc65 reserves `$00-$01` for its runtime, so your
|
|
30
|
+
> first `.res 1` in the `ZEROPAGE` segment lands at **$02**, not $00. If you
|
|
31
|
+
> hand-write asm that assumes a zero-page var is at $00 you'll clobber the
|
|
32
|
+
> runtime. Confirm actual addresses with `symbols({op:'map'})` after
|
|
33
|
+
> `build({output:'romWithDebug'})`.
|
|
34
|
+
|
|
28
35
|
## PPU memory map (separate from CPU bus!)
|
|
29
36
|
|
|
30
37
|
```
|
|
@@ -94,6 +101,15 @@ The `nes_runtime` helper `tile_set_palette(nt, x, y, palette)` does
|
|
|
94
101
|
the read-modify-write dance and the bit-twiddling — use it instead
|
|
95
102
|
of writing attributes by hand.
|
|
96
103
|
|
|
104
|
+
> **256-tile cap per pattern table (the busy-image trap).** The nametable's
|
|
105
|
+
> tile index is 8-bit, so a single pattern table holds at most **256 unique
|
|
106
|
+
> tiles** — and a per-frame BG can therefore use at most 256 distinct tiles.
|
|
107
|
+
> Auto-converting a busy full-screen illustration almost always needs more than
|
|
108
|
+
> 256 unique 8×8 tiles and **overflows**; `encodeArt({stage:'tilemap'})` warns
|
|
109
|
+
> when it does. The only real workaround is mid-frame CHR bank switching
|
|
110
|
+
> (an MMC3-class mapper) — the bundled NROM presets can't do it, so design BG
|
|
111
|
+
> art to reuse tiles (≤256 unique per table).
|
|
112
|
+
|
|
97
113
|
## Palettes
|
|
98
114
|
|
|
99
115
|
32 bytes at $3F00-$3F1F:
|
|
@@ -329,6 +345,25 @@ incorrectly aligned."
|
|
|
329
345
|
scrolling worlds you need to manage the nametable buffer + bank
|
|
330
346
|
switching yourself.
|
|
331
347
|
|
|
348
|
+
## MCP debug & inspection tooling
|
|
349
|
+
|
|
350
|
+
The shipped fceumm core is patched for live introspection — read state
|
|
351
|
+
instead of guessing:
|
|
352
|
+
|
|
353
|
+
- **Sprites:** `sprites({op:'inspect'})` decodes live OAM.
|
|
354
|
+
- **Palette:** `palette({source:'live'})` reads the live 32-byte palette RAM.
|
|
355
|
+
- **CPU:** `cpu({op:'read'})` reads the 6502.
|
|
356
|
+
- **Background render state:** `background({view:'renderState'})` decodes
|
|
357
|
+
PPUCTRL/PPUMASK and resolves the active CHR bank (plus its file offset) —
|
|
358
|
+
this is what tells you which pattern table BG vs sprites are fetching from
|
|
359
|
+
(the bit-4 footgun above).
|
|
360
|
+
- **Memory regions:** `memory({op:'read'})` exposes OAM, Palette,
|
|
361
|
+
Nametables (CIRAM — including the 2-bit-per-16x16 attribute data that
|
|
362
|
+
selects each tile group's sub-palette, decoded by `inspectBackgroundMap`),
|
|
363
|
+
CHR (live MMC1-banked CHR — don't parse the iNES file), CPU_REGS,
|
|
364
|
+
PPU_REGS, and APU_REGS (the synthesized $4000-$4017 snapshot consumed by
|
|
365
|
+
`audioDebug`).
|
|
366
|
+
|
|
332
367
|
## Rebuilding a CHR-ROM NROM image (reverse-engineering)
|
|
333
368
|
|
|
334
369
|
The homebrew presets above are CHR-**RAM** (the CPU uploads tiles at runtime).
|
|
@@ -267,6 +267,57 @@ Loadable via genesis_plus_gx (`loadMedia`).
|
|
|
267
267
|
- Game Gear-specific buttons add port $00 read (Start) and stereo
|
|
268
268
|
PSG control on port $06.
|
|
269
269
|
|
|
270
|
+
## MCP debug & inspection tooling
|
|
271
|
+
|
|
272
|
+
SMS and Game Gear share the genesis_plus_gx (gpgx, patched) core, so the
|
|
273
|
+
inspectors below behave identically on both. This section is the **canonical
|
|
274
|
+
shared reference**; the Game Gear MENTAL_MODEL points back here and only lists
|
|
275
|
+
its own deltas (12-bit CRAM, etc.). Reach for these when something renders
|
|
276
|
+
wrong and you can't see why from the source alone — they read the *live* core
|
|
277
|
+
state, which beats trusting comments.
|
|
278
|
+
|
|
279
|
+
- **`sprites({op:'inspect'})`** — decodes the live SAT (sprite attribute
|
|
280
|
+
table) and renders a sprite-sheet PNG. Reports each slot's X/Y/tile plus
|
|
281
|
+
`spriteTileDataBase` (the VRAM address the VDP is actually fetching sprite
|
|
282
|
+
tiles from). This is the tool that catches the $D0-terminator and the
|
|
283
|
+
R6/$2000-vs-$0000 sprite-bank footguns described above — trust its bytes
|
|
284
|
+
over any comment.
|
|
285
|
+
- **`palette({source:'live'})`** — reads CRAM and converts to RGB. On SMS
|
|
286
|
+
that's **6-bit BGR** (2-2-2, 32 bytes). On Game Gear it's **12-bit BGR**
|
|
287
|
+
(4-4-4, 64 bytes) — see the GG MENTAL_MODEL for that delta.
|
|
288
|
+
- **`tiles({op:'png'})`** — dumps VRAM tile patterns as a sheet. The format
|
|
289
|
+
is 4bpp bitplane-interleaved (32 bytes/tile); the 16 KB VRAM renders as a
|
|
290
|
+
512-tile sheet.
|
|
291
|
+
- **`cpu({op:'read'})`** — Z80 register dump: A/F, BC/DE/HL, IX/IY, the
|
|
292
|
+
shadow register set, the flag bits, and interrupt state (IM mode / IFF).
|
|
293
|
+
- **`audioDebug({op:'inspect', chip:'psg'})`** — decodes the live SN76489:
|
|
294
|
+
3 tone channels + 1 noise channel. The PSG region is shared by SMS, GG, and
|
|
295
|
+
Genesis (same gpgx region).
|
|
296
|
+
- **`background({view:'renderState'})`** — reads the VDP registers and reports
|
|
297
|
+
the derived addresses (name table, BG-tile base, sprite-tile base, SAT base),
|
|
298
|
+
the scroll registers, and the display-enable / mode state. Use this to
|
|
299
|
+
confirm the R2/R4/R5/R6 baseline matches where you actually uploaded data.
|
|
300
|
+
|
|
301
|
+
### Memory regions (`memory({op:'read', region:…})`)
|
|
302
|
+
|
|
303
|
+
| Region | Contents |
|
|
304
|
+
|-----------------|------------------------------------------------------|
|
|
305
|
+
| `sms_vram` | 16 KB VRAM — tiles + name table + SAT + sprite tiles |
|
|
306
|
+
| `sms_cram` | 32-byte palette (2-2-2 BGR) |
|
|
307
|
+
| `sms_vdp_regs` | the 11 VDP control registers ($00-$0A) |
|
|
308
|
+
| `sms_z80_regs` | Z80 register snapshot |
|
|
309
|
+
| `gg_vram` | Game Gear VRAM |
|
|
310
|
+
| `gg_cram` | Game Gear 64-byte palette (4-4-4 BGR) |
|
|
311
|
+
|
|
312
|
+
### Disassembly (`disasm({target:…})`)
|
|
313
|
+
|
|
314
|
+
`disasm({target:'rom'})`, `disasm({target:'references'})`, and
|
|
315
|
+
`disasm({target:'project'})` all run the live ROM through the native binutils
|
|
316
|
+
**z80 `objdump`** (WASM, `-m z80`). It has full prefix coverage —
|
|
317
|
+
CB/ED/DD/FD/DDCB/FDCB — and feeds the same auto-label, register-annotation,
|
|
318
|
+
file-offset, and `untilReturn` pipeline used by the NES and SNES
|
|
319
|
+
disassemblers.
|
|
320
|
+
|
|
270
321
|
## Horizontal scrolling (for side-scrollers)
|
|
271
322
|
|
|
272
323
|
The `platformer` scaffold is single-screen. To make it a side-scroller:
|
|
@@ -208,6 +208,27 @@ per-voice vol/pitch/ADSR + `env` (0 = silent regardless of vol) + `bufLastSample
|
|
|
208
208
|
produced output" from "muted by mixer." GOTCHA: S-DSP FLG is $6C, KOFF is $5C
|
|
209
209
|
(many refs swap them); power-on FLG=$E0 means your driver MUST clear bit 6.
|
|
210
210
|
|
|
211
|
+
## MCP debug & inspection tooling
|
|
212
|
+
|
|
213
|
+
The shipped snes9x core is patched for deep introspection — both audio and
|
|
214
|
+
video are fully readable, so you assert live state instead of guessing:
|
|
215
|
+
|
|
216
|
+
- **Sprites:** `sprites({op:'inspect'})` decodes live OAM (per-sprite
|
|
217
|
+
renderable/hidden, resolved tile VRAM addr, CGRAM palette range, and the
|
|
218
|
+
uninitialized-OBJ-palette warning described under "The OBJ stable-path
|
|
219
|
+
recipe").
|
|
220
|
+
- **Palette:** `palette({source:'live'})` reads live CGRAM.
|
|
221
|
+
- **CPUs:** `cpu({op:'read', cpu:'main'})` for the 65816, `cpu({op:'read',
|
|
222
|
+
cpu:'spc700'})` for the sound CPU.
|
|
223
|
+
- **Audio:** the S-DSP is fully decodable — full per-voice state plus the
|
|
224
|
+
master mixer (see "Debugging sound" above for `audioDebug`).
|
|
225
|
+
- **Memory regions:** `memory({op:'read'})` exposes OAM, CGRAM, ARAM (SPC700
|
|
226
|
+
audio RAM), and **FillRAM**. Note the FillRAM quirk: snes9x mirrors the
|
|
227
|
+
PPU registers $2100-$213F (OBSEL/BGMODE/TM/TS/color-math, etc.) into
|
|
228
|
+
FillRAM indexed by the FULL address (e.g. `FillRAM[0x2101]` = OBSEL), so
|
|
229
|
+
the PPU register state is readable through the `snes_fillram` region — no
|
|
230
|
+
core patch needed.
|
|
231
|
+
|
|
211
232
|
## ROM layout (LoROM)
|
|
212
233
|
|
|
213
234
|
```
|
|
@@ -193,6 +193,49 @@ Expected. tcc-65816.wasm + wla-65816.wasm cold-load + compile takes
|
|
|
193
193
|
1-2s. Subsequent builds reuse the warm worker pool (R12 crash
|
|
194
194
|
isolation infrastructure). Steady-state builds are sub-second.
|
|
195
195
|
|
|
196
|
+
## "asm build fails with `ok:false` and an empty/cryptic `issues[]`" (asar idioms)
|
|
197
|
+
|
|
198
|
+
The `language:"asm"` SNES path runs through **asar**, which **silently crashes**
|
|
199
|
+
(a heap-pointer Emscripten exit code, no diagnostic) on a handful of source
|
|
200
|
+
idioms. romdev's asar wrapper has a preflight that catches the common cases and,
|
|
201
|
+
when asar dies with `ok:false` and no parsed errors, retries with `--verbose` and
|
|
202
|
+
synthesizes a fallback `issues[]` entry with a hint. The idioms to avoid:
|
|
203
|
+
|
|
204
|
+
- **`$ - label` size expressions** (current-PC minus a label) crash asar. Use an
|
|
205
|
+
explicit `end_label - start_label` difference instead:
|
|
206
|
+
```asm
|
|
207
|
+
; WRONG — crashes asar silently
|
|
208
|
+
my_size = $ - my_data
|
|
209
|
+
; RIGHT
|
|
210
|
+
my_data_end:
|
|
211
|
+
my_size = my_data_end - my_data
|
|
212
|
+
```
|
|
213
|
+
- **Opcode + operand arithmetic on an `=`-defined symbol**, e.g.
|
|
214
|
+
`STA SYMBOL + N` where `SYMBOL` is a `=` constant (not a label), can also crash
|
|
215
|
+
silently. The preflight flags the label-arithmetic-across-bank case
|
|
216
|
+
(`label-arithmetic constant`); for the rest, if you see `ok:false` with no real
|
|
217
|
+
error, suspect a `SYMBOL + N` and rewrite to a plain label or pre-compute the
|
|
218
|
+
address.
|
|
219
|
+
- **Bank-border crossed.** If your `org` + `dw`/data runs past `$00FFFF` you've
|
|
220
|
+
crossed a bank boundary and the layout is wrong. Native interrupt vectors live
|
|
221
|
+
at `$FFE4-$FFEE`, emulation vectors at `$FFF4-$FFFF` — keep your header/vector
|
|
222
|
+
block where the layout expects it. Use
|
|
223
|
+
`scaffold({op:'snippets', platform:"snes", mode:"get", name:"lorom_header.asm"})`
|
|
224
|
+
for the canonical layout (and `lorom_multibank.asm` for multi-bank).
|
|
225
|
+
|
|
226
|
+
(This is the asar/asm path. The default PVSnesLib **C** path goes through
|
|
227
|
+
tcc-65816 + wla-65816 and has its own C89 trap, above.)
|
|
228
|
+
|
|
229
|
+
## "CHR and tilemap overlap in VRAM / background renders as garbage tiles"
|
|
230
|
+
|
|
231
|
+
SNES CHR (tile patterns) and the BG tilemap share the 64 KB VRAM, addressed in
|
|
232
|
+
**words**. CHR starts at word `$0000`; if your CHR is 16 KB it occupies words
|
|
233
|
+
`$0000-$1FFF`, so a tilemap placed at word `$2000` collides with the tail of CHR
|
|
234
|
+
and you get garbage. **Put the tilemap at word `$4000` or later when your CHR is
|
|
235
|
+
big.** Verify the bases with `background({view:'renderState', platform:'snes'})`
|
|
236
|
+
(it decodes the BG char/map base) and cross-check the tile region with
|
|
237
|
+
`tiles({op:'png'})` at the CHR base.
|
|
238
|
+
|
|
196
239
|
## "I want to use real graphics from a PNG"
|
|
197
240
|
|
|
198
241
|
`gfx4snes` (the PVSnesLib companion tool) converts PNG → .pic + .pal.
|
package/src/playtest/playtest.js
CHANGED
|
@@ -231,6 +231,32 @@ const KEY_TO_LIBRETRO_BIT = {
|
|
|
231
231
|
w: 11, // R shoulder
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
+
// C64-only keyboard fallback: PC key → the virtual C64 button name the host's
|
|
235
|
+
// C64 layer maps to the key matrix (Space/Run-Stop/Return/F1-F7). Lets a human
|
|
236
|
+
// with NO controller still reach the C64 keyboard keys games need to start.
|
|
237
|
+
// (Arrows + Z give the joystick + Fire via KEY_TO_LIBRETRO_BIT above.)
|
|
238
|
+
const C64_KEYBOARD_FALLBACK = {
|
|
239
|
+
f1: "c64_f1", f2: "c64_f3", f3: "c64_f5", f4: "c64_f7", // F1-F4 → C64 F1/F3/F5/F7
|
|
240
|
+
space: "west", // Space
|
|
241
|
+
return: "r2", // Return (also START via the standard map — harmless)
|
|
242
|
+
escape: "l2", // Run/Stop (note: ESC also closes — see handler order)
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Human-readable C64 controls (controller + keyboard), relayed to the user when
|
|
246
|
+
// a C64 game is in the playtest window so they're not guessing.
|
|
247
|
+
export const C64_BINDINGS_HELP = `C64 — a CONTROLLER alone is enough (no keyboard needed):
|
|
248
|
+
D-pad / Left stick Joystick (port 2 by default)
|
|
249
|
+
Z / bottom face Fire
|
|
250
|
+
X face / Space key Space
|
|
251
|
+
L2 Run/Stop
|
|
252
|
+
R2 / Enter Return
|
|
253
|
+
Right stick ↑/←/→/↓ F1 / F3 / F5 / F7 (the 1-player / start keys)
|
|
254
|
+
Top face F1 (also)
|
|
255
|
+
|
|
256
|
+
No controller? Keyboard fallback: Arrows = joystick, Z = Fire, F1-F4 = C64
|
|
257
|
+
F1/F3/F5/F7, Space = Space, Enter = Return, ESC = Run/Stop (hold; ESC tapped
|
|
258
|
+
also closes the window). Switch joystick port with input({op:'joyport'}).`;
|
|
259
|
+
|
|
234
260
|
// Human-readable summary printed by --help and at playtest startup.
|
|
235
261
|
export const KEYBOARD_BINDINGS_HELP = `Keyboard:
|
|
236
262
|
Arrow keys D-pad
|
|
@@ -578,6 +604,7 @@ export async function playtest(args) {
|
|
|
578
604
|
// into its own port object; the agent's setInput is overwritten each
|
|
579
605
|
// tick (matching prior behavior). Select+Start on any controller quits.
|
|
580
606
|
let quit = false;
|
|
607
|
+
const isC64 = h.status?.platform === "c64";
|
|
581
608
|
function readControllerInto(port, inst) {
|
|
582
609
|
if (!inst) return;
|
|
583
610
|
const btn = inst.buttons || {};
|
|
@@ -595,6 +622,18 @@ export async function playtest(args) {
|
|
|
595
622
|
else if (lx < -STICK_DEADZONE) port.left = true;
|
|
596
623
|
if (ly > STICK_DEADZONE) port.down = true;
|
|
597
624
|
else if (ly < -STICK_DEADZONE) port.up = true;
|
|
625
|
+
// C64: the RIGHT stick selects the function keys (F1/F3/F5/F7) — the
|
|
626
|
+
// Batocera/RetroDeck convention so a controller alone reaches the keyboard
|
|
627
|
+
// keys C64 setup screens need. Emitted as virtual buttons the host's C64
|
|
628
|
+
// layer maps to the key matrix; harmless on other platforms (no mapping).
|
|
629
|
+
if (isC64) {
|
|
630
|
+
const rx = axes.rightStickX ?? 0;
|
|
631
|
+
const ry = axes.rightStickY ?? 0;
|
|
632
|
+
if (ry < -STICK_DEADZONE) port.c64_f1 = true; // up → F1
|
|
633
|
+
else if (ry > STICK_DEADZONE) port.c64_f7 = true; // down → F7
|
|
634
|
+
if (rx < -STICK_DEADZONE) port.c64_f3 = true; // left → F3
|
|
635
|
+
else if (rx > STICK_DEADZONE) port.c64_f5 = true; // right → F5
|
|
636
|
+
}
|
|
598
637
|
}
|
|
599
638
|
|
|
600
639
|
const port0 = {};
|
|
@@ -619,6 +658,15 @@ export async function playtest(args) {
|
|
|
619
658
|
for (const [keyName, bit] of Object.entries(KEY_TO_LIBRETRO_BIT)) {
|
|
620
659
|
if (heldKeys.has(keyName)) port0[bitToName(bit)] = true;
|
|
621
660
|
}
|
|
661
|
+
// C64 keyboard fallback (no controller / mixing): map PC keys to the C64
|
|
662
|
+
// KEYBOARD keys games need — the host's C64 layer routes these virtual
|
|
663
|
+
// button names to the key matrix. (Arrows + Z=Fire already give the
|
|
664
|
+
// joystick above.) The agent relays these to the human.
|
|
665
|
+
if (isC64) {
|
|
666
|
+
for (const [keyName, vbtn] of Object.entries(C64_KEYBOARD_FALLBACK)) {
|
|
667
|
+
if (heldKeys.has(keyName)) port0[vbtn] = true;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
622
670
|
const isRewinding = heldKeys.has("r") && rewindBuffer.length > 0;
|
|
623
671
|
if (isRewinding) {
|
|
624
672
|
// Restore the previous snapshot and run one frame to produce its visual.
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { buildForPlatform } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/toolchains/index.js";
|
|
3
|
-
import { resolveCore } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/cores/registry.js";
|
|
4
|
-
import { LibretroHost } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/host/LibretroHost.js";
|
|
5
|
-
|
|
6
|
-
const LIBDIR = "/home/monteslu/code/cliemu/romdev/packages/romdev/src/platforms/msx/lib/c";
|
|
7
|
-
const DIR = "/home/monteslu/code/cliemu/romdev/packages/romdev/examples/msx/catch_game";
|
|
8
|
-
|
|
9
|
-
const SRC = readFileSync(`${DIR}/main.c`, "utf8");
|
|
10
|
-
const LIB = readFileSync(`${LIBDIR}/msx_vdp.c`, "utf8");
|
|
11
|
-
const CRT0 = readFileSync(`${LIBDIR}/msx_crt0.s`, "utf8");
|
|
12
|
-
const HDR = readFileSync(`${LIBDIR}/msx_hw.h`, "utf8");
|
|
13
|
-
|
|
14
|
-
const PLATFORM = "msx";
|
|
15
|
-
|
|
16
|
-
console.log("== building ==");
|
|
17
|
-
const build = await buildForPlatform({
|
|
18
|
-
platform: PLATFORM,
|
|
19
|
-
sources: { "main.c": SRC, "msx_vdp.c": LIB, "msx_crt0.s": CRT0 },
|
|
20
|
-
includes: { "msx_hw.h": HDR },
|
|
21
|
-
crt0: ".module empty\n",
|
|
22
|
-
sourceName: "main.c",
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
if (!build.binary) {
|
|
26
|
-
console.log(build.log || build.stderr || JSON.stringify(build));
|
|
27
|
-
console.log("BUILD FAILED stage=", build.stage, "exit=", build.exitCode);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
console.log("build OK, bytes=", build.binary.length);
|
|
31
|
-
|
|
32
|
-
const c = resolveCore(PLATFORM);
|
|
33
|
-
const h = new LibretroHost();
|
|
34
|
-
await h.loadCore(c.jsPath, c.wasmPath);
|
|
35
|
-
await h.loadMedia({ platform: PLATFORM, bytes: build.binary });
|
|
36
|
-
|
|
37
|
-
// ---- honest pixel helpers over raw RGBA ----
|
|
38
|
-
function nonBlackFraction(rgba) {
|
|
39
|
-
let nb = 0;
|
|
40
|
-
for (let i = 0; i < rgba.length; i += 4) {
|
|
41
|
-
if (rgba[i] > 16 || rgba[i + 1] > 16 || rgba[i + 2] > 16) nb++;
|
|
42
|
-
}
|
|
43
|
-
return nb / (rgba.length / 4);
|
|
44
|
-
}
|
|
45
|
-
// Find the brightest-pixel X centroid in a horizontal band of rows (the basket
|
|
46
|
-
// rides near the bottom; the basket sprite is white). Returns -1 if no bright px.
|
|
47
|
-
function brightCentroidX(frame, y0, y1) {
|
|
48
|
-
const { width, height, rgba } = frame;
|
|
49
|
-
let sx = 0, n = 0;
|
|
50
|
-
for (let y = y0; y < Math.min(y1, height); y++) {
|
|
51
|
-
for (let x = 0; x < width; x++) {
|
|
52
|
-
const i = (y * width + x) * 4;
|
|
53
|
-
// white basket: all channels high
|
|
54
|
-
if (rgba[i] > 180 && rgba[i + 1] > 180 && rgba[i + 2] > 180) { sx += x; n++; }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return n ? sx / n : -1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// boot past the C-BIOS logo + a few game frames
|
|
61
|
-
for (let i = 0; i < 320; i++) h.stepFrames(1);
|
|
62
|
-
|
|
63
|
-
const sa = h.screenshotRgba();
|
|
64
|
-
writeFileSync(`${DIR}/shot_before.png`, Buffer.from(h.screenshot().pngBase64, "base64"));
|
|
65
|
-
// basket band: BASKET_Y=160 in a 192-tall internal buffer; band 150..180
|
|
66
|
-
const fracA = nonBlackFraction(sa.rgba);
|
|
67
|
-
const basketA = brightCentroidX(sa, Math.floor(sa.height * 0.78), Math.floor(sa.height * 0.95));
|
|
68
|
-
console.log(`before: ${sa.width}x${sa.height} nonBlack=${(fracA*100).toFixed(2)}% basketX=${basketA.toFixed(1)}`);
|
|
69
|
-
|
|
70
|
-
// Drive RIGHT for many frames
|
|
71
|
-
for (let i = 0; i < 120; i++) { h.setInput({ ports: [{ right: true }] }); h.stepFrames(1); }
|
|
72
|
-
const sb = h.screenshotRgba();
|
|
73
|
-
writeFileSync(`${DIR}/shot_after_right.png`, Buffer.from(h.screenshot().pngBase64, "base64"));
|
|
74
|
-
const basketB = brightCentroidX(sb, Math.floor(sb.height * 0.78), Math.floor(sb.height * 0.95));
|
|
75
|
-
console.log(`after RIGHT: basketX=${basketB.toFixed(1)}`);
|
|
76
|
-
|
|
77
|
-
// Drive LEFT for many frames
|
|
78
|
-
for (let i = 0; i < 200; i++) { h.setInput({ ports: [{ left: true }] }); h.stepFrames(1); }
|
|
79
|
-
const sc = h.screenshotRgba();
|
|
80
|
-
writeFileSync(`${DIR}/shot_after_left.png`, Buffer.from(h.screenshot().pngBase64, "base64"));
|
|
81
|
-
const basketC = brightCentroidX(sc, Math.floor(sc.height * 0.78), Math.floor(sc.height * 0.95));
|
|
82
|
-
console.log(`after LEFT: basketX=${basketC.toFixed(1)}`);
|
|
83
|
-
|
|
84
|
-
// ---- verdict ----
|
|
85
|
-
const visible = fracA > 0.005; // playfield is not black
|
|
86
|
-
const movedRight = basketB > basketA + 5; // basket moved right with input
|
|
87
|
-
const movedLeft = basketC < basketB - 5; // basket moved back left
|
|
88
|
-
console.log("VISIBLE:", visible, "MOVED_RIGHT:", movedRight, "MOVED_LEFT:", movedLeft);
|
|
89
|
-
if (visible && movedRight && movedLeft) {
|
|
90
|
-
console.log("VERIFIED_OK");
|
|
91
|
-
} else {
|
|
92
|
-
console.log("VERIFY_INCOMPLETE");
|
|
93
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { buildForPlatform } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/toolchains/index.js";
|
|
3
|
-
import { resolveCore } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/cores/registry.js";
|
|
4
|
-
import { LibretroHost } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/host/LibretroHost.js";
|
|
5
|
-
|
|
6
|
-
const LIB = "/home/monteslu/code/cliemu/romdev/packages/romdev/src/platforms/pce/lib/c";
|
|
7
|
-
const DIR = "/home/monteslu/code/cliemu/romdev/packages/romdev/examples/pce/catch_game";
|
|
8
|
-
|
|
9
|
-
const read = (p) => fs.readFileSync(p, "utf8");
|
|
10
|
-
|
|
11
|
-
const sources = {
|
|
12
|
-
"main.c": read(DIR + "/main.c"),
|
|
13
|
-
"pce_video.c": read(LIB + "/pce_video.c"),
|
|
14
|
-
"pce_input.c": read(LIB + "/pce_input.c"),
|
|
15
|
-
"pce_sound.c": read(LIB + "/pce_sound.c"),
|
|
16
|
-
};
|
|
17
|
-
const headers = { "pce_hw.h": read(LIB + "/pce_hw.h") };
|
|
18
|
-
|
|
19
|
-
console.log("Building catch_game...");
|
|
20
|
-
const build = await buildForPlatform({
|
|
21
|
-
platform: "pce",
|
|
22
|
-
sources,
|
|
23
|
-
headers,
|
|
24
|
-
includes: headers,
|
|
25
|
-
sourceName: "main.c",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (!build || !build.binary) {
|
|
29
|
-
console.log("BUILD FAILED");
|
|
30
|
-
console.log(JSON.stringify(build, null, 2));
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
console.log("BUILD OK, binary bytes =", build.binary.length);
|
|
34
|
-
if (build.log) console.log("log tail:", String(build.log).slice(-400));
|
|
35
|
-
|
|
36
|
-
const PLATFORM = "pce";
|
|
37
|
-
const h = new LibretroHost();
|
|
38
|
-
const c = resolveCore(PLATFORM);
|
|
39
|
-
await h.loadCore(c.jsPath, c.wasmPath);
|
|
40
|
-
await h.loadMedia({ platform: PLATFORM, bytes: build.binary });
|
|
41
|
-
|
|
42
|
-
// boot
|
|
43
|
-
for (let i = 0; i < 120; i++) h.stepFrames(1);
|
|
44
|
-
|
|
45
|
-
function nonBlack(shot) {
|
|
46
|
-
const buf = Buffer.from(shot.pngBase64, "base64");
|
|
47
|
-
// crude: count distinct-ish bytes; rely on file size + later pixel scan
|
|
48
|
-
return buf.length;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// screenshot BEFORE input
|
|
52
|
-
let shotA = h.screenshot();
|
|
53
|
-
fs.writeFileSync(DIR + "/shot_before.png", Buffer.from(shotA.pngBase64, "base64"));
|
|
54
|
-
console.log("before:", shotA.width, "x", shotA.height, "png bytes", nonBlack(shotA));
|
|
55
|
-
|
|
56
|
-
// drive RIGHT for ~60 frames to move catcher and let fruit fall/catch
|
|
57
|
-
for (let i = 0; i < 90; i++) {
|
|
58
|
-
h.setInput({ ports: [{ right: true }] });
|
|
59
|
-
h.stepFrames(1);
|
|
60
|
-
}
|
|
61
|
-
// then LEFT for a bit
|
|
62
|
-
for (let i = 0; i < 60; i++) {
|
|
63
|
-
h.setInput({ ports: [{ left: true }] });
|
|
64
|
-
h.stepFrames(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let shotB = h.screenshot();
|
|
68
|
-
fs.writeFileSync(DIR + "/shot_after.png", Buffer.from(shotB.pngBase64, "base64"));
|
|
69
|
-
console.log("after:", shotB.width, "x", shotB.height, "png bytes", nonBlack(shotB));
|
|
70
|
-
|
|
71
|
-
// pixel diff: decode both PNGs to raw via the host's framebuffer if available
|
|
72
|
-
const same = shotA.pngBase64 === shotB.pngBase64;
|
|
73
|
-
console.log("identical PNG?", same);
|
|
74
|
-
|
|
75
|
-
console.log("done — see shot_before.png / shot_after.png");
|
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
|
-
}
|