@henols/c64-re-tools 2.1.2 → 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-disk/scripts/lib/containers.ts +2 -1
- package/skills/c64-emulator/SKILL.md +2 -0
- package/skills/c64-knowledge/SKILL.md +6 -2
- package/skills/c64-knowledge/scripts/knowledge.ts +6 -12
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -1
- package/skills/c64-memory-map/SKILL.md +2 -0
- package/skills/c64-memory-map/scripts/memory-map.ts +22 -3
- package/skills/c64-provenance/SKILL.md +4 -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 +13 -4
- package/skills/c64-source/SKILL.md +1 -1
- package/skills/c64-source/scripts/lib/source.ts +18 -4
- package/skills/c64-source/scripts/source.ts +9 -13
- package/skills/c64-static-analysis/SKILL.md +4 -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 +17 -14
- package/skills/c64-testing/SKILL.md +6 -3
- package/skills/c64-testing/references/scenario-format.md +5 -3
- package/skills/c64-testing/scripts/lib/diff-files.ts +27 -8
- package/skills/c64-testing/scripts/lib/runs.ts +23 -7
- package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
- package/skills/c64-testing/scripts/testing.ts +5 -2
- package/skills/c64-unpacker/SKILL.md +2 -2
- package/skills/c64-unpacker/scripts/lib/loads.ts +4 -1
- package/skills/c64-unpacker/scripts/lib/machine.ts +31 -0
- package/skills/c64-unpacker/scripts/unpacker.ts +4 -1
- package/src/host/vice/session.ts +98 -11
- package/src/host-client/image.ts +22 -8
- package/src/host-client/steps.ts +2 -2
- package/src/host-client/tools.ts +4 -3
- package/src/host-client/transfer.ts +8 -3
- package/src/knowledge/write.ts +64 -28
- package/src/mcp/output.ts +1 -1
- package/src/mcp/server.ts +7 -0
- package/src/mcp/tools/debug.ts +2 -1
- package/src/mcp/tools/media.ts +5 -1
- package/src/media.ts +15 -0
- package/src/opcodes.ts +31 -1
- package/src/platform.ts +14 -1
- package/src/project.ts +8 -1
- package/src/script.ts +63 -2
|
@@ -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,7 +123,10 @@ 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
|
{
|
|
@@ -109,7 +109,7 @@ The scripts refuse a step in the form of version 2.0, for example `{ "runUntil":
|
|
|
109
109
|
4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
|
|
110
110
|
- `image.bin`: all 64 KiB of RAM at the entry.
|
|
111
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.
|
|
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.
|
|
113
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.
|
|
114
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.
|
|
115
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.
|
|
@@ -132,7 +132,7 @@ A multi-load program loads more parts from the disk while it runs, for example o
|
|
|
132
132
|
```
|
|
133
133
|
|
|
134
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.
|
|
135
|
-
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.
|
|
136
136
|
4. Put the part that you work on into the knowledge with `image <out>/<name>/image.json` of the c64-knowledge skill.
|
|
137
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`.
|
|
138
138
|
|
|
@@ -13,7 +13,7 @@ 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 { entryStateFile, json, machineState, readRam, readyState, vectorsOf } from "./machine.ts";
|
|
16
|
+
import { entryStateFile, json, machineState, readRam, readyState, removeStaleFiles, vectorsOf } from "./machine.ts";
|
|
17
17
|
import { FIRST_SEGMENT_ADDRESS } from "./stages.ts";
|
|
18
18
|
|
|
19
19
|
|
|
@@ -190,11 +190,14 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
190
190
|
);
|
|
191
191
|
const entry = entryStateFile(registers, state, vectors, clean);
|
|
192
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"])]);
|
|
193
195
|
const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
|
|
194
196
|
captures.push({
|
|
195
197
|
capture: step.capture,
|
|
196
198
|
out: directory,
|
|
197
199
|
files: written,
|
|
200
|
+
...(removed.length === 0 ? {} : { removed }),
|
|
198
201
|
pc: formatC64Address(registers.pc),
|
|
199
202
|
...(keyWait ? { keyWait: true } : {}),
|
|
200
203
|
segments: segments.map(showRange),
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
// 0-3, the chip registers read without side effects, and the vectors. The
|
|
3
3
|
// capture and loads commands of the unpacker share it.
|
|
4
4
|
|
|
5
|
+
import { readdirSync, rmSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
5
8
|
import { formatC64Address } from "#src/c64.ts";
|
|
6
9
|
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
10
|
+
import { writeRefusal } from "#src/host-client/transfer.ts";
|
|
7
11
|
import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
12
|
+
import { projectRoot, resolveProjectPath } from "#src/project.ts";
|
|
8
13
|
|
|
9
14
|
type Registers = Awaited<ReturnType<ViceSessionClient["registersGet"]>>;
|
|
10
15
|
|
|
@@ -102,4 +107,30 @@ function entryState(registers: Registers, state: MachineState, vectors: ReturnTy
|
|
|
102
107
|
};
|
|
103
108
|
}
|
|
104
109
|
|
|
110
|
+
/** The names of the files that a capture writes for its content: a PRG for each segment, and the drive RAM. */
|
|
111
|
+
const CAPTURE_FILE = /^(?:segment-[0-9a-f]{4}-[0-9a-f]{4}\.prg|drive8\.prg)$/;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Removes from the capture folder `folder` (a project path) each file that an
|
|
115
|
+
* earlier capture wrote and this capture did not: a segment PRG of another
|
|
116
|
+
* range, or the drive RAM. image.json lists only the files of this capture.
|
|
117
|
+
* Every other file stays. Gives the removed files as project paths.
|
|
118
|
+
*/
|
|
119
|
+
export function removeStaleFiles(folder: string, written: readonly string[]): string[] {
|
|
120
|
+
const path = resolveProjectPath(folder, projectRoot());
|
|
121
|
+
const removed: string[] = [];
|
|
122
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
123
|
+
// A symbolic link is not a file of a capture: it stays.
|
|
124
|
+
if (!entry.isFile() || !CAPTURE_FILE.test(entry.name) || written.includes(entry.name)) continue;
|
|
125
|
+
const file = `${folder}/${entry.name}`;
|
|
126
|
+
try {
|
|
127
|
+
rmSync(join(path, entry.name));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw writeRefusal(error, file, "--out") ?? error;
|
|
130
|
+
}
|
|
131
|
+
removed.push(file);
|
|
132
|
+
}
|
|
133
|
+
return removed.sort();
|
|
134
|
+
}
|
|
135
|
+
|
|
105
136
|
export const json = (value: unknown) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -19,7 +19,7 @@ import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, ty
|
|
|
19
19
|
import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
|
|
20
20
|
import { loads, readLoadSteps } from "./lib/loads.ts";
|
|
21
21
|
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
|
|
22
|
-
import { entryStateFile, machineState, readRam, readyState, vectorsOf } from "./lib/machine.ts";
|
|
22
|
+
import { entryStateFile, machineState, readRam, readyState, removeStaleFiles, vectorsOf } from "./lib/machine.ts";
|
|
23
23
|
|
|
24
24
|
/** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
|
|
25
25
|
const UNPACKED_BYTES = 256;
|
|
@@ -537,12 +537,15 @@ async function capture(
|
|
|
537
537
|
// A clean machine at the READY prompt shows what the loader and the stages changed.
|
|
538
538
|
const entry = entryStateFile(registers, state, vectors, await readyState(client));
|
|
539
539
|
write("entry_state.json", entry.bytes);
|
|
540
|
+
// A capture again into the folder: the segment PRGs of the earlier capture that image.json no longer lists go.
|
|
541
|
+
const removed = removeStaleFiles(options.out, files);
|
|
540
542
|
return {
|
|
541
543
|
output: {
|
|
542
544
|
captured: true,
|
|
543
545
|
pc: formatC64Address(registers.pc),
|
|
544
546
|
out: options.out,
|
|
545
547
|
files: written,
|
|
548
|
+
...(removed.length === 0 ? {} : { removed }),
|
|
546
549
|
segments: segments.map(showRange),
|
|
547
550
|
...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
|
|
548
551
|
stages: followed.stages.length,
|