@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
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// run, or two observations of runs (also two of one run), compare as they are.
|
|
6
6
|
|
|
7
7
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
8
|
+
import { Refusal } from "#src/errors.ts";
|
|
9
|
+
import { isObject } from "#src/host-client/steps.ts";
|
|
8
10
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
9
11
|
import { readProjectFile, writeReported } from "#src/host-client/transfer.ts";
|
|
10
12
|
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
@@ -41,33 +43,92 @@ function observation(path: string): MachineState {
|
|
|
41
43
|
observed[at] = readProjectFile(`${folder}${(value as { file: string }).file}`).bytes.toString("hex");
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
return { observed, values: cpuValues(seen.registers ?? {}) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The CPU registers as values: each register by its name, the pc from a number or "$xxxx", and each flag as cpu.flags.<flag>. */
|
|
50
|
+
function cpuValues(registers: Record<string, unknown>): Record<string, number | boolean> {
|
|
51
|
+
const values: Record<string, number | boolean> = {};
|
|
52
|
+
for (const [register, value] of Object.entries(registers)) {
|
|
46
53
|
if (typeof value === "number") values[`cpu.${register}`] = value;
|
|
47
54
|
else if (typeof value === "string" && value.startsWith("$")) values[`cpu.${register}`] = parseC64Address(value);
|
|
55
|
+
else if (register === "flags" && isObject(value)) for (const [flag, set] of Object.entries(value)) if (typeof set === "boolean") values[`cpu.flags.${flag}`] = set;
|
|
48
56
|
}
|
|
49
|
-
return
|
|
57
|
+
return values;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
/** The
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
/** The size of the header of a module in a VICE snapshot file: the name (16 bytes), the version (2 bytes) and the size (4 bytes). */
|
|
61
|
+
const MODULE_HEADER = 22;
|
|
62
|
+
const VSF_MAGIC = "VICE Snapshot File\x1a";
|
|
63
|
+
const VSF_VERSION = "VICE Version\x1a";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The 64 KiB of C64 RAM that a VICE snapshot file holds: the C64MEM module,
|
|
67
|
+
* after the CPU port and the cartridge lines (4 bytes). Undefined when the
|
|
68
|
+
* file has no such module.
|
|
69
|
+
*/
|
|
70
|
+
export function snapshotRam(file: Uint8Array): Uint8Array | undefined {
|
|
71
|
+
const bytes = Buffer.from(file);
|
|
72
|
+
if (bytes.subarray(0, VSF_MAGIC.length).toString("latin1") !== VSF_MAGIC) return undefined;
|
|
73
|
+
// The magic, the version (2 bytes) and the machine name (16 bytes); a newer VICE adds its own version (8 bytes).
|
|
74
|
+
let at = VSF_MAGIC.length + 2 + 16;
|
|
75
|
+
if (bytes.subarray(at, at + VSF_VERSION.length).toString("latin1") === VSF_VERSION) at += VSF_VERSION.length + 8;
|
|
76
|
+
while (at + MODULE_HEADER <= bytes.length) {
|
|
77
|
+
const name = bytes.subarray(at, at + 16).toString("latin1").replace(/\0+$/, "");
|
|
78
|
+
const size = bytes.readUInt32LE(at + 18);
|
|
79
|
+
if (size < MODULE_HEADER) return undefined;
|
|
80
|
+
if (name === "C64MEM") return at + MODULE_HEADER + 4 + 0x10000 <= bytes.length ? bytes.subarray(at + MODULE_HEADER + 4, at + MODULE_HEADER + 4 + 0x10000) : undefined;
|
|
81
|
+
at += size;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The state of a snapshot: restored in the session, then read with the
|
|
88
|
+
* readers of the runs. The RAM must be the RAM of the file: else the
|
|
89
|
+
* snapshot did not restore, and the state would be wrong.
|
|
90
|
+
*/
|
|
91
|
+
async function snapshotState(client: ViceSessionClient, path: string, ntsc: boolean): Promise<MachineState> {
|
|
92
|
+
const file = readProjectFile(path).bytes;
|
|
93
|
+
const saved = snapshotRam(file);
|
|
94
|
+
if (saved === undefined) throw new Refusal("invalid-input", `${path} is not a snapshot of a C64: it has no C64 memory. Give a file that c64_snapshot save or a save step of a run wrote.`);
|
|
95
|
+
const restored = await client.snapshotRestore(file);
|
|
55
96
|
const memory = await readMemoryInParts(client, 0x0000, 0x10000, "ram");
|
|
97
|
+
// $00 and $01 are the CPU port: the file keeps them apart from the RAM.
|
|
98
|
+
if (("restored" in restored && !restored.restored) || !memory.subarray(2).equals(Buffer.from(saved.subarray(2)))) {
|
|
99
|
+
throw new Refusal(
|
|
100
|
+
"invalid-input",
|
|
101
|
+
`${path} did not restore: the RAM after the restore is not the RAM of the file. A snapshot restores only in a machine of the video standard that saved it. ${ntsc ? "Leave out --ntsc for a PAL snapshot" : "Give --ntsc for an NTSC snapshot"}, or use the emulator version that saved it.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
56
104
|
const colorRam = Buffer.from((await client.ioRead(0xd800, 0x400)).data, "hex").map((value) => value & 0x0f);
|
|
57
105
|
const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
|
|
58
|
-
const values: Record<string, number> = {};
|
|
106
|
+
const values: Record<string, number | boolean> = {};
|
|
59
107
|
vic.forEach((value, index) => (values[`vic.${formatC64Address(0xd000 + index)}`] = value));
|
|
60
108
|
for (const chip of (await client.cia("both")).chips) {
|
|
61
109
|
for (const field of ["portA", "portB", "ddrA", "ddrB", "timerA", "timerB", "latchA", "latchB", "controlA", "controlB", "interruptMask"] as const) values[`cia${chip.id}.${field}`] = chip[field];
|
|
62
110
|
}
|
|
63
111
|
const sid = await client.sid();
|
|
64
112
|
for (const voice of sid.voices) for (const [field, value] of Object.entries(voice)) if (field !== "index" && typeof value === "number") values[`sid.voice${voice.index}.${field}`] = value;
|
|
65
|
-
|
|
66
|
-
for (const [field, value] of Object.entries(registers)) if (typeof value === "number") values[`cpu.${field}`] = value;
|
|
113
|
+
Object.assign(values, cpuValues({ ...(await client.registersGet("c64")) }));
|
|
67
114
|
return { memory: { base: 0, bytes: memory }, colorRam, values };
|
|
68
115
|
}
|
|
69
116
|
|
|
70
117
|
const show = (range: ChangedRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end), ...(range.a === undefined ? {} : { a: range.a, b: range.b }) });
|
|
118
|
+
/** A changed value as a person reads it: the pc as "$xxxx". */
|
|
119
|
+
const showValue = (item: { name: string; a: number | boolean; b: number | boolean }) => (item.name === "cpu.pc" && typeof item.a === "number" && typeof item.b === "number" ? { name: item.name, a: formatC64Address(item.a), b: formatC64Address(item.b) } : item);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* How many items two observations have in common, by name: the items that the
|
|
123
|
+
* diff compared. With none, the reason names what each side has, so that a
|
|
124
|
+
* count of 0 does not read as "equal".
|
|
125
|
+
*/
|
|
126
|
+
export function comparedItems(what: string, a: readonly string[], b: readonly string[], first: string, second: string): { compared: number; reason?: string } {
|
|
127
|
+
const compared = a.filter((name) => b.includes(name)).length;
|
|
128
|
+
if (compared > 0) return { compared };
|
|
129
|
+
const has = (names: readonly string[]) => (names.length === 0 ? "none" : names.join(", "));
|
|
130
|
+
return { compared, reason: `No ${what} was compared: the observations have none in common. ${first} has ${has(a)}; ${second} has ${has(b)}.` };
|
|
131
|
+
}
|
|
71
132
|
|
|
72
133
|
/** The differences of two states of one kind: snapshots, memory files or observations of runs. */
|
|
73
134
|
export async function diffFiles(first: string, second: string, options: { gap: number; max: number; ntsc: boolean; at?: number; out?: string }): Promise<Record<string, unknown>> {
|
|
@@ -90,8 +151,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
|
|
|
90
151
|
} else {
|
|
91
152
|
const client = await ViceSessionClient.open({ videoStandard: options.ntsc ? "ntsc" : "pal" });
|
|
92
153
|
try {
|
|
93
|
-
a = await snapshotState(client, first);
|
|
94
|
-
b = await snapshotState(client, second);
|
|
154
|
+
a = await snapshotState(client, first, options.ntsc);
|
|
155
|
+
b = await snapshotState(client, second, options.ntsc);
|
|
95
156
|
} finally {
|
|
96
157
|
await client.close();
|
|
97
158
|
}
|
|
@@ -101,7 +162,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
|
|
|
101
162
|
const part = (item: { changedBytes: number; ranges: ChangedRange[] } | undefined) => (item === undefined ? undefined : { changedBytes: item.changedBytes, ...listed(item.ranges, max, show, rest) });
|
|
102
163
|
let written: ReturnType<typeof writeReported> | undefined;
|
|
103
164
|
if (options.out !== undefined) {
|
|
104
|
-
|
|
165
|
+
// The names of the result: registers holds the changed values.
|
|
166
|
+
const all = { a: first, b: second, ...(diff.memory === undefined ? {} : { memory: diff.memory.ranges.map(show) }), ...(diff.colorRam === undefined ? {} : { colorRam: diff.colorRam.ranges.map(show) }), registers: (diff.values ?? []).map(showValue), observed: diff.observed ?? [] };
|
|
105
167
|
written = writeReported(options.out, `${JSON.stringify(all, null, 2)}\n`, { what: "--out" });
|
|
106
168
|
}
|
|
107
169
|
return {
|
|
@@ -109,8 +171,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
|
|
|
109
171
|
b: second,
|
|
110
172
|
...(diff.memory === undefined ? {} : { memory: part(diff.memory) }),
|
|
111
173
|
...(diff.colorRam === undefined ? {} : { colorRam: part(diff.colorRam) }),
|
|
112
|
-
...(diff.values === undefined ? {} : { registers: listed(diff.values, max,
|
|
113
|
-
...(diff.observed === undefined ? {} : { observed: listed(diff.observed, max, undefined, rest) }),
|
|
174
|
+
...(diff.values === undefined ? {} : { registers: { ...(kind === "observation" ? comparedItems("register", Object.keys(a.values ?? {}), Object.keys(b.values ?? {}), first, second) : {}), ...listed(diff.values, max, showValue, rest) } }),
|
|
175
|
+
...(diff.observed === undefined ? {} : { observed: { ...comparedItems("memory item", Object.keys(a.observed ?? {}), Object.keys(b.observed ?? {}), first, second), ...listed(diff.observed, max, undefined, rest) } }),
|
|
114
176
|
...(written === undefined ? {} : written),
|
|
115
177
|
};
|
|
116
178
|
}
|
|
@@ -10,8 +10,8 @@ export interface MachineState {
|
|
|
10
10
|
memory?: { base: number; bytes: Uint8Array };
|
|
11
11
|
/** Colour RAM $D800-$DBFF, the low 4 bits of each byte. */
|
|
12
12
|
colorRam?: Uint8Array;
|
|
13
|
-
/** Named single values: VIC-II, CIA and SID registers and the CPU registers, as "vic.$d011", "cia1.timerA", "cpu.pc". */
|
|
14
|
-
values?: Record<string, number>;
|
|
13
|
+
/** Named single values: VIC-II, CIA and SID registers and the CPU registers, as "vic.$d011", "cia1.timerA", "cpu.pc", and each CPU flag as "cpu.flags.c". */
|
|
14
|
+
values?: Record<string, number | boolean>;
|
|
15
15
|
/** The memory items of an observation of a run: the address or name, and its bytes as hex. */
|
|
16
16
|
observed?: Record<string, string>;
|
|
17
17
|
}
|
|
@@ -27,7 +27,7 @@ export interface ChangedRange {
|
|
|
27
27
|
export interface StateDiff {
|
|
28
28
|
memory?: { changedBytes: number; ranges: ChangedRange[] };
|
|
29
29
|
colorRam?: { changedBytes: number; ranges: ChangedRange[] };
|
|
30
|
-
values?: Array<{ name: string; a: number; b: number }>;
|
|
30
|
+
values?: Array<{ name: string; a: number | boolean; b: number | boolean }>;
|
|
31
31
|
observed?: Array<{ address: string; a: string; b: string }>;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -14,7 +14,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
14
14
|
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
15
15
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
16
16
|
import { READY_FRAMES } from "#src/host-client/entry-state.ts";
|
|
17
|
-
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, keyboardBytes, stepKind, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
17
|
+
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, keyboardBytes, renamedField, stepKind, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
18
18
|
import { readProjectFile, type Written } from "#src/host-client/transfer.ts";
|
|
19
19
|
import { PAL_FRAME_CYCLES, type C64RunSession, type CoreStatus } from "#src/native/c64run.ts";
|
|
20
20
|
import type { Workspace } from "#src/native/staging.ts";
|
|
@@ -46,7 +46,7 @@ export interface StateItem {
|
|
|
46
46
|
size: number;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** The input of
|
|
49
|
+
/** The input of testing.ts record. */
|
|
50
50
|
export interface RecordFile {
|
|
51
51
|
name: string;
|
|
52
52
|
program: string;
|
|
@@ -145,21 +145,28 @@ export function joystickMask(direction: Extract<ActionStep, { joystick: unknown
|
|
|
145
145
|
return direction.split("-").reduce((mask, part) => mask | (bits[part] ?? 0), fire ? 0x10 : 0);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/** A state item of the command line: a name or a $ address, with :size for more than one byte. */
|
|
149
|
-
export function parseStateItem(text: string): StateItem {
|
|
148
|
+
/** A state item of the command line: a name or a $ address, with :size for more than one byte. `option` names the option of the item in a refusal. */
|
|
149
|
+
export function parseStateItem(text: string, option = "--state"): StateItem {
|
|
150
150
|
const match = /^([^:]+)(?::(\d+))?$/.exec(text);
|
|
151
151
|
const size = Number(match?.[2] ?? 1);
|
|
152
|
-
if (match === null || !isInteger(size, 1, MAX_STATE_BYTES)) throw new Refusal("invalid-input",
|
|
152
|
+
if (match === null || !isInteger(size, 1, MAX_STATE_BYTES)) throw new Refusal("invalid-input", `${option} ${text}: give a name or a $ address, with :size from 1 to ${MAX_STATE_BYTES}`);
|
|
153
153
|
return { address: match[1]!, size };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
/** Checks the memory items of state or pin (`what`): names, $ addresses, or { address, size }. */
|
|
157
|
+
export function checkState(value: unknown, fail: (message: string) => never, what = "state"): StateItem[] {
|
|
158
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_STATE_ITEMS) fail(`${what} must be a list of 1 to ${MAX_STATE_ITEMS} memory items`);
|
|
158
159
|
return (value as unknown[]).map((item, index) => {
|
|
159
160
|
const spec = typeof item === "string" ? { address: item } : item;
|
|
160
|
-
if (
|
|
161
|
+
if (isObject(spec)) {
|
|
162
|
+
for (const key of Object.keys(spec)) {
|
|
163
|
+
const renamed = renamedField(key, ["address", "size"]);
|
|
164
|
+
if (key !== "address" && key !== "size") fail(`${what} item ${index + 1} has the field ${key}. ${renamed ?? "A memory item has address and size, and nothing else"}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!isObject(spec) || typeof spec.address !== "string") fail(`${what} item ${index + 1} must be a name, a $ address, or { address, size }`);
|
|
161
168
|
const size = (spec as { size?: unknown }).size ?? 1;
|
|
162
|
-
if (!isInteger(size, 1, MAX_STATE_BYTES)) fail(
|
|
169
|
+
if (!isInteger(size, 1, MAX_STATE_BYTES)) fail(`${what} item ${index + 1}: size must be from 1 to ${MAX_STATE_BYTES}`);
|
|
163
170
|
return { address: (spec as { address: string }).address, size: size as number };
|
|
164
171
|
});
|
|
165
172
|
}
|
|
@@ -195,7 +202,7 @@ export function readRecordFile(path: string): RecordFile {
|
|
|
195
202
|
if ((value.checkpoint === undefined) !== (value.state === undefined)) fail("give checkpoint and state together, or neither: a replay can give them");
|
|
196
203
|
if (value.checkpoint !== undefined && (typeof value.checkpoint !== "string" || value.checkpoint === "")) fail("checkpoint must be the name or $ address of a routine");
|
|
197
204
|
if (value.pin !== undefined && value.checkpoint === undefined) fail("pin needs checkpoint and state");
|
|
198
|
-
const pin = value.pin === undefined ? undefined : checkState(value.pin, fail);
|
|
205
|
+
const pin = value.pin === undefined ? undefined : checkState(value.pin, fail, "pin");
|
|
199
206
|
const compare = value.checkpoint === undefined ? undefined : { checkpoint: value.checkpoint as string, state: checkState(value.state, fail), ...(pin === undefined ? {} : { pin }) };
|
|
200
207
|
const inputs = checkInputs(value.inputs, fail);
|
|
201
208
|
if (!Array.isArray(value.steps) || value.steps.length === 0) fail("steps must be a list with at least one step");
|
|
@@ -219,7 +226,14 @@ export function readRecordFile(path: string): RecordFile {
|
|
|
219
226
|
/** What the replay core cannot do of a step that is right for VICE: it has the C64 alone, no raster wait and no conditions. */
|
|
220
227
|
function checkCoreStep(step: ActionStep, at: string, fail: (message: string) => never): void {
|
|
221
228
|
if ("write" in step && step.write.space !== "c64") fail(`${at}: the replay core has the C64 alone, not ${step.write.space}`);
|
|
222
|
-
if ("press" in step)
|
|
229
|
+
if ("press" in step) {
|
|
230
|
+
try {
|
|
231
|
+
keyCodes(step.press);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (isRefusal(error)) fail(`${at}: ${error.message}`);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
223
237
|
if ("type" in step) {
|
|
224
238
|
const keys = keyboardBytes(step.type);
|
|
225
239
|
if (typeof keys !== "string" && keys.length > QUEUE_SIZE) fail(`${at}: the KERNAL keyboard queue holds ${QUEUE_SIZE} keys; give more type steps, or press for keys of the keyboard matrix`);
|
|
@@ -242,10 +256,31 @@ export function readRecording(folder: string): Recording {
|
|
|
242
256
|
if (isRefusal(error)) throw error;
|
|
243
257
|
throw new Refusal("invalid-input", `recording ${path}: ${(error as Error).message}`);
|
|
244
258
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!
|
|
259
|
+
const fail = (message: string): never => {
|
|
260
|
+
throw new Refusal("invalid-input", `recording ${path}: ${message}`);
|
|
261
|
+
};
|
|
262
|
+
if (!isObject(value) || value.format !== RECORDING_FORMAT) return fail(`it is not a recording of format ${RECORDING_FORMAT}`);
|
|
263
|
+
for (const key of ["name", "program", "sha256", "symbols", "hash"]) if (typeof value[key] !== "string") fail(`${key} is missing`);
|
|
264
|
+
for (const key of ["inputs", "events", "sites"]) if (!Array.isArray(value[key])) fail(`${key} is missing`);
|
|
265
|
+
if (!isInteger(value.ticks, 1, Number.MAX_SAFE_INTEGER)) fail("ticks is missing");
|
|
266
|
+
// A recording of an older version can have the step forms of the 2.0 files: refused with the form to write, never replayed wrong.
|
|
267
|
+
const remedy = "Record the session again with record, or write the 2.1 form in recording.json";
|
|
268
|
+
if (value.compare !== undefined) {
|
|
269
|
+
const compare = isObject(value.compare) ? value.compare : fail("compare must be an object with checkpoint, state and pin");
|
|
270
|
+
const where = (message: string): never => fail(`compare: ${message}. ${remedy}`);
|
|
271
|
+
if (typeof compare.checkpoint !== "string" || compare.checkpoint === "") where("checkpoint must be the name or $ address of a routine");
|
|
272
|
+
checkState(compare.state, where);
|
|
273
|
+
if (compare.pin !== undefined) checkState(compare.pin, where, "pin");
|
|
274
|
+
}
|
|
275
|
+
(value.events as unknown[]).forEach((event, index) => {
|
|
276
|
+
const at = `event ${index + 1}`;
|
|
277
|
+
const where = (message: string): never => fail(`${message}. ${remedy}`);
|
|
278
|
+
if (!isObject(event) || !isObject(event.action) || !isObject(event.key)) return where(`${at} must be an object with action and key`);
|
|
279
|
+
const named = typeof event.step === "number" ? `${at} (step ${event.step})` : at;
|
|
280
|
+
if (event.action.restore === true && Object.keys(event.action).length === 1) return;
|
|
281
|
+
// The action with the defaults filled in, as the record file gave it.
|
|
282
|
+
event.action = checkActionStep(stepKind(event.action, ACTION_STEP_KINDS, named, where), event.action, named, where, { press: true });
|
|
283
|
+
});
|
|
249
284
|
return value as unknown as Recording;
|
|
250
285
|
}
|
|
251
286
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
|
|
9
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
10
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
9
11
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
10
12
|
import { C64RunSession } from "#src/native/c64run.ts";
|
|
11
13
|
import { localToolContext } from "#src/native/local.ts";
|
|
@@ -35,7 +37,7 @@ export async function replay(values: ReplayOptions): Promise<ReplayResult> {
|
|
|
35
37
|
const folder = values.folder.replace(/\/+$/, "").replace(/\/recording\.json$/, "");
|
|
36
38
|
const recording = readRecording(folder);
|
|
37
39
|
const given: Comparison | undefined =
|
|
38
|
-
values.checkpoint === undefined ? undefined : { checkpoint: values.checkpoint, state: values.state.map(parseStateItem), ...(values.pin.length === 0 ? {} : { pin: values.pin.map(parseStateItem) }) };
|
|
40
|
+
values.checkpoint === undefined ? undefined : { checkpoint: values.checkpoint, state: values.state.map((item) => parseStateItem(item, "--state")), ...(values.pin.length === 0 ? {} : { pin: values.pin.map((item) => parseStateItem(item, "--pin")) }) };
|
|
39
41
|
const compare = given ?? recording.compare;
|
|
40
42
|
if (values.rebuild !== undefined && compare === undefined) throw new UsageError("the recording has no checkpoint and state: give --checkpoint and --state");
|
|
41
43
|
const result = (outcome: ReplayResult["result"], side: ReplayResult["side"], fields: Partial<ReplayResult> = {}): ReplayResult => ({
|
|
@@ -47,6 +49,15 @@ export async function replay(values: ReplayOptions): Promise<ReplayResult> {
|
|
|
47
49
|
compared: 0,
|
|
48
50
|
...fields,
|
|
49
51
|
});
|
|
52
|
+
// The files of the rebuild are checked before the replay core starts.
|
|
53
|
+
for (const [option, check] of [["--rebuild", () => values.rebuild !== undefined && readPrg(values.rebuild)], ["--symbols", () => values.symbols !== undefined && namesOf(values.symbols)]] as const) {
|
|
54
|
+
try {
|
|
55
|
+
check();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isRefusal(error)) throw new Refusal(error.code, `${option}: ${error.message}`);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
50
61
|
const sha256 = createHash("sha256").update(readProjectFile(recording.program).bytes).digest("hex");
|
|
51
62
|
if (sha256 !== recording.sha256) return result("INCONCLUSIVE", "original", { reason: `${recording.program} is not the program of the recording (its SHA-256 changed); record the session again` });
|
|
52
63
|
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
7
7
|
import { runProgram } from "#src/host-client/autostart.ts";
|
|
8
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
8
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
9
|
-
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, runActionStep, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, checkStepNames, isInteger, isObject, refuseOldStep, renamedField, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
11
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
10
12
|
import { MAX_MEMORY_READ, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, WireFailure, type TraceHit, type TraceKind } from "#src/host-client/tools.ts";
|
|
11
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
12
14
|
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
@@ -35,6 +37,40 @@ const TRACE_KEYS = ["trace", "points", "frames", "maxHits"];
|
|
|
35
37
|
const OBSERVE_KEYS = ["observe", "memory", "registers", "vicii", "sprites"];
|
|
36
38
|
/** A name of a file that a step writes: letters, digits, dots, underscores or hyphens. */
|
|
37
39
|
const NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
40
|
+
/** The file of the observations, which every run writes. */
|
|
41
|
+
const RESULTS = "results.json";
|
|
42
|
+
|
|
43
|
+
/** The file name of an observed memory item that goes into its own file: more than one read gives. */
|
|
44
|
+
const memoryFile = (observe: string, address: string) => `${observe}-${address.replace(/^\$/, "")}.bin`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The files that each step of a run writes into --out, by step. Two steps
|
|
48
|
+
* that write one name (also in another case: some file systems do not tell
|
|
49
|
+
* them apart) are refused: the second would overwrite the first.
|
|
50
|
+
*/
|
|
51
|
+
function checkFileNames(steps: RunStep[], fail: (message: string) => never): void {
|
|
52
|
+
const writers = new Map<string, string>([[RESULTS, `the run itself (${RESULTS})`]]);
|
|
53
|
+
steps.forEach((step, index) => {
|
|
54
|
+
const named: [string, string[]] =
|
|
55
|
+
"save" in step
|
|
56
|
+
? ["save", [`${step.save}.vsf`]]
|
|
57
|
+
: "screen" in step
|
|
58
|
+
? ["screen", [`${step.screen}.png`]]
|
|
59
|
+
: "coverage" in step
|
|
60
|
+
? ["coverage", [`${step.coverage}.bin`]]
|
|
61
|
+
: "trace" in step
|
|
62
|
+
? ["trace", [`${step.trace}.json`]]
|
|
63
|
+
: "observe" in step
|
|
64
|
+
? ["observe", step.memory === "all" ? [`${step.observe}.bin`] : (step.memory ?? []).flatMap((item) => (typeof item !== "string" && (item.size ?? 1) > MAX_MEMORY_READ ? [memoryFile(step.observe, item.address)] : []))]
|
|
65
|
+
: ["", []];
|
|
66
|
+
const [kind, files] = named;
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const other = writers.get(file.toLowerCase());
|
|
69
|
+
if (other !== undefined) fail(`step ${index + 1} (${kind}) writes ${file}, and ${other} writes that file too. Give one of them another name`);
|
|
70
|
+
writers.set(file.toLowerCase(), `step ${index + 1} (${kind})`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
38
74
|
|
|
39
75
|
function readRun(path: string): Run {
|
|
40
76
|
let value: unknown;
|
|
@@ -60,8 +96,10 @@ function readRun(path: string): Run {
|
|
|
60
96
|
const at = `step ${index + 1}`;
|
|
61
97
|
if (!isObject(step)) return fail(`${at} must be an object`);
|
|
62
98
|
const kinds = "observe" in step ? ["observe"] : "trace" in step ? ["trace"] : Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key) || (OWN_KINDS as readonly string[]).includes(key));
|
|
63
|
-
if (kinds.length
|
|
99
|
+
if (kinds.length > 1) return fail(`${at} has ${kinds.length} step kinds: ${kinds.join(", ")}. Give each one its own step`);
|
|
100
|
+
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...VICE_STEP_KINDS, ...OWN_KINDS].join(", ")}`);
|
|
64
101
|
const kind = kinds[0]!;
|
|
102
|
+
refuseOldStep(kind, step, at, fail);
|
|
65
103
|
const allowed = kind === "observe" ? OBSERVE_KEYS : kind === "trace" ? TRACE_KEYS : [kind];
|
|
66
104
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
67
105
|
if ((ACTION_STEP_KINDS as readonly string[]).includes(kind)) {
|
|
@@ -81,7 +119,8 @@ function readRun(path: string): Run {
|
|
|
81
119
|
const points = step.points;
|
|
82
120
|
if (!Array.isArray(points) || points.length === 0 || points.length > MAX_TRACE_POINTS) return fail(`${at}: points is a list of 1 to ${MAX_TRACE_POINTS} objects with kind and start`);
|
|
83
121
|
for (const point of points as unknown[]) {
|
|
84
|
-
if (!isObject(point)
|
|
122
|
+
if (!isObject(point)) return fail(`${at}: a point is an object with kind, start and end`);
|
|
123
|
+
for (const key of Object.keys(point)) if (!["kind", "start", "end"].includes(key)) fail(`${at}: a point has the field ${key}. ${renamedField(key, ["kind", "start", "end"]) ?? "A point has kind, start and end, and nothing else"}`);
|
|
85
124
|
if (!(TRACE_KINDS as readonly unknown[]).includes(point.kind)) fail(`${at}: a point kind is exec, read or write`);
|
|
86
125
|
if (typeof point.start !== "string" || (point.end !== undefined && typeof point.end !== "string")) fail(`${at}: a point needs start, and end when it is a range, as names or $ addresses`);
|
|
87
126
|
}
|
|
@@ -92,6 +131,16 @@ function readRun(path: string): Run {
|
|
|
92
131
|
if (kind !== "observe") return;
|
|
93
132
|
if (labels.has(name as string)) fail(`${at} needs a new observation name`);
|
|
94
133
|
labels.add(name as string);
|
|
134
|
+
const itemFields = ["address", "size", "view"];
|
|
135
|
+
if (Array.isArray(step.memory)) {
|
|
136
|
+
for (const item of step.memory as unknown[]) {
|
|
137
|
+
if (!isObject(item)) continue;
|
|
138
|
+
for (const key of Object.keys(item)) {
|
|
139
|
+
const renamed = renamedField(key, itemFields);
|
|
140
|
+
if (renamed !== undefined && !itemFields.includes(key)) fail(`${at}: a memory object has the field ${key}. ${renamed}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
95
144
|
const memoryItem = (item: unknown) =>
|
|
96
145
|
typeof item === "string" ||
|
|
97
146
|
(isObject(item) &&
|
|
@@ -106,9 +155,35 @@ function readRun(path: string): Run {
|
|
|
106
155
|
if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
|
|
107
156
|
if (step.sprites !== undefined && (!Array.isArray(step.sprites) || !step.sprites.every((sprite) => isInteger(sprite, 0, 7)))) fail(`${at}: sprites is a list of numbers from 0 to 7`);
|
|
108
157
|
});
|
|
158
|
+
checkFileNames(value.steps as RunStep[], fail);
|
|
109
159
|
return value as unknown as Run;
|
|
110
160
|
}
|
|
111
161
|
|
|
162
|
+
/** The remedy for a start program that is no PRG. */
|
|
163
|
+
const PROGRAM_REMEDY = "A run starts a PRG. Get the PRG out of the disk, tape or cartridge with disk.ts read (or disk.ts crt) of the c64-disk skill";
|
|
164
|
+
|
|
165
|
+
/** Checks each file that the run reads before the emulator starts: the start program or snapshot, and each snapshot step. A refusal names the run file and the step. */
|
|
166
|
+
function checkRunFiles(path: string, run: Run): void {
|
|
167
|
+
const named = (where: string, check: () => unknown) => {
|
|
168
|
+
try {
|
|
169
|
+
check();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (isRefusal(error)) throw new Refusal(error.code, `run ${path}: ${where}: ${error.message}`);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
if ("program" in run.start) {
|
|
176
|
+
const { program } = run.start;
|
|
177
|
+
named("start.program", () => readPrg(program, PROGRAM_REMEDY));
|
|
178
|
+
} else {
|
|
179
|
+
const { snapshot } = run.start;
|
|
180
|
+
named("start.snapshot", () => readProjectFile(snapshot));
|
|
181
|
+
}
|
|
182
|
+
run.steps.forEach((step, index) => {
|
|
183
|
+
if ("snapshot" in step) named(`step ${index + 1}: snapshot`, () => readProjectFile(step.snapshot));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
112
187
|
/** A trace hit with $ addresses, as a person reads it. */
|
|
113
188
|
function showHit(hit: TraceHit) {
|
|
114
189
|
return { ...hit, pc: formatC64Address(hit.pc), ...(hit.address === undefined ? {} : { address: formatC64Address(hit.address) }) };
|
|
@@ -117,6 +192,7 @@ function showHit(hit: TraceHit) {
|
|
|
117
192
|
/** Runs the run file and writes its files into `out`. failed: a step did not run. */
|
|
118
193
|
export async function runFile(path: string, out: string): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
119
194
|
const run = readRun(path);
|
|
195
|
+
checkRunFiles(path, run);
|
|
120
196
|
const symbols = namesOf(run.symbols ?? "none");
|
|
121
197
|
const at = (location: string): number => {
|
|
122
198
|
if (location.startsWith("$")) return parseC64Address(location);
|
|
@@ -124,6 +200,10 @@ export async function runFile(path: string, out: string): Promise<{ output: Reco
|
|
|
124
200
|
if (address === undefined) throw new UsageError(`the symbols have no ${location}`);
|
|
125
201
|
return address;
|
|
126
202
|
};
|
|
203
|
+
// The names of the action steps resolve before the emulator starts.
|
|
204
|
+
run.steps.forEach((step, index) => {
|
|
205
|
+
if ((ACTION_STEP_KINDS as readonly string[]).some((kind) => kind in step) && !("observe" in step) && !("trace" in step)) checkStepNames([step as ActionStep], at, `run ${path}`, index + 1);
|
|
206
|
+
});
|
|
127
207
|
const files: string[] = [];
|
|
128
208
|
const written: Written[] = [];
|
|
129
209
|
const write = (name: string, bytes: Uint8Array) => {
|
|
@@ -185,7 +265,7 @@ export async function runFile(path: string, out: string): Promise<{ output: Reco
|
|
|
185
265
|
// More than one read gives goes to a file: results.json stays small.
|
|
186
266
|
if (size <= MAX_MEMORY_READ) memory[spec.address] = bytes.toString("hex");
|
|
187
267
|
else {
|
|
188
|
-
const file =
|
|
268
|
+
const file = memoryFile(step.observe, spec.address);
|
|
189
269
|
write(file, bytes);
|
|
190
270
|
memory[spec.address] = { file };
|
|
191
271
|
}
|
|
@@ -7,8 +7,9 @@ import { isRefusal, Refusal } from "#src/errors.ts";
|
|
|
7
7
|
import { c64AddressOf, formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
8
8
|
import { MAX_BASELINES, MAX_MASK_RECTANGLES, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
|
|
9
9
|
import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
|
|
10
|
-
import { ACTION_STEP_KINDS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type ActionStepKind, type Location } from "#src/host-client/steps.ts";
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, checkLocation, isInteger, isObject, refuseOldStep, renamedField, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind, type Location } from "#src/host-client/steps.ts";
|
|
11
11
|
import { applyEntryState, parseEntryState } from "#src/host-client/entry-state.ts";
|
|
12
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
12
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
13
14
|
import { readMemoryInParts, type ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
14
15
|
import { openForRead } from "#src/knowledge/database.ts";
|
|
@@ -90,6 +91,8 @@ export interface ScenarioResult {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
const STEP_KINDS = ["reset", "load", "autostart", "snapshot", "normalize", ...ACTION_STEP_KINDS, "observe"];
|
|
94
|
+
/** The step kinds that a scenario runs: press is for the replay core only. */
|
|
95
|
+
const SHOWN_KINDS = ["reset", "load", "autostart", "snapshot", "normalize", ...VICE_STEP_KINDS, "observe"];
|
|
93
96
|
|
|
94
97
|
/** The keys each kind of step can have. */
|
|
95
98
|
const STEP_KEYS: Record<string, readonly string[]> = {
|
|
@@ -128,8 +131,10 @@ export function readScenario(path: string): Scenario {
|
|
|
128
131
|
if (!isObject(step)) return fail(`${at} must be an object`);
|
|
129
132
|
// An observe step names what it observes with "memory" and "registers" too.
|
|
130
133
|
const kinds = "observe" in step ? ["observe"] : Object.keys(step).filter((key) => STEP_KINDS.includes(key));
|
|
131
|
-
if (kinds.length
|
|
134
|
+
if (kinds.length > 1) return fail(`${at} has ${kinds.length} step kinds: ${kinds.join(", ")}. Give each one its own step`);
|
|
135
|
+
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${SHOWN_KINDS.join(", ")}`);
|
|
132
136
|
const kind = kinds[0]!;
|
|
137
|
+
refuseOldStep(kind, step, at, fail);
|
|
133
138
|
const allowed = [...(STEP_KEYS[kind] ?? [kind]), ...(kind === "observe" ? [] : ["side"])];
|
|
134
139
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
135
140
|
if (step.side !== undefined && !(SIDES as unknown[]).includes(step.side)) fail(`${at}: side must be original or rebuild`);
|
|
@@ -180,12 +185,15 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
180
185
|
checkLocation(item, `${at}: memory`, fail);
|
|
181
186
|
continue;
|
|
182
187
|
}
|
|
183
|
-
|
|
188
|
+
const itemFields = ["address", "size", "view", "tolerance", "exclude"];
|
|
189
|
+
if (!isObject(item)) return fail(`${at}: memory must be a list of names, $ addresses or objects with address`);
|
|
190
|
+
for (const key of Object.keys(item)) if (!itemFields.includes(key)) fail(`${at}: a memory object has the field ${key}. ${renamedField(key, itemFields) ?? `It has ${itemFields.join(", ")}, and nothing else`}`);
|
|
184
191
|
if (item.view !== undefined && item.view !== "cpu" && item.view !== "ram") fail(`${at}: memory.view must be cpu or ram`);
|
|
185
192
|
if (item.exclude !== undefined) {
|
|
186
193
|
if (!Array.isArray(item.exclude) || item.exclude.length === 0) fail(`${at}: memory.exclude must be a list of objects with address and reason`);
|
|
187
194
|
for (const excluded of item.exclude as unknown[]) {
|
|
188
|
-
if (!isObject(excluded)
|
|
195
|
+
if (!isObject(excluded)) return fail(`${at}: memory.exclude must be a list of objects with address and reason`);
|
|
196
|
+
for (const key of Object.keys(excluded)) if (key !== "address" && key !== "reason") fail(`${at}: an exclude has the field ${key}. ${renamedField(key, ["address", "reason"]) ?? "It has address and reason, and nothing else"}`);
|
|
189
197
|
checkLocation(excluded.address, `${at}: memory.exclude.address`, fail);
|
|
190
198
|
if (typeof excluded.reason !== "string" || excluded.reason.trim() === "") fail(`${at}: an exclude needs a reason: why this byte may differ`);
|
|
191
199
|
}
|
|
@@ -276,6 +284,33 @@ export function namesOf(source: string, kinds: "code" | "all" = "all"): Map<stri
|
|
|
276
284
|
return names;
|
|
277
285
|
}
|
|
278
286
|
|
|
287
|
+
/** The remedy for a scenario program that is no PRG. */
|
|
288
|
+
const PROGRAM_REMEDY = "A scenario program is a PRG. Get the PRG out of the disk, tape or cartridge with disk.ts read (or disk.ts crt) of the c64-disk skill";
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Checks each file that the scenario names before an emulator starts: the
|
|
292
|
+
* program and the symbols of each side, and each snapshot file. A missing
|
|
293
|
+
* file is not-found; a program that is no PRG and a symbols file with no
|
|
294
|
+
* names are invalid-input. Gives the names of each side.
|
|
295
|
+
*/
|
|
296
|
+
export function scenarioNames(scenario: Scenario): Map<Side, Map<string, number>> {
|
|
297
|
+
const names = new Map<Side, Map<string, number>>();
|
|
298
|
+
const named = <T>(field: string, read: () => T): T => {
|
|
299
|
+
try {
|
|
300
|
+
return read();
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (isRefusal(error)) throw new Refusal(error.code, `${field}: ${error.message}`);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
for (const side of SIDES) {
|
|
307
|
+
named(`${side}.program`, () => readPrg(scenario[side].program, PROGRAM_REMEDY));
|
|
308
|
+
names.set(side, named(`${side}.symbols`, () => namesOf(scenario[side].symbols)));
|
|
309
|
+
}
|
|
310
|
+
for (const step of scenario.steps) if ("snapshot" in step) readProjectFile(step.snapshot);
|
|
311
|
+
return names;
|
|
312
|
+
}
|
|
313
|
+
|
|
279
314
|
/** A failure that makes the comparison impossible: the result is INCONCLUSIVE. */
|
|
280
315
|
class Inconclusive extends Error {}
|
|
281
316
|
|
|
@@ -466,12 +501,14 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
|
|
|
466
501
|
}
|
|
467
502
|
|
|
468
503
|
/** Runs the scenario on the original, then on the rebuild, in one VICE session. With out, a FAIL leaves files there. */
|
|
469
|
-
export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string } = {}): Promise<ScenarioResult> {
|
|
504
|
+
export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string; names?: Map<Side, Map<string, number>> } = {}): Promise<ScenarioResult> {
|
|
470
505
|
const checkpoints = scenario.steps.flatMap((step) => ("observe" in step ? [step.observe] : []));
|
|
471
506
|
const observed = new Map<Side, Map<string, Observation>>();
|
|
507
|
+
// The names of both sides before either side runs: a bad symbols file is an error, not a result.
|
|
508
|
+
const names = options.names ?? new Map(SIDES.map((side) => [side, namesOf(scenario[side].symbols)]));
|
|
472
509
|
for (const side of SIDES) {
|
|
473
510
|
try {
|
|
474
|
-
observed.set(side, await runSide(client, scenario, side,
|
|
511
|
+
observed.set(side, await runSide(client, scenario, side, names.get(side)!, options.out !== undefined));
|
|
475
512
|
} catch (error) {
|
|
476
513
|
if (error instanceof Inconclusive) return { result: "INCONCLUSIVE", scenario: scenario.name, reason: error.message, checkpoints, differences: [] };
|
|
477
514
|
if (error instanceof WireFailure) {
|