@henols/c64-re-tools 2.0.0-rc.7 → 2.0.0-rc.8
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/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/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
|
@@ -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/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,
|
package/src/protocol/vice.ts
CHANGED
|
@@ -168,9 +168,14 @@ export interface CiaState {
|
|
|
168
168
|
ddrB: number;
|
|
169
169
|
timerA: number;
|
|
170
170
|
timerB: number;
|
|
171
|
+
/** The values that a timer loads at an underflow: write-only on the chip, from VICE's dump (D63). */
|
|
172
|
+
latchA: number;
|
|
173
|
+
latchB: number;
|
|
171
174
|
controlA: number;
|
|
172
175
|
controlB: number;
|
|
173
176
|
interruptStatus: number;
|
|
177
|
+
/** The interrupt sources that are on: bits 0-4 of a write to $xd0d with bit 7 set. */
|
|
178
|
+
interruptMask: number;
|
|
174
179
|
tod: { hours: number; minutes: number; seconds: number; tenths: number };
|
|
175
180
|
}
|
|
176
181
|
|
|
@@ -256,6 +261,24 @@ export interface Rectangle {
|
|
|
256
261
|
height: number;
|
|
257
262
|
}
|
|
258
263
|
|
|
264
|
+
/** The visible frame with normal borders that a screen capture holds, in pixels. */
|
|
265
|
+
export const FRAME_SIZE: Record<VideoStandard, { width: number; height: number }> = {
|
|
266
|
+
pal: { width: 384, height: 272 },
|
|
267
|
+
ntsc: { width: 384, height: 247 },
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/** True when the mask rectangles leave no pixel of the frame to compare. */
|
|
271
|
+
export function maskCoversFrame(mask: readonly Rectangle[], standard: VideoStandard): boolean {
|
|
272
|
+
const { width, height } = FRAME_SIZE[standard];
|
|
273
|
+
const masked = new Uint8Array(width * height);
|
|
274
|
+
for (const rectangle of mask) {
|
|
275
|
+
for (let y = rectangle.y; y < Math.min(height, rectangle.y + rectangle.height); y++) {
|
|
276
|
+
if (rectangle.x < width) masked.fill(1, y * width + rectangle.x, y * width + Math.min(width, rectangle.x + rectangle.width));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return !masked.includes(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
259
282
|
export interface ScreenComparison {
|
|
260
283
|
match: boolean;
|
|
261
284
|
mismatchingPixels: number;
|
|
@@ -312,8 +335,8 @@ export interface MachineStatus {
|
|
|
312
335
|
window: boolean;
|
|
313
336
|
/** Present only while the C64 CPU is stopped. */
|
|
314
337
|
pc?: number;
|
|
315
|
-
/**
|
|
316
|
-
cartridge
|
|
338
|
+
/** True while a cartridge from an autostart is attached (D64). */
|
|
339
|
+
cartridge: boolean;
|
|
317
340
|
}
|
|
318
341
|
|
|
319
342
|
export interface MemoryReadParams {
|
|
@@ -370,6 +393,12 @@ export interface ExecutionParams {
|
|
|
370
393
|
/** step and next: defaults to 1; advance-frames: required. */
|
|
371
394
|
count?: number;
|
|
372
395
|
space: Space;
|
|
396
|
+
/**
|
|
397
|
+
* advance-frames only: the raster position that ends a frame, instead of the
|
|
398
|
+
* position where this call starts. Runs that stop between frames then keep
|
|
399
|
+
* the frames of one timeline (D62).
|
|
400
|
+
*/
|
|
401
|
+
anchor?: { line: number; cycle: number };
|
|
373
402
|
}
|
|
374
403
|
|
|
375
404
|
export interface ExecutionResult {
|
|
@@ -421,8 +450,11 @@ export interface ViceOperations {
|
|
|
421
450
|
/** Attachment: the PRG bytes, load address first. */
|
|
422
451
|
programLoad: { params: { address?: number }; result: { state: RunState; loadAddress: number; size: number } };
|
|
423
452
|
/** Attachment: the program or image bytes; `type` is the file's extension. `index` is VICE's file number: 0 loads "*", 1 is the first directory entry. */
|
|
424
|
-
/**
|
|
425
|
-
|
|
453
|
+
/**
|
|
454
|
+
* `emulatorFlaw`: for a tape image, a known flaw of the host's VICE version that a tape load meets (D56).
|
|
455
|
+
* `joysticksCentred`: the control ports that held a direction or fire and were centred first (R3-05).
|
|
456
|
+
*/
|
|
457
|
+
autostart: { params: { type: string; index: number; run: boolean }; result: { state: RunState; emulatorFlaw?: string; joysticksCentred?: Array<1 | 2> } };
|
|
426
458
|
/** Attachment: the disk image bytes; `type` is the file's extension. */
|
|
427
459
|
diskAttach: { params: { type: string }; result: { attached: boolean } };
|
|
428
460
|
/** pc is the reset vector when run is false: the CPU stops there. */
|
|
@@ -748,7 +780,7 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
748
780
|
return { space: params.space, values: result } as ViceOperations[O]["params"];
|
|
749
781
|
}
|
|
750
782
|
case "execution": {
|
|
751
|
-
onlyFields(params, ["action", "space", "count"]);
|
|
783
|
+
onlyFields(params, ["action", "space", "count", "anchor"]);
|
|
752
784
|
if (!isOneOf(EXECUTION_ACTIONS, params.action)) invalid(`action must be one of ${EXECUTION_ACTIONS.join(", ")}`);
|
|
753
785
|
if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
|
|
754
786
|
const counted = params.action === "step" || params.action === "next" || params.action === "advance-frames";
|
|
@@ -760,6 +792,13 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
760
792
|
}
|
|
761
793
|
const result: ExecutionParams = { action: params.action, space: params.space };
|
|
762
794
|
if (counted) result.count = (params.count as number | undefined) ?? 1;
|
|
795
|
+
if (params.anchor !== undefined) {
|
|
796
|
+
if (params.action !== "advance-frames") invalid(`anchor is not used with action ${params.action}`);
|
|
797
|
+
const anchor = params.anchor;
|
|
798
|
+
if (!isObject(anchor) || !isInteger(anchor.line, 0, 511) || !isInteger(anchor.cycle, 0, 127)) invalid("anchor needs line from 0 to 511 and cycle from 0 to 127");
|
|
799
|
+
onlyFields(anchor as Record<string, unknown>, ["line", "cycle"]);
|
|
800
|
+
result.anchor = { line: anchor.line as number, cycle: anchor.cycle as number };
|
|
801
|
+
}
|
|
763
802
|
return result as ViceOperations[O]["params"];
|
|
764
803
|
}
|
|
765
804
|
case "runUntil": {
|
|
@@ -972,7 +1011,7 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
972
1011
|
typeof value.warp === "boolean" &&
|
|
973
1012
|
typeof value.window === "boolean" &&
|
|
974
1013
|
(value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
|
|
975
|
-
|
|
1014
|
+
typeof value.cartridge === "boolean";
|
|
976
1015
|
if (!ok) throw new ProtocolError("status result is malformed");
|
|
977
1016
|
break;
|
|
978
1017
|
}
|
|
@@ -1059,6 +1098,9 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1059
1098
|
if (!isOneOf(RUN_STATES, value.state)) throw new ProtocolError(`${op} result is malformed`);
|
|
1060
1099
|
if (op === "reset" && value.pc !== undefined && !isInteger(value.pc, 0, 0xffff)) throw new ProtocolError("reset result is malformed");
|
|
1061
1100
|
if (op === "autostart" && !(value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string")) throw new ProtocolError("autostart result is malformed");
|
|
1101
|
+
if (op === "autostart" && !(value.joysticksCentred === undefined || (Array.isArray(value.joysticksCentred) && value.joysticksCentred.every((port) => port === 1 || port === 2)))) {
|
|
1102
|
+
throw new ProtocolError("autostart result is malformed");
|
|
1103
|
+
}
|
|
1062
1104
|
break;
|
|
1063
1105
|
}
|
|
1064
1106
|
case "programLoad": {
|
|
@@ -1235,8 +1277,8 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1235
1277
|
isObject(chip) &&
|
|
1236
1278
|
(chip.id === 1 || chip.id === 2) &&
|
|
1237
1279
|
(["portA", "portB", "ddrA", "ddrB", "controlA", "controlB", "interruptStatus"] as const).every((name) => isInteger(chip[name], 0, 0xff)) &&
|
|
1238
|
-
isInteger(chip.
|
|
1239
|
-
isInteger(chip
|
|
1280
|
+
isInteger(chip.interruptMask, 0, 0x1f) &&
|
|
1281
|
+
(["timerA", "timerB", "latchA", "latchB"] as const).every((name) => isInteger(chip[name], 0, 0xffff)) &&
|
|
1240
1282
|
isObject(chip.tod) &&
|
|
1241
1283
|
isInteger(chip.tod.hours, 0, 23) &&
|
|
1242
1284
|
isInteger(chip.tod.minutes, 0, 99) &&
|