@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21

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.
Files changed (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -0,0 +1,162 @@
1
+ // One listing of the image with its knowledge (D78): the lines of a range as
2
+ // ACME text with names, operand expressions and comments, the access class of
3
+ // the bytes from coverage files, and the references in and out. Nothing here
4
+ // reads files: knowledge.ts gives the lookups.
5
+
6
+ import { COVERAGE } from "#src/c64.ts";
7
+ import { MODE_LENGTH, OPCODES, type Mode } from "#src/opcodes.ts";
8
+
9
+ export interface ListingSource {
10
+ /** The image byte at an address, or undefined outside the image. */
11
+ byteAt: (address: number) => number | undefined;
12
+ /** The name of an address: a symbol, else a platform name. */
13
+ nameOf: (address: number) => string | undefined;
14
+ /** The analyzer names of an address (D74), for reference. */
15
+ secondNames: (address: number) => string[];
16
+ /** The knowledge regions, in address order. */
17
+ regions: ReadonlyArray<{ start: number; end: number; type: string }>;
18
+ /** The line and side comments of an address. */
19
+ comments: (address: number) => { line: string[]; side?: string };
20
+ /** The operand expression of the instruction at an address (D65), as ACME text. */
21
+ operand: (address: number) => string | undefined;
22
+ /** The references to and from an address. */
23
+ references: (address: number) => { in: number; out: number };
24
+ /** Joined coverage files, or undefined without them. */
25
+ coverage?: Uint8Array;
26
+ }
27
+
28
+ export interface ListingLine {
29
+ address: number;
30
+ /** The bytes as hex, separated by spaces. */
31
+ bytes: string;
32
+ label?: string;
33
+ analyzerNames?: string[];
34
+ text: string;
35
+ /** From the coverage: x executed, r read, w written, - none. */
36
+ access?: string;
37
+ referencesIn: number;
38
+ referencesOut: number;
39
+ lineComments?: string[];
40
+ comment?: string;
41
+ }
42
+
43
+ const CODE = new Set(["code", "jumps"]);
44
+ const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
45
+ const hex4 = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
46
+ const DATA_PER_LINE = 8;
47
+
48
+ /** The access class of bytes from the coverage: x, r and w as they occur, or - for none. */
49
+ function accessOf(coverage: Uint8Array | undefined, start: number, length: number): string | undefined {
50
+ if (coverage === undefined) return undefined;
51
+ let flags = 0;
52
+ for (let offset = 0; offset < length; offset++) flags |= coverage[start + offset]!;
53
+ const text = `${flags & (COVERAGE.ramExecute | COVERAGE.romExecute) ? "x" : ""}${flags & (COVERAGE.ramRead | COVERAGE.romRead) ? "r" : ""}${flags & COVERAGE.ramWrite ? "w" : ""}`;
54
+ return text === "" ? "-" : text;
55
+ }
56
+
57
+ /** The operand of an instruction in ACME syntax, with the name of its target where one exists. */
58
+ function operandOf(mode: Mode, address: number, low: number, high: number, nameOf: ListingSource["nameOf"]): string {
59
+ const word = low | (high << 8);
60
+ const named = (target: number, digits: 2 | 4) => nameOf(target) ?? (digits === 2 ? hex2(target) : hex4(target));
61
+ switch (mode) {
62
+ case "imp":
63
+ return "";
64
+ case "imm":
65
+ return `#${hex2(low)}`;
66
+ case "zp":
67
+ return named(low, 2);
68
+ case "zpx":
69
+ return `${named(low, 2)},x`;
70
+ case "zpy":
71
+ return `${named(low, 2)},y`;
72
+ case "abs":
73
+ return named(word, 4);
74
+ case "abx":
75
+ return `${named(word, 4)},x`;
76
+ case "aby":
77
+ return `${named(word, 4)},y`;
78
+ case "ind":
79
+ return `(${named(word, 4)})`;
80
+ case "izx":
81
+ return `(${named(low, 2)},x)`;
82
+ case "izy":
83
+ return `(${named(low, 2)}),y`;
84
+ case "rel":
85
+ return named((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff, 4);
86
+ }
87
+ }
88
+
89
+ /** The lines of start..end: instructions in code regions, decoded from the region start, and bytes elsewhere. */
90
+ export function listing(source: ListingSource, start: number, end: number): ListingLine[] {
91
+ const lines: ListingLine[] = [];
92
+ const line = (address: number, length: number, text: string): ListingLine => {
93
+ const bytes = Array.from({ length }, (_, offset) => source.byteAt(address + offset)!.toString(16).padStart(2, "0")).join(" ");
94
+ const label = source.nameOf(address);
95
+ const second = source.secondNames(address).filter((name) => name !== label);
96
+ const { line: lineComments, side } = source.comments(address);
97
+ const references = source.references(address);
98
+ const access = accessOf(source.coverage, address, length);
99
+ return {
100
+ address,
101
+ bytes,
102
+ ...(label === undefined ? {} : { label }),
103
+ ...(second.length === 0 ? {} : { analyzerNames: second }),
104
+ text,
105
+ ...(access === undefined ? {} : { access }),
106
+ referencesIn: references.in,
107
+ referencesOut: references.out,
108
+ ...(lineComments.length === 0 ? {} : { lineComments }),
109
+ ...(side === undefined ? {} : { comment: side }),
110
+ };
111
+ };
112
+ const regionAt = (address: number) => source.regions.find((region) => address >= region.start && address <= region.end);
113
+ // Start at the code region that holds the first address, so the instructions keep their boundaries.
114
+ const first = regionAt(start);
115
+ let address = first !== undefined && CODE.has(first.type) ? first.start : start;
116
+ while (address <= end) {
117
+ const byte = source.byteAt(address);
118
+ if (byte === undefined) {
119
+ address++;
120
+ continue;
121
+ }
122
+ const region = regionAt(address);
123
+ if (region !== undefined && CODE.has(region.type)) {
124
+ const opcode = OPCODES[byte]!;
125
+ const length = MODE_LENGTH[opcode.mode];
126
+ const bytes = Array.from({ length: length - 1 }, (_, offset) => source.byteAt(address + 1 + offset));
127
+ if (address + length - 1 <= region.end && bytes.every((value) => value !== undefined)) {
128
+ const operand = source.operand(address) ?? operandOf(opcode.mode, address, bytes[0] ?? 0, bytes[1] ?? 0, source.nameOf);
129
+ if (address >= start) lines.push(line(address, length, operand === "" ? opcode.mnemonic : `${opcode.mnemonic} ${operand}`));
130
+ address += length;
131
+ continue;
132
+ }
133
+ }
134
+ // Bytes up to the next label, region edge or end of the line.
135
+ const values: number[] = [];
136
+ let at = address;
137
+ while (at <= end && values.length < DATA_PER_LINE && source.byteAt(at) !== undefined && (at === address || (source.nameOf(at) === undefined && regionAt(at) === region))) {
138
+ values.push(source.byteAt(at)!);
139
+ at++;
140
+ }
141
+ if (address >= start) lines.push(line(address, values.length, `!byte ${values.map(hex2).join(",")}`));
142
+ address = at;
143
+ }
144
+ return lines;
145
+ }
146
+
147
+ /** A listing as text, one line for each line, with its line comments above it. */
148
+ export function listingText(lines: readonly ListingLine[]): string {
149
+ const out: string[] = [];
150
+ for (const item of lines) {
151
+ for (const comment of item.lineComments ?? []) out.push(` ; ${comment}`);
152
+ const head = `${hex4(item.address)} ${item.bytes.padEnd(9)} ${(item.access ?? "").padEnd(3)}`;
153
+ const body = (item.label === undefined ? "" : `${item.label} `).padEnd(20) + item.text;
154
+ const notes = [
155
+ ...(item.comment === undefined ? [] : [item.comment]),
156
+ ...(item.referencesIn + item.referencesOut === 0 ? [] : [`refs in ${item.referencesIn} out ${item.referencesOut}`]),
157
+ ...(item.analyzerNames === undefined ? [] : [`also ${item.analyzerNames.join(", ")}`]),
158
+ ];
159
+ out.push(`${head} ${body.padEnd(44)}${notes.length === 0 ? "" : ` ; ${notes.join(" | ")}`}`.trimEnd());
160
+ }
161
+ return `${out.join("\n")}\n`;
162
+ }
@@ -24,18 +24,29 @@ The script looks up local reference data. It does not use the host runtime.
24
24
  node <skill>/scripts/memmap.ts at '$d020' '$0314' '$ffd2'
25
25
  node <skill>/scripts/memmap.ts decode '$d018' '$15' --bank '$97'
26
26
  node <skill>/scripts/memmap.ts list vic
27
+ node <skill>/scripts/memmap.ts export --out src/hw.asm
27
28
  ```
28
29
 
29
30
  | Command | Result |
30
31
  |---|---|
31
32
  | `at <address> ...` | For each address: `kind` (`platform` or `application`), the area, the name and the meaning. |
32
- | `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. |
33
+ | `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F`, `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`. |
33
34
  | `list <vic\|sid\|cia1\|cia2\|kernal\|system>` | All entries of that group. |
35
+ | `export --out <file>` | Writes all platform names to a project file as an ACME include: one line for each name, with its meaning as a comment. A zero-page name is `NAME = $xx`. Other names are `NAME = $xxxx`. |
34
36
 
35
37
  Write an address or a value as `$` and hex digits, as a decimal number, or as `%` and binary digits. Put a `$` value in single quotes in a shell.
36
38
 
37
39
  The script prints one JSON object.
38
40
 
41
+ ### Project folder
42
+
43
+ - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
44
+ - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
45
+ - Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
46
+ - Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
47
+ - Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
48
+ - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
49
+
39
50
  ## Workflow
40
51
 
41
52
  1. Run `at` for the addresses in the code or the question.
@@ -45,6 +56,7 @@ The script prints one JSON object.
45
56
  3. For an I/O register, `mirrorOf` shows the register that a mirror address repeats. `visible` tells that an address in `$D000`-`$DFFF` is visible only when `$01` shows I/O at `$D000`. This area holds the chip registers, the color RAM and the cartridge I/O areas.
46
57
  4. For a byte in a multi-byte entry, for example the high byte of a vector, `entryStart` gives the first address of the entry.
47
58
  5. To understand a value in a register, run `decode`. For the memory configuration, decode the value of `$01`.
59
+ 6. For source that you write by hand, run `export` and include the file with `!source`. The c64-source skill uses the same names without the file.
48
60
 
49
61
  The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F` (the sprite and interrupt bits), `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`.
50
62
 
@@ -59,7 +71,7 @@ A conclusion about the application can be project knowledge. For example: "the p
59
71
 
60
72
  ## Result
61
73
 
62
- The platform meaning of each address, or the decoded meaning of a register value.
74
+ The platform meaning of each address, or the decoded meaning of a register value. For `export`: the file, the count of names and the bytes.
63
75
 
64
76
  The exit status is 0 for a result. The exit status is 2 for a wrong argument.
65
77
 
@@ -1,14 +1,17 @@
1
1
  // The c64-memory-map script: the platform meaning of C64 addresses and of
2
- // values in platform registers. Local lookup only; nothing is stored.
2
+ // values in platform registers. Local lookup only; nothing is stored. export
3
+ // writes the platform names to a project file as an ACME include (D52).
3
4
 
4
5
  import { parseArgs } from "node:util";
5
6
 
7
+ import { writeProjectFile } from "#src/host-client/transfer.ts";
8
+ import { decode, IO, lookup, platformNames, ROM, SYSTEM, type Entry } from "#src/platform.ts";
6
9
  import { runScript, UsageError } from "#src/script.ts";
7
- import { decode, IO, lookup, ROM, SYSTEM, type Entry } from "./platform.ts";
8
10
 
9
11
  const USAGE = `memmap.ts at <address> [<address> ...]
10
12
  memmap.ts decode <register> <value> [--bank <$dd00 value>]
11
13
  memmap.ts list <vic|sid|cia1|cia2|kernal|system>
14
+ memmap.ts export --out <file> the platform names as an ACME include (NAME = $xxxx)
12
15
 
13
16
  Addresses and values are $ and hex digits, decimal, or % and binary digits.
14
17
  The result is one JSON object.`;
@@ -27,7 +30,7 @@ function number(text: string, max: number, what: string): number {
27
30
  const show = (entry: Entry) => ({ start: hex(entry.start), ...(entry.end !== entry.start ? { end: hex(entry.end) } : {}), name: entry.name, meaning: entry.meaning });
28
31
 
29
32
  function run(argv: string[]): unknown {
30
- const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { bank: { type: "string" }, help: { type: "boolean" } } });
33
+ const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { bank: { type: "string" }, out: { type: "string" }, help: { type: "boolean" } } });
31
34
  if (values.help || positionals.length === 0) throw new UsageError("");
32
35
  const [command, ...rest] = positionals;
33
36
  switch (command) {
@@ -73,9 +76,17 @@ function run(argv: string[]): unknown {
73
76
  if (list === undefined || rest.length !== 1) throw new UsageError(`list takes one of ${Object.keys(lists).join(", ")}`);
74
77
  return { entries: list.map(show) };
75
78
  }
79
+ case "export": {
80
+ if (rest.length !== 0 || values.out === undefined) throw new UsageError("export takes --out <file> and nothing else");
81
+ const names = platformNames();
82
+ const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${hex(item.address, item.address < 0x100 ? 2 : 4)}`.padEnd(24) + `; ${item.meaning}`)];
83
+ const bytes = Buffer.from(`${lines.join("\n")}\n`, "utf8");
84
+ writeProjectFile(values.out, bytes, { what: "--out" });
85
+ return { out: values.out, names: names.length, bytes: bytes.length };
86
+ }
76
87
  default:
