romdevtools 0.85.0 → 0.86.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/CHANGELOG.md +22 -0
- package/package.json +6 -4
- package/src/cheats/gamegenie.js +101 -0
- package/src/host/LibretroHost.js +43 -1
- package/src/mcp/tools/cheats.js +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.86.0 — 2026-07-01
|
|
8
|
+
|
|
9
|
+
- **GameTank Game Genie — a brand-new cheat-code format.** Nobody had made GameTank cheat codes
|
|
10
|
+
before; this adds a Game-Genie-style read-substitution device to the emulated GameTank core plus
|
|
11
|
+
a code format that is HARDWARE-COMPATIBLE (the same codes would work on a physical Game Genie
|
|
12
|
+
built for the console's open cart bus).
|
|
13
|
+
- Core (`romdev-core-gametank@0.2.0`): a value-override cheat device in the shared debug lib
|
|
14
|
+
(`romdev_cheat_set`/`romdev_cheat_read`/`romdev_cheat_get`, 24 slots). `MemoryRead` returns the
|
|
15
|
+
substitute byte on an address match, with optional compare-against-original (survives bank
|
|
16
|
+
switching) — exactly what a hardware Game Genie does. Any core that adds the one-line
|
|
17
|
+
`romdev_cheat_read` call to its bus read gets this for free.
|
|
18
|
+
- Format: `encodeGameTankGameGenie`/`decodeGameTankGameGenie` — a distinct 16-letter wheel
|
|
19
|
+
(`KLMNPQRSTVWXYZ23`), 16-bit address + 8-bit value (+ optional compare), scrambled with a
|
|
20
|
+
checksum. Plain codes are `XXX-XXXX`, compare codes `XXXX-XXXXX` (e.g. `$8100→0x42` = KTM-LPK3).
|
|
21
|
+
Wired into every device dispatcher; `cheats({op:'make', platform:'gametank', …})` generates
|
|
22
|
+
codes and `cheats({op:'apply'})` applies them live. GameTank's `retro_cheat_set` is a stub, so
|
|
23
|
+
`LibretroHost.setCheat` routes GameTank cheats through the romdev value-override device.
|
|
24
|
+
- **jsgame test no longer skips.** It ran only under `--experimental-vm-modules`, so the default
|
|
25
|
+
suite skipped it. It now runs in an isolated forked child process (SDL is main-thread-only;
|
|
26
|
+
rungame leaks handles) driven over rawr JSON-RPC — the child carries the flag and is killed when
|
|
27
|
+
the RPC resolves, so the main suite stays clean. Suite: 1099 tests, 1099 pass, 0 skipped.
|
|
28
|
+
|
|
7
29
|
## 0.85.0 — 2026-07-01
|
|
8
30
|
|
|
9
31
|
- **Native-runtime game kinds — wasmcart + jsgame are now first-class hosts.** romdev is a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "romdevtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 17 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, PlayStation, N64, Dreamcast, ...) 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",
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"postinstall": "node src/install/postinstall.mjs",
|
|
14
14
|
"smoke": "node --enable-source-maps src/cli/smoke.js",
|
|
15
15
|
"mcp": "node --enable-source-maps src/mcp/server.js",
|
|
16
|
-
"test": "node --test 'src/**/*.test.js' 'test/**/*.test.js'"
|
|
17
|
-
"test:jsgame": "node --experimental-vm-modules --test-force-exit --test test/jsgame-host.test.js"
|
|
16
|
+
"test": "node --test 'src/**/*.test.js' 'test/**/*.test.js'"
|
|
18
17
|
},
|
|
19
18
|
"keywords": [
|
|
20
19
|
"mcp",
|
|
@@ -59,7 +58,7 @@
|
|
|
59
58
|
"romdev-core-fceumm": "0.11.0",
|
|
60
59
|
"romdev-core-flycast": "0.2.0",
|
|
61
60
|
"romdev-core-gambatte": "0.10.0",
|
|
62
|
-
"romdev-core-gametank": "0.
|
|
61
|
+
"romdev-core-gametank": "0.2.0",
|
|
63
62
|
"romdev-core-geargrafx": "0.8.0",
|
|
64
63
|
"romdev-core-gpgx": "0.13.0",
|
|
65
64
|
"romdev-core-handy": "0.8.0",
|
|
@@ -111,5 +110,8 @@
|
|
|
111
110
|
"homepage": "https://github.com/monteslu/romdev#readme",
|
|
112
111
|
"bugs": {
|
|
113
112
|
"url": "https://github.com/monteslu/romdev/issues"
|
|
113
|
+
},
|
|
114
|
+
"devDependencies": {
|
|
115
|
+
"rawr": "^1.0.1"
|
|
114
116
|
}
|
|
115
117
|
}
|
package/src/cheats/gamegenie.js
CHANGED
|
@@ -277,6 +277,98 @@ export function decodeSnesGameGenie(code) {
|
|
|
277
277
|
return { address, value };
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
// ── GameTank Game Genie ─────────────────────────────────────────────────
|
|
281
|
+
// A NEW format for Clyde Shaffer's open GameTank console — no prior art exists
|
|
282
|
+
// (nobody has made GameTank cheat codes before). The GameTank CPU (W65C02S) sees a
|
|
283
|
+
// flat 16-bit address space, so a code encodes a 16-bit READ address + an 8-bit
|
|
284
|
+
// substitute value, plus an optional 8-bit compare byte (the compare form survives
|
|
285
|
+
// the cart's flash bank switching, exactly like NES/GB compare codes). The device
|
|
286
|
+
// intercepts the CPU's bus read and substitutes the value — identical behaviour to
|
|
287
|
+
// a hardware Game Genie you could build for the console's open cart bus.
|
|
288
|
+
//
|
|
289
|
+
// Encoding: a distinct 16-letter wheel (so codes read as GameTank, not NES). The
|
|
290
|
+
// payload nibbles are laid out and lightly scrambled so a small address change moves
|
|
291
|
+
// several letters (the GG "feel"), then a 1-nibble checksum guards typos.
|
|
292
|
+
// plain: 6 payload nibbles (addr16 + val8) + 1 checksum = 7 letters, shown "XXX-XXXX"
|
|
293
|
+
// compare: 8 payload nibbles (addr16 + val8 + cmp8) + 1 checksum = 9 letters, "XXXX-XXXXX"
|
|
294
|
+
const GT_GG_LETTERS = "KLMNPQRSTVWXYZ23"; // 16 distinct glyphs, no vowels/0-1-O-I ambiguity
|
|
295
|
+
|
|
296
|
+
function gtNibblesToLetters(nibs) {
|
|
297
|
+
return nibs.map((x) => GT_GG_LETTERS[x & 0xF]).join("");
|
|
298
|
+
}
|
|
299
|
+
function gtLettersToNibbles(code) {
|
|
300
|
+
const clean = code.replace(/-/g, "").toUpperCase();
|
|
301
|
+
const n = [];
|
|
302
|
+
for (const ch of clean) {
|
|
303
|
+
const v = GT_GG_LETTERS.indexOf(ch);
|
|
304
|
+
if (v < 0) return null;
|
|
305
|
+
n.push(v);
|
|
306
|
+
}
|
|
307
|
+
return n;
|
|
308
|
+
}
|
|
309
|
+
// Simple nibble checksum: XOR of all payload nibbles, folded to 4 bits.
|
|
310
|
+
function gtChecksum(payloadNibs) {
|
|
311
|
+
let c = 0;
|
|
312
|
+
for (const x of payloadNibs) c ^= x & 0xF;
|
|
313
|
+
return c & 0xF;
|
|
314
|
+
}
|
|
315
|
+
// Scramble/unscramble the payload-nibble ORDER (a fixed permutation) so a code
|
|
316
|
+
// doesn't read as plain hex. Self-inverse pairs keep decode trivial.
|
|
317
|
+
const GT_PERM6 = [3, 0, 5, 1, 4, 2]; // plain: 6 nibbles
|
|
318
|
+
const GT_PERM8 = [5, 2, 7, 0, 4, 1, 6, 3]; // compare: 8 nibbles
|
|
319
|
+
function applyPerm(arr, perm) { return perm.map((i) => arr[i]); }
|
|
320
|
+
function invertPerm(perm) {
|
|
321
|
+
const inv = new Array(perm.length);
|
|
322
|
+
perm.forEach((p, i) => { inv[p] = i; });
|
|
323
|
+
return inv;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** ENCODE → GameTank Game Genie. `address` 0..0xFFFF, `value` 0..0xFF, optional
|
|
327
|
+
* `compare` 0..0xFF. Returns the dashed letter code, or null if out of range. */
|
|
328
|
+
export function encodeGameTankGameGenie({ address, value, compare }) {
|
|
329
|
+
if (address == null || value == null) return null;
|
|
330
|
+
const a = address & 0xFFFF, v = value & 0xFF;
|
|
331
|
+
if (address < 0 || address > 0xFFFF || value < 0 || value > 0xFF) return null;
|
|
332
|
+
|
|
333
|
+
// payload nibbles, MSB-first: addr[15:12],addr[11:8],addr[7:4],addr[3:0],val[7:4],val[3:0]
|
|
334
|
+
let payload = [(a >> 12) & 0xF, (a >> 8) & 0xF, (a >> 4) & 0xF, a & 0xF, (v >> 4) & 0xF, v & 0xF];
|
|
335
|
+
let perm = GT_PERM6;
|
|
336
|
+
if (compare != null) {
|
|
337
|
+
if (compare < 0 || compare > 0xFF) return null;
|
|
338
|
+
const c = compare & 0xFF;
|
|
339
|
+
payload = payload.concat([(c >> 4) & 0xF, c & 0xF]);
|
|
340
|
+
perm = GT_PERM8;
|
|
341
|
+
}
|
|
342
|
+
const scrambled = applyPerm(payload, perm);
|
|
343
|
+
const sum = gtChecksum(payload);
|
|
344
|
+
const nibs = scrambled.concat([sum]);
|
|
345
|
+
const letters = gtNibblesToLetters(nibs);
|
|
346
|
+
// dash after the first 3 (plain) / 4 (compare) letters for readability
|
|
347
|
+
const cut = compare != null ? 4 : 3;
|
|
348
|
+
return letters.slice(0, cut) + "-" + letters.slice(cut);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Decode a GameTank Game Genie code → { address, value, compare? } or null. */
|
|
352
|
+
export function decodeGameTankGameGenie(code) {
|
|
353
|
+
const n = gtLettersToNibbles(code);
|
|
354
|
+
if (!n) return null;
|
|
355
|
+
let perm, payloadLen;
|
|
356
|
+
if (n.length === 7) { perm = GT_PERM6; payloadLen = 6; }
|
|
357
|
+
else if (n.length === 9) { perm = GT_PERM8; payloadLen = 8; }
|
|
358
|
+
else return null;
|
|
359
|
+
|
|
360
|
+
const scrambled = n.slice(0, payloadLen);
|
|
361
|
+
const sum = n[payloadLen];
|
|
362
|
+
const payload = applyPerm(scrambled, invertPerm(perm));
|
|
363
|
+
if (gtChecksum(payload) !== sum) return null; // typo / not a valid GameTank code
|
|
364
|
+
|
|
365
|
+
const address = ((payload[0] << 12) | (payload[1] << 8) | (payload[2] << 4) | payload[3]) & 0xFFFF;
|
|
366
|
+
const value = ((payload[4] << 4) | payload[5]) & 0xFF;
|
|
367
|
+
if (payloadLen === 6) return { address, value };
|
|
368
|
+
const compare = ((payload[6] << 4) | payload[7]) & 0xFF;
|
|
369
|
+
return { address, value, compare };
|
|
370
|
+
}
|
|
371
|
+
|
|
280
372
|
// ── Game Boy GameShark ──────────────────────────────────────────────────
|
|
281
373
|
// 8 hex digits "TTVVAAAA": TT = type/RAM-bank byte, VV = replacement value,
|
|
282
374
|
// AAAA = the RAM address in LITTLE-ENDIAN order. (Distinct from GB Game Genie,
|
|
@@ -340,6 +432,11 @@ export function detectDevice(code, platform) {
|
|
|
340
432
|
if (hex8 || hyphenHex) return "action-replay"; // SMS/GG Action Replay
|
|
341
433
|
if (/^[0-9A-F]{3}-[0-9A-F]{3}/i.test(c)) return "game-genie";
|
|
342
434
|
return "unknown";
|
|
435
|
+
case "gametank":
|
|
436
|
+
// 7-letter (XXX-XXXX) or 9-letter (XXXX-XXXXX) GameTank wheel code.
|
|
437
|
+
if (/^[KLMNPQRSTVWXYZ23]{3}-[KLMNPQRSTVWXYZ23]{4}$/i.test(c) ||
|
|
438
|
+
/^[KLMNPQRSTVWXYZ23]{4}-[KLMNPQRSTVWXYZ23]{5}$/i.test(c)) return "game-genie";
|
|
439
|
+
return "unknown";
|
|
343
440
|
default:
|
|
344
441
|
return "unknown";
|
|
345
442
|
}
|
|
@@ -370,6 +467,8 @@ export function decodeCode(code, platform) {
|
|
|
370
467
|
case "gg":
|
|
371
468
|
// SMS/GG Action Replay raw-hex (addr/val, no scramble).
|
|
372
469
|
return decodeProActionReplay(c, platform);
|
|
470
|
+
case "gametank":
|
|
471
|
+
return decodeGameTankGameGenie(c);
|
|
373
472
|
default:
|
|
374
473
|
return null;
|
|
375
474
|
}
|
|
@@ -476,6 +575,7 @@ export function nativeDevicesFor(platform) {
|
|
|
476
575
|
case "snes": return ["pro-action-replay", "game-genie"];
|
|
477
576
|
case "gb": case "gbc": return ["game-genie", "gameshark"];
|
|
478
577
|
case "sms": case "gg": return ["action-replay"];
|
|
578
|
+
case "gametank": return ["game-genie"];
|
|
479
579
|
default: return ["raw"];
|
|
480
580
|
}
|
|
481
581
|
}
|
|
@@ -494,6 +594,7 @@ export function encodeForDevice({ address, value, compare }, platform, device) {
|
|
|
494
594
|
else if (["genesis", "megadrive", "md"].includes(platform)) code = encodeGenesisGameGenie({ address, value });
|
|
495
595
|
else if (["gb", "gbc"].includes(platform)) code = encodeGbGameGenie({ address, value, compare });
|
|
496
596
|
else if (platform === "snes") code = encodeSnesGameGenie({ address, value });
|
|
597
|
+
else if (platform === "gametank") code = encodeGameTankGameGenie({ address, value, compare });
|
|
497
598
|
return code ? { code, device: "game-genie" } : null;
|
|
498
599
|
}
|
|
499
600
|
case "pro-action-replay": {
|
package/src/host/LibretroHost.js
CHANGED
|
@@ -214,6 +214,11 @@ export const PLATFORM_VIRTUAL_EXT = {
|
|
|
214
214
|
msx: ".rom",
|
|
215
215
|
};
|
|
216
216
|
import { RETRO_DEVICE_JOYPAD, ROMDEV_PIXEL_FORMAT_RGBA8888 } from "./retroConstants.js";
|
|
217
|
+
import { decodeCode as decodeCheatCode } from "../cheats/gamegenie.js";
|
|
218
|
+
|
|
219
|
+
// Platforms whose core stubs retro_cheat_set but ships romdev's value-override cheat
|
|
220
|
+
// device (romdev_cheat_set) — cheats route through that read-substitution instead.
|
|
221
|
+
const CHEAT_PREFER_ROMDEV_DEVICE = new Set(["gametank"]);
|
|
217
222
|
|
|
218
223
|
// C64 controller→keyboard map (the Batocera/RetroDeck model: a CONTROLLER alone
|
|
219
224
|
// plays C64 — no physical keyboard needed — by mapping spare buttons/stick to
|
|
@@ -1536,7 +1541,18 @@ export class LibretroHost {
|
|
|
1536
1541
|
* (Older bundled cores predate the cheat-export build flag.) */
|
|
1537
1542
|
cheatsSupported() {
|
|
1538
1543
|
const mod = this.mod;
|
|
1539
|
-
|
|
1544
|
+
// Real cheat support = EITHER the core's retro_cheat_set actually applies codes,
|
|
1545
|
+
// OR romdev's value-override cheat device (romdev_cheat_set) is present. Some cores
|
|
1546
|
+
// (GameTank) stub retro_cheat_set but ship the romdev_cheat_* read-substitution
|
|
1547
|
+
// device — that's the working path for them.
|
|
1548
|
+
return !!(mod && (typeof mod._romdev_cheat_set === "function" || typeof mod._retro_cheat_set === "function"));
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/** True if this core applies cheats via romdev's value-override device (romdev_cheat_set)
|
|
1552
|
+
* rather than the libretro retro_cheat_set (which is a stub on some cores). */
|
|
1553
|
+
_usesRomdevCheatDevice() {
|
|
1554
|
+
return !!(this.mod && typeof this.mod._romdev_cheat_set === "function" &&
|
|
1555
|
+
CHEAT_PREFER_ROMDEV_DEVICE.has(this.status.platform));
|
|
1540
1556
|
}
|
|
1541
1557
|
|
|
1542
1558
|
/**
|
|
@@ -1553,6 +1569,32 @@ export class LibretroHost {
|
|
|
1553
1569
|
*/
|
|
1554
1570
|
setCheat(index, code, enabled = true) {
|
|
1555
1571
|
const mod = this._needMod();
|
|
1572
|
+
|
|
1573
|
+
// romdev value-override device (GameTank etc.): the core's retro_cheat_set is a
|
|
1574
|
+
// stub, but romdev_cheat_set installs a hardware-faithful read substitution. Decode
|
|
1575
|
+
// the code to {address,value,compare} and hand it to the device.
|
|
1576
|
+
if (this._usesRomdevCheatDevice()) {
|
|
1577
|
+
const decoded = decodeCheatCode(String(code), this.status.platform);
|
|
1578
|
+
if (!decoded || decoded.address == null || decoded.value == null) {
|
|
1579
|
+
throw new Error(
|
|
1580
|
+
`setCheat: could not decode '${code}' for ${this.status.platform}. ` +
|
|
1581
|
+
`Provide a GameTank Game Genie code or a raw ADDR:VAL[:COMPARE].`,
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
mod._romdev_cheat_set(
|
|
1585
|
+
index >>> 0,
|
|
1586
|
+
decoded.address & 0xFFFF,
|
|
1587
|
+
decoded.value & 0xFF,
|
|
1588
|
+
(decoded.compare ?? 0) & 0xFF,
|
|
1589
|
+
decoded.compare != null ? 1 : 0,
|
|
1590
|
+
enabled ? 1 : 0,
|
|
1591
|
+
);
|
|
1592
|
+
if (!this._activeCheats) this._activeCheats = new Map();
|
|
1593
|
+
if (enabled) this._activeCheats.set(index, code);
|
|
1594
|
+
else this._activeCheats.delete(index);
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1556
1598
|
if (typeof mod._retro_cheat_set !== "function") {
|
|
1557
1599
|
throw new Error(
|
|
1558
1600
|
"this core build does not expose the cheat interface (retro_cheat_set). " +
|
package/src/mcp/tools/cheats.js
CHANGED
|
@@ -18,6 +18,9 @@ const GG_ADDR_RANGE = {
|
|
|
18
18
|
snes: [0x008000, 0xFFFFFF], // mapped ROM ($xx:8000-$xx:FFFF); the Game Genie
|
|
19
19
|
// ROM device patches here. (Pro Action Replay is
|
|
20
20
|
// a RAM poke — for a ROM patch we pick GG below.)
|
|
21
|
+
gametank: [0x0000, 0xFFFF], // flat 16-bit CPU space. GameTank's Game Genie is a
|
|
22
|
+
// READ substitution on the bus, so any read address is
|
|
23
|
+
// valid (RAM or cart ROM). A NEW format — see gamegenie.js.
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
// The native ROM-PATCH device per platform (installs a read-intercept), as
|
|
@@ -94,6 +97,7 @@ const SUPPORTED = new Set([
|
|
|
94
97
|
const MAKE_CHEAT_PLATFORMS = [
|
|
95
98
|
"nes", "gb", "gbc", "snes", "genesis", "sms", "gg",
|
|
96
99
|
"atari2600", "atari7800", "lynx", "gba", "c64", "pce", "msx",
|
|
100
|
+
"gametank", // NEW: GameTank Game Genie (read-substitution device) — see gamegenie.js
|
|
97
101
|
];
|
|
98
102
|
|
|
99
103
|
// gameCheats indexes whose codes are predominantly ENCRYPTED at the source
|