@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
|
@@ -15,19 +15,34 @@ const semantic = (row: { origin: string }) => isSemantic(row.origin);
|
|
|
15
15
|
/**
|
|
16
16
|
* Seeds from current semantic knowledge: routines from the user or
|
|
17
17
|
* the LLM are entry points, their non-code regions are data, and their names
|
|
18
|
-
* are labels. Analyzer facts are never seeds: fed back, an analyzer's
|
|
19
|
-
* earlier guess would force the same answer again, and a re-analysis
|
|
20
|
-
* not retire it.
|
|
18
|
+
* are labels. Analyzer facts are never seeds alone: fed back, an analyzer's
|
|
19
|
+
* own earlier guess would force the same answer again, and a re-analysis
|
|
20
|
+
* could not retire it. A data or variable symbol of the user or the LLM is a
|
|
21
|
+
* decision that its address is data: the non-code region that holds it is
|
|
22
|
+
* data, also a region of an analyzer, else the byte of the symbol. Where a
|
|
23
|
+
* code region holds the symbol (a variable in the operand of self-modifying
|
|
24
|
+
* code) or the address executed, the symbol gives no data. A region with an
|
|
25
|
+
* executed address or an entry point in it gives only the byte of the symbol.
|
|
21
26
|
*/
|
|
22
|
-
export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoints: number[] = [], space: Space = DEFAULT_SPACE): Seeds {
|
|
27
|
+
export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoints: number[] = [], space: Space = DEFAULT_SPACE, executed: (address: number) => boolean = () => false): Seeds {
|
|
23
28
|
const symbols = listSymbols(db, undefined, undefined, undefined, space).filter(semantic);
|
|
24
29
|
const entryPoints = [...new Set([...extraEntryPoints, ...symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address)])].sort((a, b) => a - b);
|
|
30
|
+
const regions = listRegions(db, undefined, undefined, undefined, space);
|
|
31
|
+
// A JMP table is code and dual-use bytes are both: neither is a data range.
|
|
32
|
+
const isData = (region: { type: string }) => !CODE_REGION_TYPES.includes(region.type as never) && region.type !== "dual-use";
|
|
33
|
+
const dataRanges = regions.filter((region) => semantic(region) && isData(region)).map((region) => ({ start: region.start, end: region.end }));
|
|
34
|
+
const covered = (range: { start: number; end: number }) => dataRanges.some((seed) => seed.start <= range.end && range.start <= seed.end);
|
|
35
|
+
const holdsCode = (range: { start: number; end: number }) => entryPoints.some((entry) => entry >= range.start && entry <= range.end) || Array.from({ length: range.end - range.start + 1 }, (_, offset) => range.start + offset).some(executed);
|
|
36
|
+
for (const symbol of symbols.filter((row) => row.kind === "data" || row.kind === "variable").sort((a, b) => a.address - b.address)) {
|
|
37
|
+
const own = { start: symbol.address, end: symbol.address };
|
|
38
|
+
const region = regions.find((row) => row.start <= symbol.address && symbol.address <= row.end);
|
|
39
|
+
if ((region !== undefined && !isData(region)) || holdsCode(own) || covered(own)) continue;
|
|
40
|
+
const range = region !== undefined && !holdsCode(region) ? { start: region.start, end: region.end } : own;
|
|
41
|
+
dataRanges.push(range);
|
|
42
|
+
}
|
|
25
43
|
return {
|
|
26
44
|
entryPoints,
|
|
27
|
-
dataRanges:
|
|
28
|
-
// A JMP table is code and dual-use bytes are both: neither is a data range.
|
|
29
|
-
.filter((region) => semantic(region) && !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
|
|
30
|
-
.map((region) => ({ start: region.start, end: region.end })),
|
|
45
|
+
dataRanges: dataRanges.sort((a, b) => a.start - b.start),
|
|
31
46
|
labels: symbols.map((symbol) => ({ address: symbol.address, name: symbol.name })),
|
|
32
47
|
};
|
|
33
48
|
}
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
// that moves the address needs an operand expression there. They are only
|
|
4
4
|
// reported, never written: the LLM records the expressions with c64-knowledge.
|
|
5
5
|
//
|
|
6
|
-
// The scan reads
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// The scan reads the instructions of the code regions, decoded from the start
|
|
7
|
+
// of each region: bytes of data are never read as an instruction. It reads
|
|
8
|
+
// short straight runs: LDA/LDX/LDY #imm and STA/STX/STY to zero page or
|
|
9
|
+
// absolute, plus SEI, CLI, CLC, SEC and NOP. Any other instruction ends the
|
|
10
|
+
// run, so no register value is guessed across it.
|
|
9
11
|
|
|
10
|
-
import { instructionLength } from "#src/c64.ts";
|
|
11
12
|
import type { ImageByte } from "./coverage.ts";
|
|
13
|
+
import { instructionAt, type Instruction } from "./macros.ts";
|
|
12
14
|
|
|
13
15
|
const LOADS: Record<number, "a" | "x" | "y"> = { 0xa9: "a", 0xa2: "x", 0xa0: "y" };
|
|
14
16
|
const STORES: Record<number, "a" | "x" | "y"> = { 0x85: "a", 0x8d: "a", 0x86: "x", 0x8e: "x", 0x84: "y", 0x8c: "y" };
|
|
@@ -50,46 +52,57 @@ interface Store {
|
|
|
50
52
|
to: number;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
/** The stores of immediates in the straight run that starts at `start`. */
|
|
54
|
-
function storesFrom(
|
|
55
|
+
/** The stores of immediates in the straight run of decoded instructions that starts at `start`. */
|
|
56
|
+
function storesFrom(run: ReadonlyArray<Instruction & { at: number }>, start: number): Store[] {
|
|
55
57
|
const last: Partial<Record<"a" | "x" | "y", { load: number; value: number }>> = {};
|
|
56
58
|
const stores: Store[] = [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const opcode = byteAt(at);
|
|
60
|
-
if (opcode === undefined) break;
|
|
61
|
-
const length = instructionLength(opcode);
|
|
62
|
-
if (at + length - 1 > range.end) break;
|
|
63
|
-
const operand = length === 2 ? byteAt(at + 1)! : length === 3 ? byteAt(at + 1)! | (byteAt(at + 2)! << 8) : 0;
|
|
64
|
-
if (LOADS[opcode] !== undefined) last[LOADS[opcode]] = { load: at, value: operand };
|
|
59
|
+
for (const { opcode, value, at } of run.slice(start, start + MAX_RUN)) {
|
|
60
|
+
if (LOADS[opcode] !== undefined) last[LOADS[opcode]] = { load: at, value };
|
|
65
61
|
else if (STORES[opcode] !== undefined) {
|
|
66
62
|
const loaded = last[STORES[opcode]];
|
|
67
63
|
if (loaded === undefined) break;
|
|
68
|
-
stores.push({ ...loaded, to:
|
|
64
|
+
stores.push({ ...loaded, to: value });
|
|
69
65
|
} else if (!NEUTRAL.has(opcode)) break;
|
|
70
|
-
at += length;
|
|
71
66
|
}
|
|
72
67
|
return stores;
|
|
73
68
|
}
|
|
74
69
|
|
|
70
|
+
/** The instructions of each code region, decoded from its start, up to a byte that is no instruction. */
|
|
71
|
+
function decoded(byteAt: ImageByte, code: ReadonlyArray<{ start: number; end: number }>): Array<Array<Instruction & { at: number }>> {
|
|
72
|
+
return [...code]
|
|
73
|
+
.sort((a, b) => a.start - b.start)
|
|
74
|
+
.map((region) => {
|
|
75
|
+
const run: Array<Instruction & { at: number }> = [];
|
|
76
|
+
for (let at = region.start; at <= region.end; ) {
|
|
77
|
+
const instruction = instructionAt(byteAt, at, region.end);
|
|
78
|
+
if (instruction === undefined) break;
|
|
79
|
+
run.push({ ...instruction, at });
|
|
80
|
+
at += instruction.length;
|
|
81
|
+
}
|
|
82
|
+
return run;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
/**
|
|
76
|
-
* Finds the pointer candidates in the image: an immediate
|
|
77
|
-
* vector or to a zero-page pair that (zp),y
|
|
78
|
-
*
|
|
79
|
-
*
|
|
87
|
+
* Finds the pointer candidates in the code regions of the image: an immediate
|
|
88
|
+
* pair stored to a vector or to a zero-page pair that a (zp),y instruction of
|
|
89
|
+
* the code uses, where the pair makes an address in the image or a platform
|
|
90
|
+
* name; and an immediate stored to a sprite pointer of the default screen whose
|
|
91
|
+
* sprite data is in a sprite region.
|
|
80
92
|
*/
|
|
81
93
|
export function findPointers(
|
|
82
94
|
byteAt: ImageByte,
|
|
83
95
|
range: { start: number; end: number },
|
|
96
|
+
code: ReadonlyArray<{ start: number; end: number }>,
|
|
84
97
|
known: { platform: (address: number) => boolean; sprites: ReadonlyArray<{ start: number; end: number }> },
|
|
85
98
|
): PointerCandidate[] {
|
|
86
|
-
const
|
|
87
|
-
|
|
99
|
+
const runs = decoded(byteAt, code);
|
|
100
|
+
const indirect = new Set(runs.flatMap((run) => run.filter((instruction) => INDIRECT_Y.has(instruction.opcode)).map((instruction) => instruction.value)));
|
|
88
101
|
const inImage = (address: number) => address >= range.start && address <= range.end && byteAt(address) !== undefined;
|
|
89
102
|
const found = new Map<string, PointerCandidate>();
|
|
90
|
-
for (
|
|
91
|
-
if (LOADS[
|
|
92
|
-
const stores = storesFrom(
|
|
103
|
+
for (const [run, index] of runs.flatMap((run) => run.map((_, index) => [run, index] as const))) {
|
|
104
|
+
if (LOADS[run[index]!.opcode] === undefined) continue;
|
|
105
|
+
const stores = storesFrom(run, index);
|
|
93
106
|
for (const low of stores) {
|
|
94
107
|
const high = stores.find((store) => store.to === low.to + 1);
|
|
95
108
|
const target = high === undefined ? undefined : low.value | (high.value << 8);
|
|
@@ -17,7 +17,7 @@ import { analyzedBytes, programCoverage, requireProgram } from "#src/knowledge/p
|
|
|
17
17
|
import { importFindings, runProvenance, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
|
|
18
18
|
import { currentRevision, listMacros, listMacroUses, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
19
19
|
import { DEFAULT_SPACE, SEMANTIC_ORIGINS, type Analyzer } from "#src/knowledge/schema.ts";
|
|
20
|
-
import { platformNames } from "#src/platform.ts";
|
|
20
|
+
import { platformNames, platformNamesFor, romUseOfProgram, type RomUse } from "#src/platform.ts";
|
|
21
21
|
import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
|
|
22
22
|
import { codeEntries, coverageConflicts, coverageEntries, coverageSeedKinds, executedStarts, imageReader, romOnlyRanges, type ImageByte } from "./lib/coverage.ts";
|
|
23
23
|
import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./lib/findings.ts";
|
|
@@ -52,17 +52,19 @@ function showMacro(candidate: MacroCandidate) {
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* The macro candidates in the current code regions of every origin,
|
|
55
|
-
* with the names of the knowledge in their args
|
|
55
|
+
* with the names of the knowledge in their args, else the platform names of
|
|
56
|
+
* the ROMs that the program uses, as c64-source names them. A candidate is left out when
|
|
56
57
|
* a name or a reference target is inside it, or a patch or a use overlaps it:
|
|
57
58
|
* c64-source would write its instructions. The same use in the knowledge is
|
|
58
59
|
* known. With `batchPath`, the candidates go to an apply batch with the
|
|
59
60
|
* macros they need; a macro of the catalogue that the knowledge has with
|
|
60
61
|
* another body leaves its candidates out of the batch.
|
|
61
62
|
*/
|
|
62
|
-
function macroCandidates(space: string, byteAt: ImageByte, batchPath: string | undefined) {
|
|
63
|
+
function macroCandidates(space: string, byteAt: ImageByte, batchPath: string | undefined, romUse: RomUse) {
|
|
63
64
|
const db = openForRead();
|
|
64
65
|
try {
|
|
65
|
-
|
|
66
|
+
// The platform names that c64-source uses: a name of a ROM that the program does not use is no arg.
|
|
67
|
+
const platform = space === DEFAULT_SPACE ? new Map(platformNamesFor(romUse).map((entry) => [entry.address, entry.name])) : new Map<number, string>();
|
|
66
68
|
const semantic = new Map(listSymbols(db, undefined, undefined, [...SEMANTIC_ORIGINS], space).map((symbol) => [symbol.address, symbol.name]));
|
|
67
69
|
const code = listRegions(db, undefined, "code", undefined, space);
|
|
68
70
|
const found = findMacroUses(byteAt, code, (address) => semantic.get(address) ?? platform.get(address));
|
|
@@ -197,7 +199,7 @@ async function analyze(call: Call): Promise<unknown> {
|
|
|
197
199
|
const idioms = findIdioms(inProgram, { start: loaded.start, end: loaded.end }, covered);
|
|
198
200
|
|
|
199
201
|
const revision = currentRevision(reader);
|
|
200
|
-
const seeds = seedsFromKnowledge(reader, entries, space);
|
|
202
|
+
const seeds = seedsFromKnowledge(reader, entries, space, executed.executed);
|
|
201
203
|
// Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound.
|
|
202
204
|
const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage, inProgram);
|
|
203
205
|
const known = [...seeds.entryPoints, ...codeEntries(reader, byteAt, space)];
|
|
@@ -216,20 +218,21 @@ async function analyze(call: Call): Promise<unknown> {
|
|
|
216
218
|
const coverageUsed = fromCoverage.filter((entry) => seeds.entryPoints.includes(entry) || codeStarts.includes(entry)).length;
|
|
217
219
|
// Platform names are C64 names: drive code and banks get none.
|
|
218
220
|
const platform = space === DEFAULT_SPACE ? platformNames() : [];
|
|
219
|
-
const pointers = findPointers(byteAt, { start: loaded.start, end: loaded.end }, {
|
|
220
|
-
platform: (address) => platform.some((entry) => address >= entry.address && address <= entry.end),
|
|
221
|
-
sprites: listRegions(reader, undefined, "sprite", undefined, space),
|
|
222
|
-
});
|
|
223
221
|
reader?.close();
|
|
224
222
|
/** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
|
|
225
223
|
const idiomsResult = () => {
|
|
226
224
|
const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description: "Idioms in the program" }));
|
|
227
225
|
// The macro candidates read the code regions after both imports.
|
|
228
|
-
const macros = macroCandidates(space, byteAt, batchPath);
|
|
229
|
-
// Platform pitfalls are C64 facts: drive code and banks get none.
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
226
|
+
const macros = macroCandidates(space, byteAt, batchPath, romUseOfProgram(coverage, program.state));
|
|
227
|
+
// Platform pitfalls are C64 facts: drive code and banks get none. Pitfalls and pointers read the code regions after both imports.
|
|
228
|
+
const codeDb = openForRead();
|
|
229
|
+
const code = listRegions(codeDb, undefined, "code", undefined, space);
|
|
230
|
+
const pitfalls = space === DEFAULT_SPACE ? findPitfalls(byteAt, code) : [];
|
|
231
|
+
const pointers = findPointers(byteAt, { start: loaded.start, end: loaded.end }, code, {
|
|
232
|
+
platform: (address) => platform.some((entry) => address >= entry.address && address <= entry.end),
|
|
233
|
+
sprites: listRegions(codeDb, undefined, "sprite", undefined, space),
|
|
234
|
+
});
|
|
235
|
+
codeDb?.close();
|
|
233
236
|
return {
|
|
234
237
|
idioms: {
|
|
235
238
|
...idioms.counts,
|
|
@@ -26,7 +26,7 @@ node <skill>/scripts/testing.ts test tests/moves-left.json --out .c64-re-tools/l
|
|
|
26
26
|
node <skill>/scripts/testing.ts checklist tests/moves-left.json --item 'FIRE starts the game.'
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`testing.ts test` gives `result`: PASS, FAIL or INCONCLUSIVE. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json`
|
|
29
|
+
`testing.ts test` gives `result`: PASS, FAIL or INCONCLUSIVE. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json`. `memory.json` has all bytes of each memory item that differs, and each byte that differs. `files` in the result names them. A PASS writes no files. To keep the screens of a PASS, use a run (below).
|
|
30
30
|
|
|
31
31
|
`testing.ts checklist` gives a Markdown checklist for a human tester in `text`.
|
|
32
32
|
|
|
@@ -40,7 +40,7 @@ A run does steps on one program, with no comparison, and keeps the results as fi
|
|
|
40
40
|
node <skill>/scripts/testing.ts run .c64-re-tools/local/runs/rooms/run.json --out .c64-re-tools/local/runs/rooms
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The run file gives where to start (a program, or a snapshot file), the steps, and optional symbols. To go on from the live emulator, save its state with `c64_snapshot` action `save` and `out`, and start the run from that file. A run has the action steps of a scenario: `write`, `registers`, `joystick`, `type`, `frames` and `runUntil`. It also has these steps:
|
|
43
|
+
The run file gives where to start (a program, or a snapshot file), the steps, and optional symbols. A run file has no names unless it gives `"symbols": "knowledge"` or a JSON file of names. To go on from the live emulator, save its state with `c64_snapshot` action `save` and `out`, and start the run from that file. A run has the action steps of a scenario: `write`, `registers`, `joystick`, `type`, `frames` and `runUntil`. It also has these steps:
|
|
44
44
|
|
|
45
45
|
| Step | Effect |
|
|
46
46
|
|---|---|
|
|
@@ -122,6 +122,8 @@ node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/results.json
|
|
|
122
122
|
- Two memory files of a run: the changed ranges. For a file of less than 64 KiB, `--address` gives the address of its first byte.
|
|
123
123
|
- Two observations of runs, also of one run: the changed memory items and registers. `compared` gives the count of items that both observations have. With `compared: 0`, `reason` tells what each observation has. Then the diff compared nothing, and the observations are not equal.
|
|
124
124
|
|
|
125
|
+
For two snapshot files, `$00` and `$01` are the CPU port of the snapshot: the direction register and the data register. They are not the RAM under the port. `--ntsc` is only for snapshot files, and `--address` is only for memory files. The script refuses these options for the other kinds.
|
|
126
|
+
|
|
125
127
|
`registers` gives each changed register and chip value. `cpu.pc` is a `$` address. Each CPU flag is `cpu.flags.<flag>`, for example `cpu.flags.c`, with `true` or `false`.
|
|
126
128
|
|
|
127
129
|
The script refuses a snapshot that does not restore in its emulator. An example is a PAL snapshot with `--ntsc`. Give `--ntsc` only for snapshots of an NTSC machine.
|
|
@@ -159,7 +161,7 @@ The script refuses a snapshot that does not restore in its emulator. An example
|
|
|
159
161
|
5. Run `testing.ts test`.
|
|
160
162
|
6. Read the result:
|
|
161
163
|
- `PASS`: all observations are equal, or the differences are in the allowed tolerance.
|
|
162
|
-
- `FAIL`: `differences` lists each difference with the checkpoint
|
|
164
|
+
- `FAIL`: `differences` lists each difference with the checkpoint and the item. A memory item gives `bytes`: the count of the bytes that differ, and the first of these bytes. Each byte has the address, the original value and the rebuild value. `--max-listed` sets how many bytes it lists (default 50). `memory.json` in `--out` has all of them. Another item gives the original value and the rebuild value.
|
|
163
165
|
- Each value has one form. The pc and each address are `$xxxx`. Memory is lowercase hex. Other register values and chip values are decimal numbers.
|
|
164
166
|
- `INCONCLUSIVE`: `reason` tells why the script could not compare. This is not a failure of the program.
|
|
165
167
|
7. For a FAIL, examine the first difference with the c64-emulator skill.
|
|
@@ -189,6 +191,7 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
|
|
|
189
191
|
## Failure and conflicts
|
|
190
192
|
|
|
191
193
|
- `invalid-input`: the scenario file has an error. The message names the step. Change the step and run the test again.
|
|
194
|
+
- INCONCLUSIVE, `step N (kind): ...`: the step failed on one side. `reason` names the step and why.
|
|
192
195
|
- `invalid-input` that names the 2.0 form: a step, a memory item or a recording has a form of version 2.0. An example is `"at"` in place of `"address"`. The message gives the form to write. Change the file. Record a recording of version 2.0 again.
|
|
193
196
|
- `not-found` before the run: a file of the scenario or the run file is missing, for example the rebuild program. Build the rebuild, or change the path.
|
|
194
197
|
- INCONCLUSIVE, "did not reach the checkpoint": one side did not reach the checkpoint in the frame limit. Find the reason with the c64-emulator skill. Do not report a FAIL.
|
|
@@ -47,7 +47,7 @@ Before the first step of each side, the script does a hard reset. The CPU stops
|
|
|
47
47
|
| `{ "type": { "mode": "text", "text": "RUN\n" } }` | Put at most 1024 keys into the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. |
|
|
48
48
|
| `{ "type": { "mode": "petscii", "bytes": [147] } }` | Put PETSCII codes from 1 to 255 into the keyboard queue of the KERNAL. Use it for keys that mode `text` does not have. |
|
|
49
49
|
| `{ "runUntil": { "target": { "kind": "address", "address": "main_loop" }, "timeoutFrames": 600 } }` | Run until the CPU gets to an address. |
|
|
50
|
-
| `{ "runUntil": { "target": { "kind": "memory", "address": "game_state", "operator": "eq", "value": 2 }, "timeoutFrames": 600 } }` | Run until a byte
|
|
50
|
+
| `{ "runUntil": { "target": { "kind": "memory", "address": "game_state", "operator": "eq", "value": 2 }, "timeoutFrames": 600 } }` | Run until a write to the byte makes the comparison with `value` true. `operator` is `eq`, `ne`, `lt`, `lte`, `gt` or `gte`. The run does not stop for a byte that is already true at the start. It also does not stop for a register that the hardware changes, for example `$d012`. For a raster line, use a `raster` target. |
|
|
51
51
|
| `{ "runUntil": { "target": { "kind": "raster", "line": 250 }, "timeoutFrames": 5 } }` | Run until the raster beam gets to a line (0 to 311 for PAL, 0 to 262 for NTSC). Optional: `cycle` from 0 to 64. |
|
|
52
52
|
| `{ "runUntil": { "target": { "kind": "address", "address": "irq_tick", "condition": { "kind": "raster", "line": 250 } }, "timeoutFrames": 600 } }` | Run until the CPU gets to an address while the condition is true. |
|
|
53
53
|
| `{ "snapshot": "snapshots/start.vsf" }` | Restore a snapshot file. Usually with `side`: the snapshot of one program is not a state of the other. |
|
|
@@ -90,7 +90,7 @@ A run file (for `testing.ts run`) has:
|
|
|
90
90
|
|
|
91
91
|
- `name`: a short name.
|
|
92
92
|
- `start`: `{ "program": "game.prg" }` to autostart a program with a BASIC start line, `{ "program": "game.prg", "load": true }` to load it with the CPU stopped (set `pc` in a step), or `{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" }`.
|
|
93
|
-
- `symbols`: optional, `knowledge`, `none` (default), or a JSON file as for a subject.
|
|
93
|
+
- `symbols`: optional, `knowledge`, `none` (default), or a JSON file as for a subject. Without `symbols`, a run has no names: give `"symbols": "knowledge"` to use the names of the knowledge.
|
|
94
94
|
- `videoStandard`: optional, `pal` (default) or `ntsc`.
|
|
95
95
|
- `steps`: the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of a scenario, and the run steps `snapshot`, `save`, `screen`, `coverage`, `trace` and `observe`.
|
|
96
96
|
|
|
@@ -124,7 +124,9 @@ Example: visit two rooms from the same start.
|
|
|
124
124
|
|
|
125
125
|
The script checks the scenario before it starts the emulator. A step with an unknown key or a wrong value stops the script with `invalid-input`. The message names the step.
|
|
126
126
|
|
|
127
|
-
The script also examines each file before it starts the emulator: the program and the symbols of each side, and each snapshot file. A missing file stops the script with `not-found`. A program that is not a PRG, for example a disk image, stops the script with `invalid-input`.
|
|
127
|
+
The script also examines each file before it starts the emulator: the program and the symbols of each side, and each snapshot file. A missing file stops the script with `not-found`. A program that is not a PRG, for example a disk image, stops the script with `invalid-input`. A snapshot file that does not start with the signature of a VICE snapshot file also stops the script with `invalid-input`. The message names the step and the file.
|
|
128
|
+
|
|
129
|
+
A run also examines each name of its steps before it starts the emulator. An unknown name stops the run with `invalid-input`, and the message names the step. In a scenario, a side that does not have a name gives INCONCLUSIVE, and `reason` names the step.
|
|
128
130
|
|
|
129
131
|
A step in the form of version 2.0 stops the script with `invalid-input`. The message names the old field and gives the step to write. For example, the script refuses `{ "runUntil": { "at": "main_loop" }, "timeoutFrames": 300 }` and gives `{"runUntil": {"target": {"kind": "address", "address": "main_loop"}, "timeoutFrames": 300}}`. A memory item with `at` gets the same refusal: write `address`.
|
|
130
132
|
|
|
@@ -10,6 +10,7 @@ import { isObject } from "#src/host-client/steps.ts";
|
|
|
10
10
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
11
11
|
import { readProjectFile, writeReported } from "#src/host-client/transfer.ts";
|
|
12
12
|
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
13
|
+
import { checkSnapshotFile, hasSignature, VSF_SIGNATURE } from "#src/media.ts";
|
|
13
14
|
import { listed, UsageError } from "#src/script.ts";
|
|
14
15
|
import { diffStates, type ChangedRange, type MachineState } from "./diff.ts";
|
|
15
16
|
|
|
@@ -59,30 +60,40 @@ function cpuValues(registers: Record<string, unknown>): Record<string, number |
|
|
|
59
60
|
|
|
60
61
|
/** The size of the header of a module in a VICE snapshot file: the name (16 bytes), the version (2 bytes) and the size (4 bytes). */
|
|
61
62
|
const MODULE_HEADER = 22;
|
|
62
|
-
const VSF_MAGIC = "VICE Snapshot File\x1a";
|
|
63
63
|
const VSF_VERSION = "VICE Version\x1a";
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* The
|
|
67
|
-
*
|
|
68
|
-
* file has no such module.
|
|
66
|
+
* The data of the C64MEM module of a VICE snapshot file: the data and the
|
|
67
|
+
* direction register of the CPU port, the cartridge lines (2 bytes), then
|
|
68
|
+
* the 64 KiB of RAM. Undefined when the file has no such module.
|
|
69
69
|
*/
|
|
70
|
-
|
|
70
|
+
function c64Memory(file: Uint8Array): Buffer | undefined {
|
|
71
71
|
const bytes = Buffer.from(file);
|
|
72
|
-
if (
|
|
72
|
+
if (!hasSignature("vsf", bytes)) return undefined;
|
|
73
73
|
// The magic, the version (2 bytes) and the machine name (16 bytes); a newer VICE adds its own version (8 bytes).
|
|
74
|
-
let at =
|
|
74
|
+
let at = VSF_SIGNATURE.length + 2 + 16;
|
|
75
75
|
if (bytes.subarray(at, at + VSF_VERSION.length).toString("latin1") === VSF_VERSION) at += VSF_VERSION.length + 8;
|
|
76
76
|
while (at + MODULE_HEADER <= bytes.length) {
|
|
77
77
|
const name = bytes.subarray(at, at + 16).toString("latin1").replace(/\0+$/, "");
|
|
78
78
|
const size = bytes.readUInt32LE(at + 18);
|
|
79
79
|
if (size < MODULE_HEADER) return undefined;
|
|
80
|
-
if (name === "C64MEM") return at + MODULE_HEADER + 4 + 0x10000 <= bytes.length ? bytes.subarray(at + MODULE_HEADER
|
|
80
|
+
if (name === "C64MEM") return at + MODULE_HEADER + 4 + 0x10000 <= bytes.length ? bytes.subarray(at + MODULE_HEADER, at + MODULE_HEADER + 4 + 0x10000) : undefined;
|
|
81
81
|
at += size;
|
|
82
82
|
}
|
|
83
83
|
return undefined;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** The 64 KiB of C64 RAM that a VICE snapshot file holds, also the RAM under the CPU port at $00/$01. Undefined when the file has no C64MEM module. */
|
|
87
|
+
export function snapshotRam(file: Uint8Array): Uint8Array | undefined {
|
|
88
|
+
return c64Memory(file)?.subarray(4);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The CPU port of a VICE snapshot file: the direction register ($00) and the data register ($01). Undefined when the file has no C64MEM module. */
|
|
92
|
+
export function snapshotCpuPort(file: Uint8Array): { direction: number; data: number } | undefined {
|
|
93
|
+
const memory = c64Memory(file);
|
|
94
|
+
return memory === undefined ? undefined : { data: memory[0]!, direction: memory[1]! };
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
/**
|
|
87
98
|
* The state of a snapshot: restored in the session, then read with the
|
|
88
99
|
* readers of the runs. The RAM must be the RAM of the file: else the
|
|
@@ -101,6 +112,10 @@ async function snapshotState(client: ViceSessionClient, path: string, ntsc: bool
|
|
|
101
112
|
`${path} did not restore: the RAM after the restore is not the RAM of the file. A snapshot restores only in a machine of the video standard that saved it. ${ntsc ? "Leave out --ntsc for a PAL snapshot" : "Give --ntsc for an NTSC snapshot"}, or use the emulator version that saved it.`,
|
|
102
113
|
);
|
|
103
114
|
}
|
|
115
|
+
// $00 and $01 are the CPU port of the file, not the RAM under it: a program reads and writes the port there.
|
|
116
|
+
const port = snapshotCpuPort(file)!;
|
|
117
|
+
memory[0] = port.direction;
|
|
118
|
+
memory[1] = port.data;
|
|
104
119
|
const colorRam = Buffer.from((await client.ioRead(0xd800, 0x400)).data, "hex").map((value) => value & 0x0f);
|
|
105
120
|
const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
|
|
106
121
|
const values: Record<string, number | boolean> = {};
|
|
@@ -134,6 +149,8 @@ export function comparedItems(what: string, a: readonly string[], b: readonly st
|
|
|
134
149
|
export async function diffFiles(first: string, second: string, options: { gap: number; max: number; ntsc: boolean; at?: number; out?: string }): Promise<Record<string, unknown>> {
|
|
135
150
|
const kind = kindOf(first);
|
|
136
151
|
if (kindOf(second) !== kind) throw new UsageError("give two states of one kind: two snapshots, two memory files or two observations");
|
|
152
|
+
if (options.at !== undefined && kind !== "memory") throw new UsageError(`--address is only for two memory files (.bin). ${kind === "snapshot" ? "A snapshot file holds all 64 KiB" : "An observation gives the address of each item"}: leave out --address`);
|
|
153
|
+
if (options.ntsc && kind !== "snapshot") throw new UsageError(`--ntsc is only for two snapshot files (.vsf), which restore in an emulator. ${kind === "memory" ? "Memory files" : "Observations"} compare as they are: leave out --ntsc`);
|
|
137
154
|
const { gap, max } = options;
|
|
138
155
|
let a: MachineState;
|
|
139
156
|
let b: MachineState;
|
|
@@ -149,6 +166,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
|
|
|
149
166
|
a = { memory: { base, bytes: left } };
|
|
150
167
|
b = { memory: { base, bytes: right } };
|
|
151
168
|
} else {
|
|
169
|
+
// Both files are snapshot files before the emulator starts.
|
|
170
|
+
for (const path of [first, second]) checkSnapshotFile(path, readProjectFile(path).bytes);
|
|
152
171
|
const client = await ViceSessionClient.open({ videoStandard: options.ntsc ? "ntsc" : "pal" });
|
|
153
172
|
try {
|
|
154
173
|
a = await snapshotState(client, first, options.ntsc);
|
|
@@ -12,6 +12,7 @@ import { readPrg } from "#src/host-client/image.ts";
|
|
|
12
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";
|
|
13
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
14
14
|
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
15
|
+
import { checkSnapshotFile } from "#src/media.ts";
|
|
15
16
|
import { UsageError } from "#src/script.ts";
|
|
16
17
|
import { namesOf } from "./scenario.ts";
|
|
17
18
|
|
|
@@ -162,7 +163,7 @@ function readRun(path: string): Run {
|
|
|
162
163
|
/** The remedy for a start program that is no PRG. */
|
|
163
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";
|
|
164
165
|
|
|
165
|
-
/** Checks each file that the run reads before the emulator starts: the start program
|
|
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. */
|
|
166
167
|
function checkRunFiles(path: string, run: Run): void {
|
|
167
168
|
const named = (where: string, check: () => unknown) => {
|
|
168
169
|
try {
|
|
@@ -177,10 +178,10 @@ function checkRunFiles(path: string, run: Run): void {
|
|
|
177
178
|
named("start.program", () => readPrg(program, PROGRAM_REMEDY));
|
|
178
179
|
} else {
|
|
179
180
|
const { snapshot } = run.start;
|
|
180
|
-
named("start.snapshot", () => readProjectFile(snapshot));
|
|
181
|
+
named("start.snapshot", () => checkSnapshotFile(snapshot, readProjectFile(snapshot).bytes));
|
|
181
182
|
}
|
|
182
183
|
run.steps.forEach((step, index) => {
|
|
183
|
-
if ("snapshot" in step) named(`step ${index + 1}: snapshot`, () => readProjectFile(step.snapshot));
|
|
184
|
+
if ("snapshot" in step) named(`step ${index + 1}: snapshot`, () => checkSnapshotFile(step.snapshot, readProjectFile(step.snapshot).bytes));
|
|
184
185
|
});
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -197,12 +198,27 @@ export async function runFile(path: string, out: string): Promise<{ output: Reco
|
|
|
197
198
|
const at = (location: string): number => {
|
|
198
199
|
if (location.startsWith("$")) return parseC64Address(location);
|
|
199
200
|
const address = symbols.get(location);
|
|
200
|
-
if (address
|
|
201
|
-
|
|
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}`);
|
|
202
206
|
};
|
|
203
|
-
//
|
|
207
|
+
// Every name of the steps resolves before the emulator starts. A refusal names the step.
|
|
204
208
|
run.steps.forEach((step, index) => {
|
|
205
|
-
if (
|
|
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
|
+
}
|
|
206
222
|
});
|
|
207
223
|
const files: string[] = [];
|
|
208
224
|
const written: Written[] = [];
|