77
88
  throw new UsageError(`unknown command ${command}`);
78
89
  }
79
90
  }
80
91
 
81
- await runScript(() => run(process.argv.slice(2)), { usage: USAGE });
92
+ await runScript((argv) => run(argv), { usage: USAGE });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: c64-provenance
3
- description: Use this skill to compare two or more releases of a C64 program, for example cracked versions from different groups. It finds the bytes where the releases differ and shows which releases agree there. Use the result to separate the content of the application from changes that a release added. Do not use it to run or analyze the code.
3
+ description: Use this skill to compare two or more releases of a C64 program, for example cracked versions from different groups. It finds the bytes where the releases differ and shows which releases agree there. It can run each release and tell how the program uses each difference. It also gives evidence for each range of the program in the knowledge. The evidence is the unpack stage, the access, and the code that the entry does not reach. Use the result to separate the content of the application from changes that a release added. It also keeps a library of routines that several programs share. It finds them in the program of the knowledge and writes their names as a batch for c64-knowledge. Do not use it to analyze the code.
4
4
  ---
5
5
 
6
6
  # c64-provenance
@@ -9,21 +9,28 @@ description: Use this skill to compare two or more releases of a C64 program, fo
9
9
 
10
10
  Compare releases of a C64 program. Find the bytes where they differ and which releases agree. Use this to find which bytes are probably the original application and which are changes of one release.
