@henols/c64-re-tools 2.0.0-rc.8 → 2.0.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/README.md +98 -115
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +43 -8
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +1 -1
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/src/trace.ts +55 -23
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
package/src/host/vice/session.ts
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
// session starts headless and can move the machine into a VICE with a window
|
|
4
4
|
// and back; breakpoints, watchpoints and the rest go along.
|
|
5
5
|
|
|
6
|
-
import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { isOneOf, WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
+
import { DISK_IMAGE_TYPES, type DiskFile, type DiskImageType } from "../../protocol/tools.ts";
|
|
8
|
+
import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type TraceHit, type TraceKind, type TraceResult, type PointStopReason, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
|
|
8
9
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
10
|
import { tmpdir } from "node:os";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
12
13
|
|
|
13
|
-
import { formatC64Address } from "../../c64.ts";
|
|
14
|
+
import { formatC64Address, instructionLength } from "../../c64.ts";
|
|
15
|
+
import { encodePng, type IndexedFrame } from "../../graphics.ts";
|
|
16
|
+
import { instructionText, operandAddress } from "../../opcodes.ts";
|
|
14
17
|
import type { ProcessSupervisor } from "../../native/processes.ts";
|
|
15
18
|
import { trace } from "../../trace.ts";
|
|
16
19
|
import type { ViceSessionFactory, ViceSessionHandle } from "../server.ts";
|
|
@@ -26,7 +29,7 @@ import {
|
|
|
26
29
|
} from "./adapter.ts";
|
|
27
30
|
import { decodeProgramCounter, MonitorConnectionError, MonitorError, MonitorTimeoutError, ResponseType, type MonitorResponse } from "./binary-monitor.ts";
|
|
28
31
|
import { decodeCia, decodeSid, decodeSprite, decodeVicii } from "./chips.ts";
|
|
29
|
-
import { compareFrames, differenceImage
|
|
32
|
+
import { compareFrames, differenceImage } from "./screen.ts";
|
|
30
33
|
import { launchVice, startErrorLines, type ViceMode, type ViceProcess } from "./process.ts";
|
|
31
34
|
import { TextMonitorError, TextMonitorTimeoutError } from "./text-monitor.ts";
|
|
32
35
|
|
|
@@ -121,10 +124,14 @@ export interface SessionOptions {
|
|
|
121
124
|
launch?: ViceLauncher;
|
|
122
125
|
/** Makes the host remove a path if it dies; returns the release. */
|
|
123
126
|
ownPath?: (path: string) => () => void;
|
|
124
|
-
/** The known flaws of this VICE version
|
|
127
|
+
/** The known flaws of this VICE version; keyboard and joystick results name them. */
|
|
125
128
|
flaws?: readonly string[];
|
|
129
|
+
/** Lists the entries of a disk image, so an autostart index is checked first. Without it, VICE gets the index unchecked. */
|
|
130
|
+
directory?: DiskDirectory;
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
export type DiskDirectory = (type: DiskImageType, image: Buffer) => Promise<DiskFile[]>;
|
|
134
|
+
|
|
128
135
|
/**
|
|
129
136
|
* Where VICE keeps a staged file: an autostarted disk stays in drive 8, a tape
|
|
130
137
|
* in the datasette and a cartridge in the port, until the same place gets
|
|
@@ -151,6 +158,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
151
158
|
readonly #untilReturnLimitMs: number;
|
|
152
159
|
readonly #stepLimitMs: number;
|
|
153
160
|
readonly #launch: ViceLauncher | undefined;
|
|
161
|
+
readonly #directory: DiskDirectory | undefined;
|
|
154
162
|
/** Session files that outlive one VICE: snapshots, staged media, handovers. */
|
|
155
163
|
readonly #workDir: string;
|
|
156
164
|
readonly #releaseWorkDir: () => void;
|
|
@@ -177,6 +185,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
177
185
|
#driveStop = false;
|
|
178
186
|
/** Counts stops caused by checkpoints or a CPU JAM rather than by a monitor command. */
|
|
179
187
|
#haltCount = 0;
|
|
188
|
+
/** What stopped the machine last, when a user breakpoint or watchpoint or a JAM did; cleared when it runs again. */
|
|
189
|
+
#lastStop: { stopReason: PointStopReason; pointId?: number } | undefined;
|
|
180
190
|
#queue: Promise<unknown> = Promise.resolve();
|
|
181
191
|
#closed = false;
|
|
182
192
|
#lost = false;
|
|
@@ -218,6 +228,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
218
228
|
this.#untilReturnLimitMs = options.untilReturnLimitMs ?? UNTIL_RETURN_LIMIT_MS;
|
|
219
229
|
this.#stepLimitMs = options.stepLimitMs ?? RUNNING_COMMAND_TIMEOUT_MS;
|
|
220
230
|
this.#launch = options.launch;
|
|
231
|
+
this.#directory = options.directory;
|
|
221
232
|
this.#inputFlaw = options.flaws === undefined || options.flaws.length === 0 ? undefined : options.flaws.join(" ");
|
|
222
233
|
this.#workDir = mkdtempSync(join(tmpdir(), "c64-re-tools-session-"));
|
|
223
234
|
this.#releaseWorkDir = options.ownPath?.(this.#workDir) ?? (() => {});
|
|
@@ -374,14 +385,16 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
374
385
|
return this.#programLoad(params as ViceOperations["programLoad"]["params"], onlyAttachment(attachments));
|
|
375
386
|
case "autostart": {
|
|
376
387
|
const { type, index, run } = params as ViceOperations["autostart"]["params"];
|
|
377
|
-
const
|
|
388
|
+
const bytes = onlyAttachment(attachments);
|
|
389
|
+
if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
|
|
390
|
+
const file = this.#stage(bytes, type);
|
|
378
391
|
// VICE keeps a cartridge through every reset and autostart, as a real C64 does.
|
|
379
392
|
if (type !== "crt" && this.#cartridge) {
|
|
380
393
|
await this.#observe(() => this.#machine.detachCartridge());
|
|
381
394
|
this.#cartridge = false;
|
|
382
395
|
this.#replaceStaged("cartridge", undefined);
|
|
383
396
|
}
|
|
384
|
-
// Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types
|
|
397
|
+
// Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types.
|
|
385
398
|
const centred: Array<1 | 2> = [];
|
|
386
399
|
for (const [port, joystick] of this.#joysticks) {
|
|
387
400
|
if (joystick.direction === "center" && !joystick.fire) continue;
|
|
@@ -445,6 +458,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
445
458
|
return this.#snapshotFile(params as ViceOperations["snapshotFile"]["params"], attachments, reply);
|
|
446
459
|
case "cpuHistory":
|
|
447
460
|
return this.#cpuHistory(params as ViceOperations["cpuHistory"]["params"]);
|
|
461
|
+
case "trace":
|
|
462
|
+
return this.#trace(params as ViceOperations["trace"]["params"]);
|
|
448
463
|
case "backtrace": {
|
|
449
464
|
const { depth, space } = params as ViceOperations["backtrace"]["params"];
|
|
450
465
|
return this.#observe(async () => {
|
|
@@ -462,8 +477,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
462
477
|
return { cycles: (now - this.#timingStart).toString() };
|
|
463
478
|
}
|
|
464
479
|
case "profile": {
|
|
465
|
-
const
|
|
466
|
-
|
|
480
|
+
const request = params as ViceOperations["profile"]["params"];
|
|
481
|
+
if (request.action === "clear") {
|
|
482
|
+
await this.#observe(() => this.#machine.startProfiler());
|
|
483
|
+
return { cleared: true };
|
|
484
|
+
}
|
|
485
|
+
return { entries: await this.#observe(() => this.#machine.profile(request.limit)) };
|
|
467
486
|
}
|
|
468
487
|
case "memmap": {
|
|
469
488
|
const request = params as ViceOperations["memmap"]["params"];
|
|
@@ -508,7 +527,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
508
527
|
const warp = await this.#observe(() => this.#machine.warp());
|
|
509
528
|
// Read after #observe: it resumes a running machine, unless a breakpoint or a JAM stopped it meanwhile.
|
|
510
529
|
const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window", cartridge: this.#cartridge };
|
|
511
|
-
if (status.state === "stopped")
|
|
530
|
+
if (status.state === "stopped") {
|
|
531
|
+
status.pc = await this.#pc();
|
|
532
|
+
// VICE 3.9 sends no JAM event: the opcode at the pc tells.
|
|
533
|
+
const stop = this.#lastStop ?? ((await this.#jammedAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
|
|
534
|
+
if (stop !== undefined) Object.assign(status, stop);
|
|
535
|
+
}
|
|
512
536
|
return status;
|
|
513
537
|
}
|
|
514
538
|
|
|
@@ -539,7 +563,13 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
539
563
|
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
564
|
}
|
|
541
565
|
const outcome = await this.#runFrames(params.count, undefined, params.anchor);
|
|
542
|
-
return {
|
|
566
|
+
return {
|
|
567
|
+
state: "stopped",
|
|
568
|
+
pc: outcome.pc,
|
|
569
|
+
advancedFrames: outcome.frames,
|
|
570
|
+
stopReason: outcome.end === "target" ? "frames" : outcome.end,
|
|
571
|
+
...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }),
|
|
572
|
+
};
|
|
543
573
|
}
|
|
544
574
|
await this.#stop();
|
|
545
575
|
if (params.action === "until-return") {
|
|
@@ -600,7 +630,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
600
630
|
* checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
|
|
601
631
|
* The machine is stopped afterwards.
|
|
602
632
|
*/
|
|
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" }> {
|
|
633
|
+
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8"; pointId?: number }> {
|
|
604
634
|
await this.#stop();
|
|
605
635
|
const lines = RASTER[this.#videoStandard].lines;
|
|
606
636
|
// A frame ends where the raster passes the anchor; without one, where this run starts.
|
|
@@ -642,13 +672,127 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
642
672
|
}
|
|
643
673
|
// A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
|
|
644
674
|
const driveStop = this.#driveStop;
|
|
645
|
-
|
|
675
|
+
const pointId = end === "target" ? undefined : this.#userPoint(stop.hits)?.pointId;
|
|
676
|
+
return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64", ...(pointId === undefined ? {} : { pointId }) };
|
|
646
677
|
}
|
|
647
678
|
} finally {
|
|
648
679
|
await this.#machine.deleteCheckpoint(clock);
|
|
649
680
|
}
|
|
650
681
|
}
|
|
651
682
|
|
|
683
|
+
/**
|
|
684
|
+
* A trace: runs the machine for up to `frames` frames, counted as
|
|
685
|
+
* #runFrames counts them, with a stopping checkpoint for each point. At each
|
|
686
|
+
* hit the session records the instruction, its registers and the raster
|
|
687
|
+
* position, and runs on. It ends at the frame limit, at `maxHits` hits, or
|
|
688
|
+
* where a user point or a JAM stops the program. VICE also reports the hits
|
|
689
|
+
* of a checkpoint that does not stop, but without the PC, the registers or
|
|
690
|
+
* the raster position, so each point stops.
|
|
691
|
+
*/
|
|
692
|
+
async #trace(params: ViceOperations["trace"]["params"]): Promise<TraceResult> {
|
|
693
|
+
await this.#stop();
|
|
694
|
+
const lines = RASTER[this.#videoStandard].lines;
|
|
695
|
+
const start = await this.#machine.rasterPosition();
|
|
696
|
+
const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
|
|
697
|
+
const ending = rasterWindowExpression(start.line, start.cycle, lines);
|
|
698
|
+
const operation: Record<TraceKind, number> = { exec: 0x04, read: 0x01, write: 0x02 };
|
|
699
|
+
const points: number[] = [];
|
|
700
|
+
const hits: TraceHit[] = [];
|
|
701
|
+
let frames = 0;
|
|
702
|
+
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
703
|
+
try {
|
|
704
|
+
for (const point of params.points) points.push(await this.#machine.addCheckpoint({ start: point.start, end: point.end, operation: operation[point.kind], space: "c64" }));
|
|
705
|
+
await this.#machine.setCondition(clock, arming);
|
|
706
|
+
let armingPhase = true;
|
|
707
|
+
for (;;) {
|
|
708
|
+
const next = this.#nextStop(HALF_FRAME_LIMIT_MS);
|
|
709
|
+
await this.#resume();
|
|
710
|
+
const stop = await next;
|
|
711
|
+
if (stop === undefined) {
|
|
712
|
+
await this.#stop();
|
|
713
|
+
throw new WireFailure("operation-failed", "The emulator did not reach the next frame in time.");
|
|
714
|
+
}
|
|
715
|
+
const traced = points.flatMap((checkpoint, index) => (stop.hits.includes(checkpoint) ? [index] : []));
|
|
716
|
+
for (const index of traced) {
|
|
717
|
+
hits.push(await this.#traceHit(index, params.points[index]!.kind, frames));
|
|
718
|
+
if (hits.length >= params.maxHits) return { hits, frames, end: "hits", pc: stop.pc };
|
|
719
|
+
}
|
|
720
|
+
if (stop.hits.includes(clock)) {
|
|
721
|
+
if (armingPhase) await this.#machine.setCondition(clock, ending);
|
|
722
|
+
else {
|
|
723
|
+
frames++;
|
|
724
|
+
if (frames >= params.frames) return { hits, frames, end: "frames", pc: stop.pc };
|
|
725
|
+
await this.#machine.setCondition(clock, arming);
|
|
726
|
+
}
|
|
727
|
+
armingPhase = !armingPhase;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (traced.length > 0) continue;
|
|
731
|
+
const point = this.#userPoint(stop.hits);
|
|
732
|
+
if (point !== undefined) return { hits, frames, end: point.stopReason, pc: stop.pc };
|
|
733
|
+
if (await this.#jammedAt(stop.pc)) return { hits, frames, end: "jam", pc: stop.pc };
|
|
734
|
+
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this trace.");
|
|
735
|
+
}
|
|
736
|
+
} finally {
|
|
737
|
+
await this.#machine.deleteCheckpoint(clock);
|
|
738
|
+
for (const checkpoint of points) await this.#machine.deleteCheckpoint(checkpoint);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* One hit of a trace point. An exec point stops before its instruction: the
|
|
744
|
+
* registers and the raster position are the ones of now. A read or write
|
|
745
|
+
* point stops after the instruction that made the access: the last entry of
|
|
746
|
+
* the CPU history is that instruction, with its registers and its clock.
|
|
747
|
+
*/
|
|
748
|
+
async #traceHit(point: number, kind: TraceKind, frame: number): Promise<TraceHit> {
|
|
749
|
+
const position = await this.#machine.rasterPosition();
|
|
750
|
+
const byteAt = async (address: number) => Number.parseInt(await this.#machine.readMemory({ address, size: 1, space: "c64", view: "cpu" }), 16);
|
|
751
|
+
if (kind === "exec") {
|
|
752
|
+
const registers = await this.#machine.readRegisters("c64");
|
|
753
|
+
const bytes = [...Buffer.from(await this.#machine.readMemory({ address: registers.pc, size: 3, space: "c64", view: "cpu" }), "hex")];
|
|
754
|
+
const length = instructionLength(bytes[0]!);
|
|
755
|
+
return {
|
|
756
|
+
point,
|
|
757
|
+
pc: registers.pc,
|
|
758
|
+
bytes: Buffer.from(bytes.slice(0, length)).toString("hex"),
|
|
759
|
+
text: instructionText(bytes, registers.pc),
|
|
760
|
+
a: registers.a,
|
|
761
|
+
x: registers.x,
|
|
762
|
+
y: registers.y,
|
|
763
|
+
sp: registers.sp,
|
|
764
|
+
frame,
|
|
765
|
+
rasterLine: position.line,
|
|
766
|
+
rasterCycle: position.cycle,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const [entry] = await this.#machine.history(1, "c64");
|
|
770
|
+
if (entry === undefined) throw new WireFailure("operation-failed", "The emulator gave no CPU history at a trace hit.");
|
|
771
|
+
const now = await this.#machine.clock();
|
|
772
|
+
const { lines, cycles } = RASTER[this.#videoStandard];
|
|
773
|
+
const span = BigInt(lines * cycles);
|
|
774
|
+
const at = Number((((BigInt(position.line * cycles + position.cycle) - (now - entry.clock)) % span) + span) % span);
|
|
775
|
+
const bytes = [...Buffer.from(entry.bytes, "hex")];
|
|
776
|
+
// A zero-page pointer is read now: the instruction did not change it.
|
|
777
|
+
const pointer = new Map<number, number>();
|
|
778
|
+
for (const address of [bytes[1] ?? 0, ((bytes[1] ?? 0) + 1) & 0xff, ((bytes[1] ?? 0) + entry.x) & 0xff, ((bytes[1] ?? 0) + entry.x + 1) & 0xff]) pointer.set(address, await byteAt(address));
|
|
779
|
+
const address = operandAddress(bytes, entry.x, entry.y, (zp) => pointer.get(zp) ?? 0);
|
|
780
|
+
return {
|
|
781
|
+
point,
|
|
782
|
+
pc: entry.address,
|
|
783
|
+
bytes: entry.bytes,
|
|
784
|
+
text: instructionText(bytes, entry.address),
|
|
785
|
+
a: entry.a,
|
|
786
|
+
x: entry.x,
|
|
787
|
+
y: entry.y,
|
|
788
|
+
sp: entry.sp,
|
|
789
|
+
frame,
|
|
790
|
+
rasterLine: Math.floor(at / cycles),
|
|
791
|
+
rasterCycle: at % cycles,
|
|
792
|
+
...(address === undefined ? {} : { address, value: await byteAt(address) }),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
652
796
|
/**
|
|
653
797
|
* Completes a stop that a drive 8 checkpoint caused: the computer runs to
|
|
654
798
|
* its next instruction boundary, where a computer checkpoint stops it, so
|
|
@@ -659,6 +803,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
659
803
|
async #completeDriveStop(): Promise<number | undefined> {
|
|
660
804
|
if (!this.#driveStop) return undefined;
|
|
661
805
|
this.#driveStop = false;
|
|
806
|
+
// The drive checkpoint stopped the machine; the stop at the boundary keeps that reason.
|
|
807
|
+
const reason = this.#lastStop;
|
|
662
808
|
const boundary = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
663
809
|
try {
|
|
664
810
|
for (let attempt = 0; attempt < DRIVE_STOP_TRIES; attempt++) {
|
|
@@ -669,6 +815,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
669
815
|
// The drive can hit its checkpoint again before the computer gets to the next instruction.
|
|
670
816
|
if (stop.hits.includes(boundary)) {
|
|
671
817
|
this.#driveStop = false;
|
|
818
|
+
this.#lastStop = reason;
|
|
672
819
|
trace().event("vice.drive-stop", { pc: stop.pc, resumes: attempt + 1 });
|
|
673
820
|
return stop.pc;
|
|
674
821
|
}
|
|
@@ -683,14 +830,20 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
683
830
|
/** Names what caused a stop during a frame-clocked run. */
|
|
684
831
|
#stopCause(stop: Stop, clock: number, target: number | undefined): Exclude<RunEnd, "frames" | "jam"> | "clock" | "unknown" {
|
|
685
832
|
if (target !== undefined && stop.hits.includes(target)) return "target";
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
if (isPoint(this.#breakpoints)) return "breakpoint";
|
|
689
|
-
if (isPoint(this.#watchpoints)) return "watchpoint";
|
|
833
|
+
const point = this.#userPoint(stop.hits);
|
|
834
|
+
if (point !== undefined) return point.stopReason;
|
|
690
835
|
if (stop.hits.includes(clock)) return "clock";
|
|
691
836
|
return "unknown";
|
|
692
837
|
}
|
|
693
838
|
|
|
839
|
+
/** The user breakpoint, else the user watchpoint, among the hit VICE checkpoints. */
|
|
840
|
+
#userPoint(hits: number[]): { stopReason: "breakpoint" | "watchpoint"; pointId: number } | undefined {
|
|
841
|
+
for (const [stopReason, registry] of [["breakpoint", this.#breakpoints], ["watchpoint", this.#watchpoints]] as const) {
|
|
842
|
+
for (const [pointId, point] of registry) if (hits.includes(point.checkpoint)) return { stopReason, pointId };
|
|
843
|
+
}
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
|
|
694
847
|
async #jammedAt(pc: number): Promise<boolean> {
|
|
695
848
|
const opcode = await this.#machine.readMemory({ address: pc, size: 1, space: "c64", view: "cpu" });
|
|
696
849
|
return JAM_OPCODES.has(Number.parseInt(opcode, 16));
|
|
@@ -734,9 +887,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
734
887
|
if (expression !== undefined) await this.#machine.setCondition(checkpoint, expression);
|
|
735
888
|
const outcome = await this.#runFrames(params.timeoutFrames, { checkpoint, deferred });
|
|
736
889
|
const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
|
|
737
|
-
// A stop event carries the computer's PC; a stop in the drive reports the drive's
|
|
890
|
+
// A stop event carries the computer's PC; a stop in the drive reports the drive's.
|
|
738
891
|
const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
|
|
739
|
-
return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space };
|
|
892
|
+
return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space, ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }) };
|
|
740
893
|
} finally {
|
|
741
894
|
await this.#machine.deleteCheckpoint(checkpoint);
|
|
742
895
|
}
|
|
@@ -801,7 +954,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
801
954
|
if (params.action === "list") return { breakpoints: [...this.#breakpoints.values()].map(publicView) };
|
|
802
955
|
if (params.action === "remove") return this.#removePoint(this.#breakpoints, "breakpoint", params.id);
|
|
803
956
|
if (params.action !== "add") return publicView(await this.#changePoint(this.#breakpoints, "breakpoint", params));
|
|
804
|
-
// A breakpoint over a range stops at the first instruction executed in it: exec, not a watch
|
|
957
|
+
// A breakpoint over a range stops at the first instruction executed in it: exec, not a watch.
|
|
805
958
|
const added = await this.#addPoint({
|
|
806
959
|
...(params.size === undefined ? pointRange({ address: params.address }) : { start: params.address, end: params.address + params.size - 1, operation: 0x04 }),
|
|
807
960
|
space: params.space,
|
|
@@ -1002,7 +1155,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1002
1155
|
return { restored: true, state: "stopped" };
|
|
1003
1156
|
}
|
|
1004
1157
|
|
|
1005
|
-
/** A snapshot as bytes
|
|
1158
|
+
/** A snapshot as bytes: export a named one into `reply`, or restore from the attached .vsf. */
|
|
1006
1159
|
async #snapshotFile(params: ViceOperations["snapshotFile"]["params"], attachments: Buffer[], reply: Buffer[]): Promise<ViceOperations["snapshotFile"]["result"]> {
|
|
1007
1160
|
if (params.action === "export") {
|
|
1008
1161
|
const file = this.#snapshots.get(params.name);
|
|
@@ -1012,7 +1165,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1012
1165
|
return { exported: true, bytes: bytes.length };
|
|
1013
1166
|
}
|
|
1014
1167
|
const bytes = onlyAttachment(attachments);
|
|
1015
|
-
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot
|
|
1168
|
+
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot.
|
|
1016
1169
|
if (!bytes.subarray(0, VSF_MAGIC.length).equals(VSF_MAGIC)) throw new WireFailure("invalid-input", "The file is not a VICE snapshot file (.vsf). Give a file that c64_snapshot save wrote.");
|
|
1017
1170
|
const file = this.#stage(bytes, "vsf");
|
|
1018
1171
|
try {
|
|
@@ -1185,6 +1338,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1185
1338
|
*/
|
|
1186
1339
|
async #resumingCommand(send: () => Promise<void>): Promise<void> {
|
|
1187
1340
|
this.#state = "running";
|
|
1341
|
+
this.#lastStop = undefined;
|
|
1188
1342
|
try {
|
|
1189
1343
|
await send();
|
|
1190
1344
|
} catch (error) {
|
|
@@ -1360,10 +1514,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1360
1514
|
const stop: Stop = { pc: decodeProgramCounter(event.body), hits: this.#checkpointHits };
|
|
1361
1515
|
if (stop.hits.length > 0 || event.type === ResponseType.jam) this.#haltCount++;
|
|
1362
1516
|
if (stop.hits.some((hit) => this.#adapter?.isDriveCheckpoint(hit))) this.#driveStop = true;
|
|
1517
|
+
this.#lastStop = event.type === ResponseType.jam ? { stopReason: "jam" } : this.#userPoint(stop.hits);
|
|
1363
1518
|
this.#checkpointHits = [];
|
|
1364
1519
|
this.#state = "stopped";
|
|
1365
1520
|
this.#stopWaiters = this.#stopWaiters.filter((waiter) => !waiter(stop));
|
|
1366
1521
|
} else if (event.type === ResponseType.resumed) {
|
|
1522
|
+
this.#lastStop = undefined;
|
|
1367
1523
|
this.#state = "running";
|
|
1368
1524
|
}
|
|
1369
1525
|
}
|
|
@@ -1400,6 +1556,17 @@ async function setPoint(
|
|
|
1400
1556
|
return checkpoint;
|
|
1401
1557
|
}
|
|
1402
1558
|
|
|
1559
|
+
/**
|
|
1560
|
+
* Refuses an autostart index that selects no program: VICE numbers the
|
|
1561
|
+
* entries from 1, and the caller from 0, as the c64-disk directory list does.
|
|
1562
|
+
* A DEL entry holds no program, and an index past the last entry selects nothing.
|
|
1563
|
+
*/
|
|
1564
|
+
function checkDirectoryEntry(entries: DiskFile[], index: number): void {
|
|
1565
|
+
const entry = entries[index - 1];
|
|
1566
|
+
if (entry === undefined) throw new WireFailure("invalid-input", `The disk has ${entries.length} directory entries, so index ${index - 1} selects no entry. The c64-disk directory list gives the index of each entry.`);
|
|
1567
|
+
if (entry.type === "del") throw new WireFailure("invalid-input", `Directory entry ${index - 1} (${entry.name}) is a DEL entry and holds no program. Give the index of a PRG entry.`);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1403
1570
|
/** The production factory: launch a headless VICE per session through the host's supervisor. */
|
|
1404
1571
|
export function viceSessionFactory(options: {
|
|
1405
1572
|
supervisor: ProcessSupervisor;
|
|
@@ -1407,6 +1574,7 @@ export function viceSessionFactory(options: {
|
|
|
1407
1574
|
log?: (line: string) => void;
|
|
1408
1575
|
/** The known flaws of the VICE version that the host checked at start. */
|
|
1409
1576
|
flaws?: readonly string[];
|
|
1577
|
+
directory?: DiskDirectory;
|
|
1410
1578
|
}): ViceSessionFactory {
|
|
1411
1579
|
return async ({ videoStandard, log = options.log ?? (() => {}) }) => {
|
|
1412
1580
|
const launch: ViceLauncher = (mode, launchLog) =>
|
|
@@ -1421,6 +1589,11 @@ export function viceSessionFactory(options: {
|
|
|
1421
1589
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
1422
1590
|
});
|
|
1423
1591
|
const vice = await launch("headless", () => {});
|
|
1424
|
-
return ViceSession.start(vice, videoStandard, log, {
|
|
1592
|
+
return ViceSession.start(vice, videoStandard, log, {
|
|
1593
|
+
launch,
|
|
1594
|
+
ownPath: (path) => options.supervisor.ownPath(path),
|
|
1595
|
+
...(options.flaws === undefined ? {} : { flaws: options.flaws }),
|
|
1596
|
+
...(options.directory === undefined ? {} : { directory: options.directory }),
|
|
1597
|
+
});
|
|
1425
1598
|
};
|
|
1426
1599
|
}
|
|
@@ -42,7 +42,7 @@ export async function runProgram(client: ViceSessionClient, path: string, prg: U
|
|
|
42
42
|
/**
|
|
43
43
|
* Types RUN at the READY prompt and stops where BASIC starts the first line,
|
|
44
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
|
|
45
|
+
* its entry at the same raster line and timer values. False when BASIC
|
|
46
46
|
* runs no line within RUN_LIMIT_FRAMES.
|
|
47
47
|
*/
|
|
48
48
|
export async function typeRun(client: ViceSessionClient): Promise<boolean> {
|
|
@@ -37,7 +37,7 @@ export function hostEndpoints(env: NodeJS.ProcessEnv = process.env): Endpoint[]
|
|
|
37
37
|
return ["127.0.0.1", "host.docker.internal", "host.containers.internal"].map((host) => ({ host, port: DEFAULT_HOST_PORT }));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** A host that does not run or cannot be reached is machine-unavailable for every role
|
|
40
|
+
/** A host that does not run or cannot be reached is machine-unavailable for every role. */
|
|
41
41
|
function unreachable(): WireFailure {
|
|
42
42
|
return new WireFailure(
|
|
43
43
|
"machine-unavailable",
|
|
@@ -1,40 +1,13 @@
|
|
|
1
|
-
// Reads the emulator's memory access map as coverage bytes
|
|
1
|
+
// Reads the emulator's memory access map as coverage bytes: one byte
|
|
2
2
|
// per address, RAM, ROM and I/O apart. The MCP tool and the skill scripts share it.
|
|
3
3
|
|
|
4
4
|
import { COVERAGE, type CodeRange } from "../c64.ts";
|
|
5
5
|
import { MAX_MEMMAP_RANGES, type MemmapRange } from "../protocol/vice.ts";
|
|
6
|
-
import { readProjectFile } from "./transfer.ts";
|
|
7
6
|
import type { ViceSessionClient } from "./vice-session.ts";
|
|
8
7
|
|
|
9
|
-
/** A full coverage file: offset n is address n
|
|
8
|
+
/** A full coverage file: offset n is address n. */
|
|
10
9
|
export const FULL_COVERAGE = 0x10000;
|
|
11
10
|
|
|
12
|
-
/** Reads full coverage files from the project, one map per file. `fail` refuses a file of another size. */
|
|
13
|
-
export function readCoverageMaps(paths: string[], fail: (message: string) => never): Uint8Array[] {
|
|
14
|
-
return paths.map((path) => {
|
|
15
|
-
const { bytes } = readProjectFile(path);
|
|
16
|
-
if (bytes.length !== FULL_COVERAGE) {
|
|
17
|
-
fail(`--coverage ${path} has ${bytes.length} bytes; a full coverage file has 65536, one for each address. Write it with c64_memmap action read and out, without start and end.`);
|
|
18
|
-
}
|
|
19
|
-
return bytes;
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Joins coverage maps: an address has every flag that any run gave it. */
|
|
24
|
-
export function joinCoverage(maps: readonly Uint8Array[]): Uint8Array {
|
|
25
|
-
const joined = new Uint8Array(FULL_COVERAGE);
|
|
26
|
-
for (const map of maps) for (let address = 0; address < FULL_COVERAGE; address++) joined[address]! |= map[address]!;
|
|
27
|
-
return joined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Reads full coverage files from the project and joins them: an address has
|
|
32
|
-
* every flag that any run gave it. `fail` refuses a file of another size.
|
|
33
|
-
*/
|
|
34
|
-
export function readCoverageFiles(paths: string[], fail: (message: string) => never): Uint8Array | undefined {
|
|
35
|
-
return paths.length === 0 ? undefined : joinCoverage(readCoverageMaps(paths, fail));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
11
|
/** The coverage bytes of start..end, read from the session in parts. */
|
|
39
12
|
export async function readCoverage(session: Pick<ViceSessionClient, "memmap">, start = 0x0000, end = 0xffff): Promise<Uint8Array> {
|
|
40
13
|
const map = new Uint8Array(end - start + 1);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// An image.json of the c64-unpacker skill (a 64 KiB memory and its segments)
|
|
2
|
+
// or a PRG, read from the project to put it into the knowledge.
|
|
3
|
+
|
|
4
|
+
import { posix } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { formatC64Address, prgRange } from "../c64.ts";
|
|
7
|
+
import { readProjectFile } from "./transfer.ts";
|
|
8
|
+
|
|
9
|
+
/** A file that is no image: the scripts report it as a bad call. */
|
|
10
|
+
export class ImageError extends Error {
|
|
11
|
+
override name = "ImageError";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Image {
|
|
15
|
+
segments: Array<{ start: number; end: number }>;
|
|
16
|
+
/** The byte at an address inside a segment. */
|
|
17
|
+
byteAt: (address: number) => number;
|
|
18
|
+
entry?: number;
|
|
19
|
+
/** entry_state.json next to image.json. */
|
|
20
|
+
statePath?: string;
|
|
21
|
+
/** The stages of the capture in image.json, as the unpacker wrote them. */
|
|
22
|
+
stages?: Array<Record<string, unknown>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Reads an image.json or a PRG, relative to the project folder. */
|
|
26
|
+
export function readImage(path: string): Image {
|
|
27
|
+
const { bytes } = readProjectFile(path);
|
|
28
|
+
if (!path.endsWith(".json")) {
|
|
29
|
+
let range: ReturnType<typeof prgRange>;
|
|
30
|
+
try {
|
|
31
|
+
range = prgRange(bytes);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new ImageError(`${path}: ${(error as Error).message}`);
|
|
34
|
+
}
|
|
35
|
+
return { segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]! };
|
|
36
|
+
}
|
|
37
|
+
const fail = (what: string): never => {
|
|
38
|
+
throw new ImageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
|
|
39
|
+
};
|
|
40
|
+
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
let parsed: unknown;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return fail((error as Error).message);
|
|
46
|
+
}
|
|
47
|
+
const manifest = isObject(parsed) ? parsed : fail("it is not a JSON object");
|
|
48
|
+
const address = (value: unknown, what: string) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail(`${what} is not a $ address`));
|
|
49
|
+
if (typeof manifest.image !== "string") fail("it has no image");
|
|
50
|
+
if (!Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it has no segments");
|
|
51
|
+
const segments = (manifest.segments as unknown[])
|
|
52
|
+
.map((segment, index) => (isObject(segment) ? segment : fail(`segment ${index + 1} is not an object`)))
|
|
53
|
+
.map((segment, index) => ({ start: address(segment.start, `segment ${index + 1} start`), end: address(segment.end, `segment ${index + 1} end`) }))
|
|
54
|
+
.sort((a, b) => a.start - b.start);
|
|
55
|
+
segments.forEach((segment, index) => {
|
|
56
|
+
if (segment.end < segment.start) fail(`the segment at ${formatC64Address(segment.start)} ends before it starts`);
|
|
57
|
+
if (index > 0 && segment.start <= segments[index - 1]!.end) fail(`the segments at ${formatC64Address(segments[index - 1]!.start)} and ${formatC64Address(segment.start)} overlap`);
|
|
58
|
+
});
|
|
59
|
+
const directory = posix.dirname(path);
|
|
60
|
+
const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
|
|
61
|
+
if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
|
|
62
|
+
const image: Image = { segments, byteAt: (at) => memory[at]!, statePath: posix.join(directory, "entry_state.json") };
|
|
63
|
+
if (manifest.entry !== undefined) image.entry = address(manifest.entry, "entry");
|
|
64
|
+
if (Array.isArray(manifest.stages)) image.stages = manifest.stages.map((stage, index) => (isObject(stage) ? stage : fail(`stage ${index + 1} is not an object`)));
|
|
65
|
+
return image;
|
|
66
|
+
}
|
package/src/host-client/steps.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The action steps of the step language
|
|
1
|
+
// The action steps of the step language: write, registers, joystick,
|
|
2
2
|
// type, frames and runUntil. c64-testing scenarios and the c64-unpacker
|
|
3
3
|
// steps file share them; each caller adds its own steps around them.
|
|
4
4
|
|
|
@@ -90,12 +90,12 @@ export function checkActionStep(kind: string, step: Record<string, unknown>, at:
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
case "frames":
|
|
93
|
-
if (!isInteger(step.frames, 1,
|
|
93
|
+
if (!isInteger(step.frames, 1, MAX_TIMEOUT_FRAMES)) fail(`${at} needs a frame count from 1 to ${MAX_TIMEOUT_FRAMES}`);
|
|
94
94
|
return;
|
|
95
95
|
case "runUntil": {
|
|
96
96
|
const target = step.runUntil;
|
|
97
97
|
const keys = isObject(target) ? Object.keys(target).sort().join(",") : "";
|
|
98
|
-
// at, raster, at with raster (the address only on that line
|
|
98
|
+
// at, raster, at with raster (the address only on that line), or memory.
|
|
99
99
|
if (!["at", "raster", "at,raster", "memory"].includes(keys)) return fail(`${at}: runUntil needs at, raster, at and raster, or memory`);
|
|
100
100
|
const until = target as Record<string, unknown>;
|
|
101
101
|
if ("raster" in until && !isInteger(until.raster, 0, MAX_RASTER_LINE)) fail(`${at}: runUntil.raster must be a raster line from 0 to ${MAX_RASTER_LINE}`);
|
|
@@ -154,7 +154,12 @@ export async function runActionStep(client: ViceSessionClient, step: ActionStep,
|
|
|
154
154
|
await client.keyboard(textToPetscii(step.type));
|
|
155
155
|
} else if ("frames" in step) {
|
|
156
156
|
await stop(client);
|
|
157
|
-
|
|
157
|
+
// One advance-frames runs at most MAX_EXECUTION_COUNT frames. Each part ends at the raster position
|
|
158
|
+
// where it started, so the parts give the same frames as one run; a breakpoint ends them all.
|
|
159
|
+
for (let left = step.frames; left > 0; left -= MAX_EXECUTION_COUNT) {
|
|
160
|
+
const part = await client.execution({ action: "advance-frames", count: Math.min(left, MAX_EXECUTION_COUNT), space: "c64" });
|
|
161
|
+
if (part.stopReason !== "frames") break;
|
|
162
|
+
}
|
|
158
163
|
} else {
|
|
159
164
|
const until = step.runUntil;
|
|
160
165
|
const target: RunTarget =
|
package/src/host-client/tools.ts
CHANGED
|
@@ -87,3 +87,4 @@ export async function hostTools(options: ToolCallOptions = {}): Promise<{ host:
|
|
|
87
87
|
export { WireFailure } from "../protocol/messages.ts";
|
|
88
88
|
export { DISK_ACTIONS } from "../protocol/tools.ts";
|
|
89
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";
|
|
90
|
+
export { MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, type TraceHit, type TraceKind } from "../protocol/vice.ts";
|