@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,207 @@
1
+ // Local evidence of packing for the c64-unpacker script: where BASIC hands
2
+ // over and how compressed the bytes look. It never names a packer.
3
+
4
+ import { basicLineText, findHandoffs, formatC64Address, instructionLength, parseTokenized } from "#src/c64.ts";
5
+
6
+ export class PackingError extends Error {}
7
+
8
+ /** A 256-byte block above this many bits per byte looks compressed. */
9
+ const HIGH_ENTROPY = 6.9;
10
+ const BLOCK = 256;
11
+
12
+ function entropy(bytes: Uint8Array): number {
13
+ const counts = new Array<number>(256).fill(0);
14
+ for (const byte of bytes) counts[byte]!++;
15
+ let bits = 0;
16
+ for (const count of counts) {
17
+ if (count === 0) continue;
18
+ const p = count / bytes.length;
19
+ bits -= p * Math.log2(p);
20
+ }
21
+ return bits;
22
+ }
23
+
24
+ /** A BASIC program at $0801, which RUN starts. */
25
+ export interface BasicStart {
26
+ /** The first address after the BASIC text. */
27
+ basicEnd: number;
28
+ /** The first SYS: its line, and its address when the argument is a constant. */
29
+ sys?: { line: number; address?: number; expression?: string };
30
+ }
31
+
32
+ /** The BASIC program of a PRG at $0801 and its first SYS, or undefined when the file is no BASIC program there. */
33
+ export function basicStart(program: Uint8Array): BasicStart | undefined {
34
+ const tokenized = parseTokenized(program);
35
+ if ("reason" in tokenized || tokenized.loadAddress !== 0x0801 || tokenized.lines.length === 0) return undefined;
36
+ const sys = findHandoffs(tokenized.lines).find((handoff) => handoff.kind === "sys");
37
+ if (sys === undefined) return { basicEnd: tokenized.basicEnd };
38
+ return { basicEnd: tokenized.basicEnd, sys: "address" in sys ? { line: sys.line, address: sys.address } : { line: sys.line, expression: "expression" in sys ? sys.expression : "" } };
39
+ }
40
+
41
+ /** The local packing evidence of one PRG. */
42
+ export interface PackingEvidence {
43
+ loadRange: { start: string; end: string };
44
+ bytes: number;
45
+ /** The first SYS line and its address (null when the argument needs run-time values, with the argument), or null when the program has no SYS. */
46
+ basicStart: { line: number; sys: string | null; expression?: string } | null;
47
+ entropy: { blocks: number; highBlocks: number; median: number | null };
48
+ /** The BASIC lines as text: a packer or a crack often puts its banner there (D43). */
49
+ basicText: string[];
50
+ /** Runs of readable PETSCII near the start of the code, raw. */
51
+ strings: string[];
52
+ /** What the code at the SYS address does in its first bytes. */
53
+ start: { at: string; jumpsOutside?: string; writesOutside?: string } | null;
54
+ /** EOR loops that store with an index near the start: decryption or decoding. */
55
+ decodeLoops: string[];
56
+ packing: "likely" | "unlikely" | "unclear";
57
+ evidence: string;
58
+ }
59
+
60
+ /** How far the start-code signals look from the SYS address. */
61
+ const START_BYTES = 256;
62
+ const MIN_STRING = 8;
63
+ const MAX_STRINGS = 10;
64
+ const EOR_OPCODES = new Set([0x41, 0x45, 0x49, 0x4d, 0x51, 0x55, 0x59, 0x5d]);
65
+ const INDEXED_STORES = new Set([0x81, 0x91, 0x95, 0x99, 0x9d]);
66
+ const BRANCHES = new Set([0x10, 0x30, 0x50, 0x70, 0x90, 0xb0, 0xd0, 0xf0]);
67
+
68
+ /** Readable PETSCII runs of at least MIN_STRING characters, the first MAX_STRINGS of them. */
69
+ function readableStrings(bytes: Uint8Array): string[] {
70
+ const found: string[] = [];
71
+ let run = "";
72
+ for (const byte of [...bytes, 0]) {
73
+ if (byte >= 0x20 && byte <= 0x5d) run += String.fromCharCode(byte);
74
+ else {
75
+ if (run.trim().length >= MIN_STRING && found.length < MAX_STRINGS) found.push(run.trim().slice(0, 40));
76
+ run = "";
77
+ }
78
+ }
79
+ return found;
80
+ }
81
+
82
+ /**
83
+ * The start-code signals: the first JMP to an address outside the loaded
84
+ * range, the first indexed store outside it, and EOR loops that store with
85
+ * an index and branch back.
86
+ */
87
+ function startSignals(program: Uint8Array, load: number, end: number, sys: number): { start: PackingEvidence["start"]; decodeLoops: string[] } {
88
+ if (sys < load || sys > end) return { start: null, decodeLoops: [] };
89
+ const byteAt = (address: number) => (address >= load && address <= end ? program[address - load + 2]! : undefined);
90
+ const outside = (address: number) => address < load || address > end;
91
+ const instructions: Array<{ at: number; opcode: number; operand: number; length: number }> = [];
92
+ for (let at = sys; at <= Math.min(end, sys + START_BYTES - 1); ) {
93
+ const opcode = byteAt(at)!;
94
+ const length = instructionLength(opcode);
95
+ const operand = length === 3 ? (byteAt(at + 1) ?? 0) | ((byteAt(at + 2) ?? 0) << 8) : length === 2 ? (byteAt(at + 1) ?? 0) : 0;
96
+ instructions.push({ at, opcode, operand, length });
97
+ at += length;
98
+ }
99
+ const jump = instructions.find((instruction) => instruction.opcode === 0x4c && outside(instruction.operand));
100
+ const store = instructions.find((instruction) => (instruction.opcode === 0x9d || instruction.opcode === 0x99) && outside(instruction.operand));
101
+ const decodeLoops = instructions
102
+ .filter((instruction, index) => {
103
+ if (!EOR_OPCODES.has(instruction.opcode)) return false;
104
+ const after = instructions.slice(index + 1, index + 9);
105
+ return after.some((next) => INDEXED_STORES.has(next.opcode)) && after.some((next) => BRANCHES.has(next.opcode) && next.operand >= 0x80);
106
+ })
107
+ .map((instruction) => formatC64Address(instruction.at));
108
+ return {
109
+ start: { at: formatC64Address(sys), ...(jump === undefined ? {} : { jumpsOutside: formatC64Address(jump.operand) }), ...(store === undefined ? {} : { writesOutside: formatC64Address(store.operand) }) },
110
+ decodeLoops,
111
+ };
112
+ }
113
+
114
+ export function inspect(program: Uint8Array): PackingEvidence {
115
+ if (program.length < 3) throw new PackingError("the file is too short for a PRG");
116
+ const load = program[0]! | (program[1]! << 8);
117
+ const end = load + program.length - 3;
118
+ const basic = basicStart(program);
119
+ const stub = basic?.sys;
120
+ // Without BASIC, all bytes count; with it, only the bytes after the BASIC text.
121
+ const from = basic === undefined ? 2 : Math.min(program.length, basic.basicEnd - load + 2);
122
+ const body = program.subarray(from);
123
+ const blocks: number[] = [];
124
+ for (let at = 0; at + BLOCK <= body.length; at += BLOCK) blocks.push(entropy(body.subarray(at, at + BLOCK)));
125
+ const high = blocks.filter((value) => value > HIGH_ENTROPY).length;
126
+ const share = blocks.length === 0 ? 0 : high / blocks.length;
127
+ const byEntropy = blocks.length < 2 ? "unclear" : share >= 0.75 ? "likely" : share <= 0.1 ? "unlikely" : "unclear";
128
+ const tokenized = parseTokenized(program);
129
+ const basicText = "lines" in tokenized && load === 0x0801 ? tokenized.lines.map(basicLineText) : [];
130
+ const signals = stub?.address === undefined ? { start: null, decodeLoops: [] } : startSignals(program, load, end, stub.address);
131
+ const startMoves = signals.start?.jumpsOutside !== undefined || signals.start?.writesOutside !== undefined || signals.decodeLoops.length > 0;
132
+ // Code that decodes or moves itself is a reason to run it, also when the bytes look plain.
133
+ const packing = byEntropy === "unlikely" && startMoves ? "unclear" : byEntropy;
134
+ const sorted = [...blocks].sort((a, b) => a - b);
135
+ return {
136
+ loadRange: { start: formatC64Address(load), end: formatC64Address(Math.min(end, 0xffff)) },
137
+ bytes: program.length - 2,
138
+ basicStart: stub === undefined ? null : stub.address === undefined ? { line: stub.line, sys: null, expression: stub.expression! } : { line: stub.line, sys: formatC64Address(stub.address) },
139
+ entropy: {
140
+ blocks: blocks.length,
141
+ highBlocks: high,
142
+ median: sorted.length === 0 ? null : Number(sorted[sorted.length >> 1]!.toFixed(2)),
143
+ },
144
+ basicText,
145
+ strings: readableStrings(body.subarray(0, 1024)),
146
+ start: signals.start,
147
+ decodeLoops: signals.decodeLoops,
148
+ packing,
149
+ evidence:
150
+ packing === "likely"
151
+ ? `${high} of ${blocks.length} blocks of 256 bytes look compressed (above ${HIGH_ENTROPY} bits per byte); code and data are usually below.`
152
+ : packing === "unlikely"
153
+ ? `Only ${high} of ${blocks.length} blocks look compressed; the bytes look like code and data. A simple cruncher can look the same: run trace to see if the program writes code and then executes it.`
154
+ : startMoves && byEntropy === "unlikely"
155
+ ? "The bytes look like code and data, but the code at the SYS address decodes or moves bytes outside the program. Run trace or stages to see what it writes and runs."
156
+ : "The entropy does not show clearly if the program is packed. Run it and look at what it writes to memory.",
157
+ };
158
+ }
159
+
160
+ /** One range of the emulator's memory map: what the program did with these addresses. */
161
+ export interface AccessRange {
162
+ start: number;
163
+ end: number;
164
+ read: boolean;
165
+ write: boolean;
166
+ execute: boolean;
167
+ }
168
+
169
+ /**
170
+ * Reads the memory map of $0000-$ffff in address order. One `read` gives a
171
+ * limited number of ranges, and `next` when more follow: the next read starts there.
172
+ */
173
+ export async function readMemoryMap(read: (start: number) => Promise<{ ranges: AccessRange[]; next?: number }>): Promise<AccessRange[]> {
174
+ const ranges: AccessRange[] = [];
175
+ for (let start: number | undefined = 0; start !== undefined; ) {
176
+ const found: { ranges: AccessRange[]; next?: number } = await read(start);
177
+ ranges.push(...found.ranges);
178
+ if (found.next !== undefined && found.next <= start) throw new Error(`the memory map did not move on from ${start}`);
179
+ start = found.next;
180
+ }
181
+ return ranges;
182
+ }
183
+
184
+ /** ROM areas: an address here can be written (RAM below) and executed (ROM) without one being the other. */
185
+ export const underRom = (address: number) => (address >= 0xa000 && address <= 0xbfff) || address >= 0xe000;
186
+
187
+ /**
188
+ * The memory that the program wrote and then executed, in pieces of code.
189
+ * `bytes` and `running` count only RAM outside the ROM areas: a write under
190
+ * a ROM and code that runs in that ROM are not one piece of code.
191
+ */
192
+ export function writtenThenExecuted(
193
+ ranges: AccessRange[],
194
+ pc: number,
195
+ ): { written: Array<{ start: number; end: number }>; counted: Array<{ start: number; end: number }>; bytes: number; running?: { start: number; end: number } } {
196
+ // VICE marks only opcode bytes as executed; a gap of at most two operand bytes stays inside one piece of code.
197
+ const written: Array<{ start: number; end: number }> = [];
198
+ for (const range of ranges.filter((candidate) => candidate.write && candidate.execute)) {
199
+ const last = written.at(-1);
200
+ if (last !== undefined && range.start - last.end <= 3 && underRom(range.start) === underRom(last.start)) last.end = range.end;
201
+ else written.push({ start: range.start, end: range.end });
202
+ }
203
+ const counted = written.filter((range) => !underRom(range.start));
204
+ const bytes = counted.reduce((sum, range) => sum + range.end - range.start + 1, 0);
205
+ const running = counted.find((range) => pc >= range.start && pc <= range.end);
206
+ return { written, counted, bytes, ...(running === undefined ? {} : { running }) };
207
+ }
@@ -0,0 +1,209 @@
1
+ // The loads command of c64-unpacker (D53): one capture for each part of a
2
+ // multi-load program. The steps file has action steps and capture steps; a
3
+ // capture writes what the C64 side changed since the previous capture as an
4
+ // image with its entry state, and with --drive the drive RAM as a PRG for
5
+ // knowledge space drive8. No tool guesses where a load ends: the steps decide.
6
+
7
+ import { COVERAGE, formatC64Address, parseC64Address } from "#src/c64.ts";
8
+ import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
9
+ import { readCoverage } from "#src/host-client/coverage.ts";
10
+ import { ACTION_STEP_KEYS, ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
11
+ import { WireFailure } from "#src/host-client/tools.ts";
12
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
13
+ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
14
+ import { UsageError } from "#src/script.ts";
15
+ import { entryState, json, machineState, readRam, vectorsOf } from "./machine.ts";
16
+
17
+ export type LoadStep = ActionStep | { capture: string };
18
+
19
+ /** A capture name: letters, digits, dots, underscores or hyphens. */
20
+ const NAME = /^[A-Za-z0-9._-]{1,64}$/;
21
+ /** The drive RAM of a 1541. */
22
+ const DRIVE_RAM = 0x0800;
23
+ /** Pages 0-3 belong to the entry state, not to the segments. */
24
+ const FIRST_SEGMENT_ADDRESS = 0x0400;
25
+ /** The KERNAL LOAD entry in the jump table. */
26
+ const KERNAL_LOAD = 0xffd5;
27
+ /** The KERNAL loop that waits for a key at READY. or at an INPUT statement. */
28
+ const KERNAL_KEY_WAIT = { start: 0xe5cd, end: 0xe5d5 };
29
+
30
+ /** Reads and checks the steps file of loads: action steps with $ addresses, and capture steps with new names. */
31
+ export function readLoadSteps(path: string): LoadStep[] {
32
+ let value: unknown;
33
+ try {
34
+ value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
35
+ } catch (error) {
36
+ if (error instanceof WireFailure) throw error;
37
+ throw new UsageError(`--steps ${path} is not JSON: ${(error as Error).message}`);
38
+ }
39
+ const fail = (message: string): never => {
40
+ throw new UsageError(`--steps ${path}: ${message}`);
41
+ };
42
+ if (!Array.isArray(value) || value.length === 0) return fail("the steps are a JSON list with at least one step");
43
+ const names = new Set<string>();
44
+ value.forEach((step: unknown, index) => {
45
+ const at = `step ${index + 1}`;
46
+ if (!isObject(step)) return fail(`${at} must be an object`);
47
+ if ("capture" in step) {
48
+ if (Object.keys(step).length !== 1) fail(`${at}: a capture step has only capture`);
49
+ if (typeof step.capture !== "string" || !NAME.test(step.capture)) fail(`${at}: capture needs a name of 1 to 64 letters, digits, dots, underscores or hyphens`);
50
+ if (names.has(step.capture as string)) fail(`${at}: the capture name ${step.capture as string} is used before`);
51
+ names.add(step.capture as string);
52
+ return;
53
+ }
54
+ const kinds = Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key));
55
+ if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...ACTION_STEP_KINDS, "capture"].join(", ")}`);
56
+ const kind = kinds[0]!;
57
+ const allowed = ACTION_STEP_KEYS[kind] ?? [kind];
58
+ for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
59
+ checkActionStep(kind, step, at, fail, false);
60
+ });
61
+ if (names.size === 0) fail("the steps have no capture step");
62
+ return value as LoadStep[];
63
+ }
64
+
65
+ /**
66
+ * Ranges from $0400 that the CPU wrote since the last capture, joined across
67
+ * gaps of up to `join` bytes, each with at least one changed byte. With
68
+ * `writesKnown`, every written byte counts, also one written with the value
69
+ * that was there (zeros over the zeros of VICE's start pattern, F-20).
70
+ * Without it, the KERNAL RAM test wrote all of RAM after the map was cleared,
71
+ * and only the changed bytes count.
72
+ */
73
+ export function changedBlocks(before: Uint8Array, after: Uint8Array, map: Uint8Array, join: number, writesKnown: boolean): Array<{ start: number; end: number; bytes: number }> {
74
+ const blocks: Array<{ start: number; end: number; bytes: number; changed: boolean }> = [];
75
+ for (let address = FIRST_SEGMENT_ADDRESS; address < 0x10000; address++) {
76
+ const changed = before[address] !== after[address];
77
+ if ((map[address]! & COVERAGE.ramWrite) === 0 || (!writesKnown && !changed)) continue;
78
+ const last = blocks.at(-1);
79
+ if (last !== undefined && address - last.end <= join) {
80
+ last.end = address;
81
+ last.bytes++;
82
+ last.changed ||= changed;
83
+ } else blocks.push({ start: address, end: address, bytes: 1, changed });
84
+ }
85
+ return blocks.filter((block) => block.changed).map(({ start, end, bytes }) => ({ start, end, bytes }));
86
+ }
87
+
88
+ const hex4 = (value: number) => value.toString(16).padStart(4, "0");
89
+
90
+ /**
91
+ * Runs the program and its steps. Each capture step writes <out>/<name>/:
92
+ * image.bin, image.json, entry_state.json, a PRG for each segment, and with
93
+ * `drive` the drive RAM as drive8.prg. Blocks below `minBlock` bytes are
94
+ * counted, not written: a game changes its variables all the time.
95
+ */
96
+ export async function loads(program: string, options: { out: string; steps: LoadStep[]; drive: boolean; minBlock: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
97
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
98
+ try {
99
+ await client.reset({ mode: "hard", run: false });
100
+ let previous: Buffer;
101
+ /** A PRG that autostart put into memory without the CPU: its bytes count as written in the first capture. */
102
+ let loadedProgram: Buffer | undefined;
103
+ /** False while the map holds the writes of the KERNAL RAM test. */
104
+ let writesKnown = true;
105
+ /** A known flaw of the host's VICE version that a tape load meets (D56). */
106
+ let emulatorFlaw: string | undefined;
107
+ if (program.toLowerCase().endsWith(".prg")) {
108
+ const { bytes } = readProjectFile(program);
109
+ if (!(await autostartLoaded(client, program, bytes))) return { output: { loaded: false, reason: `The program was not in memory after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
110
+ // The RAM before RUN, without the program, so the first capture holds the program too.
111
+ previous = await readRam(client, 0x0000, 0x10000);
112
+ const load = bytes[0]! | (bytes[1]! << 8);
113
+ previous.fill(0, load, Math.min(0x10000, load + bytes.length - 2));
114
+ loadedProgram = bytes;
115
+ await client.memmap({ action: "clear" });
116
+ // RUN as c64-testing types it: the steps start at the same time as a scenario's (D62).
117
+ if (!(await typeRun(client))) return { output: { loaded: false, reason: `After RUN, BASIC ran no program line in ${RUN_LIMIT_FRAMES} frames.` }, failed: true };
118
+ } else {
119
+ // A disk, tape or cartridge: VICE loads and starts it. The RAM test of the KERNAL puts back what it writes.
120
+ previous = await readRam(client, 0x0000, 0x10000);
121
+ await client.memmap({ action: "clear" });
122
+ ({ emulatorFlaw } = await client.autostart({ path: program, index: 0, run: true }));
123
+ writesKnown = false;
124
+ if (!program.toLowerCase().endsWith(".crt")) {
125
+ // The map starts again where the KERNAL LOAD starts, after the RAM test: then it holds only what the program wrote.
126
+ const load = await client.runUntil({ target: { kind: "address", address: KERNAL_LOAD, space: "c64" }, timeoutFrames: LOAD_LIMIT_FRAMES });
127
+ if (load.reached) {
128
+ await client.memmap({ action: "clear" });
129
+ writesKnown = true;
130
+ }
131
+ }
132
+ }
133
+
134
+ const captures: Array<Record<string, unknown>> = [];
135
+ for (const [index, step] of options.steps.entries()) {
136
+ if (!("capture" in step)) {
137
+ const notReached = await runActionStep(client, step, parseC64Address);
138
+ if (notReached !== undefined && "runUntil" in step) {
139
+ const reason = `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
140
+ return { output: { loaded: false, reason, captures }, failed: true };
141
+ }
142
+ continue;
143
+ }
144
+ if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
145
+ const registers = await client.registersGet("c64");
146
+ const memory = await readRam(client, 0x0000, 0x10000);
147
+ const map = await readCoverage(client);
148
+ if (loadedProgram !== undefined) {
149
+ // The PRG itself is part of the first capture.
150
+ const load = loadedProgram[0]! | (loadedProgram[1]! << 8);
151
+ for (let address = load; address < Math.min(0x10000, load + loadedProgram.length - 2); address++) map[address]! |= COVERAGE.ramWrite;
152
+ loadedProgram = undefined;
153
+ }
154
+ const blocks = changedBlocks(previous, memory, map, 16, writesKnown);
155
+ const segments = blocks.filter((block) => block.end - block.start + 1 >= options.minBlock);
156
+ const small = blocks.filter((block) => !segments.includes(block)).reduce((sum, block) => sum + block.bytes, 0);
157
+ const state = await machineState(client);
158
+ const vectors = vectorsOf(state, memory);
159
+ const directory = `${options.out}/${step.capture}`;
160
+ const write = (name: string, bytes: Uint8Array) => writeProjectFile(`${directory}/${name}`, bytes, { what: "--out" });
161
+ write("image.bin", memory);
162
+ const files = segments.map((segment) => {
163
+ const name = `segment-${hex4(segment.start)}-${hex4(segment.end)}.prg`;
164
+ write(name, Buffer.concat([Buffer.from([segment.start & 0xff, segment.start >> 8]), memory.subarray(segment.start, segment.end + 1)]));
165
+ return name;
166
+ });
167
+ let drive: Record<string, unknown> | undefined;
168
+ if (options.drive) {
169
+ const ram = Buffer.from((await client.memoryRead({ address: 0x0000, size: DRIVE_RAM, space: "drive8", view: "cpu" })).data, "hex");
170
+ write("drive8.prg", Buffer.concat([Buffer.from([0x00, 0x00]), ram]));
171
+ drive = { file: "drive8.prg", pc: formatC64Address((await client.registersGet("drive8")).pc), space: "drive8" };
172
+ }
173
+ const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
174
+ write(
175
+ "image.json",
176
+ json({
177
+ capture: step.capture,
178
+ entry: formatC64Address(registers.pc),
179
+ segments: segments.map((segment, at) => ({ ...showRange(segment), file: files[at] })),
180
+ vectors,
181
+ image: "image.bin",
182
+ ...(drive === undefined ? {} : { drive }),
183
+ }),
184
+ );
185
+ write("entry_state.json", json(entryState(registers, state, vectors)));
186
+ const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
187
+ captures.push({
188
+ capture: step.capture,
189
+ out: directory,
190
+ pc: formatC64Address(registers.pc),
191
+ ...(keyWait ? { keyWait: true } : {}),
192
+ segments: segments.map(showRange),
193
+ ...(small === 0 ? {} : { smallChanges: small }),
194
+ ...(drive === undefined ? {} : { drive: drive.file }),
195
+ });
196
+ previous = memory;
197
+ await client.memmap({ action: "clear" });
198
+ writesKnown = true;
199
+ }
200
+ if (captures.length > 0 && captures.every((capture) => (capture.segments as unknown[]).length === 0)) {
201
+ const waits = captures.some((capture) => capture.keyWait === true) ? " The C64 waited for a key at READY. or at an INPUT statement, so the load ended or did not start." : "";
202
+ const reason = `No capture has a segment: the program changed no block of RAM from ${formatC64Address(FIRST_SEGMENT_ADDRESS)} on.${waits}`;
203
+ return { output: { loaded: false, reason, captures, ...(emulatorFlaw === undefined ? {} : { emulatorFlaw }) }, failed: true };
204
+ }
205
+ return { output: { loaded: true, captures }, failed: false };
206
+ } finally {
207
+ await client.close();
208
+ }
209
+ }
@@ -0,0 +1,80 @@
1
+ // The machine state that a capture writes (D43): RAM, the CPU port, pages
2
+ // 0-3, the chip registers read without side effects, and the vectors. The
3
+ // capture and loads commands of the unpacker share it.
4
+
5
+ import { formatC64Address } from "#src/c64.ts";
6
+ import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
7
+
8
+ type Registers = Awaited<ReturnType<ViceSessionClient["registersGet"]>>;
9
+
10
+ const MAX_READ = 4096;
11
+
12
+ /** The I/O areas of the entry state, read without side effects (D43). */
13
+ export const IO_AREAS = { vic: [0xd000, 0x2f], sid: [0xd400, 0x1d], cia1: [0xdc00, 0x10], cia2: [0xdd00, 0x10] } as const;
14
+ /** Frames from a hard reset to the READY prompt, with margin. */
15
+ export const READY_FRAMES = 200;
16
+
17
+ /** What no register read of a CIA gives (D63). */
18
+ export interface CiaHidden {
19
+ latchA: number;
20
+ latchB: number;
21
+ interruptMask: number;
22
+ }
23
+
24
+ export interface MachineState {
25
+ port: Buffer;
26
+ pages: Buffer;
27
+ io: Record<keyof typeof IO_AREAS, Buffer>;
28
+ /** The timer latches and the interrupt mask of each CIA. */
29
+ cia: { cia1: CiaHidden; cia2: CiaHidden };
30
+ romVectors: Buffer;
31
+ }
32
+
33
+ /** Reads `size` bytes from `address` in the RAM view, in parts. */
34
+ export async function readRam(client: ViceSessionClient, address: number, size: number): Promise<Buffer> {
35
+ const parts: Buffer[] = [];
36
+ for (let at = address; at < address + size; at += MAX_READ) {
37
+ parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_READ, address + size - at), space: "c64", view: "ram" })).data, "hex"));
38
+ }
39
+ return Buffer.concat(parts);
40
+ }
41
+
42
+ /** The state the program finds at its entry: what a rebuild's loader must recreate. */
43
+ export async function machineState(client: ViceSessionClient): Promise<MachineState> {
44
+ const port = Buffer.from((await client.memoryRead({ address: 0x0000, size: 2, space: "c64", view: "cpu" })).data, "hex");
45
+ const pages = await readRam(client, 0x0000, 0x400);
46
+ const io = {} as Record<keyof typeof IO_AREAS, Buffer>;
47
+ for (const [name, [address, size]] of Object.entries(IO_AREAS)) io[name as keyof typeof IO_AREAS] = Buffer.from((await client.ioRead(address, size)).data, "hex");
48
+ const [cia1, cia2] = (await client.cia("both")).chips.map(({ latchA, latchB, interruptMask }) => ({ latchA, latchB, interruptMask }));
49
+ const romVectors = Buffer.from((await client.memoryRead({ address: 0xfffa, size: 6, space: "c64", view: "cpu" })).data, "hex");
50
+ return { port, pages, io, cia: { cia1: cia1!, cia2: cia2! }, romVectors };
51
+ }
52
+
53
+ const word = (bytes: Buffer, offset: number) => formatC64Address(bytes[offset]! | (bytes[offset + 1]! << 8));
54
+
55
+ /** The KERNAL vectors in page 3, the hardware vectors in RAM, and the ROM's own. */
56
+ export function vectorsOf(state: MachineState, memory: Buffer) {
57
+ return {
58
+ irq: word(state.pages, 0x314),
59
+ brk: word(state.pages, 0x316),
60
+ nmi: word(state.pages, 0x318),
61
+ ram: { nmi: word(memory, 0xfffa), reset: word(memory, 0xfffc), irq: word(memory, 0xfffe) },
62
+ rom: { nmi: word(state.romVectors, 0), reset: word(state.romVectors, 2), irq: word(state.romVectors, 4) },
63
+ };
64
+ }
65
+
66
+ /** The content of entry_state.json; `differsFromReset` only when the caller compared with a clean reset. */
67
+ export function entryState(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, differsFromReset?: Array<{ address: string; entry: number; reset: number }>) {
68
+ return {
69
+ registers: { pc: formatC64Address(registers.pc), a: registers.a, x: registers.x, y: registers.y, sp: registers.sp, flags: registers.flags },
70
+ cpuPort: { $00: state.port[0], $01: state.port[1] },
71
+ pages: Object.fromEntries([0, 1, 2, 3].map((page) => [formatC64Address(page << 8), state.pages.subarray(page << 8, (page + 1) << 8).toString("hex")])),
72
+ io: Object.fromEntries(Object.entries(IO_AREAS).map(([name, [address]]) => [name, { from: formatC64Address(address), bytes: state.io[name as keyof typeof IO_AREAS].toString("hex") }])),
73
+ // The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them (D63).
74
+ cia: state.cia,
75
+ vectors,
76
+ ...(differsFromReset === undefined ? {} : { differsFromReset }),
77
+ };
78
+ }
79
+
80
+ export const json = (value: unknown) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
@@ -0,0 +1,142 @@
1
+ // The stage rule of the c64-unpacker script (D43, F-17). The memory map keeps
2
+ // no order: a written and executed byte can be code that ran first and was
3
+ // written over later, as a decruncher does to its own start code. So a stage
4
+ // starts only where the CPU executes an opcode byte that the current stage
5
+ // wrote before the CPU got there. Code that the stage executed before it wrote
6
+ // it is the stage's own code: it stops the run once, and then a write watch
7
+ // over it arms it again. Written code that the stage calls, and that returns
8
+ // to the stage, is the stage's own code too (D59): a packer hands off by JMP,
9
+ // RTS or RTI and never runs its own code again.
10
+
11
+ import { COVERAGE } from "#src/c64.ts";
12
+ import { writtenBlocks } from "#src/host-client/coverage.ts";
13
+
14
+ export interface Range {
15
+ start: number;
16
+ end: number;
17
+ }
18
+
19
+ const contains = (range: Range, address: number) => address >= range.start && address <= range.end;
20
+
21
+ /** The flags of two maps from $0000 in one. */
22
+ export function mergeMaps(into: Uint8Array, from: Uint8Array): void {
23
+ for (let address = 0; address < into.length; address++) into[address]! |= from[address]!;
24
+ }
25
+
26
+ /**
27
+ * The written blocks of at least `minimum` bytes in which a chunk executed
28
+ * code that can start a stage: every executed address except own code that
29
+ * the stage has not written.
30
+ */
31
+ export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number, called: readonly Range[] = []): Range[] {
32
+ const combined = Uint8Array.from(stage);
33
+ mergeMaps(combined, chunk);
34
+ return writtenBlocks(combined, minimum).filter((block) => {
35
+ for (let address = block.start; address <= block.end; address++) {
36
+ if ((chunk[address]! & COVERAGE.ramExecute) === 0) continue;
37
+ if (inRanges(called, address)) continue;
38
+ if (own.has(address) && (combined[address]! & COVERAGE.ramWrite) === 0) continue;
39
+ return true;
40
+ }
41
+ return false;
42
+ });
43
+ }
44
+
45
+ /**
46
+ * The points of a run over the candidate blocks: exec breakpoints over all of
47
+ * them except own code that is not written yet, and write watches over that
48
+ * own code.
49
+ */
50
+ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>, written: (address: number) => boolean, called: readonly Range[] = []): { exec: Range[]; watch: Range[] } {
51
+ const exec: Range[] = [];
52
+ const watch: Range[] = [];
53
+ const add = (list: Range[], address: number) => {
54
+ const last = list[list.length - 1];
55
+ if (last !== undefined && last.end === address - 1) last.end = address;
56
+ else list.push({ start: address, end: address });
57
+ };
58
+ for (const block of blocks) {
59
+ for (let address = block.start; address <= block.end; address++) {
60
+ if (inRanges(called, address)) continue;
61
+ if (own.has(address) && !written(address)) add(watch, address);
62
+ else add(exec, address);
63
+ }
64
+ }
65
+ return { exec, watch };
66
+ }
67
+
68
+ /** True when the address lies in one of the ranges. */
69
+ export function inRanges(ranges: readonly Range[], address: number): boolean {
70
+ return ranges.some((range) => contains(range, address));
71
+ }
72
+
73
+ /**
74
+ * Where a call returns to the stage (D59): the return address on top of the
75
+ * stack, when a JSR that the stage executed pushed it. `stack` is page 1.
76
+ * Returns the address of the instruction after that JSR.
77
+ */
78
+ export function callReturn(stack: Uint8Array, sp: number, byteAt: (address: number) => number, executed: (address: number) => boolean): number | undefined {
79
+ const pushed = stack[(sp + 1) & 0xff]! | (stack[(sp + 2) & 0xff]! << 8);
80
+ const jsr = (pushed - 2) & 0xffff;
81
+ if (byteAt(jsr) !== 0x20 || !executed(jsr)) return undefined;
82
+ return (pushed + 1) & 0xffff;
83
+ }
84
+
85
+ /**
86
+ * The code that ran in a block: its executed opcode bytes with the two bytes
87
+ * after each, which can be operands, joined and cut at the block's end.
88
+ */
89
+ export function executedIn(map: Uint8Array, block: Range): Range[] {
90
+ const ranges: Range[] = [];
91
+ for (let address = block.start; address <= block.end; address++) {
92
+ if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
93
+ const end = Math.min(block.end, address + 2);
94
+ const last = ranges[ranges.length - 1];
95
+ if (last !== undefined && address <= last.end + 1) last.end = Math.max(last.end, end);
96
+ else ranges.push({ start: address, end });
97
+ }
98
+ return ranges;
99
+ }
100
+
101
+ /**
102
+ * The segments of a capture: the blocks that the stages wrote, without loader
103
+ * leftovers, joined into ranges that do not overlap or touch (F-26). A block
104
+ * is a leftover when it holds the entry of an intermediate stage that started
105
+ * after the block's writer, and no write from that stage on replaced the code
106
+ * at that entry (R2-01). Code that a stage ran before a later stage wrote over
107
+ * it never drops a block, and the block that holds the capture entry is always
108
+ * a segment. The filter comes before the join: a join first would drop the
109
+ * program with a leftover. `writtenBy` is the last stage that wrote into the
110
+ * range.
111
+ */
112
+ export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly Range[] }>, entry: number): Array<Range & { writtenBy: number }> {
113
+ const last = stages.length - 1;
114
+ const replaced = (from: number, address: number) => stages.slice(from).some((stage) => inRanges(stage.wrote, address));
115
+ const leftover = (block: Range, writer: number) =>
116
+ !contains(block, entry) && stages.some((stage, index) => index > writer && index < last && contains(block, stage.entry) && !replaced(index, stage.entry));
117
+ const blocks = stages
118
+ .flatMap((stage, index) => stage.wrote.map((range) => ({ start: range.start, end: range.end, writtenBy: index })))
119
+ .filter((block) => !leftover(block, block.writtenBy))
120
+ .sort((a, b) => a.start - b.start);
121
+ const segments: Array<Range & { writtenBy: number }> = [];
122
+ for (const block of blocks) {
123
+ const previous = segments[segments.length - 1];
124
+ if (previous !== undefined && block.start <= previous.end + 1) {
125
+ previous.end = Math.max(previous.end, block.end);
126
+ previous.writtenBy = Math.max(previous.writtenBy, block.writtenBy);
127
+ } else segments.push({ ...block });
128
+ }
129
+ return segments;
130
+ }
131
+
132
+ /** The segments without the bytes of `holes`: a segment that a hole cuts gives its parts, each with the fields of the segment. */
133
+ export function withoutRanges<T extends Range>(segments: readonly T[], holes: readonly Range[]): T[] {
134
+ let pieces = [...segments];
135
+ for (const hole of holes) {
136
+ pieces = pieces.flatMap((piece) => {
137
+ if (hole.end < piece.start || hole.start > piece.end) return [piece];
138
+ return [...(hole.start > piece.start ? [{ ...piece, end: hole.start - 1 }] : []), ...(hole.end < piece.end ? [{ ...piece, start: hole.end + 1 }] : [])];
139
+ });
140
+ }
141
+ return pieces;
142
+ }