11
11
 
12
+ Keep a library of routines that several programs share, for example a music driver or a decruncher. Find the routines of the library in a program, and get their names for that program.
13
+
12
14
  This skill does not change project knowledge.
13
15
 
14
16
  ## Inputs
15
17
 
16
- - Two or more releases: PRG files, relative to the project directory. Extract them from disk images with the c64-disk skill. Unpack packed releases with the c64-unpacker skill first.
18
+ - Two or more releases, relative to the project folder. A release is a PRG file or an `image.json` from c64-unpacker capture. Extract releases from disk images with the c64-disk skill. Unpack packed releases with the c64-unpacker skill first.
17
19
  - Optional: an address range to compare.
18
20
  - Optional: a shift for a release that loads at a different address.
21
+ - Optional: a number of frames to run each release, and a steps file.
22
+ - For one release: the program, the coverage and the other knowledge of the project. Put the program and the coverage in with `image` and `coverage` of the c64-knowledge skill.
23
+ - For the library: the program and the other knowledge of its project, and the library folder.
19
24
 
20
25
  ## Tools / execution path
21
26
 
22
- The script compares the files locally. It does not use the host runtime.
27
+ The script compares the files locally. Only `--run` uses the host runtime: each release runs in its own emulator.
23
28
 
24
29
  ```
25
30
  node <skill>/scripts/provenance.ts releases/a.prg releases/b.prg releases/c.prg
26
31
  node <skill>/scripts/provenance.ts releases/a.prg releases/b.prg --shift 2:-256 --from '$0900' --to '$09ff'
32
+ node <skill>/scripts/provenance.ts releases/a.prg .c64-re-tools/local/b/image.json --run 600 --steps .c64-re-tools/local/start-game.json
33
+ node <skill>/scripts/provenance.ts separate --out .c64-re-tools/local/separate.json
27
34
  ```
