@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: c64-memory-map
3
+ description: Use this skill to find what a C64 address means on the platform. Examples are a VIC-II, SID or CIA register, a KERNAL routine, a vector or a system variable. It also decodes the value of a hardware register, for example the memory configuration in $01 or the screen address in $D018. It tells you when an address has no platform meaning and belongs to the application.
4
+ ---
5
+
6
+ # c64-memory-map
7
+
8
+ ## Purpose
9
+
10
+ Give the C64 platform meaning of an address or of a register value.
11
+
12
+ This skill does not read the emulator and does not change project knowledge.
13
+
14
+ ## Inputs
15
+
16
+ - One or more addresses.
17
+ - Or a register and a value to decode.
18
+
19
+ ## Tools / execution path
20
+
21
+ The script looks up local reference data. It does not use the host runtime.
22
+
23
+ ```
24
+ node <skill>/scripts/memmap.ts at '$d020' '$0314' '$ffd2'
25
+ node <skill>/scripts/memmap.ts decode '$d018' '$15' --bank '$97'
26
+ node <skill>/scripts/memmap.ts list vic
27
+ node <skill>/scripts/memmap.ts export --out src/hw.asm
28
+ ```
29
+
30
+ | Command | Result |
31
+ |---|---|
32
+ | `at <address> ...` | For each address: `kind` (`platform` or `application`), the area, the name and the meaning. |
33
+ | `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. |
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 `NAME = $xxxx` line for each name, with its meaning as a comment. |
36
+
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.
38
+
39
+ The script prints one JSON object.
40
+
41
+ ## Workflow
42
+
43
+ 1. Run `at` for the addresses in the code or the question.
44
+ 2. Read `kind`:
45
+ - `platform`: the C64 gives the address a meaning. Read `name` and `meaning`.
46
+ - `application`: the address has no platform meaning. The program decides its use. Find its use with the c64-static-analysis or c64-emulator skill.
47
+ 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.
48
+ 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.
49
+ 5. To understand a value in a register, run `decode`. For the memory configuration, decode the value of `$01`.
50
+ 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.
51
+
52
+ The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F` (the sprite and interrupt bits), `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`.
53
+
54
+ ## Knowledge
55
+
56
+ Automatic write:
57
+ - none.
58
+
59
+ Platform facts are reference data. Do not store them as project knowledge.
60
+
61
+ A conclusion about the application can be project knowledge. For example: "the program writes its own IRQ handler address to `$0314`". Record it with the c64-knowledge skill.
62
+
63
+ ## Result
64
+
65
+ 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.
66
+
67
+ The exit status is 0 for a result. The exit status is 2 for a wrong argument.
68
+
69
+ ## Failure and conflicts
70
+
71
+ - `kind: application`: this is not a failure. The address has no platform meaning.
72
+ - `invalid-input`: the address, the register or the value is not correct, or no decoder exists for the register. Read the message.
73
+
74
+ Some registers have a different meaning for a read and for a write, for example `$DC0D`. The decoded result shows both.
75
+
76
+ ## Handoffs
77
+
78
+ - The value of an address at run time → c64-emulator
79
+ - A conclusion about the application → c64-knowledge
@@ -0,0 +1,92 @@
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. export
3
+ // writes the platform names to a project file as an ACME include (D52).
4
+
5
+ import { parseArgs } from "node:util";
6
+
7
+ import { writeProjectFile } from "#src/host-client/transfer.ts";
8
+ import { decode, IO, lookup, platformNames, ROM, SYSTEM, type Entry } from "#src/platform.ts";
9
+ import { runScript, UsageError } from "#src/script.ts";
10
+
11
+ const USAGE = `memmap.ts at <address> [<address> ...]
12
+ memmap.ts decode <register> <value> [--bank <$dd00 value>]
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)
15
+
16
+ Addresses and values are $ and hex digits, decimal, or % and binary digits.
17
+ The result is one JSON object.`;
18
+
19
+ const hex = (value: number, digits = 4) => `$${value.toString(16).padStart(digits, "0")}`;
20
+
21
+ function number(text: string, max: number, what: string): number {
22
+ let value = Number.NaN;
23
+ if (/^\$[0-9a-f]{1,4}$/i.test(text)) value = Number.parseInt(text.slice(1), 16);
24
+ else if (/^%[01]{1,16}$/.test(text)) value = Number.parseInt(text.slice(1), 2);
25
+ else if (/^\d{1,5}$/.test(text)) value = Number(text);
26
+ if (!(value >= 0 && value <= max)) throw new UsageError(`${what} must be from 0 to ${hex(max, max > 0xff ? 4 : 2)}, not ${text}`);
27
+ return value;
28
+ }
29
+
30
+ const show = (entry: Entry) => ({ start: hex(entry.start), ...(entry.end !== entry.start ? { end: hex(entry.end) } : {}), name: entry.name, meaning: entry.meaning });
31
+
32
+ function run(argv: string[]): unknown {
33
+ const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { bank: { type: "string" }, out: { type: "string" }, help: { type: "boolean" } } });
34
+ if (values.help || positionals.length === 0) throw new UsageError("");
35
+ const [command, ...rest] = positionals;
36
+ switch (command) {
37
+ case "at": {
38
+ if (rest.length === 0) throw new UsageError("give at least one address");
39
+ return {
40
+ addresses: rest.map((text) => {
41
+ const found = lookup(number(text, 0xffff, "an address"));
42
+ // The chip registers, color RAM and the cartridge I/O areas show only while $01 maps I/O in.
43
+ const io = found.address >= 0xd000 && found.address <= 0xdfff;
44
+ return {
45
+ ...found,
46
+ address: hex(found.address),
47
+ ...(found.mirrorOf === undefined ? {} : { mirrorOf: hex(found.mirrorOf) }),
48
+ ...(found.entryStart === undefined ? {} : { entryStart: hex(found.entryStart) }),
49
+ ...(io ? { visible: "when $01 shows I/O at $D000 (the default)" } : {}),
50
+ };
51
+ }),
52
+ };
53
+ }
54
+ case "decode": {
55
+ if (rest.length !== 2) throw new UsageError("decode takes a register and a value");
56
+ const register = number(rest[0]!, 0xffff, "the register");
57
+ const value = number(rest[1]!, 0xff, "the value");
58
+ const bank = values.bank === undefined ? undefined : number(values.bank, 0xff, "--bank");
59
+ try {
60
+ return { register: hex(register), value: hex(value, 2), name: lookup(register).name, ...decode(register, value, bank) };
61
+ } catch (error) {
62
+ if (error instanceof RangeError) throw new UsageError(error.message);
63
+ throw error;
64
+ }
65
+ }
66
+ case "list": {
67
+ const lists: Record<string, Entry[]> = {
68
+ vic: IO[0]!.registers,
69
+ sid: IO[1]!.registers,
70
+ cia1: IO[2]!.registers,
71
+ cia2: IO[3]!.registers,
72
+ kernal: ROM,
73
+ system: SYSTEM,
74
+ };
75
+ const list = lists[rest[0] ?? ""];
76
+ if (list === undefined || rest.length !== 1) throw new UsageError(`list takes one of ${Object.keys(lists).join(", ")}`);
77
+ return { entries: list.map(show) };
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
+ }
87
+ default:
88
+ throw new UsageError(`unknown command ${command}`);
89
+ }
90
+ }
91
+
92
+ await runScript(() => run(process.argv.slice(2)), { usage: USAGE });
@@ -0,0 +1,110 @@
1
+ ---
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. It can run each release and tell how the program uses each difference. With one release, it gives evidence for each range: 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. Do not use it to analyze the code.
4
+ ---
5
+
6
+ # c64-provenance
7
+
8
+ ## Purpose
9
+
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
+
12
+ This skill does not change project knowledge.
13
+
14
+ ## Inputs
15
+
16
+ - Two or more releases, relative to the project directory. 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
+ - Optional: an address range to compare.
18
+ - Optional: a shift for a release that loads at a different address.
19
+ - Optional: a number of frames to run each release, and a steps file.
20
+ - For one release: the release, coverage files, and the knowledge of the project.
21
+
22
+ ## Tools / execution path
23
+
24
+ The script compares the files locally. Only `--run` uses the host runtime: each release runs in its own emulator.
25
+
26
+ ```
27
+ node <skill>/scripts/provenance.ts releases/a.prg releases/b.prg releases/c.prg
28
+ node <skill>/scripts/provenance.ts releases/a.prg releases/b.prg --shift 2:-256 --from '$0900' --to '$09ff'
29
+ node <skill>/scripts/provenance.ts releases/a.prg unpacked/b/image.json --run 600 --steps steps/start-game.json
30
+ node <skill>/scripts/provenance.ts separate unpacked/image.json --coverage coverage/attract.bin --coverage coverage/level1.bin --out notes/separate.json
31
+ ```
32
+
33
+ | Option | Meaning |
34
+ |---|---|
35
+ | `--shift 2:-256` | Move release 2 (the first release is 1) by -256 bytes. Use it again for more releases. |
36
+ | `--from '$0900'` | Compare from this address. |
37
+ | `--to '$09ff'` | Compare up to this address. |
38
+ | `--run 600` | Run each release for this many frames, and give each listed difference its runtime evidence. |
39
+ | `--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. |
40
+
41
+ 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.
42
+
43
+ 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.
44
+
45
+ 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.
46
+
47
+ ## Workflow
48
+
49
+ 1. Run the script on the releases.
50
+ 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`.
51
+
52
+ `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.
53
+ 3. Read `differences`. Each one has an address range, the bytes of each release, and `groups`: the releases that have the same bytes there.
54
+ 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:
55
+ - `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`.
56
+ - `changed`: `true` when the bytes at the end of the run are not the bytes of the release.
57
+ - A range that each release writes before it reads is probably runtime state, not a difference of the releases.
58
+ - A range with `none` in each release is not used in this run. Run more frames or other steps before you decide.
59
+ - `run.skipped`: the releases have no differences, so no release ran.
60
+ 5. Examine each important difference:
61
+ - Disassemble the bytes of each release with the c64-emulator skill, or analyze them with the c64-static-analysis skill.
62
+ - Find what the change does, for example a trainer, a removed copy protection, or a changed text.
63
+ 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.
64
+ 7. Record the conclusion as a comment or a region with the c64-knowledge skill.
65
+
66
+ ## One release
67
+
68
+ Use `separate` when you have only one release. It gives evidence, not a decision:
69
+
70
+ - `ranges`: the image in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. In `image.json`, the last stage is the program. An earlier stage is a depacker, an intro or a loader. Only an image from c64-unpacker `capture` has stages. For an image from `loads`, `noStages` tells this.
71
+ - `access`: from the joined `--coverage` files: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs.
72
+ - `codeRegions.total`: the number of code regions in the knowledge.
73
+ - `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.
74
+ - `outside`: RAM outside the image 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.
75
+
76
+ 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` before you decide.
77
+
78
+ ## Knowledge
79
+
80
+ Automatic write:
81
+ - none.
82
+
83
+ Record a confirmed conclusion as a normal comment, region or symbol with the c64-knowledge skill. Do not record the full comparison.
84
+
85
+ ## Result
86
+
87
+ 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.
88
+
89
+ The runtime evidence is evidence only. The script does not decide which bytes are original.
90
+
91
+ 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.
92
+
93
+ ## Failure and conflicts
94
+
95
+ - Do not call releases independent when you do not know their relation. Two cracks can come from the same source.
96
+ - Do not call a byte original only because one release has it. A majority of releases is also not proof.
97
+ - `common` is empty: the releases do not cover the same addresses. Use `--shift`, or compare a range that all releases cover.
98
+ - `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.
99
+ - `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.
100
+ - `moreDifferences: true`: the script shows only the first differences. Use `--from` and `--to` to see the others.
101
+ - `not-found`: the project has no file at that path.
102
+ - `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.
103
+
104
+ ## Handoffs
105
+
106
+ - Get releases from disk images → c64-disk
107
+ - Unpack a packed release → c64-unpacker
108
+ - Run or examine the different code → c64-emulator
109
+ - Analyze the different code → c64-static-analysis
110
+ - Record a conclusion → 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
+ }