romdevtools 0.26.0 → 0.27.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 +13 -3
- package/package.json +1 -1
- package/src/mcp/tools/watch-memory.js +32 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ 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.27.0
|
|
8
|
+
|
|
9
|
+
### Added — `breakpoint(on:'pc', captureMemory:[…])` reads named RAM at the hit
|
|
10
|
+
Completes item 2 of the NES Rygar report. 0.26.0 shipped `registersAtHit` (the
|
|
11
|
+
break-instant register file) but not the memory half. Now `breakpoint(on:'pc')`
|
|
12
|
+
takes `captureMemory:[{region,offset,length,label}]` and returns those reads inline
|
|
13
|
+
as `capturedMemory`, so register + RAM inspection at a PC collapses into ONE call —
|
|
14
|
+
no follow-up `cpu`/`memory` round trips. `registersAtHit` is the true break instant
|
|
15
|
+
(core snapshot); `capturedMemory` reflects the routine's RAM side effects for the
|
|
16
|
+
hit frame (stable + what RE needs), documented as such.
|
|
17
|
+
|
|
7
18
|
## 0.26.0
|
|
8
19
|
|
|
9
20
|
### Fixed — NES `breakpoint(on:'pc')` now returns reliable break-instant registers
|
|
@@ -17,9 +28,8 @@ them (the schema's "CPU is FROZEN at this instruction" was wrong for NES).
|
|
|
17
28
|
SNAPSHOTS A/X/Y/P/S at the hit instant, exposed via `romdev_pcbreak_get`.
|
|
18
29
|
- **`breakpoint(on:'pc')` returns `registersAtHit`** — the reliable break-instant
|
|
19
30
|
register file. The schema + hit note now steer to it and explicitly warn that a
|
|
20
|
-
live `cpu({op:'read'})` after a hit is end-of-frame state on fceumm. (
|
|
21
|
-
|
|
22
|
-
hit, so there's no freeze-durability race and no extra round trip.)
|
|
31
|
+
live `cpu({op:'read'})` after a hit is end-of-frame state on fceumm. (The
|
|
32
|
+
`captureMemory` companion that reads named RAM inline at the hit landed in 0.27.0.)
|
|
23
33
|
- **NES `cpu({op:'read'})` core-internal fields relabeled** (item 3): `DB`,
|
|
24
34
|
`IRQlow`, `tcount`, `count` are fceumm internals (data-bus latch / IRQ bitmask /
|
|
25
35
|
cycle counters), not 6502 registers — moved out of `registers` into a labeled
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "romdevtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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",
|
|
@@ -633,7 +633,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
633
633
|
});
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
async function bpRunUntilPC({ address, maxFrames = 600, pressDuring }) {
|
|
636
|
+
async function bpRunUntilPC({ address, maxFrames = 600, pressDuring, captureMemory }) {
|
|
637
637
|
const host = getHost(sessionKey);
|
|
638
638
|
if (!host.pcBreakSupported || !host.pcBreakSupported()) {
|
|
639
639
|
return jsonContent({
|
|
@@ -669,6 +669,29 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
669
669
|
// Snapshot the registers AT the hit BEFORE clearing (last already holds the
|
|
670
670
|
// hit state; read it without clearing so registersAtHit survives).
|
|
671
671
|
const atHit = last.registersAtHit ?? host.getPCBreak(false).registersAtHit ?? null;
|
|
672
|
+
// captureMemory: read the requested regions AT the hit (before we clear/step),
|
|
673
|
+
// returned inline so break→read RAM collapses into ONE call. NOTE: registers
|
|
674
|
+
// are the true break instant (core snapshot); these RAM reads are taken now —
|
|
675
|
+
// i.e. after the hit frame finished — so on run-to-frame-end cores (fceumm)
|
|
676
|
+
// they reflect the routine's RAM SIDE EFFECTS for that frame (which is what
|
|
677
|
+
// RE wants: "what did this routine touch"), not necessarily the exact byte
|
|
678
|
+
// mid-instruction. Stable + reliable; that's the property the report leaned on.
|
|
679
|
+
let capturedMemory = null;
|
|
680
|
+
if (Array.isArray(captureMemory) && captureMemory.length) {
|
|
681
|
+
capturedMemory = {};
|
|
682
|
+
for (const m of captureMemory) {
|
|
683
|
+
const label = m.label ?? `${m.region}+${m.offset}`;
|
|
684
|
+
try {
|
|
685
|
+
const bytes = host.readMemory(m.region, m.offset, m.length ?? 1);
|
|
686
|
+
capturedMemory[label] = {
|
|
687
|
+
region: m.region, offset: m.offset, length: m.length ?? 1,
|
|
688
|
+
hex: Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""),
|
|
689
|
+
};
|
|
690
|
+
} catch (e) {
|
|
691
|
+
capturedMemory[label] = { region: m.region, offset: m.offset, error: String(e?.message ?? e) };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
672
695
|
const fin = host.getPCBreak(true); // clear hit
|
|
673
696
|
// registersAtHit (NES/fceumm and any core that snapshots regs on hit) is the
|
|
674
697
|
// RELIABLE break-instant register file. The LIVE register file (a follow-up
|
|
@@ -677,7 +700,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
677
700
|
// end-of-frame state. Prefer registersAtHit; only fall back to a live read on
|
|
678
701
|
// cores that don't snapshot.
|
|
679
702
|
const frozenNote = atHit
|
|
680
|
-
? "registersAtHit holds the register file CAPTURED AT this instruction (A/X/Y/P/S) — use THESE, not a follow-up cpu({op:'read'}), which on NES/fceumm returns end-of-frame state, not the break instant. For
|
|
703
|
+
? "registersAtHit holds the register file CAPTURED AT this instruction (A/X/Y/P/S) — use THESE, not a follow-up cpu({op:'read'}), which on NES/fceumm returns end-of-frame state, not the break instant. For RAM at the hit, pass captureMemory:[{region,offset,length}] to get it inline (capturedMemory) in THIS call instead of a follow-up read. frame({op:'stepInstruction'}) to single-step from here."
|
|
681
704
|
: "This core does not snapshot registers at the hit. cpu({op:'read'}) reflects the CPU state now; on cores that run-to-frame-end (fceumm) that is NOT the break instant — prefer the RAM side effects (memory({op:'read'})) over the live register file.";
|
|
682
705
|
return attachObserverFrame(jsonContent({
|
|
683
706
|
hit: true,
|
|
@@ -685,6 +708,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
685
708
|
pc: last.lastPC != null ? "$" + last.lastPC.toString(16).toUpperCase() : null,
|
|
686
709
|
pcRaw: last.lastPC,
|
|
687
710
|
...(atHit ? { registersAtHit: atHit } : {}),
|
|
711
|
+
...(capturedMemory ? { capturedMemory } : {}),
|
|
688
712
|
frame: host.status.frameCount,
|
|
689
713
|
framesRun,
|
|
690
714
|
hits: fin.hits,
|
|
@@ -780,6 +804,12 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
780
804
|
offset: z.number().int().min(0).describe("byte offset within the region"),
|
|
781
805
|
label: z.string().optional().describe("human name for this guard byte"),
|
|
782
806
|
})).optional().describe("on:'write' exact — ABORT GUARD for a pressDuring run: caller-named 'is this scenario still valid?' bytes (e.g. the area/scene id, the player object-active flag). If ANY changes mid-run the watchpoint stops IMMEDIATELY and returns {aborted:true, abortedBy, before, after} — so a driven scenario that derailed (player died → title screen) doesn't burn all maxFrames and return a meaningless found:false. Each is sampled once per frame (cheap)."),
|
|
807
|
+
captureMemory: z.array(z.object({
|
|
808
|
+
region: z.enum(MEMORY_REGIONS).describe("memory region to read"),
|
|
809
|
+
offset: z.number().int().min(0).describe("byte offset within the region"),
|
|
810
|
+
length: z.number().int().min(1).max(256).default(1).describe("bytes to read"),
|
|
811
|
+
label: z.string().optional().describe("human name for this read (else 'region+offset')"),
|
|
812
|
+
})).optional().describe("on:'pc' — read these memory regions AT the hit and return them inline as `capturedMemory` (collapses break→read-RAM into ONE call, the token win). Pair with `registersAtHit` to get the routine's register + RAM state in a single round trip (e.g. capture the ZP pointer bytes a decoder just wrote). NOTE: registersAtHit is the true break instant (core snapshot); these RAM reads are taken after the hit frame finishes, so on run-to-frame-end cores (fceumm) they're the routine's RAM side effects for that frame — stable + reliable, which is exactly what RE needs."),
|
|
783
813
|
},
|
|
784
814
|
safeTool(async (args) => {
|
|
785
815
|
switch (args.on) {
|