28
35
 
29
36
  | Option | Meaning |
@@ -31,19 +38,90 @@ node <skill>/scripts/provenance.ts releases/a.prg releases/b.prg --shift 2:-256
31
38
  | `--shift 2:-256` | Move release 2 (the first release is 1) by -256 bytes. Use it again for more releases. |
32
39
  | `--from '$0900'` | Compare from this address. |
33
40
  | `--to '$09ff'` | Compare up to this address. |
41
+ | `--run 600` | Run each release for this many frames, and give each listed difference its runtime evidence. |
42
+ | `--steps steps/start-game.json` | With `--run`: the action steps of the c64-unpacker steps file to run before the frames, for example a key to start the game. |
43
+
44
+ The script puts each PRG at its load address. An image has bytes only in its segments. The script compares the addresses where all releases have bytes. It prints one JSON object.
45
+
46
+ With `--shift`, the addresses of `segments`, `common` and `differences` are the release addresses plus the `shift` of the release. The `start` and `end` of `runtime` are the addresses in the release itself, without the shift.
47
+
48
+ With `--run`, a PRG starts with autostart and RUN. An image starts from its bytes: the script writes the segments and the entry state from `entry_state.json` into the emulator, and starts at the entry. Watchpoints on each difference stop the run at its first access.
49
+
50
+ ### Project folder
34
51
 
