@henols/c64-re-tools 2.0.0-rc.7 → 2.0.0-rc.9
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/README.md +3 -1
- package/package.json +1 -1
- package/skills/c64-basic/SKILL.md +1 -1
- package/skills/c64-basic/scripts/basic.ts +1 -1
- package/skills/c64-emulator/SKILL.md +2 -1
- package/skills/c64-knowledge/SKILL.md +1 -1
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-provenance/SKILL.md +4 -1
- package/skills/c64-provenance/scripts/compare.ts +9 -6
- package/skills/c64-provenance/scripts/provenance.ts +13 -7
- package/skills/c64-source/SKILL.md +6 -5
- package/skills/c64-source/scripts/generate.ts +19 -0
- package/skills/c64-source/scripts/loader.ts +172 -7
- package/skills/c64-source/scripts/source.ts +7 -3
- package/skills/c64-static-analysis/SKILL.md +1 -1
- package/skills/c64-static-analysis/scripts/idioms.ts +2 -2
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/scenario.ts +7 -5
- package/skills/c64-unpacker/SKILL.md +2 -2
- package/skills/c64-unpacker/scripts/loads.ts +4 -3
- package/skills/c64-unpacker/scripts/machine.ts +13 -1
- package/skills/c64-unpacker/scripts/unpack.ts +40 -13
- package/src/c64.ts +6 -1
- package/src/host/main.ts +27 -6
- package/src/host/vice/adapter.ts +14 -0
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/screen.ts +3 -2
- package/src/host/vice/session.ts +23 -7
- package/src/host-client/autostart.ts +11 -1
- package/src/host-client/tools.ts +1 -1
- package/src/mcp/tools/debug.ts +6 -3
- package/src/mcp/tools/machine.ts +2 -2
- package/src/mcp/tools/media.ts +2 -0
- package/src/mcp/tools/video.ts +5 -2
- package/src/protocol/vice.ts +50 -8
- package/src/trace.ts +55 -23
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// explicit, and the result is PASS, FAIL or INCONCLUSIVE.
|
|
5
5
|
|
|
6
6
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
7
|
-
import { MAX_BASELINES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
|
|
7
|
+
import { MAX_BASELINES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
|
|
8
8
|
import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { ACTION_STEP_KEYS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
10
10
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
@@ -129,7 +129,7 @@ export function readScenario(path: string): Scenario {
|
|
|
129
129
|
const allowed = [...(STEP_KEYS[kind] ?? [kind]), ...(kind === "observe" ? [] : ["side"])];
|
|
130
130
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
131
131
|
if (step.side !== undefined && !(SIDES as unknown[]).includes(step.side)) fail(`${at}: side must be original or rebuild`);
|
|
132
|
-
checkStep(kind, step, at, fail);
|
|
132
|
+
checkStep(kind, step, at, fail, scenario.videoStandard ?? "pal");
|
|
133
133
|
if (kind === "observe") {
|
|
134
134
|
if (labels.has(step.observe as string)) fail(`${at} needs a new checkpoint name`);
|
|
135
135
|
labels.add(step.observe as string);
|
|
@@ -142,7 +142,7 @@ export function readScenario(path: string): Scenario {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/** Checks the value of one step against its kind. */
|
|
145
|
-
function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
145
|
+
function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
|
|
146
146
|
switch (kind) {
|
|
147
147
|
case "reset":
|
|
148
148
|
if (step.reset !== "hard" && step.reset !== "soft") fail(`${at}: reset must be hard or soft`);
|
|
@@ -165,11 +165,11 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
|
|
|
165
165
|
case "runUntil":
|
|
166
166
|
return checkActionStep(kind, step, at, fail);
|
|
167
167
|
default:
|
|
168
|
-
checkObserve(step, at, fail);
|
|
168
|
+
checkObserve(step, at, fail, videoStandard);
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
172
|
+
function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
|
|
173
173
|
if (typeof step.observe !== "string" || step.observe === "") fail(`${at} needs a new checkpoint name`);
|
|
174
174
|
if (step.memory !== undefined) {
|
|
175
175
|
if (!Array.isArray(step.memory) || step.memory.length === 0) fail(`${at}: memory must be a list of names, $ addresses or objects with at`);
|
|
@@ -216,6 +216,8 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
216
216
|
isInteger(rectangle.height, 1, 4096);
|
|
217
217
|
if (!ok) fail(`${at}: a screen.mask rectangle needs whole numbers x and y from 0, and width and height from 1`);
|
|
218
218
|
}
|
|
219
|
+
// The emulator refuses such a compare too, but only after the original side ran (b2/03).
|
|
220
|
+
if (maskCoversFrame(screen.mask as Array<{ x: number; y: number; width: number; height: number }>, videoStandard)) fail(`${at}: screen.mask covers the whole screen, so nothing is compared`);
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
|
|
@@ -36,7 +36,7 @@ node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out
|
|
|
36
36
|
| `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
|
|
37
37
|
| `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
|
|
38
38
|
| `--entry '$c000'` | `trace`, `stages` and `capture`: start the program at this address. Without it, the script types RUN. A file that is not a BASIC program at `$0801` needs it. |
|
|
39
|
-
| `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. |
|
|
39
|
+
| `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. The scripts start the program as a c64-testing scenario does. Thus the same steps in a scenario get the program to the entry at the same time. |
|
|
40
40
|
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
41
41
|
| `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
|
|
42
42
|
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
@@ -76,7 +76,7 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
|
|
|
76
76
|
- `image.bin`: all 64 KiB of RAM at the entry.
|
|
77
77
|
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
78
78
|
- One PRG for each segment.
|
|
79
|
-
- `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
|
|
79
|
+
- `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
|
|
80
80
|
5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it.
|
|
81
81
|
6. Give `image.bin` to the c64-static-analysis skill with its `--flat64k` option, or give it the segment PRGs.
|
|
82
82
|
7. Use `--range` only when you know the area of the program. Then the capture writes that one segment.
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// image with its entry state, and with --drive the drive RAM as a PRG for
|
|
5
5
|
// knowledge space drive8. No tool guesses where a load ends: the steps decide.
|
|
6
6
|
|
|
7
|
-
import { COVERAGE, formatC64Address, parseC64Address
|
|
8
|
-
import { autostartLoaded, LOAD_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
|
|
7
|
+
import { COVERAGE, formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
8
|
+
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
10
10
|
import { ACTION_STEP_KEYS, ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
11
11
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
@@ -113,7 +113,8 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
113
113
|
previous.fill(0, load, Math.min(0x10000, load + bytes.length - 2));
|
|
114
114
|
loadedProgram = bytes;
|
|
115
115
|
await client.memmap({ action: "clear" });
|
|
116
|
-
|
|
116
|
+
// RUN as c64-testing types it: the steps start at the same time as a scenario's (D62).
|
|
117
|
+
if (!(await typeRun(client))) return { output: { loaded: false, reason: `After RUN, BASIC ran no program line in ${RUN_LIMIT_FRAMES} frames.` }, failed: true };
|
|
117
118
|
} else {
|
|
118
119
|
// A disk, tape or cartridge: VICE loads and starts it. The RAM test of the KERNAL puts back what it writes.
|
|
119
120
|
previous = await readRam(client, 0x0000, 0x10000);
|
|
@@ -14,10 +14,19 @@ export const IO_AREAS = { vic: [0xd000, 0x2f], sid: [0xd400, 0x1d], cia1: [0xdc0
|
|
|
14
14
|
/** Frames from a hard reset to the READY prompt, with margin. */
|
|
15
15
|
export const READY_FRAMES = 200;
|
|
16
16
|
|
|
17
|
+
/** What no register read of a CIA gives (D63). */
|
|
18
|
+
export interface CiaHidden {
|
|
19
|
+
latchA: number;
|
|
20
|
+
latchB: number;
|
|
21
|
+
interruptMask: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export interface MachineState {
|
|
18
25
|
port: Buffer;
|
|
19
26
|
pages: Buffer;
|
|
20
27
|
io: Record<keyof typeof IO_AREAS, Buffer>;
|
|
28
|
+
/** The timer latches and the interrupt mask of each CIA. */
|
|
29
|
+
cia: { cia1: CiaHidden; cia2: CiaHidden };
|
|
21
30
|
romVectors: Buffer;
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -36,8 +45,9 @@ export async function machineState(client: ViceSessionClient): Promise<MachineSt
|
|
|
36
45
|
const pages = await readRam(client, 0x0000, 0x400);
|
|
37
46
|
const io = {} as Record<keyof typeof IO_AREAS, Buffer>;
|
|
38
47
|
for (const [name, [address, size]] of Object.entries(IO_AREAS)) io[name as keyof typeof IO_AREAS] = Buffer.from((await client.ioRead(address, size)).data, "hex");
|
|
48
|
+
const [cia1, cia2] = (await client.cia("both")).chips.map(({ latchA, latchB, interruptMask }) => ({ latchA, latchB, interruptMask }));
|
|
39
49
|
const romVectors = Buffer.from((await client.memoryRead({ address: 0xfffa, size: 6, space: "c64", view: "cpu" })).data, "hex");
|
|
40
|
-
return { port, pages, io, romVectors };
|
|
50
|
+
return { port, pages, io, cia: { cia1: cia1!, cia2: cia2! }, romVectors };
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
const word = (bytes: Buffer, offset: number) => formatC64Address(bytes[offset]! | (bytes[offset + 1]! << 8));
|
|
@@ -60,6 +70,8 @@ export function entryState(registers: Registers, state: MachineState, vectors: R
|
|
|
60
70
|
cpuPort: { $00: state.port[0], $01: state.port[1] },
|
|
61
71
|
pages: Object.fromEntries([0, 1, 2, 3].map((page) => [formatC64Address(page << 8), state.pages.subarray(page << 8, (page + 1) << 8).toString("hex")])),
|
|
62
72
|
io: Object.fromEntries(Object.entries(IO_AREAS).map(([name, [address]]) => [name, { from: formatC64Address(address), bytes: state.io[name as keyof typeof IO_AREAS].toString("hex") }])),
|
|
73
|
+
// The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them (D63).
|
|
74
|
+
cia: state.cia,
|
|
63
75
|
vectors,
|
|
64
76
|
...(differsFromReset === undefined ? {} : { differsFromReset }),
|
|
65
77
|
};
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
|
|
11
|
-
import { COVERAGE, EXECUTE, formatC64Address, parseC64Address, READ,
|
|
12
|
-
import { autostartLoaded, LOAD_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
|
|
11
|
+
import { COVERAGE, EXECUTE, formatC64Address, parseC64Address, READ, WRITE } from "#src/c64.ts";
|
|
12
|
+
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
13
13
|
import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
|
|
14
14
|
import { checkActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
15
15
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
@@ -107,10 +107,8 @@ async function trace(program: string, options: { frames: number; entry?: number;
|
|
|
107
107
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
108
108
|
try {
|
|
109
109
|
// Loaded but not started, so the memory map can start empty: the KERNAL start-up writes all of RAM.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (options.entry !== undefined) await client.registersSet("c64", { pc: options.entry });
|
|
113
|
-
else await client.keyboard(textToPetscii("RUN\n"));
|
|
110
|
+
const notStarted = await startLoaded(client, program, bytes, options.entry);
|
|
111
|
+
if (notStarted !== undefined) return { output: { traced: false, reason: notStarted }, failed: true };
|
|
114
112
|
const stepFailed = await runSteps(client, options.steps);
|
|
115
113
|
if (stepFailed !== undefined) return { output: { traced: false, reason: stepFailed }, failed: true };
|
|
116
114
|
let ran = 0;
|
|
@@ -191,6 +189,26 @@ const SYS_FRAMES = 50;
|
|
|
191
189
|
* The first PC in the program after its BASIC text, for a SYS whose address
|
|
192
190
|
* BASIC computes: an exec breakpoint over that part of the program stops there.
|
|
193
191
|
*/
|
|
192
|
+
/**
|
|
193
|
+
* Starts the program as a c64-testing scenario does (D62): hard reset, load,
|
|
194
|
+
* an empty memory map, then RUN up to the first BASIC line, or the pc at
|
|
195
|
+
* `entry`. Gives why it did not start.
|
|
196
|
+
*/
|
|
197
|
+
async function startLoaded(client: ViceSessionClient, program: string, bytes: Uint8Array, entry: number | undefined): Promise<string | undefined> {
|
|
198
|
+
await client.reset({ mode: "hard", run: false });
|
|
199
|
+
if (!(await autostartLoaded(client, program, bytes))) return `The program was not in memory at ${formatC64Address(bytes[0]! | (bytes[1]! << 8))} after ${LOAD_LIMIT_FRAMES} frames.`;
|
|
200
|
+
await client.memmap({ action: "clear" });
|
|
201
|
+
if (entry !== undefined) await client.registersSet("c64", { pc: entry });
|
|
202
|
+
else if (!(await typeRun(client))) return `After RUN, BASIC ran no program line in ${RUN_LIMIT_FRAMES} frames.`;
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The raster position now: where a step's frames end (D62). */
|
|
207
|
+
async function rasterAt(client: ViceSessionClient): Promise<{ line: number; cycle: number }> {
|
|
208
|
+
const { timing } = await client.observe({ timing: true });
|
|
209
|
+
return { line: timing!.rasterLine, cycle: timing!.rasterCycle };
|
|
210
|
+
}
|
|
211
|
+
|
|
194
212
|
async function firstPcAfterBasic(client: ViceSessionClient, from: number, to: number): Promise<number | undefined> {
|
|
195
213
|
return from > to ? undefined : firstPcIn(client, [{ start: from, end: to }]);
|
|
196
214
|
}
|
|
@@ -246,10 +264,11 @@ async function follow(
|
|
|
246
264
|
const load = bytes[0]! | (bytes[1]! << 8);
|
|
247
265
|
const basic = basicStart(bytes);
|
|
248
266
|
if (options.entry === undefined && basic === undefined) throw new UsageError(NO_BASIC);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
const notStarted = await startLoaded(client, program, bytes, options.entry);
|
|
268
|
+
if (notStarted !== undefined) return { stages: [], frames: 0, reached: false, failed: notStarted };
|
|
269
|
+
// The frames of each step end where a scenario's do: the first step counts from RUN, a later one from where it starts (D62).
|
|
270
|
+
let anchor: { line: number; cycle: number } | undefined = await rasterAt(client);
|
|
271
|
+
const framed = () => (anchor === undefined ? {} : { anchor });
|
|
253
272
|
const found: Stage[] = [];
|
|
254
273
|
const start = options.entry ?? basic!.sys?.address ?? (await firstPcAfterBasic(client, basic!.basicEnd, load + bytes.length - 3));
|
|
255
274
|
if (start === undefined) return { stages: [], frames: 0, reached: false, failed: `After RUN, the CPU did not execute the program after its BASIC text (${formatC64Address(basic!.basicEnd)} on) in ${SYS_FRAMES} frames. Give --entry <address>.` };
|
|
@@ -311,7 +330,7 @@ async function follow(
|
|
|
311
330
|
await client.snapshot({ action: "save", name: CALL_SNAPSHOT });
|
|
312
331
|
const point = await client.breakpoint({ action: "add", address: back, space: "c64" });
|
|
313
332
|
try {
|
|
314
|
-
const run = await client.execution({ action: "advance-frames", count: frames, space: "c64" });
|
|
333
|
+
const run = await client.execution({ action: "advance-frames", count: frames, space: "c64", ...framed() });
|
|
315
334
|
if ((run.pc ?? (await client.registersGet("c64")).pc) === back) return run.advancedFrames ?? 0;
|
|
316
335
|
await client.snapshot({ action: "restore", name: CALL_SNAPSHOT });
|
|
317
336
|
return undefined;
|
|
@@ -330,7 +349,7 @@ async function follow(
|
|
|
330
349
|
}
|
|
331
350
|
const written = (address: number) => ((stage.map[address]! | map[address]!) & COVERAGE.ramWrite) !== 0;
|
|
332
351
|
await arm(stagePoints(candidates, stage.own, written, stage.called));
|
|
333
|
-
const run = await client.execution({ action: "advance-frames", count: left, space: "c64" });
|
|
352
|
+
const run = await client.execution({ action: "advance-frames", count: left, space: "c64", ...framed() });
|
|
334
353
|
const advanced = run.advancedFrames ?? 0;
|
|
335
354
|
used += advanced;
|
|
336
355
|
left -= advanced;
|
|
@@ -365,13 +384,21 @@ async function follow(
|
|
|
365
384
|
|
|
366
385
|
/** Tracks the stages for some frames; true when the CPU got to stopAt. */
|
|
367
386
|
const track = async (frames: number): Promise<boolean> => {
|
|
387
|
+
anchor ??= await rasterAt(client);
|
|
388
|
+
try {
|
|
389
|
+
return await trackFrom(frames);
|
|
390
|
+
} finally {
|
|
391
|
+
anchor = undefined;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
const trackFrom = async (frames: number): Promise<boolean> => {
|
|
368
395
|
let left = frames;
|
|
369
396
|
let stopped = false;
|
|
370
397
|
while (left > 0 && !ended && !stopped && found.length < MAX_STAGES) {
|
|
371
398
|
await client.snapshot({ action: "save", name: STAGE_SNAPSHOT });
|
|
372
399
|
await client.memmap({ action: "clear" });
|
|
373
400
|
const chunk = Math.min(STAGE_CHUNK_FRAMES, left);
|
|
374
|
-
const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64" });
|
|
401
|
+
const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64", ...framed() });
|
|
375
402
|
const firstPc = first.pc ?? (await client.registersGet("c64")).pc;
|
|
376
403
|
const map = await readCoverage(client);
|
|
377
404
|
const candidates = stageCandidates(stage.map, map, stage.own, options.minStage, stage.called);
|
package/src/c64.ts
CHANGED
|
@@ -163,7 +163,9 @@ const MIN_BASIC_LOAD = 0x0400;
|
|
|
163
163
|
* zero, the "marker" is the first machine code after the BASIC text (SYS
|
|
164
164
|
* lines that jump right behind themselves). The stored links are not read:
|
|
165
165
|
* BASIC relinks a loaded program, and a crack can store odd links to hide
|
|
166
|
-
* lines from LIST. A load address below $0400 is a memory dump (F-12)
|
|
166
|
+
* lines from LIST. A load address below $0400 is a memory dump (F-12), and so
|
|
167
|
+
* is a file whose bytes run past $ffff: a 64 KiB dump from $0000 always does
|
|
168
|
+
* (b2/01, R3-02).
|
|
167
169
|
*/
|
|
168
170
|
export function parseTokenized(program: Uint8Array): TokenizedProgram {
|
|
169
171
|
if (program.length < 4) return { reason: "The file is too short for a BASIC program." };
|
|
@@ -171,6 +173,9 @@ export function parseTokenized(program: Uint8Array): TokenizedProgram {
|
|
|
171
173
|
if (loadAddress < MIN_BASIC_LOAD) {
|
|
172
174
|
return { reason: `The load address ${formatC64Address(loadAddress)} is below $0400, in the zero page, the stack or the system pages. The file is a memory dump or machine code, not a BASIC program.` };
|
|
173
175
|
}
|
|
176
|
+
if (loadAddress + program.length - 2 > 0x10000) {
|
|
177
|
+
return { reason: `The file holds ${program.length - 2} bytes from ${formatC64Address(loadAddress)}, which runs past $ffff. The file is a memory dump, not a BASIC program.` };
|
|
178
|
+
}
|
|
174
179
|
const lines: TokenizedLine[] = [];
|
|
175
180
|
let at = 2;
|
|
176
181
|
const ended = (basicEnd: number): TokenizedProgram => ({ loadAddress, basicEnd, textBytes: at, lines });
|
package/src/host/main.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { PACKAGE } from "../package.ts";
|
|
|
15
15
|
const HELP = `c64-re-tools-host
|
|
16
16
|
|
|
17
17
|
Usage:
|
|
18
|
-
npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host [--port <port>]
|
|
18
|
+
npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host [--port <port>] [--log-console] [--log-dir <directory>]
|
|
19
19
|
c64-re-tools-host --help
|
|
20
20
|
|
|
21
21
|
Runs the c64-re-tools Host Runtime in the foreground. It first starts VICE
|
|
@@ -27,8 +27,15 @@ connected c64-re-tools-mcp process, and runs the tools that come with VICE
|
|
|
27
27
|
emulator and tool it started.
|
|
28
28
|
|
|
29
29
|
Options:
|
|
30
|
-
--port <port>
|
|
31
|
-
--
|
|
30
|
+
--port <port> Listen on this port instead (0 picks a free port).
|
|
31
|
+
--log-console Also print each request with its result, and each run
|
|
32
|
+
of c1541 or petcat, to the console.
|
|
33
|
+
--log-dir <directory> Write the same events as JSON lines to a file in this
|
|
34
|
+
directory, with a copy of each VICE log.
|
|
35
|
+
--help Show this help.
|
|
36
|
+
|
|
37
|
+
Without --log-console and --log-dir the host logs only its start, its stop,
|
|
38
|
+
connections and failures. C64RT_TRACE does not apply to the host.`;
|
|
32
39
|
|
|
33
40
|
function log(line: string): void {
|
|
34
41
|
process.stderr.write(`c64-re-tools-host: ${line}\n`);
|
|
@@ -36,9 +43,12 @@ function log(line: string): void {
|
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
async function main(): Promise<number> {
|
|
39
|
-
let values: { help?: boolean; port?: string };
|
|
46
|
+
let values: { help?: boolean; port?: string; "log-console"?: boolean; "log-dir"?: string };
|
|
40
47
|
try {
|
|
41
|
-
({ values } = parseArgs({
|
|
48
|
+
({ values } = parseArgs({
|
|
49
|
+
options: { help: { type: "boolean" }, port: { type: "string" }, "log-console": { type: "boolean" }, "log-dir": { type: "string" } },
|
|
50
|
+
strict: true,
|
|
51
|
+
}));
|
|
42
52
|
} catch (error) {
|
|
43
53
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP}\n`);
|
|
44
54
|
return 2;
|
|
@@ -47,7 +57,18 @@ async function main(): Promise<number> {
|
|
|
47
57
|
process.stdout.write(`${HELP}\n`);
|
|
48
58
|
return 0;
|
|
49
59
|
}
|
|
50
|
-
|
|
60
|
+
const logDir = values["log-dir"];
|
|
61
|
+
if (logDir === "") {
|
|
62
|
+
process.stderr.write("--log-dir needs a directory.\n");
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
startTrace("host", {
|
|
66
|
+
target: {
|
|
67
|
+
...(logDir === undefined ? {} : { dir: { path: logDir, given: `--log-dir ${logDir}` } }),
|
|
68
|
+
...(values["log-console"] ? { echo: (line: string) => process.stderr.write(`c64-re-tools-host: ${line}\n`) } : {}),
|
|
69
|
+
},
|
|
70
|
+
warn: log,
|
|
71
|
+
});
|
|
51
72
|
|
|
52
73
|
const port = values.port === undefined ? DEFAULT_HOST_PORT : parsePort(values.port);
|
|
53
74
|
if (port === undefined) {
|
package/src/host/vice/adapter.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type Comparison, type Condition, type BacktraceFrame, type HistoryEntry
|
|
|
6
6
|
import { COVERAGE, formatC64Address, formatC64Byte } from "../../c64.ts";
|
|
7
7
|
import { decodeDisplay, decodePalette, visibleFrame, type IndexedFrame } from "./screen.ts";
|
|
8
8
|
import type { TextMonitor } from "./text-monitor.ts";
|
|
9
|
+
import type { CiaHidden } from "./chips.ts";
|
|
9
10
|
import {
|
|
10
11
|
BinaryMonitor,
|
|
11
12
|
Command,
|
|
@@ -431,6 +432,19 @@ export class ViceAdapter {
|
|
|
431
432
|
return bytes.toString("hex");
|
|
432
433
|
}
|
|
433
434
|
|
|
435
|
+
/**
|
|
436
|
+
* The timer latches and the interrupt mask of the CIA at `base`, from VICE's
|
|
437
|
+
* text-monitor dump: no register read gives them (D63).
|
|
438
|
+
*/
|
|
439
|
+
async ciaHidden(base: 0xdc00 | 0xdd00): Promise<CiaHidden> {
|
|
440
|
+
const answer = await this.#computerText(`io ${hex4(base)}`);
|
|
441
|
+
const mask = /\(mask: ([0-9a-f]{2})\)/i.exec(answer);
|
|
442
|
+
const latchA = /Timer A: [0-9a-f]{4} \(latched ([0-9a-f]{4})\)/i.exec(answer);
|
|
443
|
+
const latchB = /Timer B: [0-9a-f]{4} \(latched ([0-9a-f]{4})\)/i.exec(answer);
|
|
444
|
+
if (mask === null || latchA === null || latchB === null) throw unknownForm("CIA dump", answer.split("\n"));
|
|
445
|
+
return { latchA: Number.parseInt(latchA[1]!, 16), latchB: Number.parseInt(latchB[1]!, 16), interruptMask: Number.parseInt(mask[1]!, 16) & 0x1f };
|
|
446
|
+
}
|
|
447
|
+
|
|
434
448
|
/** Reads C64 I/O registers through the I/O bank, whatever the CPU has banked in, without side effects. */
|
|
435
449
|
async readIo(start: number, size: number): Promise<Uint8Array> {
|
|
436
450
|
const response = await this.#monitor.request(
|
package/src/host/vice/chips.ts
CHANGED
|
@@ -57,8 +57,15 @@ function bcd(value: number): number {
|
|
|
57
57
|
return (value >> 4) * 10 + (value & 0x0f);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** The parts of a CIA that no register read gives: the timer latches and the interrupt mask. */
|
|
61
|
+
export interface CiaHidden {
|
|
62
|
+
latchA: number;
|
|
63
|
+
latchB: number;
|
|
64
|
+
interruptMask: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
/** `registers` holds $dc00-$dc0f or $dd00-$dd0f. TOD hours become 0-23. */
|
|
61
|
-
export function decodeCia(id: 1 | 2, registers: Uint8Array): CiaState {
|
|
68
|
+
export function decodeCia(id: 1 | 2, registers: Uint8Array, hidden: CiaHidden): CiaState {
|
|
62
69
|
const word = (low: number) => registers[low]! | (registers[low + 1]! << 8);
|
|
63
70
|
const hourRegister = registers[0x0b]!;
|
|
64
71
|
const pm = (hourRegister & 0x80) !== 0;
|
|
@@ -70,9 +77,12 @@ export function decodeCia(id: 1 | 2, registers: Uint8Array): CiaState {
|
|
|
70
77
|
ddrB: registers[0x03]!,
|
|
71
78
|
timerA: word(0x04),
|
|
72
79
|
timerB: word(0x06),
|
|
80
|
+
latchA: hidden.latchA,
|
|
81
|
+
latchB: hidden.latchB,
|
|
73
82
|
controlA: registers[0x0e]!,
|
|
74
83
|
controlB: registers[0x0f]!,
|
|
75
84
|
interruptStatus: registers[0x0d]!,
|
|
85
|
+
interruptMask: hidden.interruptMask,
|
|
76
86
|
tod: {
|
|
77
87
|
hours: (bcd(hourRegister & 0x1f) % 12) + (pm ? 12 : 0),
|
|
78
88
|
minutes: bcd(registers[0x0a]! & 0x7f),
|
package/src/host/vice/screen.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { crc32, deflateSync } from "node:zlib";
|
|
5
5
|
|
|
6
6
|
import type { VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
+
import { FRAME_SIZE } from "../../protocol/vice.ts";
|
|
7
8
|
|
|
8
9
|
export interface IndexedFrame {
|
|
9
10
|
width: number;
|
|
@@ -44,8 +45,8 @@ export function decodeDisplay(body: Buffer): RawDisplay {
|
|
|
44
45
|
* offsets: PAL 384×272, NTSC 384×247.
|
|
45
46
|
*/
|
|
46
47
|
const VISIBLE: Record<VideoStandard, { dx: number; dy: number; width: number; height: number }> = {
|
|
47
|
-
pal: { dx: -32, dy: -36,
|
|
48
|
-
ntsc: { dx: -32, dy: -23,
|
|
48
|
+
pal: { dx: -32, dy: -36, ...FRAME_SIZE.pal },
|
|
49
|
+
ntsc: { dx: -32, dy: -23, ...FRAME_SIZE.ntsc },
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
/** Crops the visible bordered frame out of VICE's full raster buffer. */
|
package/src/host/vice/session.ts
CHANGED
|
@@ -381,12 +381,21 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
381
381
|
this.#cartridge = false;
|
|
382
382
|
this.#replaceStaged("cartridge", undefined);
|
|
383
383
|
}
|
|
384
|
+
// Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types (R3-05).
|
|
385
|
+
const centred: Array<1 | 2> = [];
|
|
386
|
+
for (const [port, joystick] of this.#joysticks) {
|
|
387
|
+
if (joystick.direction === "center" && !joystick.fire) continue;
|
|
388
|
+
const centre: JoystickState = { port, direction: "center", fire: false };
|
|
389
|
+
await this.#observe(() => this.#machine.setJoystick(centre));
|
|
390
|
+
this.#joysticks.set(port, centre);
|
|
391
|
+
centred.push(port);
|
|
392
|
+
}
|
|
384
393
|
await this.#runningCommand(() => this.#machine.autostart(file, index, run));
|
|
385
394
|
if (type === "crt") this.#cartridge = true;
|
|
386
395
|
this.#replaceStaged(stagedKind(type), file);
|
|
387
396
|
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
388
397
|
this.#timingStart = 0n;
|
|
389
|
-
return { state: "running", ...(type === "tap" && this.#inputFlaw !== undefined ? { emulatorFlaw: this.#inputFlaw } : {}) };
|
|
398
|
+
return { state: "running", ...(type === "tap" && this.#inputFlaw !== undefined ? { emulatorFlaw: this.#inputFlaw } : {}), ...(centred.length === 0 ? {} : { joysticksCentred: centred }) };
|
|
390
399
|
}
|
|
391
400
|
case "diskAttach": {
|
|
392
401
|
const { type } = params as ViceOperations["diskAttach"]["params"];
|
|
@@ -498,8 +507,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
498
507
|
async #status(): Promise<MachineStatus> {
|
|
499
508
|
const warp = await this.#observe(() => this.#machine.warp());
|
|
500
509
|
// Read after #observe: it resumes a running machine, unless a breakpoint or a JAM stopped it meanwhile.
|
|
501
|
-
const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window" };
|
|
502
|
-
if (this.#cartridge) status.cartridge = true;
|
|
510
|
+
const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window", cartridge: this.#cartridge };
|
|
503
511
|
if (status.state === "stopped") status.pc = await this.#pc();
|
|
504
512
|
return status;
|
|
505
513
|
}
|
|
@@ -526,7 +534,11 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
526
534
|
}
|
|
527
535
|
if (params.action === "advance-frames") {
|
|
528
536
|
if (params.count === undefined) throw new WireFailure("invalid-input", "advance-frames needs count, the number of frames");
|
|
529
|
-
const
|
|
537
|
+
const raster = RASTER[this.#videoStandard];
|
|
538
|
+
if (params.anchor !== undefined && (params.anchor.line >= raster.lines || params.anchor.cycle >= raster.cycles)) {
|
|
539
|
+
throw new WireFailure("invalid-input", `The anchor is outside the frame: a ${this.#videoStandard} frame has lines 0-${raster.lines - 1} and cycles 0-${raster.cycles - 1}.`);
|
|
540
|
+
}
|
|
541
|
+
const outcome = await this.#runFrames(params.count, undefined, params.anchor);
|
|
530
542
|
return { state: "stopped", pc: outcome.pc, advancedFrames: outcome.frames };
|
|
531
543
|
}
|
|
532
544
|
await this.#stop();
|
|
@@ -588,10 +600,11 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
588
600
|
* checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
|
|
589
601
|
* The machine is stopped afterwards.
|
|
590
602
|
*/
|
|
591
|
-
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
|
|
603
|
+
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
|
|
592
604
|
await this.#stop();
|
|
593
605
|
const lines = RASTER[this.#videoStandard].lines;
|
|
594
|
-
|
|
606
|
+
// A frame ends where the raster passes the anchor; without one, where this run starts.
|
|
607
|
+
const start = anchor ?? (await this.#machine.rasterPosition());
|
|
595
608
|
const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
|
|
596
609
|
const ending = rasterWindowExpression(start.line, start.cycle, lines);
|
|
597
610
|
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
@@ -920,7 +933,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
920
933
|
async #cias(which: CiaSelection): Promise<CiaState[]> {
|
|
921
934
|
const ids: Array<1 | 2> = which === "both" ? [1, 2] : [Number(which) as 1 | 2];
|
|
922
935
|
const chips = [];
|
|
923
|
-
for (const id of ids)
|
|
936
|
+
for (const id of ids) {
|
|
937
|
+
const base = id === 1 ? 0xdc00 : 0xdd00;
|
|
938
|
+
chips.push(decodeCia(id, await this.#machine.readIo(base, 0x10), await this.#machine.ciaHidden(base)));
|
|
939
|
+
}
|
|
924
940
|
return chips;
|
|
925
941
|
}
|
|
926
942
|
|
|
@@ -36,10 +36,20 @@ export const RUN_LIMIT_FRAMES = 300;
|
|
|
36
36
|
*/
|
|
37
37
|
export async function runProgram(client: ViceSessionClient, path: string, prg: Uint8Array): Promise<"not-loaded" | "not-started" | undefined> {
|
|
38
38
|
if (!(await autostartLoaded(client, path, prg))) return "not-loaded";
|
|
39
|
+
return (await typeRun(client)) ? undefined : "not-started";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Types RUN at the READY prompt and stops where BASIC starts the first line,
|
|
44
|
+
* before its SYS. Every tool that starts a loaded program this way reaches
|
|
45
|
+
* its entry at the same raster line and timer values (D62). False when BASIC
|
|
46
|
+
* runs no line within RUN_LIMIT_FRAMES.
|
|
47
|
+
*/
|
|
48
|
+
export async function typeRun(client: ViceSessionClient): Promise<boolean> {
|
|
39
49
|
await client.keyboard(textToPetscii("RUN\n"));
|
|
40
50
|
const started = await client.runUntil({
|
|
41
51
|
target: { kind: "memory", address: CURLIN_HIGH, operator: "ne", value: 0xff, space: "c64", view: "ram" },
|
|
42
52
|
timeoutFrames: RUN_LIMIT_FRAMES,
|
|
43
53
|
});
|
|
44
|
-
return started.reached
|
|
54
|
+
return started.reached;
|
|
45
55
|
}
|
package/src/host-client/tools.ts
CHANGED
|
@@ -86,4 +86,4 @@ export async function hostTools(options: ToolCallOptions = {}): Promise<{ host:
|
|
|
86
86
|
/** Skill scripts see failures and wire limits through the host-client, never the private protocol module. */
|
|
87
87
|
export { WireFailure } from "../protocol/messages.ts";
|
|
88
88
|
export { DISK_ACTIONS } from "../protocol/tools.ts";
|
|
89
|
-
export { JOYSTICK_DIRECTIONS, MAX_BASELINES, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES } from "../protocol/vice.ts";
|
|
89
|
+
export { JOYSTICK_DIRECTIONS, MAX_BASELINES, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES, maskCoversFrame } from "../protocol/vice.ts";
|
package/src/mcp/tools/debug.ts
CHANGED
|
@@ -41,7 +41,8 @@ export const c64Breakpoint = defineTool({
|
|
|
41
41
|
description:
|
|
42
42
|
"Manage breakpoints. A breakpoint stops the CPU when it is about to execute the instruction at an address. " +
|
|
43
43
|
"Action add sets one at address in space c64 or drive8. With a condition, it stops only when the condition is true. " +
|
|
44
|
-
"add returns the new id.
|
|
44
|
+
"add returns the new id. Breakpoints and watchpoints take their ids from one counter, so the ids of one kind can have gaps. " +
|
|
45
|
+
"Actions remove, enable and disable take that id. remove returns removed true and the id. Action list gives all breakpoints. Each breakpoint shows its condition, when it has one. " +
|
|
45
46
|
"Breakpoints stay through a reset.",
|
|
46
47
|
inputSchema: z
|
|
47
48
|
.object({
|
|
@@ -84,7 +85,8 @@ export const c64Watchpoint = defineTool({
|
|
|
84
85
|
"Manage watchpoints. A watchpoint stops the CPU when an instruction reads or writes memory in a range. " +
|
|
85
86
|
`Action add sets one at address for size bytes (1 to ${MAX_WATCH_SIZE}, default 1), with access read, write or read-write. ` +
|
|
86
87
|
"With a condition, it stops only when the condition is true. For a write, a memory condition sees the memory after the write. " +
|
|
87
|
-
"add returns the new id.
|
|
88
|
+
"add returns the new id. Breakpoints and watchpoints take their ids from one counter, so the ids of one kind can have gaps. " +
|
|
89
|
+
"Actions remove, enable and disable take that id. remove returns removed true and the id. Action list gives all watchpoints. Each watchpoint shows its condition, when it has one.",
|
|
88
90
|
inputSchema: z
|
|
89
91
|
.object({
|
|
90
92
|
action: z.enum(CHECKPOINT_ACTIONS),
|
|
@@ -205,7 +207,8 @@ export const c64Profile = defineTool({
|
|
|
205
207
|
name: "c64_profile",
|
|
206
208
|
title: "Cycle profile",
|
|
207
209
|
description:
|
|
208
|
-
"Get the routines where the C64 CPU spent the most cycles since the session started. " +
|
|
210
|
+
"Get the routines where the C64 CPU spent the most cycles since the session started or since the last c64_window. " +
|
|
211
|
+
"After c64_window or a snapshot restore, the emulator can give the cycles to a routine that ran before the restore, for example $fd50. Do not trust the routine addresses then. " +
|
|
209
212
|
`limit is the maximum number of routines (1 to ${MAX_PROFILE}, default 20). ` +
|
|
210
213
|
"Each entry gives the routine address, its total cycles with the routines it calls, and its own cycles. " +
|
|
211
214
|
"percent is the part of all cycles that the routine used itself. " +
|
package/src/mcp/tools/machine.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const c64Status = defineTool({
|
|
|
22
22
|
warp: z.boolean(),
|
|
23
23
|
window: z.boolean().describe("true when the emulator window is open (c64_window)"),
|
|
24
24
|
pc: AddressOutput.optional().describe("program counter. Only when the CPU is stopped"),
|
|
25
|
-
cartridge: z.
|
|
25
|
+
cartridge: z.boolean().describe("true while a cartridge from c64_autostart is attached"),
|
|
26
26
|
}),
|
|
27
27
|
readOnly: true,
|
|
28
28
|
async run(_input, session) {
|
|
@@ -32,8 +32,8 @@ export const c64Status = defineTool({
|
|
|
32
32
|
videoStandard: status.videoStandard,
|
|
33
33
|
warp: status.warp,
|
|
34
34
|
window: status.window,
|
|
35
|
+
cartridge: status.cartridge,
|
|
35
36
|
...(status.pc === undefined ? {} : { pc: formatC64Address(status.pc) }),
|
|
36
|
-
...(status.cartridge === undefined ? {} : { cartridge: status.cartridge }),
|
|
37
37
|
};
|
|
38
38
|
},
|
|
39
39
|
});
|
package/src/mcp/tools/media.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const c64Autostart = defineTool({
|
|
|
21
21
|
`Start a program, or a disk, tape or cartridge image, from the project (${list(AUTOSTART_TYPES)}). ` +
|
|
22
22
|
"The C64 resets and loads the file. With run true (the default), it also runs the file. A disk image stays in drive 8. " +
|
|
23
23
|
"A cartridge stays in the C64 through every reset, as on a real C64. An autostart of a file that is not a cartridge detaches it first. " +
|
|
24
|
+
"Both joysticks are centred first: joystick port 1 uses the keyboard lines, and a held direction changes the RUN that the autostart types. " +
|
|
24
25
|
"Without index, a disk image loads like LOAD\"*\",8,1. index selects one directory entry by its number in the c64-disk directory list (0 is the first entry). " +
|
|
25
26
|
"The machine runs when this tool returns, and the load continues.",
|
|
26
27
|
inputSchema: z
|
|
@@ -33,6 +34,7 @@ export const c64Autostart = defineTool({
|
|
|
33
34
|
outputSchema: z.object({
|
|
34
35
|
state: z.enum(RUN_STATES),
|
|
35
36
|
emulatorFlaw: z.string().optional().describe("for a tape image: a known flaw of this VICE version that stops a tape load"),
|
|
37
|
+
joysticksCentred: z.array(z.union([z.literal(1), z.literal(2)])).optional().describe("the joystick ports that held a direction or fire and were centred before the reset"),
|
|
36
38
|
}),
|
|
37
39
|
readOnly: false,
|
|
38
40
|
async run(input, session) {
|
package/src/mcp/tools/video.ts
CHANGED
|
@@ -74,9 +74,12 @@ const CiaOutput = z.object({
|
|
|
74
74
|
ddrB: Byte,
|
|
75
75
|
timerA: z.number().int().min(0).max(0xffff),
|
|
76
76
|
timerB: z.number().int().min(0).max(0xffff),
|
|
77
|
+
latchA: z.number().int().min(0).max(0xffff).describe("the value that timer A loads when it underflows"),
|
|
78
|
+
latchB: z.number().int().min(0).max(0xffff).describe("the value that timer B loads when it underflows"),
|
|
77
79
|
controlA: Byte,
|
|
78
80
|
controlB: Byte,
|
|
79
81
|
interruptStatus: Byte,
|
|
82
|
+
interruptMask: z.number().int().min(0).max(0x1f).describe("the interrupt sources that are on, as bits 0-4 of the interrupt control register"),
|
|
80
83
|
tod: z.object({ hours: z.number().int(), minutes: z.number().int(), seconds: z.number().int(), tenths: z.number().int() }),
|
|
81
84
|
});
|
|
82
85
|
|
|
@@ -84,8 +87,8 @@ export const c64Cia = defineTool({
|
|
|
84
87
|
name: "c64_cia",
|
|
85
88
|
title: "CIA state",
|
|
86
89
|
description:
|
|
87
|
-
"Get the state of CIA 1, CIA 2 or both. The state has the ports, the data direction registers
|
|
88
|
-
`It also has the control registers, the interrupt flags and the time of day (hours 0 to 23). ${NO_CHANGE}`,
|
|
90
|
+
"Get the state of CIA 1, CIA 2 or both. The state has the ports, the data direction registers, the current timer values and the timer latches. " +
|
|
91
|
+
`It also has the control registers, the interrupt flags, the interrupt mask and the time of day (hours 0 to 23). ${NO_CHANGE}`,
|
|
89
92
|
inputSchema: z.object({ cia: z.enum(CIA_SELECTIONS).default("both") }).strict(),
|
|
90
93
|
outputSchema: z.object({ chips: z.array(CiaOutput) }),
|
|
91
94
|
readOnly: true,
|