@henols/c64-re-tools 2.1.0 → 2.1.2
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-assembler/SKILL.md +7 -3
- package/skills/c64-assembler/scripts/assembler.ts +29 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +3 -2
- package/skills/c64-disk/scripts/disk.ts +26 -12
- package/skills/c64-disk/scripts/lib/containers.ts +10 -11
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +5 -3
- package/skills/c64-docs/scripts/lib/reference.ts +2 -2
- package/skills/c64-emulator/SKILL.md +9 -2
- package/skills/c64-knowledge/SKILL.md +9 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
- package/skills/c64-memory-map/SKILL.md +2 -1
- package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
- package/skills/c64-provenance/SKILL.md +6 -4
- package/skills/c64-provenance/scripts/lib/library.ts +18 -6
- package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
- package/skills/c64-provenance/scripts/provenance.ts +14 -10
- package/skills/c64-source/SKILL.md +3 -0
- package/skills/c64-source/scripts/lib/source.ts +9 -31
- package/skills/c64-source/scripts/source.ts +14 -10
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
- package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
- package/skills/c64-testing/SKILL.md +14 -3
- package/skills/c64-testing/references/recording-format.md +2 -0
- package/skills/c64-testing/references/scenario-format.md +6 -0
- package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
- package/skills/c64-testing/scripts/lib/diff.ts +3 -3
- package/skills/c64-testing/scripts/lib/recording.ts +50 -15
- package/skills/c64-testing/scripts/lib/replay.ts +12 -1
- package/skills/c64-testing/scripts/lib/runs.ts +84 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
- package/skills/c64-testing/scripts/testing.ts +20 -7
- package/skills/c64-unpacker/SKILL.md +17 -10
- package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
- package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +148 -13
- package/src/host-client/image.ts +40 -8
- package/src/host-client/steps.ts +145 -9
- package/src/host-client/tools.ts +1 -1
- package/src/host-client/transfer.ts +33 -18
- package/src/knowledge/expression.ts +1 -1
- package/src/knowledge/history.ts +74 -4
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/names.ts +2 -1
- package/src/knowledge/program.ts +32 -18
- package/src/knowledge/read.ts +46 -0
- package/src/knowledge/schema.ts +4 -2
- package/src/knowledge/text.ts +2 -0
- package/src/knowledge/write.ts +58 -16
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +5 -0
- package/src/mcp/server.ts +20 -4
- package/src/mcp/tools/debug.ts +36 -9
- package/src/mcp/tools/execution.ts +18 -5
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +14 -2
- package/src/mcp/tools/memory.ts +11 -6
- package/src/mcp/tools/video.ts +5 -3
- package/src/media.ts +42 -0
- package/src/opcodes.ts +51 -18
- package/src/project.ts +111 -3
- package/src/protocol/vice.ts +8 -2
- package/src/script.ts +55 -3
package/src/host/vice/session.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
import { decodeProgramCounter, MonitorConnectionError, MonitorError, MonitorTimeoutError, ResponseType, type MonitorResponse } from "./binary-monitor.ts";
|
|
31
31
|
import { decodeCia, decodeSid, decodeSprite, decodeVicii } from "./chips.ts";
|
|
32
32
|
import { compareFrames, differenceImage } from "./screen.ts";
|
|
33
|
+
import { isRefusal } from "../../errors.ts";
|
|
34
|
+
import { checkMedia } from "../../media.ts";
|
|
33
35
|
import { launchVice, startErrorLines, type ViceMode, type ViceProcess } from "./process.ts";
|
|
34
36
|
import { TextMonitorError, TextMonitorTimeoutError } from "./text-monitor.ts";
|
|
35
37
|
|
|
@@ -187,6 +189,23 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
187
189
|
#haltCount = 0;
|
|
188
190
|
/** What stopped the machine last, when a user breakpoint or watchpoint or a JAM did; cleared when it runs again. */
|
|
189
191
|
#lastStop: { stopReason: PointStopReason; pointId?: number } | undefined;
|
|
192
|
+
/**
|
|
193
|
+
* The pc of the JAM that holds the CPU. A jammed CPU runs again only after a
|
|
194
|
+
* reset, a snapshot restore or an autostart: VICE 3.10 keeps it jammed after a
|
|
195
|
+
* pc write, and reports the JAM only once.
|
|
196
|
+
*/
|
|
197
|
+
#jammed: number | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* An autostart can still be loading: VICE goes on with it after a snapshot
|
|
200
|
+
* restore and writes the program over the restored memory. Only a reset stops it.
|
|
201
|
+
*/
|
|
202
|
+
#autostarting = false;
|
|
203
|
+
/** The C64 clock just after the last snapshot restore: VICE keeps the history of before it. */
|
|
204
|
+
#historySince: bigint | undefined;
|
|
205
|
+
/** VICE records no CPU history after a JAM until the next reset. */
|
|
206
|
+
#historyStopped = false;
|
|
207
|
+
/** The CPU ran again after a JAM without a reset: the CPU history that VICE gives is old. */
|
|
208
|
+
#historyOld = false;
|
|
190
209
|
#queue: Promise<unknown> = Promise.resolve();
|
|
191
210
|
#closed = false;
|
|
192
211
|
#lost = false;
|
|
@@ -386,6 +405,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
386
405
|
case "autostart": {
|
|
387
406
|
const { type, index, run } = params as ViceOperations["autostart"]["params"];
|
|
388
407
|
const bytes = onlyAttachment(attachments);
|
|
408
|
+
checkMedia(type, bytes);
|
|
389
409
|
if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
|
|
390
410
|
const file = this.#stage(bytes, type);
|
|
391
411
|
// VICE keeps a cartridge through every reset and autostart, as a real C64 does.
|
|
@@ -404,6 +424,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
404
424
|
centred.push(port);
|
|
405
425
|
}
|
|
406
426
|
await this.#runningCommand(() => this.#machine.autostart(file, index, run));
|
|
427
|
+
this.#clearJam();
|
|
428
|
+
this.#autostarting = true;
|
|
407
429
|
if (type === "crt") this.#cartridge = true;
|
|
408
430
|
this.#replaceStaged(stagedKind(type), file);
|
|
409
431
|
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
@@ -412,7 +434,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
412
434
|
}
|
|
413
435
|
case "diskAttach": {
|
|
414
436
|
const { type } = params as ViceOperations["diskAttach"]["params"];
|
|
415
|
-
const
|
|
437
|
+
const bytes = onlyAttachment(attachments);
|
|
438
|
+
checkMedia(type, bytes);
|
|
439
|
+
const file = this.#stage(bytes, type);
|
|
416
440
|
await this.#observe(() => this.#machine.attachDisk(file));
|
|
417
441
|
this.#replaceStaged("disk", file);
|
|
418
442
|
return { attached: true };
|
|
@@ -431,7 +455,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
431
455
|
case "screenCapture": {
|
|
432
456
|
const { baseline } = params as ViceOperations["screenCapture"]["params"];
|
|
433
457
|
if (baseline !== undefined && !this.#baselines.has(baseline) && this.#baselines.size >= MAX_BASELINES) {
|
|
434
|
-
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_BASELINES} baselines.
|
|
458
|
+
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_BASELINES} baselines. Remove one first with c64_screen action remove.`);
|
|
435
459
|
}
|
|
436
460
|
const shot = await this.#observe(() => this.#machine.captureFrame(this.#videoStandard));
|
|
437
461
|
if (baseline !== undefined) this.#baselines.set(baseline, shot);
|
|
@@ -530,15 +554,25 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
530
554
|
if (status.state === "stopped") {
|
|
531
555
|
status.pc = await this.#pc();
|
|
532
556
|
// VICE 3.9 sends no JAM event: the opcode at the pc tells.
|
|
533
|
-
const stop = this.#lastStop ?? ((await this.#
|
|
557
|
+
const stop = this.#lastStop ?? ((await this.#jamAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
|
|
534
558
|
if (stop !== undefined) Object.assign(status, stop);
|
|
559
|
+
// A point in the drive stopped it: the pc is the drive's, as advance-frames and run-until give it.
|
|
560
|
+
const point = status.pointId === undefined ? undefined : (status.stopReason === "watchpoint" ? this.#watchpoints : this.#breakpoints).get(status.pointId);
|
|
561
|
+
status.space = point?.space === "drive8" ? "drive8" : "c64";
|
|
562
|
+
if (status.space === "drive8") status.pc = (await this.#machine.readRegisters("drive8")).pc;
|
|
535
563
|
}
|
|
536
564
|
return status;
|
|
537
565
|
}
|
|
538
566
|
|
|
539
567
|
async #execution(params: ExecutionParams): Promise<ExecutionResult> {
|
|
568
|
+
if (params.space === "drive8" && (params.action === "pause" || params.action === "resume")) {
|
|
569
|
+
throw new WireFailure("unsupported-in-space", `${params.action} stops or runs the whole machine, the computer and the disk drive together: use space c64.`);
|
|
570
|
+
}
|
|
540
571
|
if (params.action === "resume") {
|
|
541
|
-
if (this.#state === "stopped")
|
|
572
|
+
if (this.#state === "stopped") {
|
|
573
|
+
await this.#refuseJammed("resume");
|
|
574
|
+
await this.#resume();
|
|
575
|
+
}
|
|
542
576
|
return { state: "running" };
|
|
543
577
|
}
|
|
544
578
|
if (params.action === "pause") {
|
|
@@ -546,6 +580,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
546
580
|
// A drive checkpoint can stop the machine just before the pause does.
|
|
547
581
|
return { state: "stopped", pc: (await this.#completeDriveStop()) ?? (await this.#pc()) };
|
|
548
582
|
}
|
|
583
|
+
if (params.space === "drive8" && params.action === "advance-frames") {
|
|
584
|
+
throw new WireFailure("unsupported-in-space", "advance-frames counts the video frames of the computer: use space c64. A breakpoint or watchpoint in space drive8 also stops it.");
|
|
585
|
+
}
|
|
549
586
|
if (params.space === "drive8") {
|
|
550
587
|
// Stock VICE runs the 1541 CPU in batches that catch up with the computer's clock;
|
|
551
588
|
// neither monitor can stop it after exactly one of its instructions.
|
|
@@ -565,13 +602,16 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
565
602
|
const outcome = await this.#runFrames(params.count, undefined, params.anchor);
|
|
566
603
|
return {
|
|
567
604
|
state: "stopped",
|
|
568
|
-
|
|
605
|
+
// A stop event carries the computer's PC; a stop in the drive reports the drive's.
|
|
606
|
+
pc: outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc,
|
|
607
|
+
space: outcome.space,
|
|
569
608
|
advancedFrames: outcome.frames,
|
|
570
609
|
stopReason: outcome.end === "target" ? "frames" : outcome.end,
|
|
571
610
|
...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }),
|
|
572
611
|
};
|
|
573
612
|
}
|
|
574
613
|
await this.#stop();
|
|
614
|
+
await this.#refuseJammed(params.action);
|
|
575
615
|
if (params.action === "until-return") {
|
|
576
616
|
const stopped = this.#nextStop(this.#untilReturnLimitMs);
|
|
577
617
|
await this.#resumingCommand(() => this.#machine.untilReturn());
|
|
@@ -599,6 +639,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
599
639
|
|
|
600
640
|
async #reset(params: { mode: "soft" | "hard"; run: boolean }): Promise<{ state: RunState; pc?: number }> {
|
|
601
641
|
await this.#stop();
|
|
642
|
+
this.#clearJam();
|
|
602
643
|
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
603
644
|
this.#timingStart = 0n;
|
|
604
645
|
if (params.run) {
|
|
@@ -637,11 +678,15 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
637
678
|
const start = anchor ?? (await this.#machine.rasterPosition());
|
|
638
679
|
const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
|
|
639
680
|
const ending = rasterWindowExpression(start.line, start.cycle, lines);
|
|
681
|
+
const startPc = await this.#pc();
|
|
682
|
+
if (await this.#jamAt(startPc)) return { end: "jam", frames: 0, pc: startPc, space: "c64" };
|
|
640
683
|
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
641
684
|
try {
|
|
642
685
|
await this.#machine.setCondition(clock, arming);
|
|
643
686
|
let armingPhase = true;
|
|
644
687
|
let frames = 0;
|
|
688
|
+
// VICE reports a JAM only once until a reset: a jammed CPU stops the clock at the same pc again and again.
|
|
689
|
+
let clockPc = startPc;
|
|
645
690
|
for (;;) {
|
|
646
691
|
const next = this.#nextStop(HALF_FRAME_LIMIT_MS);
|
|
647
692
|
await this.#resume();
|
|
@@ -652,6 +697,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
652
697
|
}
|
|
653
698
|
const end = this.#stopCause(stop, clock, target?.checkpoint);
|
|
654
699
|
if (end === "clock") {
|
|
700
|
+
if (stop.pc === clockPc && (await this.#jamAt(stop.pc))) return { end: "jam", frames, pc: stop.pc, space: "c64" };
|
|
701
|
+
clockPc = stop.pc;
|
|
655
702
|
if (armingPhase) {
|
|
656
703
|
await this.#machine.setCondition(clock, ending);
|
|
657
704
|
if (target?.deferred) {
|
|
@@ -667,7 +714,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
667
714
|
continue;
|
|
668
715
|
}
|
|
669
716
|
if (end === "unknown") {
|
|
670
|
-
if (await this.#
|
|
717
|
+
if (await this.#jamAt(stop.pc)) return { end: "jam", frames, pc: stop.pc, space: "c64" };
|
|
671
718
|
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this run.");
|
|
672
719
|
}
|
|
673
720
|
// A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
|
|
@@ -699,6 +746,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
699
746
|
const points: number[] = [];
|
|
700
747
|
const hits: TraceHit[] = [];
|
|
701
748
|
let frames = 0;
|
|
749
|
+
const startPc = await this.#pc();
|
|
750
|
+
if (await this.#jamAt(startPc)) return { hits, frames, end: "jam", pc: startPc };
|
|
751
|
+
let clockPc = startPc;
|
|
702
752
|
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
703
753
|
try {
|
|
704
754
|
for (const point of params.points) points.push(await this.#machine.addCheckpoint({ start: point.start, end: point.end, operation: operation[point.kind], space: "c64" }));
|
|
@@ -718,6 +768,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
718
768
|
if (hits.length >= params.maxHits) return { hits, frames, end: "hits", pc: stop.pc };
|
|
719
769
|
}
|
|
720
770
|
if (stop.hits.includes(clock)) {
|
|
771
|
+
if (stop.pc === clockPc && (await this.#jamAt(stop.pc))) return { hits, frames, end: "jam", pc: stop.pc };
|
|
772
|
+
clockPc = stop.pc;
|
|
721
773
|
if (armingPhase) await this.#machine.setCondition(clock, ending);
|
|
722
774
|
else {
|
|
723
775
|
frames++;
|
|
@@ -730,7 +782,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
730
782
|
if (traced.length > 0) continue;
|
|
731
783
|
const point = this.#userPoint(stop.hits);
|
|
732
784
|
if (point !== undefined) return { hits, frames, end: point.stopReason, pc: stop.pc };
|
|
733
|
-
if (await this.#
|
|
785
|
+
if (await this.#jamAt(stop.pc)) return { hits, frames, end: "jam", pc: stop.pc };
|
|
734
786
|
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this trace.");
|
|
735
787
|
}
|
|
736
788
|
} finally {
|
|
@@ -844,9 +896,43 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
844
896
|
return undefined;
|
|
845
897
|
}
|
|
846
898
|
|
|
847
|
-
|
|
899
|
+
/**
|
|
900
|
+
* True when a JAM holds the CPU at `pc`: the session saw a JAM since the last
|
|
901
|
+
* reset, restore or autostart, or the opcode at `pc` is a JAM. The CPU stops
|
|
902
|
+
* on such an opcode, so it executes nothing more. The session keeps the JAM.
|
|
903
|
+
*/
|
|
904
|
+
async #jamAt(pc: number): Promise<boolean> {
|
|
905
|
+
if (this.#jammed !== undefined) return true;
|
|
848
906
|
const opcode = await this.#machine.readMemory({ address: pc, size: 1, space: "c64", view: "cpu" });
|
|
849
|
-
|
|
907
|
+
if (!JAM_OPCODES.has(Number.parseInt(opcode, 16))) return false;
|
|
908
|
+
this.#jam(pc);
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
#jam(pc: number): void {
|
|
913
|
+
this.#jammed = pc;
|
|
914
|
+
this.#historyStopped = true;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** A reset or an autostart starts a jammed CPU and the CPU history again. */
|
|
918
|
+
#clearJam(): void {
|
|
919
|
+
this.#autostarting = false;
|
|
920
|
+
// A reset sets the clock back to 0: the history from before it is of this run again.
|
|
921
|
+
this.#historySince = undefined;
|
|
922
|
+
this.#jammed = undefined;
|
|
923
|
+
this.#historyStopped = false;
|
|
924
|
+
this.#historyOld = false;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Refuses an action that needs the CPU to execute, while a JAM holds it. */
|
|
928
|
+
async #refuseJammed(action: string): Promise<void> {
|
|
929
|
+
const pc = await this.#pc();
|
|
930
|
+
if (!(await this.#jamAt(pc))) return;
|
|
931
|
+
throw new WireFailure(
|
|
932
|
+
"operation-failed",
|
|
933
|
+
`The CPU is jammed by the JAM opcode at ${formatC64Address(this.#jammed ?? pc)}, so ${action} cannot execute an instruction. ` +
|
|
934
|
+
"The CPU runs again only after c64_reset, a snapshot restore or c64_autostart.",
|
|
935
|
+
);
|
|
850
936
|
}
|
|
851
937
|
|
|
852
938
|
/** Runs until a target is reached or `timeoutFrames` frames pass. */
|
|
@@ -1050,10 +1136,18 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1050
1136
|
* stop, whose position is known, mapped back through the frame.
|
|
1051
1137
|
*/
|
|
1052
1138
|
async #cpuHistory(params: { limit: number; space: "c64" | "drive8" }): Promise<{ entries: HistoryEntry[] }> {
|
|
1139
|
+
if (params.space === "c64" && this.#historyOld) {
|
|
1140
|
+
throw new WireFailure(
|
|
1141
|
+
"operation-failed",
|
|
1142
|
+
"VICE records no CPU history after a JAM until the next reset, and the CPU ran after the JAM. The history would show old instructions. " +
|
|
1143
|
+
"Use c64_reset to record again, or use c64_trace.",
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1053
1146
|
return this.#observe(async () => {
|
|
1054
|
-
const
|
|
1055
|
-
if (params.space === "drive8") return { entries:
|
|
1147
|
+
const all = await this.#machine.history(params.limit, params.space);
|
|
1148
|
+
if (params.space === "drive8") return { entries: all.map(({ clock: _clock, ...entry }) => entry) };
|
|
1056
1149
|
const now = await this.#machine.clock();
|
|
1150
|
+
const raw = entriesSince(all, this.#historySince, now);
|
|
1057
1151
|
const position = await this.#machine.rasterPosition();
|
|
1058
1152
|
const { lines, cycles } = RASTER[this.#videoStandard];
|
|
1059
1153
|
const frame = BigInt(lines * cycles);
|
|
@@ -1128,7 +1222,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1128
1222
|
const existing = this.#snapshots.get(params.name);
|
|
1129
1223
|
if (params.action === "save") {
|
|
1130
1224
|
if (existing === undefined && this.#snapshots.size >= MAX_SNAPSHOTS) {
|
|
1131
|
-
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_SNAPSHOTS} snapshots.
|
|
1225
|
+
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_SNAPSHOTS} snapshots. Remove one first with action remove.`);
|
|
1132
1226
|
}
|
|
1133
1227
|
const file = existing ?? join(this.#workDir, `snapshot-${++this.#snapshotFiles}.vsf`);
|
|
1134
1228
|
await this.#observe(() => this.#machine.saveSnapshot(file));
|
|
@@ -1148,7 +1242,13 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1148
1242
|
async #restoreFrom(file: string): Promise<{ restored: true; state: "stopped" }> {
|
|
1149
1243
|
await this.#stop();
|
|
1150
1244
|
const elapsed = (await this.#machine.clock()) - this.#timingStart;
|
|
1245
|
+
// The restore replaces the whole machine state, so the reset that stops the autostart leaves no trace.
|
|
1246
|
+
if (this.#autostarting) await this.#reset({ mode: "soft", run: false });
|
|
1151
1247
|
await this.#machine.restoreSnapshot(file);
|
|
1248
|
+
this.#historySince = await this.#machine.clock();
|
|
1249
|
+
// A restore starts a jammed CPU again, but VICE still records no CPU history.
|
|
1250
|
+
this.#jammed = undefined;
|
|
1251
|
+
if (this.#historyStopped) this.#historyOld = true;
|
|
1152
1252
|
this.#state = "stopped";
|
|
1153
1253
|
// The cycle clock jumped with the restore; the stopwatch keeps its reading, as a window move does.
|
|
1154
1254
|
this.#timingStart = (await this.#machine.clock()) - elapsed;
|
|
@@ -1168,13 +1268,26 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1168
1268
|
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot.
|
|
1169
1269
|
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.");
|
|
1170
1270
|
const file = this.#stage(bytes, "vsf");
|
|
1271
|
+
// VICE resets the machine after a snapshot file that it cannot read: a backup keeps the state before the restore.
|
|
1272
|
+
const backup = join(this.#workDir, "before-restore.vsf");
|
|
1273
|
+
await this.#stop();
|
|
1274
|
+
await this.#machine.saveSnapshot(backup);
|
|
1171
1275
|
try {
|
|
1172
1276
|
return await this.#restoreFrom(file);
|
|
1173
1277
|
} catch (error) {
|
|
1278
|
+
const kept = await this.#restoreFrom(backup).then(
|
|
1279
|
+
() => true,
|
|
1280
|
+
() => false,
|
|
1281
|
+
);
|
|
1282
|
+
if (!kept) throw new WireFailure("machine-state-lost", "The emulator did not restore this snapshot file, and the machine state from before the restore is lost. Load the program again.");
|
|
1174
1283
|
if (error instanceof WireFailure) throw error;
|
|
1175
|
-
throw new WireFailure(
|
|
1284
|
+
throw new WireFailure(
|
|
1285
|
+
"invalid-input",
|
|
1286
|
+
"The emulator did not restore this snapshot file. The machine state is as before. A snapshot file restores only in the emulator version and with the machine settings that saved it.",
|
|
1287
|
+
);
|
|
1176
1288
|
} finally {
|
|
1177
1289
|
rmSync(file, { force: true });
|
|
1290
|
+
rmSync(backup, { force: true });
|
|
1178
1291
|
}
|
|
1179
1292
|
}
|
|
1180
1293
|
|
|
@@ -1255,6 +1368,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1255
1368
|
if (this.#closed) throw new WireFailure("machine-unavailable", "The emulator session is closed.");
|
|
1256
1369
|
this.#vice = vice;
|
|
1257
1370
|
this.#adapter = adapter;
|
|
1371
|
+
// A new emulator: no JAM holds its CPU, and it records CPU history.
|
|
1372
|
+
this.#clearJam();
|
|
1258
1373
|
this.#mode = mode;
|
|
1259
1374
|
for (const [point, checkpoint] of points) point.checkpoint = checkpoint;
|
|
1260
1375
|
this.#timingStart = clock - handover.elapsed;
|
|
@@ -1446,6 +1561,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1446
1561
|
|
|
1447
1562
|
async #translate(error: unknown, runningBefore: boolean, halts: number): Promise<unknown> {
|
|
1448
1563
|
if (error instanceof WireFailure) return error;
|
|
1564
|
+
// A refusal of a shared module (for example the media check) goes on the wire as the host's own.
|
|
1565
|
+
if (isRefusal(error)) return new WireFailure(error.code, error.message);
|
|
1449
1566
|
if (this.#closed) return new WireFailure("machine-unavailable", "The emulator session is closed.");
|
|
1450
1567
|
if (error instanceof MonitorConnectionError || error instanceof TextMonitorError || error instanceof StepTimeout) {
|
|
1451
1568
|
this.#log(error.message);
|
|
@@ -1515,6 +1632,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1515
1632
|
if (stop.hits.length > 0 || event.type === ResponseType.jam) this.#haltCount++;
|
|
1516
1633
|
if (stop.hits.some((hit) => this.#adapter?.isDriveCheckpoint(hit))) this.#driveStop = true;
|
|
1517
1634
|
this.#lastStop = event.type === ResponseType.jam ? { stopReason: "jam" } : this.#userPoint(stop.hits);
|
|
1635
|
+
if (event.type === ResponseType.jam) this.#jam(stop.pc);
|
|
1518
1636
|
this.#checkpointHits = [];
|
|
1519
1637
|
this.#state = "stopped";
|
|
1520
1638
|
this.#stopWaiters = this.#stopWaiters.filter((waiter) => !waiter(stop));
|
|
@@ -1525,6 +1643,23 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1525
1643
|
}
|
|
1526
1644
|
}
|
|
1527
1645
|
|
|
1646
|
+
/**
|
|
1647
|
+
* The history entries (oldest first) that the CPU executed after a restore at
|
|
1648
|
+
* clock `since`, with the clock at `now`: the last entries, back to the first
|
|
1649
|
+
* one outside since to now or the first one whose clock does not go up to the
|
|
1650
|
+
* next. The others are of before the restore.
|
|
1651
|
+
*/
|
|
1652
|
+
export function entriesSince<T extends { clock: bigint }>(entries: readonly T[], since: bigint | undefined, now: bigint): T[] {
|
|
1653
|
+
if (since === undefined) return [...entries];
|
|
1654
|
+
let first = entries.length;
|
|
1655
|
+
while (first > 0) {
|
|
1656
|
+
const entry = entries[first - 1]!;
|
|
1657
|
+
if (entry.clock < since || entry.clock > now || (first < entries.length && entry.clock >= entries[first]!.clock)) break;
|
|
1658
|
+
first--;
|
|
1659
|
+
}
|
|
1660
|
+
return entries.slice(first);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1528
1663
|
/** The one attachment of an operation that takes a file (see attachmentCount); refuses any other count. */
|
|
1529
1664
|
function onlyAttachment(attachments: Buffer[]): Buffer {
|
|
1530
1665
|
const [bytes] = attachments;
|
package/src/host-client/image.ts
CHANGED
|
@@ -21,18 +21,50 @@ export interface Image {
|
|
|
21
21
|
stages?: Array<Record<string, unknown>>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/** The kinds of media files that are no PRG, by extension. */
|
|
25
|
+
const MEDIA_KINDS: Record<string, string> = {
|
|
26
|
+
d64: "a disk image",
|
|
27
|
+
d71: "a disk image",
|
|
28
|
+
d81: "a disk image",
|
|
29
|
+
g64: "a disk image",
|
|
30
|
+
x64: "a disk image",
|
|
31
|
+
t64: "a tape file",
|
|
32
|
+
tap: "a tape file",
|
|
33
|
+
crt: "a cartridge file",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The remedy for a disk, tape or cartridge file where a PRG must be: get the PRG out of it. */
|
|
37
|
+
export const MEDIA_REMEDY = "Get the PRG out of it with disk.ts read (or disk.ts crt for a cartridge) of the c64-disk skill";
|
|
38
|
+
|
|
39
|
+
/** Refuses a disk, tape or cartridge file by its extension (`type`, lowercase, without the dot): invalid-input with `remedy`. */
|
|
40
|
+
function refuseMedia(path: string, type: string, remedy: string): void {
|
|
41
|
+
const media = MEDIA_KINDS[type];
|
|
42
|
+
if (media !== undefined) throw new Refusal("invalid-input", `${path} is ${media} (.${type}), not a PRG. ${remedy}.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reads a PRG of the project: a 2-byte load address, and bytes that end at
|
|
47
|
+
* $ffff or before. Refuses a disk, tape or cartridge file and any other file
|
|
48
|
+
* that is no PRG with invalid-input; `remedy` tells what to do in its place.
|
|
49
|
+
*/
|
|
50
|
+
export function readPrg(path: string, remedy = MEDIA_REMEDY): { bytes: Buffer; start: number; end: number; body: Uint8Array } {
|
|
51
|
+
const { bytes, type } = readProjectFile(path);
|
|
52
|
+
refuseMedia(path, type, remedy);
|
|
53
|
+
try {
|
|
54
|
+
return { bytes, ...prgRange(bytes) };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (isRefusal(error)) throw new Refusal("invalid-input", `${path} is not a PRG: ${error.message} ${remedy}.`);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Reads an image.json or a PRG, relative to the project folder. Refuses a disk, tape or cartridge file as readPrg does. */
|
|
25
62
|
export function readImage(path: string): Image {
|
|
26
|
-
const { bytes } = readProjectFile(path);
|
|
27
63
|
if (!path.endsWith(".json")) {
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
range = prgRange(bytes);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
throw new Refusal("invalid-input", `${path}: ${(error as Error).message}`);
|
|
33
|
-
}
|
|
64
|
+
const range = readPrg(path);
|
|
34
65
|
return { segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]! };
|
|
35
66
|
}
|
|
67
|
+
const { bytes } = readProjectFile(path);
|
|
36
68
|
const fail = (what: string): never => {
|
|
37
69
|
throw new Refusal("invalid-input", `${path} is not an image.json of the c64-unpacker skill: ${what}`);
|
|
38
70
|
};
|
package/src/host-client/steps.ts
CHANGED
|
@@ -63,6 +63,8 @@ export type ActionStep =
|
|
|
63
63
|
|
|
64
64
|
export const ACTION_STEP_KINDS = ["write", "registers", "joystick", "type", "press", "frames", "runUntil"] as const;
|
|
65
65
|
export type ActionStepKind = (typeof ACTION_STEP_KINDS)[number];
|
|
66
|
+
/** The step kinds that VICE runs: all but press, which only the replay core has. */
|
|
67
|
+
export const VICE_STEP_KINDS = ACTION_STEP_KINDS.filter((kind) => kind !== "press");
|
|
66
68
|
|
|
67
69
|
/** The fields of each object of the steps: the same as the inputs of the MCP tools. */
|
|
68
70
|
export const STEP_FIELDS = {
|
|
@@ -110,6 +112,32 @@ export function keyboardBytes(input: { mode: "text" | "petscii"; text?: string |
|
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/**
|
|
116
|
+
* One line of JSON with a space after each colon and comma: the form that a
|
|
117
|
+
* refusal tells the caller to write.
|
|
118
|
+
*/
|
|
119
|
+
export function jsonLine(value: unknown): string {
|
|
120
|
+
if (Array.isArray(value)) return `[${value.map(jsonLine).join(", ")}]`;
|
|
121
|
+
if (isObject(value)) return `{${Object.entries(value).filter(([, item]) => item !== undefined).map(([key, item]) => `${JSON.stringify(key)}: ${jsonLine(item)}`).join(", ")}}`;
|
|
122
|
+
return JSON.stringify(value) ?? "null";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Field names of the 2.0 files and what a 2.1 object has in their place, when it has that field. */
|
|
126
|
+
const RENAMED: Record<string, Array<{ field: string; write: string }>> = {
|
|
127
|
+
at: [
|
|
128
|
+
{ field: "address", write: "address" },
|
|
129
|
+
{ field: "start", write: "start" },
|
|
130
|
+
],
|
|
131
|
+
bytes: [{ field: "data", write: 'data, the bytes as hex text, for example "data": "a900",' }],
|
|
132
|
+
equals: [{ field: "operator", write: '"operator": "eq" and "value"' }],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** What to write in place of a field of the 2.0 files, for an object with these fields; undefined when the field has no 2.1 name there. */
|
|
136
|
+
export function renamedField(key: string, fields: readonly string[]): string | undefined {
|
|
137
|
+
const renamed = RENAMED[key]?.find((item) => fields.includes(item.field));
|
|
138
|
+
return renamed === undefined ? undefined : `${key} is a 2.0 field. Write ${renamed.write} in place of ${key}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
113
141
|
/** The checks of one input: `fail` names the field of the problem. */
|
|
114
142
|
class Fields {
|
|
115
143
|
readonly #value: Record<string, unknown>;
|
|
@@ -121,7 +149,11 @@ class Fields {
|
|
|
121
149
|
this.#fail = fail;
|
|
122
150
|
if (!isObject(value)) fail(`${where} must be an object with ${fields.join(", ")}`);
|
|
123
151
|
this.#value = value as Record<string, unknown>;
|
|
124
|
-
for (const key of Object.keys(this.#value))
|
|
152
|
+
for (const key of Object.keys(this.#value)) {
|
|
153
|
+
if (fields.includes(key)) continue;
|
|
154
|
+
const renamed = renamedField(key, fields);
|
|
155
|
+
fail(`${where} has the field ${key}, which it does not take. ${renamed === undefined ? "" : `${renamed}. `}Its fields are ${fields.join(", ")}`);
|
|
156
|
+
}
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
fail(field: string, message: string): never {
|
|
@@ -256,22 +288,90 @@ function checkInput(kind: ActionStepKind, value: unknown, at: string, fail: (mes
|
|
|
256
288
|
}
|
|
257
289
|
}
|
|
258
290
|
|
|
291
|
+
/** A "$xxxx" text of a number, or the value as it is. */
|
|
292
|
+
const asAddress = (value: unknown) => (isInteger(value, 0, 0xffff) ? `$${value.toString(16).padStart(4, "0")}` : value);
|
|
293
|
+
|
|
294
|
+
/** The hex text of a list of bytes, or the value as it is. */
|
|
295
|
+
const asHex = (value: unknown) => (Array.isArray(value) && value.every((byte) => isInteger(byte, 0, 0xff)) ? Buffer.from(value as number[]).toString("hex") : value);
|
|
296
|
+
|
|
297
|
+
/** A runUntil target from the 2.0 input: at (with raster), raster, or memory with at and equals. */
|
|
298
|
+
function targetOf(input: Record<string, unknown>): unknown {
|
|
299
|
+
if ("target" in input) return input.target;
|
|
300
|
+
if (isObject(input.memory)) return { kind: "memory", address: input.memory.at ?? input.memory.address, operator: "eq", value: input.memory.equals };
|
|
301
|
+
if ("at" in input) return { kind: "address", address: input.at, ...(input.raster === undefined ? {} : { condition: { kind: "raster", line: input.raster } }) };
|
|
302
|
+
return { kind: "raster", line: input.raster };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The 2.1 form of a step that has a form of the 2.0 files: the 2.0 fields
|
|
307
|
+
* that it has and the step to write. Undefined when the step has no 2.0 form.
|
|
308
|
+
* Keys of the step other than its kind (for example side) stay.
|
|
309
|
+
*/
|
|
310
|
+
export function newStepForm(kind: string, step: Record<string, unknown>): { fields: string[]; step: Record<string, unknown> } | undefined {
|
|
311
|
+
const input = step[kind];
|
|
312
|
+
const { [kind]: _input, ...rest } = step;
|
|
313
|
+
const form = (fields: string[], value: unknown, others = rest) => ({ fields, step: { [kind]: value, ...others } });
|
|
314
|
+
switch (kind) {
|
|
315
|
+
case "runUntil": {
|
|
316
|
+
const { timeoutFrames: beside, ...others } = rest;
|
|
317
|
+
const old = isObject(input) && !("target" in input) && ("at" in input || "raster" in input || "memory" in input);
|
|
318
|
+
if (!isObject(input) || (!old && beside === undefined)) return undefined;
|
|
319
|
+
const fields = [...(old ? ["at", "raster", "memory"].filter((key) => key in input).map((key) => `runUntil.${key}`) : []), ...(beside === undefined ? [] : ["timeoutFrames beside runUntil"])];
|
|
320
|
+
const timeoutFrames = beside ?? input.timeoutFrames;
|
|
321
|
+
return form(fields, { target: targetOf(input), ...(timeoutFrames === undefined ? {} : { timeoutFrames }) }, others);
|
|
322
|
+
}
|
|
323
|
+
case "type":
|
|
324
|
+
return typeof input === "string" ? form(["type as text"], { mode: "text", text: input }) : undefined;
|
|
325
|
+
case "write": {
|
|
326
|
+
if (!isObject(input) || !("at" in input || "bytes" in input || Array.isArray(input.data))) return undefined;
|
|
327
|
+
const { at: old, bytes, address, data, ...others } = input;
|
|
328
|
+
return form(
|
|
329
|
+
["at", "bytes"].filter((key) => key in input).map((key) => `write.${key}`).concat(Array.isArray(data) ? ["write.data as a list"] : []),
|
|
330
|
+
{ address: address ?? old, data: asHex(data ?? bytes), ...others },
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
case "registers": {
|
|
334
|
+
if (!isObject(input)) return undefined;
|
|
335
|
+
const fields = Object.entries(input).flatMap(([name, value]) => ((name === "pc" && typeof value === "number") || (name !== "pc" && name !== "flags" && typeof value === "string" && c64AddressOf(value) !== undefined) ? [`registers.${name} as ${typeof value === "number" ? "a number" : "text"}`] : []));
|
|
336
|
+
if (fields.length === 0) return undefined;
|
|
337
|
+
const values = Object.fromEntries(Object.entries(input).map(([name, value]) => [name, name === "pc" ? asAddress(value) : typeof value === "string" ? (c64AddressOf(value) ?? value) : value]));
|
|
338
|
+
return form(fields, values);
|
|
339
|
+
}
|
|
340
|
+
default:
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Refuses a step in a form of the 2.0 files: the message names its 2.0 fields and gives the step to write. */
|
|
346
|
+
export function refuseOldStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
347
|
+
const found = newStepForm(kind, step);
|
|
348
|
+
if (found === undefined) return;
|
|
349
|
+
const petscii = kind === "type" ? ', or {"type": {"mode": "petscii", "bytes": [...]}} for PETSCII codes' : "";
|
|
350
|
+
fail(`${at} has the 2.0 form (${found.fields.join(", ")}). Write ${jsonLine(found.step)}${petscii}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
259
353
|
/**
|
|
260
354
|
* Checks one action step of the given kind and returns it with the defaults
|
|
261
355
|
* filled in. The step has no other key. `press` is refused unless the caller
|
|
262
|
-
* runs the steps on the replay core.
|
|
356
|
+
* runs the steps on the replay core. A step in a form of the 2.0 files is
|
|
357
|
+
* refused with the step to write.
|
|
263
358
|
*/
|
|
264
359
|
export function checkActionStep(kind: ActionStepKind, step: Record<string, unknown>, at: string, fail: (message: string) => never, options: { press?: boolean } = {}): ActionStep {
|
|
360
|
+
refuseOldStep(kind, step, at, fail);
|
|
265
361
|
for (const key of Object.keys(step)) if (key !== kind) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
266
362
|
if (kind === "press" && options.press !== true) fail(`${at}: ${PRESS_REMEDY}`);
|
|
267
363
|
return checkInput(kind, step[kind], at, fail);
|
|
268
364
|
}
|
|
269
365
|
|
|
270
|
-
/**
|
|
271
|
-
|
|
366
|
+
/**
|
|
367
|
+
* The kind of a step: its one key from `kinds`. A refusal lists `shown`: the
|
|
368
|
+
* kinds that the caller runs.
|
|
369
|
+
*/
|
|
370
|
+
export function stepKind<K extends string>(step: unknown, kinds: readonly K[], at: string, fail: (message: string) => never, shown: readonly string[] = kinds): K {
|
|
272
371
|
if (!isObject(step)) return fail(`${at} must be an object`);
|
|
273
372
|
const found = Object.keys(step).filter((key) => (kinds as readonly string[]).includes(key));
|
|
274
|
-
if (found.length
|
|
373
|
+
if (found.length > 1) return fail(`${at} has ${found.length} step kinds: ${found.join(", ")}. Give each one its own step`);
|
|
374
|
+
if (found.length !== 1) return fail(`${at} must have exactly one of ${shown.join(", ")}`);
|
|
275
375
|
return found[0] as K;
|
|
276
376
|
}
|
|
277
377
|
|
|
@@ -280,12 +380,46 @@ export function checkActionSteps(value: unknown, fail: (message: string) => neve
|
|
|
280
380
|
if (!Array.isArray(value) || value.length === 0) return fail("the steps are a JSON list with at least one step");
|
|
281
381
|
return value.map((step: unknown, index) => {
|
|
282
382
|
const at = `step ${index + 1}`;
|
|
283
|
-
return checkActionStep(stepKind(step, ACTION_STEP_KINDS, at, fail), step as Record<string, unknown>, at, fail, options);
|
|
383
|
+
return checkActionStep(stepKind(step, ACTION_STEP_KINDS, at, fail, options.press === true ? ACTION_STEP_KINDS : VICE_STEP_KINDS), step as Record<string, unknown>, at, fail, options);
|
|
284
384
|
});
|
|
285
385
|
}
|
|
286
386
|
|
|
287
|
-
/**
|
|
288
|
-
export function
|
|
387
|
+
/** The names and addresses of an action step: each location that the step resolves when it runs. */
|
|
388
|
+
export function stepLocations(step: ActionStep): Location[] {
|
|
389
|
+
if ("write" in step) return [step.write.address];
|
|
390
|
+
if ("registers" in step) return step.registers.pc === undefined ? [] : [step.registers.pc];
|
|
391
|
+
if (!("runUntil" in step)) return [];
|
|
392
|
+
const { target } = step.runUntil;
|
|
393
|
+
if (target.kind === "raster") return [];
|
|
394
|
+
if (target.kind === "memory") return [target.address];
|
|
395
|
+
return [target.address, ...(target.condition?.kind === "memory" ? [target.condition.address] : [])];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Resolves every name of the steps with `at` before the steps run, so that an
|
|
400
|
+
* unknown name stops the call before the emulator starts. A refusal of `at` becomes
|
|
401
|
+
* invalid-input that names `where` (for example the option and the file) and the step.
|
|
402
|
+
* `first` is the number of the first of the steps in the file.
|
|
403
|
+
*/
|
|
404
|
+
export function checkStepNames(steps: readonly ActionStep[], at: (location: Location) => number, where: string, first = 1): void {
|
|
405
|
+
steps.forEach((step, index) => {
|
|
406
|
+
for (const location of stepLocations(step)) {
|
|
407
|
+
try {
|
|
408
|
+
at(location);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (isRefusal(error)) throw new Refusal("invalid-input", `${where}: step ${index + first}: ${error.message}`);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Reads a steps file of the project: a JSON list of action steps. `option`
|
|
419
|
+
* names the option of the file in a refusal. With `at`, every name of the
|
|
420
|
+
* steps must resolve (checkStepNames).
|
|
421
|
+
*/
|
|
422
|
+
export function readActionSteps(path: string, option: string, at?: (location: Location) => number): ActionStep[] {
|
|
289
423
|
const fail = (message: string): never => {
|
|
290
424
|
throw new Refusal("invalid-input", `${option} ${path}: ${message}`);
|
|
291
425
|
};
|
|
@@ -296,7 +430,9 @@ export function readActionSteps(path: string, option: string): ActionStep[] {
|
|
|
296
430
|
if (error instanceof SyntaxError) return fail(`it is not JSON: ${error.message}`);
|
|
297
431
|
throw error;
|
|
298
432
|
}
|
|
299
|
-
|
|
433
|
+
const steps = checkActionSteps(value, fail);
|
|
434
|
+
if (at !== undefined) checkStepNames(steps, at, `${option} ${path}`);
|
|
435
|
+
return steps;
|
|
300
436
|
}
|
|
301
437
|
|
|
302
438
|
/** Stops the CPU when it runs: a write, a register change or an exact frame count needs it stopped. */
|
package/src/host-client/tools.ts
CHANGED
|
@@ -85,6 +85,6 @@ export async function hostTools(options: ToolCallOptions = {}): Promise<{ host:
|
|
|
85
85
|
|
|
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
|
-
export { DISK_ACTIONS } from "../protocol/tools.ts";
|
|
88
|
+
export { DISK_ACTIONS, DISK_IMAGE_TYPES } 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
90
|
export { MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, type TraceHit, type TraceKind } from "../protocol/vice.ts";
|