35
- The script puts each release at its load address and compares the addresses that all releases cover. It prints one JSON object.
52
+ - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
53
+ - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
54
+ - Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
55
+ - Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
56
+ - Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
57
+ - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
36
58
 
37
59
  ## Workflow
38
60
 
39
61
  1. Run the script on the releases.
40
- 2. Read `identicalShare`. If it is low and `suggestedShifts` is present, the releases are at different addresses. Run the script again with `--shift`.
62
+ 2. Read `identicalShare`. If it is low and `suggestedShifts` is present, the releases are at different addresses. Run the script again with a `--shift` of `suggestedShifts`. `shiftsNotSuggested` tells why a found shift does not fit in `$0000-$ffff`.
63
+
64
+ `suggestedShifts` has at most 3 shifts for each release. The shift with the most `matchingSequences` is first. Data that did not move and code that moved give different shifts. Try each shift. `current: true` marks the shift of this run.
41
65
  3. Read `differences`. Each one has an address range, the bytes of each release, and `groups`: the releases that have the same bytes there.
42
- 4. Examine each important difference:
66
+ 4. To know how the program uses the differences, run the script again with `--run` and the frames that get the program to the part you examine. Read `runtime` of each difference:
67
+ - `first`: the first access in that release: `read`, `write`, `execute` or `none`. An instruction that reads and then writes the same byte, for example INC, counts as `read`. The operand bytes of an executed instruction count as `execute`, for example the value of `LDA #$03`.
68
+ - `changed`: `true` when the bytes at the end of the run are not the bytes of the release.
69
+ - A range that each release writes before it reads is probably runtime state, not a difference of the releases.
70
+ - A range with `none` in each release is not used in this run. Run more frames or other steps before you decide.
71
+ - `run.skipped`: the releases have no differences, so no release ran.
72
+ 5. Examine each important difference:
43
73
  - Disassemble the bytes of each release with the c64-emulator skill, or analyze them with the c64-static-analysis skill.
44
74
  - Find what the change does, for example a trainer, a removed copy protection, or a changed text.
