@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
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 +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- 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 +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -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 +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- 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 +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- 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} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- 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 +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -3,22 +3,15 @@
|
|
|
3
3
|
// Observations name semantic symbols that resolve per side, comparisons are
|
|
4
4
|
// explicit, and the result is PASS, FAIL or INCONCLUSIVE.
|
|
5
5
|
|
|
6
|
-
import { formatC64Address,
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
MAX_MASK_RECTANGLES,
|
|
13
|
-
MAX_MEMORY_READ,
|
|
14
|
-
MAX_MEMORY_WRITE,
|
|
15
|
-
MAX_TIMEOUT_FRAMES,
|
|
16
|
-
WireFailure,
|
|
17
|
-
} from "#src/host-client/tools.ts";
|
|
18
|
-
import { readProjectFile } from "#src/host-client/transfer.ts";
|
|
19
|
-
import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
6
|
+
import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
7
|
+
import { MAX_BASELINES, MAX_MASK_RECTANGLES, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
|
|
8
|
+
import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
|
|
9
|
+
import { ACTION_STEP_KEYS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
10
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
11
|
+
import { readMemoryInParts, type ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
20
12
|
import { openForRead } from "#src/knowledge/database.ts";
|
|
21
|
-
import { listSymbols } from "#src/knowledge/read.ts";
|
|
13
|
+
import { listAnalyzerNames, listSymbols } from "#src/knowledge/read.ts";
|
|
14
|
+
import { programImage } from "#src/knowledge/program.ts";
|
|
22
15
|
|
|
23
16
|
export class ScenarioError extends Error {}
|
|
24
17
|
|
|
@@ -26,14 +19,18 @@ export type Side = "original" | "rebuild";
|
|
|
26
19
|
export const SIDES: Side[] = ["original", "rebuild"];
|
|
27
20
|
|
|
28
21
|
/** A "$xxxx" address or a symbol name that each side resolves on its own. */
|
|
29
|
-
export type Location
|
|
22
|
+
export type { Location };
|
|
30
23
|
|
|
31
24
|
export interface MemoryObservation {
|
|
32
25
|
at: Location;
|
|
33
|
-
/** Bytes from `at`; 1 when not given. */
|
|
26
|
+
/** Bytes from `at`; 1 when not given, at most 64 KiB. */
|
|
34
27
|
size?: number;
|
|
28
|
+
/** cpu (the default) reads as the CPU sees memory; ram reads the RAM under the ROMs and the I/O area (M23.9). */
|
|
29
|
+
view?: "cpu" | "ram";
|
|
35
30
|
/** The largest allowed difference per byte; 0 when not given. */
|
|
36
31
|
tolerance?: number;
|
|
32
|
+
/** Bytes in the range that the comparison leaves out, each with the reason (D46). */
|
|
33
|
+
exclude?: Array<{ at: Location; reason: string }>;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
export interface ScreenPolicy {
|
|
@@ -43,21 +40,21 @@ export interface ScreenPolicy {
|
|
|
43
40
|
mask?: Array<{ x: number; y: number; width: number; height: number }>;
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
|
|
43
|
+
type StepBody =
|
|
47
44
|
| { reset: "hard" | "soft" }
|
|
48
45
|
| { load: true }
|
|
49
46
|
| { autostart: true }
|
|
50
|
-
| {
|
|
51
|
-
| {
|
|
52
|
-
|
|
|
53
|
-
| { type: string }
|
|
54
|
-
| { frames: number }
|
|
55
|
-
| { runUntil: { at: Location } | { memory: { at: Location; equals: number } }; timeoutFrames: number }
|
|
47
|
+
| { snapshot: string }
|
|
48
|
+
| { normalize: true }
|
|
49
|
+
| ActionStep
|
|
56
50
|
| { observe: string; memory?: Array<Location | MemoryObservation>; registers?: ObservedRegister[]; screen?: ScreenPolicy; vicii?: boolean; sprites?: number[] };
|
|
57
51
|
|
|
52
|
+
/** A step; with side, only that side does it (D46). An observe step has no side: it compares both. */
|
|
53
|
+
export type Step = StepBody & { side?: Side };
|
|
54
|
+
|
|
58
55
|
export interface Subject {
|
|
59
56
|
program: string;
|
|
60
|
-
/** "knowledge" for knowledge
|
|
57
|
+
/** "knowledge" for the project knowledge, a project JSON file (c64-assembler output or a name → "$xxxx" map), or "none". */
|
|
61
58
|
symbols: string;
|
|
62
59
|
}
|
|
63
60
|
|
|
@@ -85,26 +82,23 @@ export interface ScenarioResult {
|
|
|
85
82
|
reason?: string;
|
|
86
83
|
checkpoints: string[];
|
|
87
84
|
differences: Difference[];
|
|
85
|
+
/** The bytes that the comparison left out, with their reasons (D46). */
|
|
86
|
+
excluded?: Array<{ checkpoint: string; at: Location; reason: string }>;
|
|
87
|
+
/** The files that a FAIL wrote with --out. */
|
|
88
|
+
files?: string[];
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
const STEP_KINDS = ["reset", "load", "autostart", "write", "registers", "joystick", "type", "frames", "runUntil", "observe"];
|
|
91
|
+
const STEP_KINDS = ["reset", "load", "autostart", "snapshot", "normalize", "write", "registers", "joystick", "type", "frames", "runUntil", "observe"];
|
|
91
92
|
|
|
92
93
|
/** The keys each kind of step can have. */
|
|
93
94
|
const STEP_KEYS: Record<string, readonly string[]> = {
|
|
94
|
-
|
|
95
|
+
...ACTION_STEP_KEYS,
|
|
95
96
|
observe: ["observe", "memory", "registers", "screen", "vicii", "sprites"],
|
|
96
97
|
};
|
|
97
98
|
|
|
98
99
|
/** The registers an observe step can compare. */
|
|
99
100
|
export const OBSERVED_REGISTERS = ["pc", "a", "x", "y", "sp", "flags"] as const;
|
|
100
101
|
type ObservedRegister = (typeof OBSERVED_REGISTERS)[number];
|
|
101
|
-
/** The registers a registers step can set. */
|
|
102
|
-
const SET_REGISTERS = ["pc", "a", "x", "y", "sp"];
|
|
103
|
-
|
|
104
|
-
type JoystickDirection = (typeof JOYSTICK_DIRECTIONS)[number];
|
|
105
|
-
|
|
106
|
-
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107
|
-
const isInteger = (value: unknown, min: number, max: number): value is number => Number.isInteger(value) && (value as number) >= min && (value as number) <= max;
|
|
108
102
|
|
|
109
103
|
/** Reads and checks a scenario file. Throws ScenarioError naming the first problem. */
|
|
110
104
|
export function readScenario(path: string): Scenario {
|
|
@@ -135,9 +129,10 @@ export function readScenario(path: string): Scenario {
|
|
|
135
129
|
const kinds = "observe" in step ? ["observe"] : Object.keys(step).filter((key) => STEP_KINDS.includes(key));
|
|
136
130
|
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${STEP_KINDS.join(", ")}`);
|
|
137
131
|
const kind = kinds[0]!;
|
|
138
|
-
const allowed = STEP_KEYS[kind] ?? [kind];
|
|
132
|
+
const allowed = [...(STEP_KEYS[kind] ?? [kind]), ...(kind === "observe" ? [] : ["side"])];
|
|
139
133
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
140
|
-
|
|
134
|
+
if (step.side !== undefined && !(SIDES as unknown[]).includes(step.side)) fail(`${at}: side must be original or rebuild`);
|
|
135
|
+
checkStep(kind, step, at, fail, scenario.videoStandard ?? "pal");
|
|
141
136
|
if (kind === "observe") {
|
|
142
137
|
if (labels.has(step.observe as string)) fail(`${at} needs a new checkpoint name`);
|
|
143
138
|
labels.add(step.observe as string);
|
|
@@ -149,20 +144,8 @@ export function readScenario(path: string): Scenario {
|
|
|
149
144
|
return scenario;
|
|
150
145
|
}
|
|
151
146
|
|
|
152
|
-
/** Checks a "$xxxx" address or a symbol name. */
|
|
153
|
-
function checkLocation(value: unknown, what: string, fail: (message: string) => never): void {
|
|
154
|
-
if (typeof value !== "string" || value === "") fail(`${what} must be a $ address or a symbol name`);
|
|
155
|
-
if ((value as string).startsWith("$")) {
|
|
156
|
-
try {
|
|
157
|
-
parseC64Address(value as string);
|
|
158
|
-
} catch {
|
|
159
|
-
fail(`${what} must be $ followed by four hex digits, not ${value as string}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
147
|
/** Checks the value of one step against its kind. */
|
|
165
|
-
function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
148
|
+
function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
|
|
166
149
|
switch (kind) {
|
|
167
150
|
case "reset":
|
|
168
151
|
if (step.reset !== "hard" && step.reset !== "soft") fail(`${at}: reset must be hard or soft`);
|
|
@@ -171,66 +154,25 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
|
|
|
171
154
|
case "autostart":
|
|
172
155
|
if (step[kind] !== true) fail(`${at}: ${kind} must be true`);
|
|
173
156
|
return;
|
|
174
|
-
case "
|
|
175
|
-
|
|
176
|
-
if (!isObject(write) || Object.keys(write).some((key) => key !== "at" && key !== "bytes")) return fail(`${at}: write needs at and bytes, and nothing else`);
|
|
177
|
-
checkLocation(write.at, `${at}: write.at`, fail);
|
|
178
|
-
if (!Array.isArray(write.bytes) || write.bytes.length === 0 || write.bytes.length > MAX_MEMORY_WRITE || !write.bytes.every((byte) => isInteger(byte, 0, 0xff))) {
|
|
179
|
-
fail(`${at}: write.bytes must be a list of 1 to ${MAX_MEMORY_WRITE} numbers from 0 to 255`);
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
case "registers": {
|
|
184
|
-
const registers = step.registers;
|
|
185
|
-
if (!isObject(registers) || Object.keys(registers).length === 0) return fail(`${at}: registers must name at least one of ${SET_REGISTERS.join(", ")}`);
|
|
186
|
-
for (const [name, value] of Object.entries(registers)) {
|
|
187
|
-
if (!SET_REGISTERS.includes(name)) fail(`${at}: registers can set only ${SET_REGISTERS.join(", ")}, not ${name}`);
|
|
188
|
-
if (typeof value === "string") checkLocation(value, `${at}: registers.${name}`, fail);
|
|
189
|
-
else if (!isInteger(value, 0, name === "pc" ? 0xffff : 0xff)) fail(`${at}: registers.${name} must be a symbol name, a $ address or a number from 0 to ${name === "pc" ? 65535 : 255}`);
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
case "joystick": {
|
|
194
|
-
const joystick = step.joystick;
|
|
195
|
-
if (!isObject(joystick) || Object.keys(joystick).some((key) => !["port", "direction", "fire"].includes(key))) return fail(`${at}: joystick needs port and direction, and fire is optional`);
|
|
196
|
-
if (joystick.port !== 1 && joystick.port !== 2) fail(`${at}: joystick.port must be 1 or 2`);
|
|
197
|
-
if (!(JOYSTICK_DIRECTIONS as readonly unknown[]).includes(joystick.direction)) fail(`${at}: joystick.direction must be one of ${JOYSTICK_DIRECTIONS.join(", ")}`);
|
|
198
|
-
if (joystick.fire !== undefined && typeof joystick.fire !== "boolean") fail(`${at}: joystick.fire must be true or false`);
|
|
157
|
+
case "snapshot":
|
|
158
|
+
if (typeof step.snapshot !== "string" || step.snapshot === "") fail(`${at}: snapshot needs a .vsf file in the project`);
|
|
199
159
|
return;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (typeof step.type !== "string" || step.type === "") return fail(`${at}: type needs text`);
|
|
203
|
-
let bytes: Uint8Array;
|
|
204
|
-
try {
|
|
205
|
-
bytes = textToPetscii(step.type);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
return fail(`${at}: ${(error as Error).message}`);
|
|
208
|
-
}
|
|
209
|
-
if (bytes.length > MAX_KEYBOARD_BYTES) fail(`${at}: type can have at most ${MAX_KEYBOARD_BYTES} keys`);
|
|
160
|
+
case "normalize":
|
|
161
|
+
if (step.normalize !== true) fail(`${at}: normalize is true: it writes the entry state of the knowledge`);
|
|
210
162
|
return;
|
|
211
|
-
|
|
163
|
+
case "write":
|
|
164
|
+
case "registers":
|
|
165
|
+
case "joystick":
|
|
166
|
+
case "type":
|
|
212
167
|
case "frames":
|
|
213
|
-
|
|
214
|
-
return;
|
|
215
|
-
case "runUntil": {
|
|
216
|
-
const target = step.runUntil;
|
|
217
|
-
if (!isObject(target) || Object.keys(target).length !== 1 || !("at" in target || "memory" in target)) return fail(`${at}: runUntil needs at or memory`);
|
|
218
|
-
if ("at" in target) checkLocation(target.at, `${at}: runUntil.at`, fail);
|
|
219
|
-
else {
|
|
220
|
-
const memory = target.memory;
|
|
221
|
-
if (!isObject(memory) || Object.keys(memory).some((key) => key !== "at" && key !== "equals")) return fail(`${at}: runUntil.memory needs at and equals, and nothing else`);
|
|
222
|
-
checkLocation(memory.at, `${at}: runUntil.memory.at`, fail);
|
|
223
|
-
if (!isInteger(memory.equals, 0, 0xff)) fail(`${at}: runUntil.memory.equals must be a number from 0 to 255`);
|
|
224
|
-
}
|
|
225
|
-
if (!isInteger(step.timeoutFrames, 1, MAX_TIMEOUT_FRAMES)) fail(`${at} needs timeoutFrames from 1 to ${MAX_TIMEOUT_FRAMES}`);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
168
|
+
case "runUntil":
|
|
169
|
+
return checkActionStep(kind, step, at, fail);
|
|
228
170
|
default:
|
|
229
|
-
checkObserve(step, at, fail);
|
|
171
|
+
checkObserve(step, at, fail, videoStandard);
|
|
230
172
|
}
|
|
231
173
|
}
|
|
232
174
|
|
|
233
|
-
function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
175
|
+
function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
|
|
234
176
|
if (typeof step.observe !== "string" || step.observe === "") fail(`${at} needs a new checkpoint name`);
|
|
235
177
|
if (step.memory !== undefined) {
|
|
236
178
|
if (!Array.isArray(step.memory) || step.memory.length === 0) fail(`${at}: memory must be a list of names, $ addresses or objects with at`);
|
|
@@ -239,9 +181,18 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
239
181
|
checkLocation(item, `${at}: memory`, fail);
|
|
240
182
|
continue;
|
|
241
183
|
}
|
|
242
|
-
if (!isObject(item) || Object.keys(item).some((key) => !["at", "size", "tolerance"].includes(key))) return fail(`${at}: a memory object has at, size and
|
|
184
|
+
if (!isObject(item) || Object.keys(item).some((key) => !["at", "size", "view", "tolerance", "exclude"].includes(key))) return fail(`${at}: a memory object has at, size, view, tolerance and exclude, and nothing else`);
|
|
185
|
+
if (item.view !== undefined && item.view !== "cpu" && item.view !== "ram") fail(`${at}: memory.view must be cpu or ram`);
|
|
186
|
+
if (item.exclude !== undefined) {
|
|
187
|
+
if (!Array.isArray(item.exclude) || item.exclude.length === 0) fail(`${at}: memory.exclude must be a list of objects with at and reason`);
|
|
188
|
+
for (const excluded of item.exclude as unknown[]) {
|
|
189
|
+
if (!isObject(excluded) || Object.keys(excluded).some((key) => key !== "at" && key !== "reason")) return fail(`${at}: an exclude has at and reason, and nothing else`);
|
|
190
|
+
checkLocation(excluded.at, `${at}: memory.exclude.at`, fail);
|
|
191
|
+
if (typeof excluded.reason !== "string" || excluded.reason.trim() === "") fail(`${at}: an exclude needs a reason: why this byte may differ`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
243
194
|
checkLocation(item.at, `${at}: memory.at`, fail);
|
|
244
|
-
if (item.size !== undefined && !isInteger(item.size, 1,
|
|
195
|
+
if (item.size !== undefined && !isInteger(item.size, 1, 0x10000)) fail(`${at}: memory.size must be a number from 1 to 65536`);
|
|
245
196
|
if (item.tolerance !== undefined && !isInteger(item.tolerance, 0, 0xff)) fail(`${at}: memory.tolerance must be a number from 0 to 255`);
|
|
246
197
|
}
|
|
247
198
|
}
|
|
@@ -269,6 +220,8 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
269
220
|
isInteger(rectangle.height, 1, 4096);
|
|
270
221
|
if (!ok) fail(`${at}: a screen.mask rectangle needs whole numbers x and y from 0, and width and height from 1`);
|
|
271
222
|
}
|
|
223
|
+
// The emulator refuses such a compare too, but only after the original side ran (b2/03).
|
|
224
|
+
if (maskCoversFrame(screen.mask as Array<{ x: number; y: number; width: number; height: number }>, videoStandard)) fail(`${at}: screen.mask covers the whole screen, so nothing is compared`);
|
|
272
225
|
}
|
|
273
226
|
}
|
|
274
227
|
if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
|
|
@@ -287,6 +240,10 @@ export function loadSymbols(subject: Subject): Map<string, number> {
|
|
|
287
240
|
if (subject.symbols === "knowledge") {
|
|
288
241
|
const db = openForRead();
|
|
289
242
|
for (const symbol of listSymbols(db)) symbols.set(symbol.name, symbol.address);
|
|
243
|
+
// An analyzer name gives an address too, when no symbol has the name and only one analyzer gives it (D74).
|
|
244
|
+
const second = new Map<string, number[]>();
|
|
245
|
+
for (const row of listAnalyzerNames(db)) second.set(row.name, [...(second.get(row.name) ?? []), row.address]);
|
|
246
|
+
for (const [name, addresses] of second) if (!symbols.has(name) && new Set(addresses).size === 1) symbols.set(name, addresses[0]!);
|
|
290
247
|
db?.close();
|
|
291
248
|
return symbols;
|
|
292
249
|
}
|
|
@@ -299,11 +256,12 @@ export function loadSymbols(subject: Subject): Map<string, number> {
|
|
|
299
256
|
// c64-assembler output, or a plain name → "$xxxx" map. A constant in the
|
|
300
257
|
// assembler output counts too: RAM variables such as "counter = $c100" lie
|
|
301
258
|
// outside the program, so the assembler reports them as constants.
|
|
302
|
-
const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?:
|
|
259
|
+
const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?: string }> }).symbols;
|
|
303
260
|
const entries: Array<[string, unknown]> = Array.isArray(assembled)
|
|
304
261
|
? assembled.flatMap((symbol): Array<[string, unknown]> => {
|
|
305
262
|
if (symbol.address !== undefined) return [[symbol.name, symbol.address]];
|
|
306
|
-
|
|
263
|
+
// A constant above $ffff has more digits and is no address.
|
|
264
|
+
if (typeof symbol.value === "string" && /^\$[0-9a-f]{4}$/.test(symbol.value)) return [[symbol.name, symbol.value]];
|
|
307
265
|
return [];
|
|
308
266
|
})
|
|
309
267
|
: Object.entries(value as Record<string, unknown>);
|
|
@@ -334,11 +292,35 @@ export function resolve(location: Location, symbols: Map<string, number>, side:
|
|
|
334
292
|
}
|
|
335
293
|
|
|
336
294
|
interface Observation {
|
|
337
|
-
memory: Array<{ label: string; bytes: number[]; tolerance: number }>;
|
|
295
|
+
memory: Array<{ label: string; address: number; bytes: number[]; tolerance: number; excluded: Array<{ offset: number; at: Location; reason: string }> }>;
|
|
338
296
|
registers: Partial<Record<ObservedRegister, unknown>>;
|
|
339
|
-
screen?: { match: boolean; mismatchingPixels: number; mismatchRatio: number; bounds?: unknown };
|
|
297
|
+
screen?: { match: boolean; mismatchingPixels: number; comparedPixels: number; mismatchRatio: number; bounds?: unknown };
|
|
340
298
|
vicii?: unknown;
|
|
341
299
|
sprites?: unknown;
|
|
300
|
+
/** PNG images for the files of a FAIL (D46): the side's screen, and on the rebuild the difference image. */
|
|
301
|
+
png?: string;
|
|
302
|
+
diffPng?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Writes pages 0-3 and $00/$01 from the entry state of the knowledge (D43, D46, D83): both sides then start from the same RAM. */
|
|
306
|
+
async function normalize(client: ViceSessionClient): Promise<void> {
|
|
307
|
+
const db = openForRead();
|
|
308
|
+
let state: { pages?: Record<string, string>; cpuPort?: Record<string, number> } | undefined;
|
|
309
|
+
try {
|
|
310
|
+
state = programImage(db)?.state as typeof state;
|
|
311
|
+
} finally {
|
|
312
|
+
db?.close();
|
|
313
|
+
}
|
|
314
|
+
if (state === undefined) throw new ScenarioError("normalize needs the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json> of the c64-knowledge skill");
|
|
315
|
+
const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => state.pages?.[page]);
|
|
316
|
+
const port = [state.cpuPort?.$00, state.cpuPort?.$01];
|
|
317
|
+
if (pages.some((page) => typeof page !== "string" || !/^[0-9a-f]{512}$/.test(page)) || port.some((value) => !isInteger(value, 0, 0xff))) {
|
|
318
|
+
throw new ScenarioError("the entry state of the knowledge is not the entry state of a c64-unpacker capture: it needs pages $0000 to $0300 and cpuPort $00 and $01");
|
|
319
|
+
}
|
|
320
|
+
if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
|
|
321
|
+
// Pages 0 to 3 in RAM from $0002, then the CPU port: the port bits switch what the CPU sees.
|
|
322
|
+
await client.memoryWrite({ address: 0x0002, data: pages.join("").slice(4), space: "c64", view: "ram" });
|
|
323
|
+
await client.memoryWrite({ address: 0x0000, data: Buffer.from(port as number[]).toString("hex"), space: "c64", view: "cpu" });
|
|
342
324
|
}
|
|
343
325
|
|
|
344
326
|
/**
|
|
@@ -350,45 +332,62 @@ export function baselineName(checkpoint: number, label: string): string {
|
|
|
350
332
|
return `test-${checkpoint}-${label}`.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
|
|
351
333
|
}
|
|
352
334
|
|
|
353
|
-
|
|
335
|
+
/** Autostart that returns only when the program runs (runProgram), or INCONCLUSIVE with the reason. */
|
|
336
|
+
async function startProgram(client: ViceSessionClient, program: string, side: Side): Promise<void> {
|
|
337
|
+
const failed = await runProgram(client, program, readProjectFile(program).bytes);
|
|
338
|
+
if (failed === "not-loaded") throw new Inconclusive(`the ${side} program was not in memory after ${LOAD_LIMIT_FRAMES} frames of autostart`);
|
|
339
|
+
if (failed === "not-started") throw new Inconclusive(`the ${side} program did not start within ${RUN_LIMIT_FRAMES} frames of RUN; autostart runs only a program with a BASIC start line`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side, symbols: Map<string, number>, files: boolean): Promise<Map<string, Observation>> {
|
|
354
343
|
const subject = scenario[side];
|
|
355
344
|
const observations = new Map<string, Observation>();
|
|
356
345
|
const at = (location: Location) => resolve(location, symbols, side);
|
|
357
|
-
const stop = async () => {
|
|
358
|
-
if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
|
|
359
|
-
};
|
|
360
346
|
// Each side starts from a hard reset, stopped at the reset vector, with the control ports released.
|
|
361
347
|
await client.reset({ mode: "hard", run: false });
|
|
362
348
|
await client.joystick({ port: 1, direction: "center", fire: false });
|
|
363
349
|
await client.joystick({ port: 2, direction: "center", fire: false });
|
|
364
350
|
let checkpoint = 0;
|
|
365
351
|
for (const step of scenario.steps) {
|
|
352
|
+
if (step.side !== undefined && step.side !== side) continue;
|
|
366
353
|
// Observe first: an observe step may also carry "registers" or "memory".
|
|
367
354
|
if ("observe" in step) {
|
|
368
355
|
checkpoint++;
|
|
369
356
|
const observation: Observation = { memory: [], registers: {} };
|
|
370
357
|
for (const item of step.memory ?? []) {
|
|
371
|
-
const spec = typeof item === "string" ? { at: item } : item;
|
|
372
|
-
const
|
|
373
|
-
|
|
358
|
+
const spec: MemoryObservation = typeof item === "string" ? { at: item } : item;
|
|
359
|
+
const address = at(spec.at);
|
|
360
|
+
const size = spec.size ?? 1;
|
|
361
|
+
const read = { data: (await readMemoryInParts(client, address, size, spec.view ?? "cpu")).toString("hex") };
|
|
362
|
+
const excluded = (spec.exclude ?? []).map((exclude) => {
|
|
363
|
+
const offset = at(exclude.at) - address;
|
|
364
|
+
if (offset < 0 || offset >= size) throw new ScenarioError(`the exclude ${exclude.at} lies outside the observed ${spec.at} (${size} bytes)`);
|
|
365
|
+
return { offset, at: exclude.at, reason: exclude.reason };
|
|
366
|
+
});
|
|
367
|
+
observation.memory.push({ label: size > 1 ? `${spec.at} (${size} bytes)` : spec.at, address, bytes: [...Buffer.from(read.data, "hex")], tolerance: spec.tolerance ?? 0, excluded });
|
|
374
368
|
}
|
|
375
369
|
if (step.registers !== undefined) {
|
|
376
370
|
const registers = await client.registersGet("c64");
|
|
377
|
-
for (const name of step.registers) observation.registers[name] = registers[name];
|
|
371
|
+
for (const name of step.registers) observation.registers[name] = name === "pc" ? formatC64Address(registers.pc) : registers[name];
|
|
378
372
|
}
|
|
379
373
|
if (step.vicii) {
|
|
380
374
|
// The raster line changes from one moment to the next; the comparison leaves it out.
|
|
381
375
|
const { rasterLine: _rasterLine, ...state } = await client.vicii();
|
|
382
|
-
observation.vicii = state;
|
|
376
|
+
observation.vicii = formatVicii(state);
|
|
383
377
|
}
|
|
384
|
-
if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites;
|
|
378
|
+
if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
|
|
385
379
|
if (step.screen !== undefined) {
|
|
386
380
|
const baseline = baselineName(checkpoint, step.observe);
|
|
387
381
|
if (side === "original") {
|
|
388
|
-
await client.screenCapture(baseline);
|
|
382
|
+
const shot = await client.screenCapture(baseline);
|
|
383
|
+
if (files) observation.png = shot.png;
|
|
389
384
|
} else {
|
|
390
|
-
const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff:
|
|
391
|
-
observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
|
|
385
|
+
const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff: files });
|
|
386
|
+
observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, comparedPixels: compared.comparedPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
|
|
387
|
+
if (files) {
|
|
388
|
+
observation.png = (await client.screenCapture()).png;
|
|
389
|
+
if (compared.diffPng !== undefined) observation.diffPng = compared.diffPng;
|
|
390
|
+
}
|
|
392
391
|
await client.screenDiscard(baseline);
|
|
393
392
|
}
|
|
394
393
|
}
|
|
@@ -399,30 +398,16 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
399
398
|
} else if ("load" in step) {
|
|
400
399
|
await client.programLoad({ path: subject.program });
|
|
401
400
|
} else if ("autostart" in step) {
|
|
402
|
-
await client
|
|
403
|
-
} else if ("
|
|
404
|
-
await
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
await client.joystick({ port: step.joystick.port, direction: step.joystick.direction, fire: step.joystick.fire ?? false });
|
|
413
|
-
} else if ("type" in step) {
|
|
414
|
-
await client.keyboard(textToPetscii(step.type));
|
|
415
|
-
} else if ("frames" in step) {
|
|
416
|
-
await stop();
|
|
417
|
-
await client.execution({ action: "advance-frames", count: step.frames, space: "c64" });
|
|
418
|
-
} else if ("runUntil" in step) {
|
|
419
|
-
const target =
|
|
420
|
-
"at" in step.runUntil
|
|
421
|
-
? { kind: "address" as const, address: at(step.runUntil.at), space: "c64" as const }
|
|
422
|
-
: { kind: "memory" as const, address: at(step.runUntil.memory.at), operator: "eq" as const, value: step.runUntil.memory.equals, space: "c64" as const, view: "cpu" as const };
|
|
423
|
-
const reached = await client.runUntil({ target, timeoutFrames: step.timeoutFrames });
|
|
424
|
-
if (!reached.reached) {
|
|
425
|
-
throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.timeoutFrames} frames (it stopped by ${reached.stopReason} at ${formatC64Address(reached.pc)})`);
|
|
401
|
+
await startProgram(client, subject.program, side);
|
|
402
|
+
} else if ("snapshot" in step) {
|
|
403
|
+
await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
|
|
404
|
+
} else if ("normalize" in step) {
|
|
405
|
+
await normalize(client);
|
|
406
|
+
} else {
|
|
407
|
+
const { side: _side, ...action } = step;
|
|
408
|
+
const notReached = await runActionStep(client, action as ActionStep, at);
|
|
409
|
+
if (notReached !== undefined && "runUntil" in step) {
|
|
410
|
+
throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.timeoutFrames} frames (it stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)})`);
|
|
426
411
|
}
|
|
427
412
|
}
|
|
428
413
|
}
|
|
@@ -436,8 +421,9 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
436
421
|
const right = rebuild.get(checkpoint)!;
|
|
437
422
|
left.memory.forEach((item, index) => {
|
|
438
423
|
const other = right.memory[index]!;
|
|
439
|
-
|
|
440
|
-
|
|
424
|
+
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
425
|
+
if (item.bytes.some((byte, at) => !skipped.has(at) && Math.abs(byte - other.bytes[at]!) > item.tolerance)) {
|
|
426
|
+
const show = (bytes: number[]) => Buffer.from(bytes).toString("hex");
|
|
441
427
|
differences.push({ checkpoint, what: `memory ${item.label}`, original: show(item.bytes), rebuild: show(other.bytes) });
|
|
442
428
|
}
|
|
443
429
|
});
|
|
@@ -453,13 +439,47 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
453
439
|
return differences;
|
|
454
440
|
}
|
|
455
441
|
|
|
456
|
-
/**
|
|
457
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Writes the files of a FAIL into `out` (D46): for a screen difference both
|
|
444
|
+
* screens and the difference image, and memory.json with each differing byte.
|
|
445
|
+
*/
|
|
446
|
+
function writeFailFiles(out: string, original: Map<string, Observation>, rebuild: Map<string, Observation>, differences: Difference[]): string[] {
|
|
447
|
+
const files: string[] = [];
|
|
448
|
+
const write = (name: string, bytes: Uint8Array) => {
|
|
449
|
+
writeProjectFile(`${out}/${name}`, bytes, { what: "--out" });
|
|
450
|
+
files.push(name);
|
|
451
|
+
};
|
|
452
|
+
const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original: string; rebuild: string }> }> = [];
|
|
453
|
+
const hex = (byte: number) => byte.toString(16).padStart(2, "0");
|
|
454
|
+
for (const difference of differences) {
|
|
455
|
+
const left = original.get(difference.checkpoint)!;
|
|
456
|
+
const right = rebuild.get(difference.checkpoint)!;
|
|
457
|
+
const name = difference.checkpoint.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
458
|
+
if (difference.what === "screen") {
|
|
459
|
+
if (left.png !== undefined) write(`${name}-original.png`, Buffer.from(left.png, "base64"));
|
|
460
|
+
if (right.png !== undefined) write(`${name}-rebuild.png`, Buffer.from(right.png, "base64"));
|
|
461
|
+
if (right.diffPng !== undefined) write(`${name}-diff.png`, Buffer.from(right.diffPng, "base64"));
|
|
462
|
+
}
|
|
463
|
+
if (difference.what.startsWith("memory ")) {
|
|
464
|
+
const index = left.memory.findIndex((item) => `memory ${item.label}` === difference.what);
|
|
465
|
+
const item = left.memory[index]!;
|
|
466
|
+
const other = right.memory[index]!;
|
|
467
|
+
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
468
|
+
const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: hex(byte), rebuild: hex(other.bytes[offset]!) }] : []));
|
|
469
|
+
memory.push({ checkpoint: difference.checkpoint, what: difference.what, bytes });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (memory.length > 0) write("memory.json", Buffer.from(`${JSON.stringify(memory, null, 2)}\n`));
|
|
473
|
+
return files;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Runs the scenario on the original, then on the rebuild, in one VICE session. With out, a FAIL leaves files there. */
|
|
477
|
+
export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string } = {}): Promise<ScenarioResult> {
|
|
458
478
|
const checkpoints = scenario.steps.flatMap((step) => ("observe" in step ? [step.observe] : []));
|
|
459
479
|
const observed = new Map<Side, Map<string, Observation>>();
|
|
460
480
|
for (const side of SIDES) {
|
|
461
481
|
try {
|
|
462
|
-
observed.set(side, await runSide(client, scenario, side, loadSymbols(scenario[side])));
|
|
482
|
+
observed.set(side, await runSide(client, scenario, side, loadSymbols(scenario[side]), options.out !== undefined));
|
|
463
483
|
} catch (error) {
|
|
464
484
|
if (error instanceof Inconclusive) return { result: "INCONCLUSIVE", scenario: scenario.name, reason: error.message, checkpoints, differences: [] };
|
|
465
485
|
if (error instanceof WireFailure) {
|
|
@@ -468,6 +488,17 @@ export async function runScenario(client: ViceSessionClient, scenario: Scenario)
|
|
|
468
488
|
throw error;
|
|
469
489
|
}
|
|
470
490
|
}
|
|
471
|
-
const
|
|
472
|
-
|
|
491
|
+
const original = observed.get("original")!;
|
|
492
|
+
const rebuild = observed.get("rebuild")!;
|
|
493
|
+
const differences = compare(original, rebuild);
|
|
494
|
+
const excluded = [...original].flatMap(([checkpoint, observation]) => observation.memory.flatMap((item) => item.excluded.map((excluded) => ({ checkpoint, at: excluded.at, reason: excluded.reason }))));
|
|
495
|
+
const files = differences.length > 0 && options.out !== undefined ? writeFailFiles(options.out, original, rebuild, differences) : [];
|
|
496
|
+
return {
|
|
497
|
+
result: differences.length === 0 ? "PASS" : "FAIL",
|
|
498
|
+
scenario: scenario.name,
|
|
499
|
+
checkpoints,
|
|
500
|
+
differences,
|
|
501
|
+
...(excluded.length === 0 ? {} : { excluded }),
|
|
502
|
+
...(files.length === 0 ? {} : { files }),
|
|
503
|
+
};
|
|
473
504
|
}
|