@henols/c64-re-tools 2.1.1 → 2.1.3
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 +6 -3
- package/skills/c64-assembler/scripts/assembler.ts +19 -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 +1 -0
- package/skills/c64-disk/scripts/disk.ts +2 -2
- package/skills/c64-disk/scripts/lib/containers.ts +4 -4
- 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 +2 -0
- package/skills/c64-emulator/SKILL.md +4 -1
- package/skills/c64-knowledge/SKILL.md +12 -6
- package/skills/c64-knowledge/scripts/knowledge.ts +41 -25
- package/skills/c64-knowledge/scripts/lib/listing.ts +16 -31
- package/skills/c64-memory-map/SKILL.md +3 -0
- package/skills/c64-memory-map/scripts/memory-map.ts +23 -4
- package/skills/c64-provenance/SKILL.md +5 -3
- package/skills/c64-provenance/scripts/lib/library.ts +28 -5
- package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
- package/skills/c64-provenance/scripts/provenance.ts +22 -10
- package/skills/c64-source/SKILL.md +2 -1
- package/skills/c64-source/scripts/lib/source.ts +25 -33
- package/skills/c64-source/scripts/source.ts +13 -17
- package/skills/c64-static-analysis/SKILL.md +6 -3
- package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
- package/skills/c64-static-analysis/scripts/static-analysis.ts +31 -19
- package/skills/c64-testing/SKILL.md +9 -5
- package/skills/c64-testing/references/scenario-format.md +5 -3
- package/skills/c64-testing/scripts/lib/diff-files.ts +41 -10
- package/skills/c64-testing/scripts/lib/recording.ts +3 -3
- package/skills/c64-testing/scripts/lib/replay.ts +1 -1
- package/skills/c64-testing/scripts/lib/runs.ts +45 -9
- package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
- package/skills/c64-testing/scripts/testing.ts +9 -6
- package/skills/c64-unpacker/SKILL.md +10 -7
- package/skills/c64-unpacker/scripts/lib/loads.ts +15 -6
- package/skills/c64-unpacker/scripts/lib/machine.ts +61 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +21 -17
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +144 -11
- package/src/host-client/image.ts +27 -13
- package/src/host-client/steps.ts +41 -5
- package/src/host-client/tools.ts +4 -3
- package/src/host-client/transfer.ts +22 -29
- package/src/knowledge/history.ts +55 -2
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/program.ts +31 -2
- package/src/knowledge/read.ts +7 -4
- package/src/knowledge/schema.ts +3 -2
- package/src/knowledge/text.ts +1 -0
- package/src/knowledge/write.ts +86 -42
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +1 -1
- package/src/mcp/server.ts +9 -1
- package/src/mcp/tools/debug.ts +31 -4
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +5 -1
- package/src/mcp/tools/memory.ts +4 -3
- package/src/media.ts +57 -0
- package/src/opcodes.ts +81 -18
- package/src/platform.ts +14 -1
- package/src/project.ts +118 -3
- package/src/protocol/vice.ts +4 -1
- package/src/script.ts +118 -5
|
@@ -145,11 +145,11 @@ 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
|
|
|
@@ -37,7 +37,7 @@ export async function replay(values: ReplayOptions): Promise<ReplayResult> {
|
|
|
37
37
|
const folder = values.folder.replace(/\/+$/, "").replace(/\/recording\.json$/, "");
|
|
38
38
|
const recording = readRecording(folder);
|
|
39
39
|
const given: Comparison | undefined =
|
|
40
|
-
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")) }) };
|
|
41
41
|
const compare = given ?? recording.compare;
|
|
42
42
|
if (values.rebuild !== undefined && compare === undefined) throw new UsageError("the recording has no checkpoint and state: give --checkpoint and --state");
|
|
43
43
|
const result = (outcome: ReplayResult["result"], side: ReplayResult["side"], fields: Partial<ReplayResult> = {}): ReplayResult => ({
|
|
@@ -5,12 +5,14 @@
|
|
|
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, refuseOldStep, renamedField, runActionStep, VICE_STEP_KINDS, 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";
|
|
10
11
|
import { readPrg } from "#src/host-client/image.ts";
|
|
11
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";
|
|
12
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
13
14
|
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
15
|
+
import { checkSnapshotFile } from "#src/media.ts";
|
|
14
16
|
import { UsageError } from "#src/script.ts";
|
|
15
17
|
import { namesOf } from "./scenario.ts";
|
|
16
18
|
|
|
@@ -161,11 +163,26 @@ function readRun(path: string): Run {
|
|
|
161
163
|
/** The remedy for a start program that is no PRG. */
|
|
162
164
|
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";
|
|
163
165
|
|
|
164
|
-
/** Checks each file that the run reads before the emulator starts: the start program
|
|
165
|
-
function checkRunFiles(run: Run): void {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
/** Checks each file that the run reads before the emulator starts: the start program, and the start snapshot and each snapshot step, which must be VICE snapshot files. A refusal names the run file and the step. */
|
|
167
|
+
function checkRunFiles(path: string, run: Run): void {
|
|
168
|
+
const named = (where: string, check: () => unknown) => {
|
|
169
|
+
try {
|
|
170
|
+
check();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (isRefusal(error)) throw new Refusal(error.code, `run ${path}: ${where}: ${error.message}`);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
if ("program" in run.start) {
|
|
177
|
+
const { program } = run.start;
|
|
178
|
+
named("start.program", () => readPrg(program, PROGRAM_REMEDY));
|
|
179
|
+
} else {
|
|
180
|
+
const { snapshot } = run.start;
|
|
181
|
+
named("start.snapshot", () => checkSnapshotFile(snapshot, readProjectFile(snapshot).bytes));
|
|
182
|
+
}
|
|
183
|
+
run.steps.forEach((step, index) => {
|
|
184
|
+
if ("snapshot" in step) named(`step ${index + 1}: snapshot`, () => checkSnapshotFile(step.snapshot, readProjectFile(step.snapshot).bytes));
|
|
185
|
+
});
|
|
169
186
|
}
|
|
170
187
|
|
|
171
188
|
/** A trace hit with $ addresses, as a person reads it. */
|
|
@@ -176,14 +193,33 @@ function showHit(hit: TraceHit) {
|
|
|
176
193
|
/** Runs the run file and writes its files into `out`. failed: a step did not run. */
|
|
177
194
|
export async function runFile(path: string, out: string): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
178
195
|
const run = readRun(path);
|
|
179
|
-
checkRunFiles(run);
|
|
196
|
+
checkRunFiles(path, run);
|
|
180
197
|
const symbols = namesOf(run.symbols ?? "none");
|
|
181
198
|
const at = (location: string): number => {
|
|
182
199
|
if (location.startsWith("$")) return parseC64Address(location);
|
|
183
200
|
const address = symbols.get(location);
|
|
184
|
-
if (address
|
|
185
|
-
|
|
201
|
+
if (address !== undefined) return address;
|
|
202
|
+
if (run.symbols === undefined || run.symbols === "none") {
|
|
203
|
+
throw new UsageError(`${location} is a name, and the run file has no symbols. A run file has no symbols unless it gives "symbols": "knowledge" for the names of the knowledge, or "symbols": "<file>" for a JSON file of names. Give one of them, or a $ address`);
|
|
204
|
+
}
|
|
205
|
+
throw new UsageError(`the symbols ${run.symbols} have no ${location}`);
|
|
186
206
|
};
|
|
207
|
+
// Every name of the steps resolves before the emulator starts. A refusal names the step.
|
|
208
|
+
run.steps.forEach((step, index) => {
|
|
209
|
+
if ("trace" in step || "observe" in step) {
|
|
210
|
+
const names = "trace" in step ? step.points.flatMap((point) => [point.start, ...(point.end === undefined ? [] : [point.end])]) : step.memory === undefined || step.memory === "all" ? [] : step.memory.map((item) => (typeof item === "string" ? item : item.address));
|
|
211
|
+
for (const name of names) {
|
|
212
|
+
try {
|
|
213
|
+
at(name);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (isRefusal(error)) throw new Refusal("invalid-input", `run ${path}: step ${index + 1}: ${error.message}`);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else if ((ACTION_STEP_KINDS as readonly string[]).some((kind) => kind in step)) {
|
|
220
|
+
checkStepNames([step as ActionStep], at, `run ${path}`, index + 1);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
187
223
|
const files: string[] = [];
|
|
188
224
|
const written: Written[] = [];
|
|
189
225
|
const write = (name: string, bytes: Uint8Array) => {
|
|
@@ -12,10 +12,12 @@ import { applyEntryState, parseEntryState } from "#src/host-client/entry-state.t
|
|
|
12
12
|
import { readPrg } from "#src/host-client/image.ts";
|
|
13
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
14
14
|
import { readMemoryInParts, type ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
15
|
+
import { checkSnapshotFile } from "#src/media.ts";
|
|
15
16
|
import { openForRead } from "#src/knowledge/database.ts";
|
|
16
17
|
import { nameTable } from "#src/knowledge/names.ts";
|
|
17
18
|
import { DEFAULT_SPACE } from "#src/knowledge/schema.ts";
|
|
18
19
|
import { programImage } from "#src/knowledge/program.ts";
|
|
20
|
+
import { listed } from "#src/script.ts";
|
|
19
21
|
|
|
20
22
|
export type Side = "original" | "rebuild";
|
|
21
23
|
export const SIDES: Side[] = ["original", "rebuild"];
|
|
@@ -70,11 +72,24 @@ export interface Scenario {
|
|
|
70
72
|
|
|
71
73
|
export type Outcome = "PASS" | "FAIL" | "INCONCLUSIVE";
|
|
72
74
|
|
|
75
|
+
/** A byte of a memory item that differs: its address, and its value on each side as two hex digits. */
|
|
76
|
+
export interface DifferingByte {
|
|
77
|
+
address: string;
|
|
78
|
+
original: string;
|
|
79
|
+
rebuild: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A difference at a checkpoint. A memory item gives the bytes that differ
|
|
84
|
+
* (at most --max-listed of them, and their count); every other item gives
|
|
85
|
+
* its value on each side.
|
|
86
|
+
*/
|
|
73
87
|
export interface Difference {
|
|
74
88
|
checkpoint: string;
|
|
75
89
|
what: string;
|
|
76
|
-
original
|
|
77
|
-
rebuild
|
|
90
|
+
original?: unknown;
|
|
91
|
+
rebuild?: unknown;
|
|
92
|
+
bytes?: { count: number; listed: DifferingByte[]; rest?: string };
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
export interface ScenarioResult {
|
|
@@ -290,8 +305,9 @@ const PROGRAM_REMEDY = "A scenario program is a PRG. Get the PRG out of the disk
|
|
|
290
305
|
/**
|
|
291
306
|
* Checks each file that the scenario names before an emulator starts: the
|
|
292
307
|
* program and the symbols of each side, and each snapshot file. A missing
|
|
293
|
-
* file is not-found; a program that is no PRG
|
|
294
|
-
*
|
|
308
|
+
* file is not-found; a program that is no PRG, a symbols file with no names
|
|
309
|
+
* and a snapshot step whose file is no VICE snapshot file are invalid-input.
|
|
310
|
+
* A refusal names the field or the step. Gives the names of each side.
|
|
295
311
|
*/
|
|
296
312
|
export function scenarioNames(scenario: Scenario): Map<Side, Map<string, number>> {
|
|
297
313
|
const names = new Map<Side, Map<string, number>>();
|
|
@@ -307,7 +323,9 @@ export function scenarioNames(scenario: Scenario): Map<Side, Map<string, number>
|
|
|
307
323
|
named(`${side}.program`, () => readPrg(scenario[side].program, PROGRAM_REMEDY));
|
|
308
324
|
names.set(side, named(`${side}.symbols`, () => namesOf(scenario[side].symbols)));
|
|
309
325
|
}
|
|
310
|
-
|
|
326
|
+
scenario.steps.forEach((step, index) => {
|
|
327
|
+
if ("snapshot" in step) named(`step ${index + 1}: snapshot`, () => checkSnapshotFile(step.snapshot, readProjectFile(step.snapshot).bytes));
|
|
328
|
+
});
|
|
311
329
|
return names;
|
|
312
330
|
}
|
|
313
331
|
|
|
@@ -368,7 +386,6 @@ async function startProgram(client: ViceSessionClient, program: string, side: Si
|
|
|
368
386
|
}
|
|
369
387
|
|
|
370
388
|
async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side, symbols: Map<string, number>, files: boolean): Promise<Map<string, Observation>> {
|
|
371
|
-
const subject = scenario[side];
|
|
372
389
|
const observations = new Map<string, Observation>();
|
|
373
390
|
const at = (location: Location) => resolve(location, symbols, side);
|
|
374
391
|
// Each side starts from a hard reset, stopped at the reset vector, with the control ports released.
|
|
@@ -376,84 +393,117 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
376
393
|
await client.joystick({ port: 1, direction: "center", fire: false });
|
|
377
394
|
await client.joystick({ port: 2, direction: "center", fire: false });
|
|
378
395
|
let checkpoint = 0;
|
|
379
|
-
for (const step of scenario.steps) {
|
|
396
|
+
for (const [index, step] of scenario.steps.entries()) {
|
|
380
397
|
if (step.side !== undefined && step.side !== side) continue;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
398
|
+
try {
|
|
399
|
+
checkpoint = await runStep(client, scenario, side, step, at, checkpoint, files, observations);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
// The step in the reason or the message: the scenario file has many steps.
|
|
402
|
+
const where = `step ${index + 1} (${stepKindOf(step)})`;
|
|
403
|
+
if (error instanceof Inconclusive) throw new Inconclusive(`${where}: ${error.message}`);
|
|
404
|
+
if (error instanceof WireFailure) throw new WireFailure(error.code, `${where}: ${error.message}`);
|
|
405
|
+
if (error instanceof Refusal) throw new Refusal(error.code, `${where}: ${error.message}`);
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return observations;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The kind of a checked step: its one key from the step kinds. */
|
|
413
|
+
const stepKindOf = (step: Step) => Object.keys(step).find((key) => STEP_KINDS.includes(key)) ?? "step";
|
|
414
|
+
|
|
415
|
+
/** Runs one step of one side. Gives the number of the last checkpoint. */
|
|
416
|
+
async function runStep(client: ViceSessionClient, scenario: Scenario, side: Side, step: Step, at: (location: Location) => number, last: number, files: boolean, observations: Map<string, Observation>): Promise<number> {
|
|
417
|
+
const subject = scenario[side];
|
|
418
|
+
let checkpoint = last;
|
|
419
|
+
// Observe first: an observe step may also carry "registers" or "memory".
|
|
420
|
+
if ("observe" in step) {
|
|
421
|
+
checkpoint++;
|
|
422
|
+
const observation: Observation = { memory: [], registers: {} };
|
|
423
|
+
for (const item of step.memory ?? []) {
|
|
424
|
+
const spec: MemoryObservation = typeof item === "string" ? { address: item } : item;
|
|
425
|
+
const address = at(spec.address);
|
|
426
|
+
const size = spec.size ?? 1;
|
|
427
|
+
const read = { data: (await readMemoryInParts(client, address, size, spec.view ?? "cpu")).toString("hex") };
|
|
428
|
+
const excluded = (spec.exclude ?? []).map((exclude) => {
|
|
429
|
+
const offset = at(exclude.address) - address;
|
|
430
|
+
if (offset < 0 || offset >= size) throw new Refusal("invalid-input", `the exclude ${exclude.address} lies outside the observed ${spec.address} (${size} bytes)`);
|
|
431
|
+
return { offset, address: exclude.address, reason: exclude.reason };
|
|
432
|
+
});
|
|
433
|
+
observation.memory.push({ label: size > 1 ? `${spec.address} (${size} bytes)` : spec.address, address, bytes: [...Buffer.from(read.data, "hex")], tolerance: spec.tolerance ?? 0, excluded });
|
|
434
|
+
}
|
|
435
|
+
if (step.registers !== undefined) {
|
|
436
|
+
const registers = await client.registersGet("c64");
|
|
437
|
+
for (const name of step.registers) observation.registers[name] = name === "pc" ? formatC64Address(registers.pc) : registers[name];
|
|
438
|
+
}
|
|
439
|
+
if (step.vicii) {
|
|
440
|
+
// The raster line changes from one moment to the next; the comparison leaves it out.
|
|
441
|
+
const { rasterLine: _rasterLine, ...state } = await client.vicii();
|
|
442
|
+
observation.vicii = formatVicii(state);
|
|
443
|
+
}
|
|
444
|
+
if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
|
|
445
|
+
if (step.screen !== undefined) {
|
|
446
|
+
const baseline = baselineName(checkpoint, step.observe);
|
|
447
|
+
if (side === "original") {
|
|
448
|
+
const shot = await client.screenCapture(baseline);
|
|
449
|
+
if (files) observation.png = shot.png;
|
|
450
|
+
} else {
|
|
451
|
+
const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff: files });
|
|
452
|
+
observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, comparedPixels: compared.comparedPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
|
|
453
|
+
if (files) {
|
|
454
|
+
observation.png = (await client.screenCapture()).png;
|
|
455
|
+
if (compared.diffPng !== undefined) observation.diffPng = compared.diffPng;
|
|
420
456
|
}
|
|
421
|
-
|
|
422
|
-
observations.set(step.observe, observation);
|
|
423
|
-
} else if ("reset" in step) {
|
|
424
|
-
// Stopped at the reset vector, so frame counts from here are exact.
|
|
425
|
-
await client.reset({ mode: step.reset, run: false });
|
|
426
|
-
} else if ("load" in step) {
|
|
427
|
-
await client.programLoad({ path: subject.program });
|
|
428
|
-
} else if ("autostart" in step) {
|
|
429
|
-
await startProgram(client, subject.program, side);
|
|
430
|
-
} else if ("snapshot" in step) {
|
|
431
|
-
await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
|
|
432
|
-
} else if ("normalize" in step) {
|
|
433
|
-
await normalize(client);
|
|
434
|
-
} else {
|
|
435
|
-
const { side: _side, ...action } = step;
|
|
436
|
-
const notReached = await runActionStep(client, action as ActionStep, at);
|
|
437
|
-
if (notReached !== undefined && "runUntil" in step) {
|
|
438
|
-
throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.runUntil.timeoutFrames} frames (it stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)})`);
|
|
457
|
+
await client.screenDiscard(baseline);
|
|
439
458
|
}
|
|
440
459
|
}
|
|
460
|
+
observations.set(step.observe, observation);
|
|
461
|
+
} else if ("reset" in step) {
|
|
462
|
+
// Stopped at the reset vector, so frame counts from here are exact.
|
|
463
|
+
await client.reset({ mode: step.reset, run: false });
|
|
464
|
+
} else if ("load" in step) {
|
|
465
|
+
await client.programLoad({ path: subject.program });
|
|
466
|
+
} else if ("autostart" in step) {
|
|
467
|
+
await startProgram(client, subject.program, side);
|
|
468
|
+
} else if ("snapshot" in step) {
|
|
469
|
+
await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
|
|
470
|
+
} else if ("normalize" in step) {
|
|
471
|
+
await normalize(client);
|
|
472
|
+
} else {
|
|
473
|
+
const { side: _side, ...action } = step;
|
|
474
|
+
const notReached = await runActionStep(client, action as ActionStep, at);
|
|
475
|
+
if (notReached !== undefined && "runUntil" in step) {
|
|
476
|
+
throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.runUntil.timeoutFrames} frames (it stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)})`);
|
|
477
|
+
}
|
|
441
478
|
}
|
|
442
|
-
return
|
|
479
|
+
return checkpoint;
|
|
443
480
|
}
|
|
444
481
|
|
|
445
|
-
|
|
446
|
-
|
|
482
|
+
type MemoryItem = Observation["memory"][number];
|
|
483
|
+
|
|
484
|
+
/** The bytes of a memory item that differ by more than its tolerance, without the excluded bytes. */
|
|
485
|
+
function differingBytes(item: MemoryItem, other: MemoryItem): DifferingByte[] {
|
|
486
|
+
const hex = (byte: number) => byte.toString(16).padStart(2, "0");
|
|
487
|
+
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
488
|
+
return 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]!) }] : []));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** The most differing bytes of one memory item that a result lists when no --max-listed is given. */
|
|
492
|
+
export const DEFAULT_LISTED_BYTES = 50;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The differences between the two sides' observations at each checkpoint,
|
|
496
|
+
* under the scenario's rules. A memory item lists at most `max` differing
|
|
497
|
+
* bytes; `rest` tells where the others are.
|
|
498
|
+
*/
|
|
499
|
+
export function compare(original: Map<string, Observation>, rebuild: Map<string, Observation>, options: { max?: number; rest?: string } = {}): Difference[] {
|
|
447
500
|
const differences: Difference[] = [];
|
|
501
|
+
const max = options.max ?? DEFAULT_LISTED_BYTES;
|
|
448
502
|
for (const [checkpoint, left] of original) {
|
|
449
503
|
const right = rebuild.get(checkpoint)!;
|
|
450
504
|
left.memory.forEach((item, index) => {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
if (item.bytes.some((byte, at) => !skipped.has(at) && Math.abs(byte - other.bytes[at]!) > item.tolerance)) {
|
|
454
|
-
const show = (bytes: number[]) => Buffer.from(bytes).toString("hex");
|
|
455
|
-
differences.push({ checkpoint, what: `memory ${item.label}`, original: show(item.bytes), rebuild: show(other.bytes) });
|
|
456
|
-
}
|
|
505
|
+
const bytes = differingBytes(item, right.memory[index]!);
|
|
506
|
+
if (bytes.length > 0) differences.push({ checkpoint, what: `memory ${item.label}`, bytes: listed(bytes, max, undefined, options.rest) });
|
|
457
507
|
});
|
|
458
508
|
for (const [name, value] of Object.entries(left.registers)) {
|
|
459
509
|
const other = right.registers[name as ObservedRegister];
|
|
@@ -469,15 +519,15 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
469
519
|
|
|
470
520
|
/**
|
|
471
521
|
* Writes the files of a FAIL into `out`: for a screen difference both
|
|
472
|
-
* screens and the difference image, and memory.json with each differing
|
|
522
|
+
* screens and the difference image, and memory.json with each differing
|
|
523
|
+
* memory item: all its bytes on each side, and each byte that differs.
|
|
473
524
|
*/
|
|
474
525
|
function writeFailFiles(out: string, original: Map<string, Observation>, rebuild: Map<string, Observation>, differences: Difference[]): Written[] {
|
|
475
526
|
const files: Written[] = [];
|
|
476
527
|
const write = (name: string, bytes: Uint8Array) => {
|
|
477
528
|
files.push(writeReported(`${out}/${name}`, bytes, { what: "--out" }));
|
|
478
529
|
};
|
|
479
|
-
const memory: Array<{ checkpoint: string; what: string;
|
|
480
|
-
const hex = (byte: number) => byte.toString(16).padStart(2, "0");
|
|
530
|
+
const memory: Array<{ checkpoint: string; what: string; address: string; original: string; rebuild: string; bytes: DifferingByte[] }> = [];
|
|
481
531
|
for (const difference of differences) {
|
|
482
532
|
const left = original.get(difference.checkpoint)!;
|
|
483
533
|
const right = rebuild.get(difference.checkpoint)!;
|
|
@@ -491,9 +541,8 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
|
|
|
491
541
|
const index = left.memory.findIndex((item) => `memory ${item.label}` === difference.what);
|
|
492
542
|
const item = left.memory[index]!;
|
|
493
543
|
const other = right.memory[index]!;
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
memory.push({ checkpoint: difference.checkpoint, what: difference.what, bytes });
|
|
544
|
+
const show = (bytes: number[]) => Buffer.from(bytes).toString("hex");
|
|
545
|
+
memory.push({ checkpoint: difference.checkpoint, what: difference.what, address: formatC64Address(item.address), original: show(item.bytes), rebuild: show(other.bytes), bytes: differingBytes(item, other) });
|
|
497
546
|
}
|
|
498
547
|
}
|
|
499
548
|
if (memory.length > 0) write("memory.json", Buffer.from(`${JSON.stringify(memory, null, 2)}\n`));
|
|
@@ -501,7 +550,7 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
|
|
|
501
550
|
}
|
|
502
551
|
|
|
503
552
|
/** Runs the scenario on the original, then on the rebuild, in one VICE session. With out, a FAIL leaves files there. */
|
|
504
|
-
export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string; names?: Map<Side, Map<string, number
|
|
553
|
+
export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string; names?: Map<Side, Map<string, number>>; max?: number } = {}): Promise<ScenarioResult> {
|
|
505
554
|
const checkpoints = scenario.steps.flatMap((step) => ("observe" in step ? [step.observe] : []));
|
|
506
555
|
const observed = new Map<Side, Map<string, Observation>>();
|
|
507
556
|
// The names of both sides before either side runs: a bad symbols file is an error, not a result.
|
|
@@ -519,7 +568,7 @@ export async function runScenario(client: ViceSessionClient, scenario: Scenario,
|
|
|
519
568
|
}
|
|
520
569
|
const original = observed.get("original")!;
|
|
521
570
|
const rebuild = observed.get("rebuild")!;
|
|
522
|
-
const differences = compare(original, rebuild);
|
|
571
|
+
const differences = compare(original, rebuild, { ...(options.max === undefined ? {} : { max: options.max }), rest: options.out === undefined ? "a larger --max-listed, or --out <dir> for memory.json with every byte" : `${options.out}/memory.json` });
|
|
523
572
|
const excluded = [...original].flatMap(([checkpoint, observation]) => observation.memory.flatMap((item) => item.excluded.map((excluded) => ({ checkpoint, address: excluded.address, reason: excluded.reason }))));
|
|
524
573
|
const files = differences.length > 0 && options.out !== undefined ? writeFailFiles(options.out, original, rebuild, differences) : [];
|
|
525
574
|
return {
|
|
@@ -47,7 +47,7 @@ async function test(call: Call): Promise<ScenarioResult> {
|
|
|
47
47
|
return { result: "INCONCLUSIVE", scenario: scenario.name, reason: `no emulator: ${error.message}`, checkpoints: [], differences: [] };
|
|
48
48
|
}
|
|
49
49
|
try {
|
|
50
|
-
return await runScenario(client, scenario, { names, ...(out === undefined ? {} : { out }) });
|
|
50
|
+
return await runScenario(client, scenario, { names, max: call.count("max-listed")!, ...(out === undefined ? {} : { out }) });
|
|
51
51
|
} finally {
|
|
52
52
|
await client.close();
|
|
53
53
|
}
|
|
@@ -123,14 +123,17 @@ const COMMANDS: CommandSpec[] = [
|
|
|
123
123
|
name: "test",
|
|
124
124
|
summary: "Runs a scenario on the original and then on the rebuild in one emulator. result is PASS, FAIL with the differences, or INCONCLUSIVE with the reason.",
|
|
125
125
|
subjects: [{ name: "<scenario.json>", help: "a scenario file, relative to the project folder. See references/scenario-format.md" }],
|
|
126
|
-
options: {
|
|
126
|
+
options: {
|
|
127
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "with a FAIL, write both screens, the difference image and memory.json into this folder: each differing memory item with all its bytes, and each byte that differs" },
|
|
128
|
+
"max-listed": { ...OPTIONS["max-listed"], help: "list at most n differing bytes of each memory item (default 50). The count is always given" },
|
|
129
|
+
},
|
|
127
130
|
run: test,
|
|
128
131
|
},
|
|
129
132
|
{
|
|
130
133
|
name: "run",
|
|
131
134
|
summary: "Runs the steps of a run file in its own emulator and writes what they ask for into --out: screens, coverage maps, snapshot files, traces, and results.json with the observations. result is FAIL when a step did not run.",
|
|
132
135
|
subjects: [{ name: "<run.json>", help: "a run file, relative to the project folder. See references/scenario-format.md, section Runs" }],
|
|
133
|
-
options: { out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the files of the steps and results.json" } },
|
|
136
|
+
options: { out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the files of the steps and results.json" } },
|
|
134
137
|
run: async (call) => {
|
|
135
138
|
const { output, failed } = await runFile(call.subject(0), call.string("out")!);
|
|
136
139
|
return { result: failed ? "FAIL" : "PASS", ...output };
|
|
@@ -141,7 +144,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
141
144
|
summary: "Plays the steps of a record file on the original in the replay core (c64run), and writes the session as a recording folder: recording.json, reads.txt and coverage.bin. result is FAIL when the session did not complete.",
|
|
142
145
|
subjects: [{ name: "<record.json>", help: "a record file, relative to the project folder. See references/recording-format.md" }],
|
|
143
146
|
options: {
|
|
144
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the recording folder, for example tests/recordings/<name>" },
|
|
147
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the recording folder, for example tests/recordings/<name>" },
|
|
145
148
|
play: { kind: "flag", help: "a person plays after the steps: the script prints the address of a page on stderr. The page shows the start screen frozen until Start (Enter). End (F12) saves the recording, Cancel (F11) ends without one" },
|
|
146
149
|
joystick: { kind: "count", value: "<1|2>", min: 1, max: 2, help: "with --play: the joystick port that the program reads, at the start (default 2). F9 on the page changes it" },
|
|
147
150
|
"http-port": { kind: "count", value: "<n>", min: 0, max: 65535, help: `with --play: the port of the page (default ${DEFAULT_HTTP_PORT})` },
|
|
@@ -158,7 +161,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
158
161
|
pin: { kind: "text", value: "<item>", multiple: true, help: `random state in RAM, in the form of --state: at each checkpoint the rebuild gets the original's values there` },
|
|
159
162
|
rebuild: { kind: "path", value: "<prg>", help: "the rebuild to compare with the original" },
|
|
160
163
|
symbols: { ...OPTIONS.symbols, help: "the names of the rebuild: the c64-assembler output (default: the symbols of the recording)" },
|
|
161
|
-
out: { ...OPTIONS.out, value: "<dir>", help: "with a FAIL, write the RAM of both sides at the first different checkpoint into this folder" },
|
|
164
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "with a FAIL, write the RAM of both sides at the first different checkpoint into this folder" },
|
|
162
165
|
},
|
|
163
166
|
notes: "A recording without names takes the names of the knowledge for the original. A $ address is an address of the original: on the rebuild it moves with the name before it.",
|
|
164
167
|
run: (call) => {
|
|
@@ -178,7 +181,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
178
181
|
"max-listed": OPTIONS["max-listed"],
|
|
179
182
|
ntsc: { kind: "flag", help: "the snapshots are of an NTSC machine (default PAL)" },
|
|
180
183
|
address: { kind: "address", value: "<address>", help: "the address of the first byte of a memory file that is shorter than 64 KiB" },
|
|
181
|
-
out: { ...OPTIONS.out, value: "<file>", help: "also write all ranges to this file" },
|
|
184
|
+
out: { ...OPTIONS.out, output: "file", value: "<file>", help: "also write all ranges to this file" },
|
|
182
185
|
},
|
|
183
186
|
run: (call) => {
|
|
184
187
|
const [address, out] = [call.address("address"), call.string("out")];
|
|
@@ -26,7 +26,7 @@ node <skill>/scripts/unpacker.ts capture original/game.prg --until '$c000' --out
|
|
|
26
26
|
node <skill>/scripts/unpacker.ts loads original/game.d64 --steps .c64-re-tools/local/parts.json --out .c64-re-tools/local/parts --drive
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`inspect` examines the file locally. `trace`, `stages`, `capture` and `loads` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). `capture` stops the program when the CPU gets to the `--until` address. In `stages` and `capture`, a `frames` step and a `runUntil` step to an address without a condition count as run time. `stages` refuses a different `runUntil` step.
|
|
29
|
+
`inspect` examines the file locally. `trace`, `stages`, `capture` and `loads` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). `wrote` lists each block that the stage wrote, also a small block. `capture` stops the program when the CPU gets to the `--until` address. In `stages` and `capture`, a `frames` step and a `runUntil` step to an address without a condition count as run time. `stages` refuses a different `runUntil` step.
|
|
30
30
|
|
|
31
31
|
| Option | Meaning |
|
|
32
32
|
|---|---|
|
|
@@ -41,11 +41,11 @@ node <skill>/scripts/unpacker.ts loads original/game.d64 --steps .c64-re-tools/l
|
|
|
41
41
|
| `--max-listed 50` | `trace` only: the most ranges that `writtenThenExecuted` lists (default 50). |
|
|
42
42
|
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
43
43
|
| `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
|
|
44
|
-
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
44
|
+
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). It does not change `wrote` or the segments. |
|
|
45
45
|
|
|
46
46
|
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script refuses an option that the command does not use. It refuses a wrong `--range` or `--exclude` before the emulator starts.
|
|
47
47
|
|
|
48
|
-
A steps file is a JSON list of steps. Each step has the input of the MCP tool of the same action. An address in a step can be a name. For example, wait 50 frames and type a space:
|
|
48
|
+
A steps file is a JSON list of steps. Each step has the input of the MCP tool of the same action. An address in a step can be a name. The script refuses an unknown name before the emulator starts. The message gives the file and the step. For example, wait 50 frames and type a space:
|
|
49
49
|
|
|
50
50
|
```json
|
|
51
51
|
[{ "frames": 50 }, { "type": { "mode": "text", "text": " " } }]
|
|
@@ -69,6 +69,7 @@ The scripts refuse a step in the form of version 2.0, for example `{ "runUntil":
|
|
|
69
69
|
- An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
|
|
70
70
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
71
71
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
72
|
+
- The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
|
|
72
73
|
- The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
|
|
73
74
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
74
75
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -107,8 +108,8 @@ The scripts refuse a step in the form of version 2.0, for example `{ "runUntil":
|
|
|
107
108
|
- A program can be relocated and not packed. Then a short copier moves the code and jumps to it. The moved code is the last stage. Make sure that the `entry` of the last stage is not code that the program writes later. An example is a routine that the program makes at run time. Disassemble the entry to make sure.
|
|
108
109
|
4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
|
|
109
110
|
- `image.bin`: all 64 KiB of RAM at the entry.
|
|
110
|
-
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
111
|
-
- One PRG for each segment.
|
|
111
|
+
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote, also a small block. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. A block that a stage before the program wrote in such a block is also not a segment, for example a variable of a loader. A block that is fully in pages 0 to 3 is not a segment, because `entry_state.json` holds these pages. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
112
|
+
- One PRG for each segment. A capture into a directory that has a capture removes the segment PRGs that the new `image.json` does not list. `removed` in the result names them. The capture keeps all other files.
|
|
112
113
|
- `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
|
|
113
114
|
5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it. Then capture again with `--exclude` for that range. Thus the source and the loader do not include the range.
|
|
114
115
|
6. Put the capture into the knowledge with `image <out>/image.json` of the c64-knowledge skill. The knowledge keeps the segments, the entry, the entry state and the stages. The other skills read them there. The files in `<out>` are not necessary after that.
|
|
@@ -131,7 +132,7 @@ A multi-load program loads more parts from the disk while it runs, for example o
|
|
|
131
132
|
```
|
|
132
133
|
|
|
133
134
|
2. Run `loads` with the steps file and a directory in `--out`. Add `--drive` when the program has its own drive code, for example a fast loader.
|
|
134
|
-
3. Each capture writes `<out>/<name>/` with `image.bin`, `image.json`, `entry_state.json` and a PRG for each segment. A segment is a block of RAM from `$0400` that the program changed after the capture before. The first capture also has the program itself.
|
|
135
|
+
3. Each capture writes `<out>/<name>/` with `image.bin`, `image.json`, `entry_state.json` and a PRG for each segment. It removes the segment PRGs and the `drive8.prg` of an earlier capture that the new `image.json` does not list. A segment is a block of RAM from `$0400` that the program changed after the capture before. The first capture also has the program itself.
|
|
135
136
|
4. Put the part that you work on into the knowledge with `image <out>/<name>/image.json` of the c64-knowledge skill.
|
|
136
137
|
5. With `--drive`, `drive8.prg` is the drive RAM at `$0000`-`$07ff`. Put it into the knowledge with `image <out>/<name>/drive8.prg --space drive8`. Then analyze it with c64-static-analysis and `--space drive8`.
|
|
137
138
|
|
|
@@ -150,7 +151,9 @@ The packing evidence, the stages, or the unpacked program with its image, segmen
|
|
|
150
151
|
|
|
151
152
|
`trace`, `stages`, `capture` and `loads` give `result: "PASS"` or `result: "FAIL"`. A FAIL has `reason`. `capture` gives the written files in `files`, each as `{out, bytes, sha256}`.
|
|
152
153
|
|
|
153
|
-
|
|
154
|
+
`capture` gives `differsFromReset`: the count of bytes that are not as a clean C64 has them at READY.
|
|
155
|
+
|
|
156
|
+
For `loads`: `captures`, with the name, the PC, the segments, `differsFromReset`, the files and the drive file of each capture. Each `entry_state.json` of `loads` has `differsFromReset` as the one of `capture` has. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
|
|
154
157
|
|
|
155
158
|
## Failure and conflicts
|
|
156
159
|
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
import { COVERAGE, formatC64Address } from "#src/c64.ts";
|
|
8
8
|
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
10
|
-
import { ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, checkStepNames, isObject, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
11
11
|
import { knowledgeAt } from "#src/knowledge/names.ts";
|
|
12
12
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
13
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
14
14
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
15
15
|
import { UsageError } from "#src/script.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { entryStateFile, json, machineState, readRam, readyState, removeStaleFiles, vectorsOf } from "./machine.ts";
|
|
17
|
+
import { FIRST_SEGMENT_ADDRESS } from "./stages.ts";
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
export type LoadStep = ActionStep | { capture: string };
|
|
@@ -22,8 +23,6 @@ export type LoadStep = ActionStep | { capture: string };
|
|
|
22
23
|
const NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
23
24
|
/** The drive RAM of a 1541. */
|
|
24
25
|
const DRIVE_RAM = 0x0800;
|
|
25
|
-
/** Pages 0-3 belong to the entry state, not to the segments. */
|
|
26
|
-
const FIRST_SEGMENT_ADDRESS = 0x0400;
|
|
27
26
|
/** The KERNAL LOAD entry in the jump table. */
|
|
28
27
|
const KERNAL_LOAD = 0xffd5;
|
|
29
28
|
/** The KERNAL loop that waits for a key at READY. or at an INPUT statement. */
|
|
@@ -57,7 +56,10 @@ export function readLoadSteps(path: string): LoadStep[] {
|
|
|
57
56
|
if (kinds.length > 1) return fail(`${at} has ${kinds.length} step kinds: ${kinds.join(", ")}. Give each one its own step`);
|
|
58
57
|
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...VICE_STEP_KINDS, "capture"].join(", ")}`);
|
|
59
58
|
// The step with its defaults filled in, as the shared schema of the action steps gives it.
|
|
60
|
-
|
|
59
|
+
const checked = checkActionStep(kinds[0] as ActionStepKind, step, at, fail);
|
|
60
|
+
// Its names resolve now: an unknown name stops the call before the emulator starts.
|
|
61
|
+
checkStepNames([checked], knowledgeAt(), `--steps ${path}`, index + 1);
|
|
62
|
+
(value as unknown[])[index] = checked;
|
|
61
63
|
});
|
|
62
64
|
if (names.size === 0) fail("the steps have no capture step");
|
|
63
65
|
return value as LoadStep[];
|
|
@@ -97,6 +99,8 @@ const hex4 = (value: number) => value.toString(16).padStart(4, "0");
|
|
|
97
99
|
export async function loads(program: string, options: { out: string; steps: LoadStep[]; drive: boolean; minBlock: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
98
100
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
99
101
|
try {
|
|
102
|
+
// A clean machine at the READY prompt shows what the program changed at each capture. The run goes on after a capture, so it is read first.
|
|
103
|
+
const clean = await readyState(client);
|
|
100
104
|
await client.reset({ mode: "hard", run: false });
|
|
101
105
|
let previous: Buffer;
|
|
102
106
|
/** A PRG that autostart put into memory without the CPU: its bytes count as written in the first capture. */
|
|
@@ -184,15 +188,20 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
184
188
|
...(drive === undefined ? {} : { drive }),
|
|
185
189
|
}),
|
|
186
190
|
);
|
|
187
|
-
|
|
191
|
+
const entry = entryStateFile(registers, state, vectors, clean);
|
|
192
|
+
write("entry_state.json", entry.bytes);
|
|
193
|
+
// A capture again into the folder: the files of the earlier capture that image.json no longer lists go.
|
|
194
|
+
const removed = removeStaleFiles(directory, [...files, ...(drive === undefined ? [] : ["drive8.prg"])]);
|
|
188
195
|
const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
|
|
189
196
|
captures.push({
|
|
190
197
|
capture: step.capture,
|
|
191
198
|
out: directory,
|
|
192
199
|
files: written,
|
|
200
|
+
...(removed.length === 0 ? {} : { removed }),
|
|
193
201
|
pc: formatC64Address(registers.pc),
|
|
194
202
|
...(keyWait ? { keyWait: true } : {}),
|
|
195
203
|
segments: segments.map(showRange),
|
|
204
|
+
differsFromReset: entry.differsFromReset,
|
|
196
205
|
...(small === 0 ? {} : { smallChanges: small }),
|
|
197
206
|
...(drive === undefined ? {} : { drive: drive.file }),
|
|
198
207
|
});
|