45
- 5. Decide which bytes are probably the original application. Use more than the byte comparison: the code, the text, and the history of the releases.
46
- 6. Record the conclusion as a comment or a region with the c64-knowledge skill.
75
+ 6. Decide which bytes are probably the original application. Use more than the byte comparison: the code, the text, and the history of the releases.
76
+ 7. Record the conclusion as a comment or a region with the c64-knowledge skill.
77
+
78
+ ## One release
79
+
80
+ Use `separate` when you have only one release. It reads the program, its stages and the coverage from the knowledge. It gives evidence, not a decision:
81
+
82
+ - `ranges`: the program in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. The last stage is the program. An earlier stage is a depacker, an intro or a loader. Only a capture of c64-unpacker `capture` has stages. For a program from `loads` or a PRG, `noStages` tells this.
83
+ - `access`: from the coverage of the knowledge: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs. A `read` byte right after executed code, or at a return address, can be a dummy read of the 6502 that the program does not use.
84
+ - `codeRegions.total`: the number of code regions in the knowledge.
85
+ - `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
86
+ - `outside`: RAM outside the program that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write. A `generated` range can have gaps of two bytes or less: these are operand bytes.
87
+
88
+ Each list has `count` and `listed`. `listed` has the first ranges, at most `--max-listed`. `--out` writes all of them. Run more coverage runs for parts that are `untouched`, and add them with `coverage` of the c64-knowledge skill, before you decide.
89
+
90
+ ## Library of routines
91
+
92
+ A library is a folder of routine files. Each file holds one routine: its bytes and its knowledge. Keep one library beside the project folders, for example `library/` at the root of the workspace. A library is not a project folder. Commit it with the projects.
93
+
94
+ `--library` is relative to the working directory, as `--project`. All other paths are relative to the project folder.
95
+
96
+ ```
97
+ node <skill>/scripts/provenance.ts routine --module '$1000' --library library --project game-a
98
+ node <skill>/scripts/provenance.ts routine --start '$1000' --end '$1026' --name play --library library --project game-a
99
+ node <skill>/scripts/provenance.ts match --library library --out .c64-re-tools/local/match.json --project game-b
100
+ ```
101
+
102
+ To save a routine:
103
+
104
+ 1. Put the program into the knowledge, and name the routine there first. The file keeps the user and LLM names, comments, regions, references and operand expressions in the range. It also keeps the code regions of each origin.
105
+ 2. Run `routine` with the start of a module (`--module`), or with `--start` and `--end`. A module ends at the next module or at the end of its segment.
106
+ - `--name` is the name of the routine and of its file. When you do not give it, the script uses the user or LLM name at the start.
107
+ - `--title` is the title of the routine. When you do not give it, the script uses the module title.
108
+ - A routine has at least 16 bytes. Its last instruction must end in the range.
109
+ 3. Read `leftOut`. It lists the rows that the file does not keep, and why. For example, the file does not keep an operand expression that uses a name outside the routine.
110
+
111
+ To find the routines in a program:
112
+
113
+ 1. Put the program into the knowledge. Then run `match`. The script examines each routine of the library at each address of the program.
114
+ - The opcodes and the other bytes must be equal.
115
+ - An address into the routine must point into the routine at its new address. `shift` is the address in the program minus the address in the library.
116
+ - An operand that addresses something outside the routine can be different. `differing` lists each one with its value in the library and in the program. Examine them. For example, the program can use other variables or another SID voice.
117
+ 2. Give `--out` to get a batch for `knowledge.ts apply`. The batch has the names, regions, comments, references and operand expressions of each routine at its addresses in the program. `match` does not change the knowledge.
118
+ 3. Read `batch.leftOut` before you apply the batch. The script leaves out a write in these conditions:
119
+ - The program has a user or LLM row at that place.
120
+ - The program uses the name at another address.
121
+ - A removal holds at that place.
122
+ - The routine occurs two times. Only the first copy gets the names.
123
+ - The script leaves out the name that an operand expression uses, or the expression does not give the operand in the program.
124
+ 4. Apply the batch with the command in `apply`. Add `--project` when you are not in the project folder. The batch is one revision of the LLM. `batch.known` counts the writes that the program has already.
47
125
 
48
126
  ## Knowledge
49
127
 
@@ -52,9 +130,13 @@ Automatic write:
52
130
 
53
131
  Record a confirmed conclusion as a normal comment, region or symbol with the c64-knowledge skill. Do not record the full comparison.
54
132
 
133
+ `match --out` writes a batch file and does not change the knowledge. Apply the batch with the c64-knowledge skill.
134
+
55
135
  ## Result
56
136
 
57
- The differing ranges with the bytes of each release and the groups that agree, and the share of identical bytes.
137
+ The differing ranges with the bytes of each release and the groups that agree, and the share of identical bytes. `common` gives the ranges where all releases have bytes. `releases` gives the segments and the `shift` of each release. With `--run`, each difference also has `runtime`, and `run` tells if each release started, or why not.
138
+
139
+ The runtime evidence is evidence only. The script does not decide which bytes are original.
58
140
 
59
141
  The exit status is 0 for a result. The exit status is 1 for an error. The exit status is 2 for a wrong argument.
60
142
 
@@ -62,10 +144,16 @@ The exit status is 0 for a result. The exit status is 1 for an error. The exit s
62
144
 
63
145
  - Do not call releases independent when you do not know their relation. Two cracks can come from the same source.
64
146
  - Do not call a byte original only because one release has it. A majority of releases is also not proof.
65
- - `common: null`: the releases do not cover the same addresses. Use `--shift`, or compare a range that all releases cover.
147
+ - `common` is empty: the releases do not cover the same addresses. Use `--shift`, or compare a range that all releases cover.
148
+ - `run.releases` shows `started: false`: a PRG without a BASIC start, or an image without `entry_state.json`. The reason tells which. The other releases still give evidence.
149
+ - `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own emulator script.
66
150
  - `moreDifferences: true`: the script shows only the first differences. Use `--from` and `--to` to see the others.
67
151
  - `not-found`: the project has no file at that path.
68
- - `invalid-input`: a file, an address or a shift is wrong. A path that goes out of the project directory is also wrong, also through a link. Read the message.
152
+ - `not-found` from `match`: the library folder does not exist, or it has no routine files. Save a routine with `routine` first.
153
+ - `conflict` from `routine`: the library has a routine with that name. Give another `--name`, or give `--replace` to save over it.
154
+ - `invalid-input` with "is a project folder": the library folder is a project. Give a folder beside the projects.
155
+ - `match` gives `count: 0`: the routine is not in the program. A packed program can hold the routine in a packed form. Unpack the program with c64-unpacker first.
156
+ - `invalid-input`: a file, an address or a shift is wrong. A path that goes out of the project folder is also wrong, also through a link. Read the message.
69
157
 
