@henols/c64-re-tools 2.1.0-rc.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +33 -24
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +177 -279
- package/skills/c64-reverse-engineering/SKILL.md +13 -12
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +48 -32
- package/skills/c64-testing/references/recording-format.md +48 -19
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +244 -54
- package/skills/c64-testing/scripts/lib/recording.ts +153 -120
- package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +1 -1
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +2 -2
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/record.ts +0 -96
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
|
|
14
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
14
15
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { listSymbols } from "#src/knowledge/read.ts";
|
|
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";
|
|
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";
|
|
21
|
-
import {
|
|
21
|
+
import { namesOf, type Outcome } from "./scenario.ts";
|
|
22
22
|
|
|
23
23
|
export const RECORDING_FORMAT = 1;
|
|
24
24
|
/**
|
|
@@ -34,9 +34,7 @@ export const DEFAULT_INPUTS = ["$dc00", "$dc01", "$d419", "$d41a", "$d41b", "$d4
|
|
|
34
34
|
export function inputsArgument(inputs: string[]): string {
|
|
35
35
|
return inputs.map((input) => input.replace(/\$/g, "")).join(",");
|
|
36
36
|
}
|
|
37
|
-
/** Frames
|
|
38
|
-
export const BOOT_FRAMES = 150;
|
|
39
|
-
/** Frames that a typed key is held, and then released. */
|
|
37
|
+
/** Frames that a pressed key is held, and then released. */
|
|
40
38
|
const KEY_FRAMES = 3;
|
|
41
39
|
const MAX_STATE_ITEMS = 16;
|
|
42
40
|
const MAX_STATE_BYTES = 4096;
|
|
@@ -44,7 +42,7 @@ const MAX_STATE_BYTES = 4096;
|
|
|
44
42
|
const REBUILD_TIME_FACTOR = 2;
|
|
45
43
|
|
|
46
44
|
export interface StateItem {
|
|
47
|
-
|
|
45
|
+
address: Location;
|
|
48
46
|
size: number;
|
|
49
47
|
}
|
|
50
48
|
|
|
@@ -73,10 +71,16 @@ export interface Comparison {
|
|
|
73
71
|
pin?: StateItem[];
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
/**
|
|
74
|
+
/** RESTORE: it starts an NMI and is no read, so the recording keeps it as an event. */
|
|
75
|
+
export interface RestoreAction {
|
|
76
|
+
restore: true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A step that is not input, or a RESTORE of the play: it runs at the same point of the program on each replay. */
|
|
77
80
|
export interface RecordedEvent {
|
|
78
|
-
step
|
|
79
|
-
|
|
81
|
+
/** The step of the record file; none for a RESTORE of the play. */
|
|
82
|
+
step?: number;
|
|
83
|
+
action: ActionStep | RestoreAction;
|
|
80
84
|
/**
|
|
81
85
|
* The point: the n-th read at the busiest program site, or a tick when no
|
|
82
86
|
* program site has read yet. An event that follows another event directly
|
|
@@ -111,13 +115,16 @@ export interface Recording {
|
|
|
111
115
|
|
|
112
116
|
export const RECORDING_FILES = { recording: "recording.json", reads: "reads.txt", coverage: "coverage.bin" } as const;
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
/** Steps that are input or time: the replay gives the program the reads that they caused, and does not do them again. */
|
|
119
|
+
const INPUT_KINDS = new Set(["joystick", "press", "frames"]);
|
|
120
|
+
/** The KERNAL keyboard queue: $0277, its length in $c6, at most 10 keys. */
|
|
121
|
+
const QUEUE = 0x0277;
|
|
122
|
+
const QUEUE_LENGTH = 0x00c6;
|
|
123
|
+
const QUEUE_SIZE = 10;
|
|
117
124
|
|
|
118
125
|
const hex = (address: number) => address.toString(16).padStart(4, "0");
|
|
119
126
|
|
|
120
|
-
/** The keys of the replay core for the characters that a
|
|
127
|
+
/** The keys of the replay core for the characters that a press step can have. */
|
|
121
128
|
const KEY_CODES = new Map<string, number>([
|
|
122
129
|
...[..."ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 +-.:@,*;=/!\"#$%&'()[]<>?"].map((character) => [character, character.charCodeAt(0)] as [string, number]),
|
|
123
130
|
["\n", 0x0d],
|
|
@@ -127,7 +134,7 @@ const KEY_CODES = new Map<string, number>([
|
|
|
127
134
|
export function keyCodes(text: string): number[] {
|
|
128
135
|
return [...text.replace(/\r\n?/g, "\n")].map((character) => {
|
|
129
136
|
const code = KEY_CODES.get(character.toUpperCase());
|
|
130
|
-
if (code === undefined) throw new
|
|
137
|
+
if (code === undefined) throw new Refusal("invalid-input", `press cannot press ${JSON.stringify(character)} on the keyboard of the replay core`);
|
|
131
138
|
return code;
|
|
132
139
|
});
|
|
133
140
|
}
|
|
@@ -142,18 +149,18 @@ export function joystickMask(direction: Extract<ActionStep, { joystick: unknown
|
|
|
142
149
|
export function parseStateItem(text: string): StateItem {
|
|
143
150
|
const match = /^([^:]+)(?::(\d+))?$/.exec(text);
|
|
144
151
|
const size = Number(match?.[2] ?? 1);
|
|
145
|
-
if (match === null || !isInteger(size, 1, MAX_STATE_BYTES)) throw new
|
|
146
|
-
return {
|
|
152
|
+
if (match === null || !isInteger(size, 1, MAX_STATE_BYTES)) throw new Refusal("invalid-input", `--state ${text}: give a name or a $ address, with :size from 1 to ${MAX_STATE_BYTES}`);
|
|
153
|
+
return { address: match[1]!, size };
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
export function checkState(value: unknown, fail: (message: string) => never): StateItem[] {
|
|
150
157
|
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_STATE_ITEMS) fail(`state must be a list of 1 to ${MAX_STATE_ITEMS} memory items`);
|
|
151
158
|
return (value as unknown[]).map((item, index) => {
|
|
152
|
-
const spec = typeof item === "string" ? {
|
|
153
|
-
if (!isObject(spec) || typeof spec.
|
|
159
|
+
const spec = typeof item === "string" ? { address: item } : item;
|
|
160
|
+
if (!isObject(spec) || typeof spec.address !== "string" || Object.keys(spec).some((key) => key !== "address" && key !== "size")) fail(`state item ${index + 1} must be a name, a $ address, or { address, size }`);
|
|
154
161
|
const size = (spec as { size?: unknown }).size ?? 1;
|
|
155
162
|
if (!isInteger(size, 1, MAX_STATE_BYTES)) fail(`state item ${index + 1}: size must be from 1 to ${MAX_STATE_BYTES}`);
|
|
156
|
-
return {
|
|
163
|
+
return { address: (spec as { address: string }).address, size: size as number };
|
|
157
164
|
});
|
|
158
165
|
}
|
|
159
166
|
|
|
@@ -166,15 +173,16 @@ function checkInputs(value: unknown, fail: (message: string) => never): string[]
|
|
|
166
173
|
return (value as string[]).map((input) => input.toLowerCase());
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
/** Reads and checks a record file.
|
|
176
|
+
/** Reads and checks a record file. Refuses it with the first problem. */
|
|
170
177
|
export function readRecordFile(path: string): RecordFile {
|
|
171
178
|
const fail = (message: string): never => {
|
|
172
|
-
throw new
|
|
179
|
+
throw new Refusal("invalid-input", `record file ${path}: ${message}`);
|
|
173
180
|
};
|
|
174
181
|
let value: unknown;
|
|
175
182
|
try {
|
|
176
183
|
value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
|
|
177
184
|
} catch (error) {
|
|
185
|
+
if (isRefusal(error)) throw error;
|
|
178
186
|
return fail((error as Error).message);
|
|
179
187
|
}
|
|
180
188
|
if (!isObject(value)) return fail("it must be a JSON object");
|
|
@@ -191,16 +199,11 @@ export function readRecordFile(path: string): RecordFile {
|
|
|
191
199
|
const compare = value.checkpoint === undefined ? undefined : { checkpoint: value.checkpoint as string, state: checkState(value.state, fail), ...(pin === undefined ? {} : { pin }) };
|
|
192
200
|
const inputs = checkInputs(value.inputs, fail);
|
|
193
201
|
if (!Array.isArray(value.steps) || value.steps.length === 0) fail("steps must be a list with at least one step");
|
|
194
|
-
(value.steps as unknown[]).
|
|
202
|
+
const steps = (value.steps as unknown[]).map((raw, index) => {
|
|
195
203
|
const at = `step ${index + 1}`;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const kind = kinds[0]!;
|
|
200
|
-
for (const key of Object.keys(step)) if (key !== kind && !(kind === "runUntil" && key === "timeoutFrames")) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
201
|
-
checkActionStep(kind, step, at, fail);
|
|
202
|
-
if (kind === "runUntil" && "raster" in (step.runUntil as object)) fail(`${at}: the replay core has no runUntil raster; use at or memory`);
|
|
203
|
-
if (kind === "type") keyCodes(step.type as string);
|
|
204
|
+
const step = checkActionStep(stepKind(raw, ACTION_STEP_KINDS, at, fail), raw as Record<string, unknown>, at, fail, { press: true });
|
|
205
|
+
checkCoreStep(step, at, fail);
|
|
206
|
+
return step;
|
|
204
207
|
});
|
|
205
208
|
return {
|
|
206
209
|
name: value.name as string,
|
|
@@ -209,10 +212,26 @@ export function readRecordFile(path: string): RecordFile {
|
|
|
209
212
|
load: value.load === true,
|
|
210
213
|
...(compare === undefined ? {} : { compare }),
|
|
211
214
|
inputs,
|
|
212
|
-
steps
|
|
215
|
+
steps,
|
|
213
216
|
};
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
/** 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
|
+
function checkCoreStep(step: ActionStep, at: string, fail: (message: string) => never): void {
|
|
221
|
+
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) keyCodes(step.press);
|
|
223
|
+
if ("type" in step) {
|
|
224
|
+
const keys = keyboardBytes(step.type);
|
|
225
|
+
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`);
|
|
226
|
+
}
|
|
227
|
+
if ("runUntil" in step) {
|
|
228
|
+
const { target } = step.runUntil;
|
|
229
|
+
if (target.kind === "raster") fail(`${at}: the replay core has no raster target; use an address or a memory target`);
|
|
230
|
+
else if (target.space !== "c64") fail(`${at}: the replay core has the C64 alone, not ${target.space}`);
|
|
231
|
+
else if (target.kind === "address" && target.condition !== undefined) fail(`${at}: the replay core has no condition at an address target`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
216
235
|
/** Reads and checks recording.json of a recording folder. */
|
|
217
236
|
export function readRecording(folder: string): Recording {
|
|
218
237
|
const path = `${folder}/${RECORDING_FILES.recording}`;
|
|
@@ -220,12 +239,13 @@ export function readRecording(folder: string): Recording {
|
|
|
220
239
|
try {
|
|
221
240
|
value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
|
|
222
241
|
} catch (error) {
|
|
223
|
-
|
|
242
|
+
if (isRefusal(error)) throw error;
|
|
243
|
+
throw new Refusal("invalid-input", `recording ${path}: ${(error as Error).message}`);
|
|
224
244
|
}
|
|
225
|
-
if (!isObject(value) || value.format !== RECORDING_FORMAT) throw new
|
|
226
|
-
for (const key of ["name", "program", "sha256", "symbols", "hash"]) if (typeof value[key] !== "string") throw new
|
|
227
|
-
for (const key of ["inputs", "events", "sites"]) if (!Array.isArray(value[key])) throw new
|
|
228
|
-
if (!isInteger(value.ticks, 1, Number.MAX_SAFE_INTEGER)) throw new
|
|
245
|
+
if (!isObject(value) || value.format !== RECORDING_FORMAT) throw new Refusal("invalid-input", `recording ${path}: it is not a recording of format ${RECORDING_FORMAT}`);
|
|
246
|
+
for (const key of ["name", "program", "sha256", "symbols", "hash"]) if (typeof value[key] !== "string") throw new Refusal("invalid-input", `recording ${path}: ${key} is missing`);
|
|
247
|
+
for (const key of ["inputs", "events", "sites"]) if (!Array.isArray(value[key])) throw new Refusal("invalid-input", `recording ${path}: ${key} is missing`);
|
|
248
|
+
if (!isInteger(value.ticks, 1, Number.MAX_SAFE_INTEGER)) throw new Refusal("invalid-input", `recording ${path}: ticks is missing`);
|
|
229
249
|
return value as unknown as Recording;
|
|
230
250
|
}
|
|
231
251
|
|
|
@@ -243,40 +263,12 @@ function sideAddresses(symbols: Map<string, number>, side: string, move: { code:
|
|
|
243
263
|
const resolve = (location: Location, kind: "code" | "data") => {
|
|
244
264
|
if (location.startsWith("$")) return move[kind](parseC64Address(location));
|
|
245
265
|
const address = symbols.get(location);
|
|
246
|
-
if (address === undefined) throw new
|
|
266
|
+
if (address === undefined) throw new Refusal("invalid-input", `the ${side} side has no symbol ${location}`);
|
|
247
267
|
return address;
|
|
248
268
|
};
|
|
249
269
|
return { code: (location) => resolve(location, "code"), data: (location) => resolve(location, "data") };
|
|
250
270
|
}
|
|
251
271
|
|
|
252
|
-
/**
|
|
253
|
-
* The symbols of a symbol source that move with the program: with "code",
|
|
254
|
-
* knowledge labels and routines, else all knowledge symbols; the address
|
|
255
|
-
* symbols of c64-assembler output; or every entry of a name → address map.
|
|
256
|
-
* Constants do not move with the code, so they are left out.
|
|
257
|
-
*/
|
|
258
|
-
export function codeLabels(symbols: string, kind: "code" | "all" = "code"): Map<string, number> {
|
|
259
|
-
if (symbols === "none") return new Map();
|
|
260
|
-
if (symbols === "knowledge") {
|
|
261
|
-
const db = openForRead();
|
|
262
|
-
try {
|
|
263
|
-
return new Map(
|
|
264
|
-
listSymbols(db)
|
|
265
|
-
.filter((row) => kind === "all" || row.kind === "label" || row.kind === "routine")
|
|
266
|
-
.map((row) => [row.name, row.address]),
|
|
267
|
-
);
|
|
268
|
-
} finally {
|
|
269
|
-
db?.close();
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const value = JSON.parse(readProjectFile(symbols).bytes.toString("utf8")) as unknown;
|
|
273
|
-
const assembled = (value as { symbols?: Array<{ name: string; kind?: string; address?: string }> }).symbols;
|
|
274
|
-
if (Array.isArray(assembled)) {
|
|
275
|
-
return new Map(assembled.filter((symbol) => symbol.address !== undefined).map((symbol) => [symbol.name, parseC64Address(symbol.address!)]));
|
|
276
|
-
}
|
|
277
|
-
return new Map(Object.entries(value as Record<string, string>).map(([name, address]) => [name, parseC64Address(address)]));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
272
|
/** The furthest a site can be from the label before it and still move with that label. */
|
|
281
273
|
const MAX_LABEL_DISTANCE = 4096;
|
|
282
274
|
|
|
@@ -311,11 +303,11 @@ export function siteMap(sites: RecordedSite[], from: Map<string, number>, to: Ma
|
|
|
311
303
|
|
|
312
304
|
/** Power-on and READY, then the program in memory as a LOAD leaves it, and RUN in the keyboard queue. */
|
|
313
305
|
async function startProgram(core: C64RunSession, prg: Uint8Array, load: boolean): Promise<CoreStatus> {
|
|
314
|
-
if (prg.length < 3) throw new
|
|
315
|
-
await core.status(`run ${
|
|
306
|
+
if (prg.length < 3) throw new Refusal("invalid-input", "the program is not a PRG file");
|
|
307
|
+
await core.status(`run ${READY_FRAMES * PAL_FRAME_CYCLES}`);
|
|
316
308
|
const start = prg[0]! | (prg[1]! << 8);
|
|
317
309
|
const end = start + prg.length - 2;
|
|
318
|
-
if (!load && start !== 0x0801) throw new
|
|
310
|
+
if (!load && start !== 0x0801) throw new Refusal("invalid-input", `the program loads at ${formatC64Address(start)}, not at $0801 with a BASIC start line: give load true and a registers step that starts it`);
|
|
319
311
|
await core.write(start, prg.subarray(2));
|
|
320
312
|
// The pointers that LOAD sets: the end of the program and of the variables.
|
|
321
313
|
for (const pointer of [0x2d, 0x2f, 0x31, 0xae]) await core.write(pointer, Uint8Array.of(end & 0xff, end >> 8));
|
|
@@ -354,39 +346,63 @@ async function runToKey(core: C64RunSession, key: RecordedEvent["key"], until: n
|
|
|
354
346
|
}
|
|
355
347
|
|
|
356
348
|
/** Does a step that is not input. Gives a reason when a runUntil does not reach its target. */
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
349
|
+
/** The status flags in the order of the P register, bit 7 to bit 0; bit 5 is always 1. */
|
|
350
|
+
const FLAG_BITS: Record<string, number> = { n: 0x80, v: 0x40, b: 0x10, d: 0x08, i: 0x04, z: 0x02, c: 0x01 };
|
|
351
|
+
|
|
352
|
+
/** A memory comparison of a memory target. */
|
|
353
|
+
const COMPARE: Record<string, (value: number, wanted: number) => boolean> = {
|
|
354
|
+
eq: (value, wanted) => value === wanted,
|
|
355
|
+
ne: (value, wanted) => value !== wanted,
|
|
356
|
+
lt: (value, wanted) => value < wanted,
|
|
357
|
+
lte: (value, wanted) => value <= wanted,
|
|
358
|
+
gt: (value, wanted) => value > wanted,
|
|
359
|
+
gte: (value, wanted) => value >= wanted,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
async function doAction(core: C64RunSession, step: ActionStep | RestoreAction, at: SideAddresses): Promise<string | undefined> {
|
|
363
|
+
if ("restore" in step) {
|
|
364
|
+
// The NMI line is low while RESTORE is down: hold it as a press step holds a key.
|
|
365
|
+
const tick = (await core.status("status")).tick;
|
|
366
|
+
await core.command("key ff down");
|
|
367
|
+
await core.status("run " + (tick + KEY_FRAMES * PAL_FRAME_CYCLES));
|
|
368
|
+
await core.command("key ff up");
|
|
369
|
+
} else if ("write" in step) {
|
|
370
|
+
await core.write(at.data(step.write.address), Buffer.from(step.write.data, "hex"));
|
|
371
|
+
} else if ("type" in step) {
|
|
372
|
+
// The KERNAL keyboard queue: the keys after the keys that wait there now.
|
|
373
|
+
const keys = keyboardBytes(step.type) as Uint8Array;
|
|
374
|
+
const waiting = (await core.peek(QUEUE_LENGTH, 1))[0]!;
|
|
375
|
+
if (waiting + keys.length > QUEUE_SIZE) return `the KERNAL keyboard queue holds ${waiting} keys, and ${keys.length} more do not fit in its ${QUEUE_SIZE}`;
|
|
376
|
+
await core.write(QUEUE + waiting, keys);
|
|
377
|
+
await core.write(QUEUE_LENGTH, Uint8Array.of(waiting + keys.length));
|
|
360
378
|
} else if ("registers" in step) {
|
|
361
379
|
const now = await core.status("run-sync");
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const values = [value("a", now.a), value("x", now.x), value("y", now.y), value("sp", now.s), now.p].map((n) => n.toString(16));
|
|
380
|
+
const { pc, flags } = step.registers;
|
|
381
|
+
let p = now.p;
|
|
382
|
+
for (const [name, set] of Object.entries(flags ?? {})) if (set !== undefined) p = set ? p | FLAG_BITS[name]! : p & ~FLAG_BITS[name]!;
|
|
383
|
+
const values = [step.registers.a ?? now.a, step.registers.x ?? now.x, step.registers.y ?? now.y, step.registers.sp ?? now.s, p].map((n) => n.toString(16));
|
|
367
384
|
await core.status(`regs ${values.join(" ")}`);
|
|
368
|
-
|
|
369
|
-
if (pc !== undefined) await core.status(`goto ${hex(typeof pc === "number" ? pc : at.code(pc))}`);
|
|
385
|
+
if (pc !== undefined) await core.status(`goto ${hex(at.code(pc))}`);
|
|
370
386
|
} else if ("runUntil" in step) {
|
|
387
|
+
const { target, timeoutFrames } = step.runUntil;
|
|
371
388
|
const start = (await core.status("status")).tick;
|
|
372
|
-
const until = start +
|
|
373
|
-
if ("memory"
|
|
374
|
-
const { at: where, equals } = step.runUntil.memory;
|
|
389
|
+
const until = start + timeoutFrames * PAL_FRAME_CYCLES;
|
|
390
|
+
if (target.kind === "memory") {
|
|
375
391
|
for (let tick = start; ; tick += PAL_FRAME_CYCLES) {
|
|
376
|
-
if ((await core.peek(at.data(
|
|
377
|
-
if (tick >= until) return `${
|
|
392
|
+
if (COMPARE[target.operator]!((await core.peek(at.data(target.address), 1))[0]!, target.value)) return undefined;
|
|
393
|
+
if (tick >= until) return `${target.address} did not become ${target.operator} ${target.value} within ${timeoutFrames} frames`;
|
|
378
394
|
await core.status(`run ${tick + PAL_FRAME_CYCLES}`);
|
|
379
395
|
}
|
|
380
396
|
}
|
|
381
|
-
|
|
382
|
-
const reached = await core.status(`run-pc ${hex(at.code(target))} ${until}`);
|
|
383
|
-
if (reached.word !== "reached") return `the program did not reach ${target} within ${
|
|
397
|
+
if (target.kind !== "address") return "the replay core has no raster target";
|
|
398
|
+
const reached = await core.status(`run-pc ${hex(at.code(target.address))} ${until}`);
|
|
399
|
+
if (reached.word !== "reached") return `the program did not reach ${target.address} within ${timeoutFrames} frames`;
|
|
384
400
|
}
|
|
385
401
|
return undefined;
|
|
386
402
|
}
|
|
387
403
|
|
|
388
404
|
function rangesArgument(state: StateItem[], at: SideAddresses): string {
|
|
389
|
-
return state.map((item) => `${hex(at.data(item.
|
|
405
|
+
return state.map((item) => `${hex(at.data(item.address))}:${item.size}`).join(",");
|
|
390
406
|
}
|
|
391
407
|
|
|
392
408
|
export interface RecordOutcome {
|
|
@@ -397,15 +413,24 @@ export interface RecordOutcome {
|
|
|
397
413
|
reason?: string;
|
|
398
414
|
}
|
|
399
415
|
|
|
400
|
-
/**
|
|
401
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Plays the steps of a record file in the replay core and records the session.
|
|
418
|
+
* `input` runs after the steps, with `restore` for a RESTORE press of the play;
|
|
419
|
+
* a reason that it gives means no recording.
|
|
420
|
+
*/
|
|
421
|
+
export async function record(
|
|
422
|
+
core: C64RunSession,
|
|
423
|
+
file: RecordFile,
|
|
424
|
+
workspace: Workspace,
|
|
425
|
+
input?: (core: C64RunSession, restore: () => Promise<string | undefined>) => Promise<string | undefined>,
|
|
426
|
+
): Promise<RecordOutcome> {
|
|
402
427
|
const prg = readProjectFile(file.program).bytes;
|
|
403
428
|
const same = (address: number) => address;
|
|
404
|
-
const at = sideAddresses(
|
|
429
|
+
const at = sideAddresses(namesOf(file.symbols), "original", { code: same, data: same });
|
|
405
430
|
// A checkpoint and state of the record file must resolve now, so that a replay can use them.
|
|
406
431
|
if (file.compare !== undefined) {
|
|
407
432
|
at.code(file.compare.checkpoint);
|
|
408
|
-
for (const item of file.compare.state) at.data(item.
|
|
433
|
+
for (const item of file.compare.state) at.data(item.address);
|
|
409
434
|
}
|
|
410
435
|
const reads = workspace.path(RECORDING_FILES.reads);
|
|
411
436
|
const coverage = workspace.path(RECORDING_FILES.coverage);
|
|
@@ -420,36 +445,37 @@ export async function record(core: C64RunSession, file: RecordFile, workspace: W
|
|
|
420
445
|
const masks = [0, 0];
|
|
421
446
|
let reason: string | undefined;
|
|
422
447
|
let afterEvent = false;
|
|
448
|
+
// An event runs after the next read at the busiest program site, here and on each
|
|
449
|
+
// replay. An event right after another event runs where that one ended.
|
|
450
|
+
const runEvent = async (action: ActionStep | RestoreAction, step?: number): Promise<string | undefined> => {
|
|
451
|
+
const tick = (await core.status("status")).tick;
|
|
452
|
+
const busiest = afterEvent ? undefined : busiestSite(await core.command("sites"));
|
|
453
|
+
const key: RecordedEvent["key"] = afterEvent
|
|
454
|
+
? { previous: true }
|
|
455
|
+
: busiest === undefined
|
|
456
|
+
? { tick: tick + 1 }
|
|
457
|
+
: { site: formatC64Address(busiest.site), addr: formatC64Address(busiest.addr), count: busiest.count + 1 };
|
|
458
|
+
if ((await runToKey(core, key, tick + 600 * PAL_FRAME_CYCLES, new Map())) !== "reached") return "the program did not read its input again within 600 frames";
|
|
459
|
+
events.push({ ...(step === undefined ? {} : { step }), action, key });
|
|
460
|
+
return doAction(core, action, at);
|
|
461
|
+
};
|
|
423
462
|
for (const [index, step] of file.steps.entries()) {
|
|
424
463
|
let tick = (await core.status("status")).tick;
|
|
425
|
-
const event = !INPUT_KINDS.has(Object.keys(step)
|
|
464
|
+
const event = !INPUT_KINDS.has(Object.keys(step)[0]!);
|
|
426
465
|
if ("frames" in step) {
|
|
427
466
|
await core.status(`run ${tick + step.frames * PAL_FRAME_CYCLES}`);
|
|
428
467
|
} else if ("joystick" in step) {
|
|
429
468
|
masks[step.joystick.port - 1] = joystickMask(step.joystick.direction, step.joystick.fire ?? false);
|
|
430
469
|
await core.command(`joy ${masks[0]!.toString(16)} ${masks[1]!.toString(16)}`);
|
|
431
|
-
} else if ("
|
|
432
|
-
for (const code of keyCodes(step.
|
|
470
|
+
} else if ("press" in step) {
|
|
471
|
+
for (const code of keyCodes(step.press)) {
|
|
433
472
|
await core.command(`key ${code.toString(16)} down`);
|
|
434
473
|
await core.status(`run ${(tick += KEY_FRAMES * PAL_FRAME_CYCLES)}`);
|
|
435
474
|
await core.command(`key ${code.toString(16)} up`);
|
|
436
475
|
await core.status(`run ${(tick += KEY_FRAMES * PAL_FRAME_CYCLES)}`);
|
|
437
476
|
}
|
|
438
477
|
} else {
|
|
439
|
-
|
|
440
|
-
// replay. An event right after another event runs where that one ended.
|
|
441
|
-
const busiest = afterEvent ? undefined : busiestSite(await core.command("sites"));
|
|
442
|
-
const key: RecordedEvent["key"] = afterEvent
|
|
443
|
-
? { previous: true }
|
|
444
|
-
: busiest === undefined
|
|
445
|
-
? { tick: tick + 1 }
|
|
446
|
-
: { site: formatC64Address(busiest.site), addr: formatC64Address(busiest.addr), count: busiest.count + 1 };
|
|
447
|
-
if ((await runToKey(core, key, tick + 600 * PAL_FRAME_CYCLES, new Map())) !== "reached") {
|
|
448
|
-
reason = `step ${index + 1}: the program did not read its input again within 600 frames`;
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
events.push({ step: index + 1, action: step, key });
|
|
452
|
-
reason = await doAction(core, step, at);
|
|
478
|
+
reason = await runEvent(step, index + 1);
|
|
453
479
|
if (reason !== undefined) {
|
|
454
480
|
reason = `step ${index + 1}: ${reason}`;
|
|
455
481
|
break;
|
|
@@ -457,7 +483,13 @@ export async function record(core: C64RunSession, file: RecordFile, workspace: W
|
|
|
457
483
|
}
|
|
458
484
|
afterEvent = event;
|
|
459
485
|
}
|
|
460
|
-
|
|
486
|
+
// The play runs frames before each RESTORE: a RESTORE never runs where an event of the steps ended.
|
|
487
|
+
const restore = async () => {
|
|
488
|
+
afterEvent = false;
|
|
489
|
+
const failed = await runEvent({ restore: true });
|
|
490
|
+
return failed === undefined ? undefined : "RESTORE: " + failed;
|
|
491
|
+
};
|
|
492
|
+
if (input !== undefined && reason === undefined) reason = await input(core, restore);
|
|
461
493
|
const end = await core.status("status");
|
|
462
494
|
const sites = (await core.command("sites")).split(" ").slice(2).map((entry): RecordedSite => {
|
|
463
495
|
const [where, reads, kind] = entry.split(":");
|
|
@@ -526,9 +558,9 @@ export async function replaySide(
|
|
|
526
558
|
const same = (address: number) => address;
|
|
527
559
|
const move = original
|
|
528
560
|
? { code: same, data: same }
|
|
529
|
-
: { code: mover(
|
|
530
|
-
const at = sideAddresses(
|
|
531
|
-
const moved = original ? [] : siteMap(recording.sites,
|
|
561
|
+
: { code: mover(namesOf(names, "code"), namesOf(subject.symbols, "code")), data: mover(namesOf(names), namesOf(subject.symbols)) };
|
|
562
|
+
const at = sideAddresses(namesOf(subject.symbols), subject.side, move);
|
|
563
|
+
const moved = original ? [] : siteMap(recording.sites, namesOf(names, "code"), namesOf(subject.symbols, "code"));
|
|
532
564
|
const map = new Map(moved);
|
|
533
565
|
const reads = workspace.path(`${subject.side}-${RECORDING_FILES.reads}`);
|
|
534
566
|
writeFileSync(reads, readProjectFile(`${folder}/${RECORDING_FILES.reads}`).bytes);
|
|
@@ -555,15 +587,16 @@ export async function replaySide(
|
|
|
555
587
|
const limit = original ? recording.ticks : recording.ticks * REBUILD_TIME_FACTOR;
|
|
556
588
|
let problem: string | undefined;
|
|
557
589
|
for (const event of recording.events) {
|
|
590
|
+
const what = event.step === undefined ? "the RESTORE of the play" : "step " + event.step;
|
|
558
591
|
const point = await runToKey(core, event.key, limit, map);
|
|
559
592
|
if (point === "halted") break;
|
|
560
593
|
if (point === "not-reached") {
|
|
561
|
-
problem =
|
|
594
|
+
problem = "the program did not come to the point of " + what;
|
|
562
595
|
break;
|
|
563
596
|
}
|
|
564
597
|
problem = await doAction(core, event.action, at);
|
|
565
598
|
if (problem !== undefined) {
|
|
566
|
-
problem =
|
|
599
|
+
problem = what + ": " + problem;
|
|
567
600
|
break;
|
|
568
601
|
}
|
|
569
602
|
}
|
|
@@ -585,7 +618,7 @@ export interface CheckpointDifference {
|
|
|
585
618
|
/** 1 = the first checkpoint. */
|
|
586
619
|
checkpoint: number;
|
|
587
620
|
tick: { original: number; rebuild: number };
|
|
588
|
-
items: Array<{
|
|
621
|
+
items: Array<{ address: Location; original: string; rebuild: string }>;
|
|
589
622
|
}
|
|
590
623
|
|
|
591
624
|
/** The pinned values of each sample line: the fields after the state items. */
|
|
@@ -604,7 +637,7 @@ export function firstDifference(expected: string[], got: string[], state: StateI
|
|
|
604
637
|
const a = allA.slice(0, state.length);
|
|
605
638
|
const b = allB.slice(0, state.length);
|
|
606
639
|
if (a.join(" ") === b.join(" ")) continue;
|
|
607
|
-
const items = state.flatMap((item, at) => (a[at] === b[at] ? [] : [{
|
|
640
|
+
const items = state.flatMap((item, at) => (a[at] === b[at] ? [] : [{ address: item.address, original: a[at] ?? "", rebuild: b[at] ?? "" }]));
|
|
608
641
|
return { checkpoint: index + 1, tick: { original: Number(tickA), rebuild: Number(tickB) }, items };
|
|
609
642
|
}
|
|
610
643
|
return undefined;
|
|
@@ -625,5 +658,5 @@ export interface ReplayResult {
|
|
|
625
658
|
movedSites?: number;
|
|
626
659
|
/** Input reads of the rebuild that the recording had no value for: they got the idle value. */
|
|
627
660
|
idleReads?: number;
|
|
628
|
-
files?:
|
|
661
|
+
files?: Written[];
|
|
629
662
|
}
|