@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
|
@@ -11,7 +11,7 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
13
|
import { Refusal } from "#src/errors.ts";
|
|
14
|
-
import { type AddressRange,
|
|
14
|
+
import { type AddressRange, coverageSeedKinds, formatC64Address, textToPetscii } from "#src/c64.ts";
|
|
15
15
|
import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
|
|
16
16
|
import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
|
|
17
17
|
import { writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
@@ -26,13 +26,13 @@ import { assemble } from "#src/native/acme.ts";
|
|
|
26
26
|
import { analyze } from "#src/native/ghidra/analyze.ts";
|
|
27
27
|
import { localToolContext } from "#src/native/local.ts";
|
|
28
28
|
import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
|
|
29
|
-
import { decode as decodeRegister, describeAddress,
|
|
29
|
+
import { decode as decodeRegister, describeAddress, platformNames, platformNamesFor, romUseOfProgram } from "#src/platform.ts";
|
|
30
30
|
import { resolveProjectPath } from "#src/project.ts";
|
|
31
31
|
import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
|
|
32
32
|
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
33
33
|
import { COPIER_SOURCE, LOADER_JUMP, loaderLimits, planLoader, rasterLine, loaderState, simulate, stopCause, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
|
|
34
|
-
import { OPCODES } from "#src/opcodes.ts";
|
|
35
|
-
import { entriesInData, entryOutsideImage,
|
|
34
|
+
import { instructionWith, OPCODES } from "#src/opcodes.ts";
|
|
35
|
+
import { entriesInData, entryOutsideImage, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Rendered } from "./lib/source.ts";
|
|
36
36
|
|
|
37
37
|
/** At most this many diagnostics or differences of one check. */
|
|
38
38
|
const MAX_DIAGNOSTICS = 20;
|
|
@@ -182,7 +182,7 @@ async function probe(laid: Layout): Promise<Set<string>> {
|
|
|
182
182
|
? formatC64Address((slot + 2 + (item.bytes[1]! < 0x80 ? item.bytes[1]! : item.bytes[1]! - 0x100)) & 0xffff)
|
|
183
183
|
: literalOperand(item.form.opcode, item.bytes.length === 3 ? item.bytes[1]! | (item.bytes[2]! << 8) : item.bytes[1]!);
|
|
184
184
|
lines.push(`* = ${formatC64Address(slot)}`);
|
|
185
|
-
lines.push(` ${
|
|
185
|
+
lines.push(` ${instructionWith(item.form.opcode, operand, item.form.suffix)}`);
|
|
186
186
|
lineOf.set(lines.length, item.form.key);
|
|
187
187
|
});
|
|
188
188
|
const built = await assembleFiles([{ name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
|
|
@@ -491,15 +491,10 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
/**
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
function romsInUse(image: ProgramImage, coverage: Uint8Array | undefined): RomUse & { from: "coverage" | "entry state" | "none" } {
|
|
499
|
-
if (coverage !== undefined) return { ...romUseOf(coverage, COVERAGE.romExecute), from: "coverage" };
|
|
500
|
-
const port = (image.state?.cpuPort as Record<string, unknown> | undefined)?.$01;
|
|
501
|
-
if (typeof port === "number") return { ...romUseOfPort(port), from: "entry state" };
|
|
502
|
-
return { basic: true, kernal: true, from: "none" };
|
|
494
|
+
/** The platform names that `used` leaves out: the names of the ROMs that the program does not use. */
|
|
495
|
+
function leftOutNames(used: ReadonlyArray<{ name: string }>): Set<string> {
|
|
496
|
+
const kept = new Set(used.map((entry) => entry.name));
|
|
497
|
+
return new Set(platformNames().flatMap((entry) => (kept.has(entry.name) ? [] : [entry.name])));
|
|
503
498
|
}
|
|
504
499
|
|
|
505
500
|
/** The first words of every file that the script writes. */
|
|
@@ -549,7 +544,8 @@ async function generate(call: Call): Promise<{ output: Record<string, unknown>;
|
|
|
549
544
|
}
|
|
550
545
|
// Read the entry state before the long steps, so a bad state stops the run at once.
|
|
551
546
|
const state = loaderWanted ? loaderState(image.state, "the entry state of the knowledge") : undefined;
|
|
552
|
-
const romUse =
|
|
547
|
+
const romUse = romUseOfProgram(coverage, image.state);
|
|
548
|
+
const platform = platformNamesFor(romUse);
|
|
553
549
|
const { instructions, dataOnly } = await decode(image, knowledge, entries, coverage);
|
|
554
550
|
const laid = layout({
|
|
555
551
|
segments: image.segments,
|
|
@@ -567,7 +563,7 @@ async function generate(call: Call): Promise<{ output: Record<string, unknown>;
|
|
|
567
563
|
macroUses: knowledge.macroUses,
|
|
568
564
|
...(coverage === undefined ? {} : { coverage }),
|
|
569
565
|
// The platform names are C64 names: drive code and banks get none. A ROM that the program does not use gives none.
|
|
570
|
-
...(space === DEFAULT_SPACE ? { platform:
|
|
566
|
+
...(space === DEFAULT_SPACE ? { platform, platformLeftOut: leftOutNames(platform), describe: describeAddress, decode: decodeRegister } : {}),
|
|
571
567
|
});
|
|
572
568
|
const asBytes = await probe(laid);
|
|
573
569
|
const header = [`Generated by c64-source from knowledge revision ${knowledge.revision}${space === DEFAULT_SPACE ? "" : ` in space ${space}`}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
|
|
@@ -734,7 +730,7 @@ await runScript({
|
|
|
734
730
|
name: "generate",
|
|
735
731
|
summary: "Writes the source files, source-map.json and not_executed.json, and checks them. result is PASS when every check passes, else FAIL.",
|
|
736
732
|
options: {
|
|
737
|
-
out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
|
|
733
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
|
|
738
734
|
entry: { ...OPTIONS.entry, multiple: true, help: "an entry point for Ghidra" },
|
|
739
735
|
space: { ...OPTIONS.space, help: "the knowledge space of the program: c64 (the default), drive8, or bank0 to bank255. The coverage and --loader are for c64 only" },
|
|
740
736
|
loader: { kind: "flag", help: "also write loader.asm and loader.prg: the loader copies the segments into place, sets the entry state of the knowledge, and jumps to the entry. It is checked in the emulator" },
|
|
@@ -40,9 +40,10 @@ Both analyzers use the knowledge from the user and the LLM as seeds:
|
|
|
40
40
|
|
|
41
41
|
- Each routine is an entry point.
|
|
42
42
|
- Each region that is not code stays data.
|
|
43
|
+
- Each symbol of the kind `data` or `variable` stays data. The region that is not code and that holds the symbol stays data, also a region of an analyzer. Without such a region, the byte of the symbol stays data. A symbol in a code region or at an executed address is not data. When the region holds an entry point or an executed address, only the byte of the symbol stays data.
|
|
43
44
|
- Each name is a label. The listing, Ghidra and the decompiler show that name.
|
|
44
45
|
|
|
45
|
-
Facts that an analyzer found are not seeds. A new analysis can then correct or remove them.
|
|
46
|
+
Facts that an analyzer found are not seeds alone. A new analysis can then correct or remove them.
|
|
46
47
|
|
|
47
48
|
More seeds come from evidence:
|
|
48
49
|
|
|
@@ -61,6 +62,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
|
|
|
61
62
|
- 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.
|
|
62
63
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
63
64
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
65
|
+
- 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.
|
|
64
66
|
- 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.
|
|
65
67
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
66
68
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -87,10 +89,11 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
|
|
|
87
89
|
- Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
|
|
88
90
|
- If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
|
|
89
91
|
- `conflicts` lists the places where the analyzer and the current knowledge disagree.
|
|
92
|
+
- DXA or Ghidra can find data where a run executed an instruction start (`executed`), or where a `user` or `llm` symbol of kind `label` or `routine` is (`knowledge`). Then the script does not write that data region, and the earlier region of the analyzer stays. `conflicts` lists each such place. Executed code and your names have priority over the guess of an analyzer.
|
|
90
93
|
- `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). It also finds the `CBM80` header of a cartridge bank at `$8000` (`cartridgeHeaders`). The two start vectors of the header become entry points. The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. A loader that copies relocated code is also such a part. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
|
|
91
|
-
- `candidates.pointers` gives code that makes an address from immediates. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair
|
|
94
|
+
- `candidates.pointers` gives code that makes an address from immediates. The script reads only the instructions of the code regions, from the start of each region. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair. A `(zp),y` instruction of the code uses the zero-page pair. Its `target` is in the program or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
|
|
92
95
|
- `candidates.pitfalls` gives code that a C64 runs in a different way than it reads. `raster-bit-8`: the instruction at `at` reads $D011, and the instruction at `store` writes the value back. A read of $D011 gives bit 8 of the raster line in bit 7. Thus the write can move the raster interrupt to line 256 or later, and on PAL past line 311 the interrupt never comes. Examine each candidate. Where the code must not keep bit 7, add a comment and test the program.
|
|
93
|
-
- `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--out-macro-batch`, `batch` gives the file, the count of writes (`writes`) and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
|
|
96
|
+
- `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. A platform name of a ROM that the program does not use is not an arg, because the c64-source skill does not use it. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--out-macro-batch`, `batch` gives the file, the count of writes (`writes`) and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
|
|
94
97
|
- When the knowledge has coverage, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data. For many conflicts, write all classifications in one `apply` batch of the c64-knowledge skill.
|
|
95
98
|
6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
|
|
96
99
|
|
|
@@ -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);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// structural findings into .c64-re-tools/knowledge/.
|
|
5
5
|
|
|
6
6
|
import { COVERAGE, formatC64Address, prgRange } from "#src/c64.ts";
|
|
7
|
-
import { Refusal } from "#src/errors.ts";
|
|
7
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
8
8
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
9
9
|
import { writeReported } from "#src/host-client/transfer.ts";
|
|
10
10
|
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
@@ -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));
|
|
@@ -121,6 +123,15 @@ function withRemedy(reason: string): string {
|
|
|
121
123
|
|
|
122
124
|
/** One conflict: what the knowledge has, and what the analyzer found, under the analyzer's name. */
|
|
123
125
|
function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<string, unknown> {
|
|
126
|
+
if (conflict.category === "code-evidence") {
|
|
127
|
+
return {
|
|
128
|
+
range: { start: formatC64Address(conflict.start), end: formatC64Address(conflict.end) },
|
|
129
|
+
[analyzer]: conflict.finding,
|
|
130
|
+
...(conflict.executed === undefined ? {} : { executed: formatC64Address(conflict.executed) }),
|
|
131
|
+
...(conflict.symbol === undefined ? {} : { knowledge: { ...conflict.symbol, address: formatC64Address(conflict.symbol.address) } }),
|
|
132
|
+
problem: conflict.executed !== undefined ? "a run executed code here: the data finding is not imported" : "the knowledge has a code name here: the data finding is not imported",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
124
135
|
if (conflict.category === "symbol") {
|
|
125
136
|
return {
|
|
126
137
|
at: formatC64Address(conflict.address),
|
|
@@ -188,7 +199,7 @@ async function analyze(call: Call): Promise<unknown> {
|
|
|
188
199
|
const idioms = findIdioms(inProgram, { start: loaded.start, end: loaded.end }, covered);
|
|
189
200
|
|
|
190
201
|
const revision = currentRevision(reader);
|
|
191
|
-
const seeds = seedsFromKnowledge(reader, entries, space);
|
|
202
|
+
const seeds = seedsFromKnowledge(reader, entries, space, executed.executed);
|
|
192
203
|
// Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound.
|
|
193
204
|
const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage, inProgram);
|
|
194
205
|
const known = [...seeds.entryPoints, ...codeEntries(reader, byteAt, space)];
|
|
@@ -207,20 +218,21 @@ async function analyze(call: Call): Promise<unknown> {
|
|
|
207
218
|
const coverageUsed = fromCoverage.filter((entry) => seeds.entryPoints.includes(entry) || codeStarts.includes(entry)).length;
|
|
208
219
|
// Platform names are C64 names: drive code and banks get none.
|
|
209
220
|
const platform = space === DEFAULT_SPACE ? platformNames() : [];
|
|
210
|
-
const pointers = findPointers(byteAt, { start: loaded.start, end: loaded.end }, {
|
|
211
|
-
platform: (address) => platform.some((entry) => address >= entry.address && address <= entry.end),
|
|
212
|
-
sprites: listRegions(reader, undefined, "sprite", undefined, space),
|
|
213
|
-
});
|
|
214
221
|
reader?.close();
|
|
215
222
|
/** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
|
|
216
223
|
const idiomsResult = () => {
|
|
217
224
|
const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description: "Idioms in the program" }));
|
|
218
225
|
// The macro candidates read the code regions after both imports.
|
|
219
|
-
const macros = macroCandidates(space, byteAt, batchPath);
|
|
220
|
-
// Platform pitfalls are C64 facts: drive code and banks get none.
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
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();
|
|
224
236
|
return {
|
|
225
237
|
idioms: {
|
|
226
238
|
...idioms.counts,
|
|
@@ -270,7 +282,7 @@ async function analyze(call: Call): Promise<unknown> {
|
|
|
270
282
|
written = writeReported(listingPath, listing, { what: "--out-listing" });
|
|
271
283
|
} catch (error) {
|
|
272
284
|
const recorded = imported.revision === null ? "The knowledge did not change." : `The knowledge has the findings as revision ${imported.revision}.`;
|
|
273
|
-
throw new WireFailure("operation-failed", `The script did not write the listing to ${listingPath}: ${(error as Error).message}. ${recorded}`);
|
|
285
|
+
throw new WireFailure(isRefusal(error) ? error.code : "operation-failed", `The script did not write the listing to ${listingPath}: ${(error as Error).message.replace(/\.$/, "")}. ${recorded}`);
|
|
274
286
|
}
|
|
275
287
|
}
|
|
276
288
|
const found = idiomsResult();
|
|
@@ -334,9 +346,9 @@ await runScript({
|
|
|
334
346
|
decompile: { kind: "address", value: "<address>", multiple: true, help: "ghidra: a routine to decompile, at most 32" },
|
|
335
347
|
space: OPTIONS.space,
|
|
336
348
|
"max-listed": { ...OPTIONS["max-listed"], help: "list at most n labels, functions or candidates (default 50, 0 for none). The counts are always given, and the knowledge has all labels and functions" },
|
|
337
|
-
"out-listing": { kind: "path", value: "<file>", help: "dxa: write the disassembly listing to this project file" },
|
|
338
|
-
"out-instructions": { kind: "path", value: "<file>", help: "ghidra: write every instruction (address, bytes, mnemonic, mode, operand, target, executed) to this project file as JSON" },
|
|
339
|
-
"out-macro-batch": { kind: "path", value: "<file>", help: "write the macro candidates as an apply batch of c64-knowledge (the macros and their uses) to this project file" },
|
|
349
|
+
"out-listing": { kind: "path", output: "file", value: "<file>", help: "dxa: write the disassembly listing to this project file" },
|
|
350
|
+
"out-instructions": { kind: "path", output: "file", value: "<file>", help: "ghidra: write every instruction (address, bytes, mnemonic, mode, operand, target, executed) to this project file as JSON" },
|
|
351
|
+
"out-macro-batch": { kind: "path", output: "file", value: "<file>", help: "write the macro candidates as an apply batch of c64-knowledge (the macros and their uses) to this project file" },
|
|
340
352
|
},
|
|
341
353
|
notes: "The program and the coverage come from the knowledge: put them in with knowledge.ts image and coverage of the c64-knowledge skill. The executed code of the coverage is seeded. Routine symbols in the knowledge are entry points too. Ghidra needs at least one entry point. DXA finds the SYS of a BASIC start by itself.",
|
|
342
354
|
run: analyze,
|
|
@@ -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
|
|---|---|
|
|
@@ -120,7 +120,9 @@ node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/results.json
|
|
|
120
120
|
|
|
121
121
|
- Two snapshot files: the script restores each one in its own emulator. It gives the changed ranges of the RAM and of the colour RAM, and the changed VIC-II, CIA, SID and CPU registers.
|
|
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
|
-
- Two observations of runs, also of one run: the changed memory items and registers.
|
|
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
|
+
|
|
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.
|
|
124
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
|
|
|
@@ -135,6 +137,7 @@ The script refuses a snapshot that does not restore in its emulator. An example
|
|
|
135
137
|
- 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.
|
|
136
138
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
137
139
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
140
|
+
- 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.
|
|
138
141
|
- 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.
|
|
139
142
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
140
143
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -154,11 +157,11 @@ The script refuses a snapshot that does not restore in its emulator. An example
|
|
|
154
157
|
1. Write the scenario file. Start the steps with a frame count, for example `{ "frames": 150 }`. Each side starts at the reset vector, and the C64 needs these frames to start.
|
|
155
158
|
2. Use symbol names, for example `player_x`, not fixed addresses. Each side finds the name in its own symbols. Use a `$` address only when both programs use the same address.
|
|
156
159
|
3. Make each checkpoint a condition, for example a `runUntil` step with an `address` target at a routine. Do not wait for a time.
|
|
157
|
-
4.
|
|
160
|
+
4. Write the symbol file of the rebuild with `--out-symbols build/game.json` of c64-assembler `build`. Give `--all-symbols`, so that the file also has the constants that the source does not use.
|
|
158
161
|
5. Run `testing.ts test`.
|
|
159
162
|
6. Read the result:
|
|
160
163
|
- `PASS`: all observations are equal, or the differences are in the allowed tolerance.
|
|
161
|
-
- `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.
|
|
162
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.
|
|
163
166
|
- `INCONCLUSIVE`: `reason` tells why the script could not compare. This is not a failure of the program.
|
|
164
167
|
7. For a FAIL, examine the first difference with the c64-emulator skill.
|
|
@@ -188,6 +191,7 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
|
|
|
188
191
|
## Failure and conflicts
|
|
189
192
|
|
|
190
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.
|
|
191
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.
|
|
192
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.
|
|
193
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> = {};
|
|
@@ -118,10 +133,24 @@ const show = (range: ChangedRange) => ({ start: formatC64Address(range.start), e
|
|
|
118
133
|
/** A changed value as a person reads it: the pc as "$xxxx". */
|
|
119
134
|
const showValue = (item: { name: string; a: number | boolean; b: number | boolean }) => (item.name === "cpu.pc" && typeof item.a === "number" && typeof item.b === "number" ? { name: item.name, a: formatC64Address(item.a), b: formatC64Address(item.b) } : item);
|
|
120
135
|
|
|
136
|
+
/**
|
|
137
|
+
* How many items two observations have in common, by name: the items that the
|
|
138
|
+
* diff compared. With none, the reason names what each side has, so that a
|
|
139
|
+
* count of 0 does not read as "equal".
|
|
140
|
+
*/
|
|
141
|
+
export function comparedItems(what: string, a: readonly string[], b: readonly string[], first: string, second: string): { compared: number; reason?: string } {
|
|
142
|
+
const compared = a.filter((name) => b.includes(name)).length;
|
|
143
|
+
if (compared > 0) return { compared };
|
|
144
|
+
const has = (names: readonly string[]) => (names.length === 0 ? "none" : names.join(", "));
|
|
145
|
+
return { compared, reason: `No ${what} was compared: the observations have none in common. ${first} has ${has(a)}; ${second} has ${has(b)}.` };
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
/** The differences of two states of one kind: snapshots, memory files or observations of runs. */
|
|
122
149
|
export async function diffFiles(first: string, second: string, options: { gap: number; max: number; ntsc: boolean; at?: number; out?: string }): Promise<Record<string, unknown>> {
|
|
123
150
|
const kind = kindOf(first);
|
|
124
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`);
|
|
125
154
|
const { gap, max } = options;
|
|
126
155
|
let a: MachineState;
|
|
127
156
|
let b: MachineState;
|
|
@@ -137,6 +166,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
|
|
|
137
166
|
a = { memory: { base, bytes: left } };
|
|
138
167
|
b = { memory: { base, bytes: right } };
|
|
139
168
|
} else {
|
|
169
|
+
// Both files are snapshot files before the emulator starts.
|
|
170
|
+
for (const path of [first, second]) checkSnapshotFile(path, readProjectFile(path).bytes);
|
|
140
171
|
const client = await ViceSessionClient.open({ videoStandard: options.ntsc ? "ntsc" : "pal" });
|
|
141
172
|
try {
|
|
142
173
|
a = await snapshotState(client, first, options.ntsc);
|
|
@@ -159,8 +190,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
|
|
|
159
190
|
b: second,
|
|
160
191
|
...(diff.memory === undefined ? {} : { memory: part(diff.memory) }),
|
|
161
192
|
...(diff.colorRam === undefined ? {} : { colorRam: part(diff.colorRam) }),
|
|
162
|
-
...(diff.values === undefined ? {} : { registers: listed(diff.values, max, showValue, rest) }),
|
|
163
|
-
...(diff.observed === undefined ? {} : { observed: listed(diff.observed, max, undefined, rest) }),
|
|
193
|
+
...(diff.values === undefined ? {} : { registers: { ...(kind === "observation" ? comparedItems("register", Object.keys(a.values ?? {}), Object.keys(b.values ?? {}), first, second) : {}), ...listed(diff.values, max, showValue, rest) } }),
|
|
194
|
+
...(diff.observed === undefined ? {} : { observed: { ...comparedItems("memory item", Object.keys(a.observed ?? {}), Object.keys(b.observed ?? {}), first, second), ...listed(diff.observed, max, undefined, rest) } }),
|
|
164
195
|
...(written === undefined ? {} : written),
|
|
165
196
|
};
|
|
166
197
|
}
|