70
158
  ## Handoffs
71
159
 
@@ -74,3 +162,4 @@ The exit status is 0 for a result. The exit status is 1 for an error. The exit s
74
162
  - Run or examine the different code → c64-emulator
75
163
  - Analyze the different code → c64-static-analysis
76
164
  - Record a conclusion → c64-knowledge
165
+ - Apply the batch of `match` → c64-knowledge
@@ -0,0 +1,152 @@
1
+ // Byte comparison of releases for the c64-provenance script. Each release
2
+ // has bytes in one or more segments: a PRG at its load address plus an
3
+ // optional shift, or the segments of an unpacker image (D50). The comparison
4
+ // covers the addresses where every release has bytes, and reports where the
5
+ // releases differ and which releases agree there. It never decides which
6
+ // bytes are original: that needs evidence beyond one comparison.
7
+
8
+ export interface Range {
9
+ start: number;
10
+ end: number;
11
+ }
12
+
13
+ export interface Release {
14
+ name: string;
15
+ /** Where the release has bytes, after the shift, in address order. */
16
+ segments: Range[];
17
+ /** The byte at an address inside a segment. */
18
+ byteAt: (address: number) => number;
19
+ }
20
+
21
+ export interface DifferingRange {
22
+ start: number;
23
+ end: number;
24
+ /** The bytes of each release in this range, by release name. */
25
+ values: Record<string, number[]>;
26
+ /** Releases with the same bytes here, largest group first. */
27
+ groups: string[][];
28
+ }
29
+
30
+ export interface Comparison {
31
+ /** The addresses where every release has bytes. */
32
+ common: Range[];
33
+ identicalBytes: number;
34
+ differingBytes: number;
35
+ differences: DifferingRange[];
36
+ /** True when more differing ranges exist than the limit allowed. */
37
+ moreDifferences: boolean;
38
+ }
39
+
40
+ /** Joins differing addresses that are at most this far apart into one range. */
41
+ const JOIN_GAP = 4;
42
+
43
+ export const covers = (release: Release, address: number) => release.segments.some((segment) => address >= segment.start && address <= segment.end);
44
+
45
+ /** The ranges where every release has bytes, inside from..to. */
46
+ export function commonRanges(releases: Release[], from = 0x0000, to = 0xffff): Range[] {
47
+ const ranges: Range[] = [];
48
+ for (let address = from; address <= to; address++) {
49
+ if (!releases.every((release) => covers(release, address))) continue;
50
+ const last = ranges.at(-1);
51
+ if (last !== undefined && last.end + 1 === address) last.end = address;
52
+ else ranges.push({ start: address, end: address });
53
+ }
54
+ return ranges;
55
+ }
56
+
57
+ export function compareReleases(releases: Release[], options: { from?: number; to?: number; maxRanges?: number } = {}): Comparison {
58
+ const common = commonRanges(releases, options.from, options.to);
59
+ let identicalBytes = 0;
60
+ const differing: number[] = [];
61
+ for (const range of common) {
62
+ for (let address = range.start; address <= range.end; address++) {
63
+ const first = releases[0]!.byteAt(address);
64
+ if (releases.every((release) => release.byteAt(address) === first)) identicalBytes++;
65
+ else differing.push(address);
66
+ }
67
+ }
68
+ // A joined range stays inside one common range, so every release has all of its bytes.
69
+ const inSameCommon = (a: number, b: number) => common.some((range) => a >= range.start && b <= range.end);
70
+ const ranges: Range[] = [];
71
+ for (const address of differing) {
72
+ const last = ranges.at(-1);
73
+ if (last !== undefined && address - last.end <= JOIN_GAP && inSameCommon(last.start, address)) last.end = address;
74
+ else ranges.push({ start: address, end: address });
75
+ }
76
+ const limit = options.maxRanges ?? 64;
77
+ const differences = ranges.slice(0, limit).map((range) => {
78
+ const values: Record<string, number[]> = {};
79
+ const groups = new Map<string, string[]>();
80
+ for (const release of releases) {
81
+ const bytes = Array.from({ length: range.end - range.start + 1 }, (_, offset) => release.byteAt(range.start + offset));
82
+ values[release.name] = bytes;
83
+ const key = bytes.join(",");
84
+ groups.set(key, [...(groups.get(key) ?? []), release.name]);
85
+ }
86
+ return { ...range, values, groups: [...groups.values()].sort((a, b) => b.length - a.length) };
87
+ });
88
+ return { common, identicalBytes, differingBytes: differing.length, differences, moreDifferences: ranges.length > limit };
89
+ }
90
+
91
+ /** The addresses of a release in order, so that a window of bytes can run over its segments. */
92
+ function addressesOf(release: Release): number[] {
93
+ return release.segments.flatMap((segment) => Array.from({ length: segment.end - segment.start + 1 }, (_, offset) => segment.start + offset));
94
+ }
95
+
96
+ /** Most shifts that suggestShifts gives: data that did not move can outvote code that did (b2/04). */
97
+ export const MAX_SUGGESTED_SHIFTS = 3;
98
+
99
+ /**
100
+ * The shifts that put `other` over `base` best: the most common address
101
+ * differences between equal 8-byte sequences, most votes first. A shift with
102
+ * less than a tenth of the best one's votes is chance. Empty when nothing matches.
103
+ */
104
+ export function suggestShifts(base: Release, other: Release): Array<{ shift: number; matches: number }> {
105
+ const WINDOW = 8;
106
+ const key = (release: Release, addresses: number[], at: number) =>
107
+ String.fromCharCode(...addresses.slice(at, at + WINDOW).map((address) => release.byteAt(address)));
108
+ const baseAddresses = addressesOf(base);
109
+ const otherAddresses = addressesOf(other);
110
+ const positions = new Map<string, number[]>();
111
+ for (let at = 0; at + WINDOW <= baseAddresses.length; at++) {
112
+ const found = positions.get(key(base, baseAddresses, at));
113
+ // Very common sequences (fill bytes) say nothing about the alignment.
114
+ if (found === undefined) positions.set(key(base, baseAddresses, at), [baseAddresses[at]!]);
115
+ else if (found.length < 8) found.push(baseAddresses[at]!);
116
+ }
117
+ const votes = new Map<number, number>();
118
+ for (let at = 0; at + WINDOW <= otherAddresses.length; at++) {
119
+ for (const address of positions.get(key(other, otherAddresses, at)) ?? []) {
120
+ const shift = address - otherAddresses[at]!;
121
+ votes.set(shift, (votes.get(shift) ?? 0) + 1);
122
+ }
123
+ }
124
+ const ranked = [...votes].map(([shift, matches]) => ({ shift, matches })).sort((a, b) => b.matches - a.matches || Math.abs(a.shift) - Math.abs(b.shift));
125
+ return ranked.filter((found) => found.matches * 10 >= ranked[0]!.matches).slice(0, MAX_SUGGESTED_SHIFTS);
126
+ }
127
+
128
+ export type ShiftSuggestion = { release: number; shift: number; matchingSequences: number } | { release: number; matchingSequences: number; reason: string };
129
+
130
+ /**
131
+ * The --shift to suggest for release `index` (0 = the first) from `found`,
132
+ * the best shift of that release over the first one (F-25). The shift is the
133
+ * full value for --shift: `applied` holds the shifts already given. A shift
134
+ * that moves a segment out of $0000-$ffff is not suggested: with two releases,
135
+ * the opposite shift of the first release is suggested when it fits, else the
136
+ * result gives the reason.
137
+ */
138
+ export function shiftSuggestion(releases: Release[], applied: number[], index: number, found: { shift: number; matches: number }): ShiftSuggestion {
139
+ const outside = (release: Release, by: number) => release.segments.find((segment) => segment.start + by < 0 || segment.end + by > 0xffff);
140
+ const hex = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
141
+ const why = (number: number, by: number, segment: Range) =>
142
+ `release ${number} moved by ${by} puts ${hex(segment.start)}-${hex(segment.end)} ${segment.start + by < 0 ? "below $0000" : "past $ffff"}`;
143
+ const own = outside(releases[index]!, found.shift);
144
+ if (own === undefined) return { release: index + 1, shift: applied[index]! + found.shift, matchingSequences: found.matches };
145
+ const reasons = [why(index + 1, found.shift, own)];
146
+ if (releases.length === 2) {
147
+ const base = outside(releases[0]!, -found.shift);
148
+ if (base === undefined) return { release: 1, shift: applied[0]! - found.shift, matchingSequences: found.matches };
149
+ reasons.push(why(1, -found.shift, base));
150
+ }
151
+ return { release: index + 1, matchingSequences: found.matches, reason: `No shift fits: ${reasons.join(", and ")}.` };
152
+ }