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

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 (75) hide show
  1. package/README.md +19 -59
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +5 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  5. package/skills/c64-assembler/scripts/compare.ts +84 -0
  6. package/skills/c64-basic/SKILL.md +1 -2
  7. package/skills/c64-disk/SKILL.md +20 -6
  8. package/skills/c64-disk/scripts/containers.ts +124 -0
  9. package/skills/c64-disk/scripts/disk.ts +59 -5
  10. package/skills/c64-emulator/SKILL.md +11 -4
  11. package/skills/c64-knowledge/SKILL.md +53 -3
  12. package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
  13. package/skills/c64-memory-map/SKILL.md +4 -1
  14. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  15. package/skills/c64-provenance/SKILL.md +37 -9
  16. package/skills/c64-provenance/scripts/compare.ts +60 -29
  17. package/skills/c64-provenance/scripts/provenance.ts +248 -19
  18. package/skills/c64-provenance/scripts/runtime.ts +179 -0
  19. package/skills/c64-provenance/scripts/separate.ts +132 -0
  20. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  21. package/skills/c64-source/SKILL.md +119 -0
  22. package/skills/c64-source/scripts/generate.ts +402 -0
  23. package/skills/c64-source/scripts/loader.ts +320 -0
  24. package/skills/c64-source/scripts/opcodes.ts +61 -0
  25. package/skills/c64-source/scripts/source.ts +558 -0
  26. package/skills/c64-static-analysis/SKILL.md +27 -4
  27. package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
  28. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  29. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  30. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  31. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  32. package/skills/c64-testing/SKILL.md +27 -3
  33. package/skills/c64-testing/references/scenario-format.md +43 -3
  34. package/skills/c64-testing/scripts/run.ts +189 -0
  35. package/skills/c64-testing/scripts/scenario.ts +153 -135
  36. package/skills/c64-testing/scripts/test.ts +16 -6
  37. package/skills/c64-unpacker/SKILL.md +65 -14
  38. package/skills/c64-unpacker/scripts/evidence.ts +86 -21
  39. package/skills/c64-unpacker/scripts/loads.ts +174 -0
  40. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  41. package/skills/c64-unpacker/scripts/unpack.ts +302 -48
  42. package/src/c64.ts +91 -0
  43. package/src/cli/status.ts +3 -1
  44. package/src/host/main.ts +10 -25
  45. package/src/host/server.ts +8 -40
  46. package/src/host/vice/adapter.ts +25 -10
  47. package/src/host/vice/session.ts +64 -20
  48. package/src/host-client/autostart.ts +45 -0
  49. package/src/host-client/connect.ts +4 -5
  50. package/src/host-client/coverage.ts +59 -0
  51. package/src/host-client/steps.ts +170 -0
  52. package/src/host-client/transfer.ts +29 -3
  53. package/src/host-client/vice-session.ts +42 -13
  54. package/src/knowledge/history.ts +40 -13
  55. package/src/knowledge/import.ts +72 -56
  56. package/src/knowledge/read.ts +203 -52
  57. package/src/knowledge/schema.ts +94 -12
  58. package/src/knowledge/write.ts +432 -123
  59. package/src/mcp/main.ts +1 -4
  60. package/src/mcp/output.ts +32 -0
  61. package/src/mcp/server.ts +4 -2
  62. package/src/mcp/tools/debug.ts +110 -24
  63. package/src/mcp/tools/input.ts +1 -0
  64. package/src/mcp/tools/media.ts +22 -6
  65. package/src/mcp/tools/memory.ts +57 -10
  66. package/src/mcp/tools/video.ts +9 -3
  67. package/src/native/discover.ts +5 -2
  68. package/src/native/ghidra/index.ts +19 -3
  69. package/src/native/ghidra/scripts/C64Export.java +33 -1
  70. package/src/native/status.ts +15 -3
  71. package/src/native/types.ts +30 -1
  72. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  73. package/src/protocol/messages.ts +17 -10
  74. package/src/protocol/vice.ts +72 -18
  75. package/src/script.ts +3 -2
@@ -1,7 +1,7 @@
1
1
  // Local evidence of packing for the c64-unpacker script: where BASIC hands
2
2
  // over and how compressed the bytes look. It never names a packer.
3
3
 
4
- import { formatC64Address } from "#src/c64.ts";
4
+ import { basicLineText, formatC64Address, instructionLength, parseTokenized } from "#src/c64.ts";
5
5
 
6
6
  export class PackingError extends Error {}
7
7
 
@@ -42,10 +42,72 @@ export interface PackingEvidence {
42
42
  /** The first BASIC line and its constant SYS address, or null when the program has none. */
43
43
  basicStart: { line: number; sys: string } | null;
44
44
  entropy: { blocks: number; highBlocks: number; median: number | null };
45
+ /** The BASIC lines as text: a packer or a crack often puts its banner there (D43). */
46
+ basicText: string[];
47
+ /** Runs of readable PETSCII near the start of the code, raw. */
48
+ strings: string[];
49
+ /** What the code at the SYS address does in its first bytes. */
50
+ start: { at: string; jumpsOutside?: string; writesOutside?: string } | null;
51
+ /** EOR loops that store with an index near the start: decryption or decoding. */
52
+ decodeLoops: string[];
45
53
  packing: "likely" | "unlikely" | "unclear";
46
54
  evidence: string;
47
55
  }
48
56
 
57
+ /** How far the start-code signals look from the SYS address. */
58
+ const START_BYTES = 256;
59
+ const MIN_STRING = 8;
60
+ const MAX_STRINGS = 10;
61
+ const EOR_OPCODES = new Set([0x41, 0x45, 0x49, 0x4d, 0x51, 0x55, 0x59, 0x5d]);
62
+ const INDEXED_STORES = new Set([0x81, 0x91, 0x95, 0x99, 0x9d]);
63
+ const BRANCHES = new Set([0x10, 0x30, 0x50, 0x70, 0x90, 0xb0, 0xd0, 0xf0]);
64
+
65
+ /** Readable PETSCII runs of at least MIN_STRING characters, the first MAX_STRINGS of them. */
66
+ function readableStrings(bytes: Uint8Array): string[] {
67
+ const found: string[] = [];
68
+ let run = "";
69
+ for (const byte of [...bytes, 0]) {
70
+ if (byte >= 0x20 && byte <= 0x5d) run += String.fromCharCode(byte);
71
+ else {
72
+ if (run.trim().length >= MIN_STRING && found.length < MAX_STRINGS) found.push(run.trim().slice(0, 40));
73
+ run = "";
74
+ }
75
+ }
76
+ return found;
77
+ }
78
+
79
+ /**
80
+ * The start-code signals: the first JMP to an address outside the loaded
81
+ * range, the first indexed store outside it, and EOR loops that store with
82
+ * an index and branch back.
83
+ */
84
+ function startSignals(program: Uint8Array, load: number, end: number, sys: number): { start: PackingEvidence["start"]; decodeLoops: string[] } {
85
+ if (sys < load || sys > end) return { start: null, decodeLoops: [] };
86
+ const byteAt = (address: number) => (address >= load && address <= end ? program[address - load + 2]! : undefined);
87
+ const outside = (address: number) => address < load || address > end;
88
+ const instructions: Array<{ at: number; opcode: number; operand: number; length: number }> = [];
89
+ for (let at = sys; at <= Math.min(end, sys + START_BYTES - 1); ) {
90
+ const opcode = byteAt(at)!;
91
+ const length = instructionLength(opcode);
92
+ const operand = length === 3 ? (byteAt(at + 1) ?? 0) | ((byteAt(at + 2) ?? 0) << 8) : length === 2 ? (byteAt(at + 1) ?? 0) : 0;
93
+ instructions.push({ at, opcode, operand, length });
94
+ at += length;
95
+ }
96
+ const jump = instructions.find((instruction) => instruction.opcode === 0x4c && outside(instruction.operand));
97
+ const store = instructions.find((instruction) => (instruction.opcode === 0x9d || instruction.opcode === 0x99) && outside(instruction.operand));
98
+ const decodeLoops = instructions
99
+ .filter((instruction, index) => {
100
+ if (!EOR_OPCODES.has(instruction.opcode)) return false;
101
+ const after = instructions.slice(index + 1, index + 9);
102
+ return after.some((next) => INDEXED_STORES.has(next.opcode)) && after.some((next) => BRANCHES.has(next.opcode) && next.operand >= 0x80);
103
+ })
104
+ .map((instruction) => formatC64Address(instruction.at));
105
+ return {
106
+ start: { at: formatC64Address(sys), ...(jump === undefined ? {} : { jumpsOutside: formatC64Address(jump.operand) }), ...(store === undefined ? {} : { writesOutside: formatC64Address(store.operand) }) },
107
+ decodeLoops,
108
+ };
109
+ }
110
+
49
111
  export function inspect(program: Uint8Array): PackingEvidence {
50
112
  if (program.length < 3) throw new PackingError("the file is too short for a PRG");
51
113
  const load = program[0]! | (program[1]! << 8);
@@ -58,7 +120,13 @@ export function inspect(program: Uint8Array): PackingEvidence {
58
120
  for (let at = 0; at + BLOCK <= body.length; at += BLOCK) blocks.push(entropy(body.subarray(at, at + BLOCK)));
59
121
  const high = blocks.filter((value) => value > HIGH_ENTROPY).length;
60
122
  const share = blocks.length === 0 ? 0 : high / blocks.length;
61
- const packing = blocks.length < 2 ? "unclear" : share >= 0.75 ? "likely" : share <= 0.1 ? "unlikely" : "unclear";
123
+ const byEntropy = blocks.length < 2 ? "unclear" : share >= 0.75 ? "likely" : share <= 0.1 ? "unlikely" : "unclear";
124
+ const tokenized = parseTokenized(program);
125
+ const basicText = "lines" in tokenized && load === 0x0801 ? tokenized.lines.map(basicLineText) : [];
126
+ const signals = stub === undefined ? { start: null, decodeLoops: [] } : startSignals(program, load, end, stub.address);
127
+ const startMoves = signals.start?.jumpsOutside !== undefined || signals.start?.writesOutside !== undefined || signals.decodeLoops.length > 0;
128
+ // Code that decodes or moves itself is a reason to run it, also when the bytes look plain.
129
+ const packing = byEntropy === "unlikely" && startMoves ? "unclear" : byEntropy;
62
130
  const sorted = [...blocks].sort((a, b) => a - b);
63
131
  return {
64
132
  loadRange: { start: formatC64Address(load), end: formatC64Address(Math.min(end, 0xffff)) },
@@ -69,13 +137,19 @@ export function inspect(program: Uint8Array): PackingEvidence {
69
137
  highBlocks: high,
70
138
  median: sorted.length === 0 ? null : Number(sorted[sorted.length >> 1]!.toFixed(2)),
71
139
  },
140
+ basicText,
141
+ strings: readableStrings(body.subarray(0, 1024)),
142
+ start: signals.start,
143
+ decodeLoops: signals.decodeLoops,
72
144
  packing,
73
145
  evidence:
74
146
  packing === "likely"
75
147
  ? `${high} of ${blocks.length} blocks of 256 bytes look compressed (above ${HIGH_ENTROPY} bits per byte); code and data are usually below.`
76
148
  : packing === "unlikely"
77
149
  ? `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.`
78
- : "The entropy does not show clearly if the program is packed. Run it and look at what it writes to memory.",
150
+ : startMoves && byEntropy === "unlikely"
151
+ ? "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."
152
+ : "The entropy does not show clearly if the program is packed. Run it and look at what it writes to memory.",
79
153
  };
80
154
  }
81
155
 
@@ -88,27 +162,18 @@ export interface AccessRange {
88
162
  execute: boolean;
89
163
  }
90
164
 
91
- /** The addresses that one memory map read covers before it is split. */
92
- const MEMMAP_WINDOW = 0x1000;
93
-
94
165
  /**
95
- * Reads the memory map of $0000-$ffff in windows, in address order. `read`
96
- * gives at most `maxRanges` ranges for one window; a window that gives that
97
- * many can have more, so it is read again as two halves.
166
+ * Reads the memory map of $0000-$ffff in address order. One `read` gives a
167
+ * limited number of ranges, and `next` when more follow: the next read starts there.
98
168
  */
99
- export async function readMemoryMap(read: (start: number, end: number) => Promise<AccessRange[]>, maxRanges: number): Promise<AccessRange[]> {
169
+ export async function readMemoryMap(read: (start: number) => Promise<{ ranges: AccessRange[]; next?: number }>): Promise<AccessRange[]> {
100
170
  const ranges: AccessRange[] = [];
101
- const window = async (start: number, end: number): Promise<void> => {
102
- const found = await read(start, end);
103
- if (found.length < maxRanges || start === end) {
104
- ranges.push(...found);
105
- return;
106
- }
107
- const middle = Math.floor((start + end) / 2);
108
- await window(start, middle);
109
- await window(middle + 1, end);
110
- };
111
- for (let start = 0; start <= 0xffff; start += MEMMAP_WINDOW) await window(start, start + MEMMAP_WINDOW - 1);
171
+ for (let start: number | undefined = 0; start !== undefined; ) {
172
+ const found: { ranges: AccessRange[]; next?: number } = await read(start);
173
+ ranges.push(...found.ranges);
174
+ if (found.next !== undefined && found.next <= start) throw new Error(`the memory map did not move on from ${start}`);
175
+ start = found.next;
176
+ }
112
177
  return ranges;
113
178
  }
114
179
 
@@ -0,0 +1,174 @@
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, textToPetscii } from "#src/c64.ts";
8
+ import { autostartLoaded, LOAD_LIMIT_FRAMES } 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
+
26
+ /** Reads and checks the steps file of loads: action steps with $ addresses, and capture steps with new names. */
27
+ export function readLoadSteps(path: string): LoadStep[] {
28
+ let value: unknown;
29
+ try {
30
+ value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
31
+ } catch (error) {
32
+ if (error instanceof WireFailure) throw error;
33
+ throw new UsageError(`--steps ${path} is not JSON: ${(error as Error).message}`);
34
+ }
35
+ const fail = (message: string): never => {
36
+ throw new UsageError(`--steps ${path}: ${message}`);
37
+ };
38
+ if (!Array.isArray(value) || value.length === 0) return fail("the steps are a JSON list with at least one step");
39
+ const names = new Set<string>();
40
+ value.forEach((step: unknown, index) => {
41
+ const at = `step ${index + 1}`;
42
+ if (!isObject(step)) return fail(`${at} must be an object`);
43
+ if ("capture" in step) {
44
+ if (Object.keys(step).length !== 1) fail(`${at}: a capture step has only capture`);
45
+ 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`);
46
+ if (names.has(step.capture as string)) fail(`${at}: the capture name ${step.capture as string} is used before`);
47
+ names.add(step.capture as string);
48
+ return;
49
+ }
50
+ const kinds = Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key));
51
+ if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...ACTION_STEP_KINDS, "capture"].join(", ")}`);
52
+ const kind = kinds[0]!;
53
+ const allowed = ACTION_STEP_KEYS[kind] ?? [kind];
54
+ for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
55
+ checkActionStep(kind, step, at, fail, false);
56
+ });
57
+ if (names.size === 0) fail("the steps have no capture step");
58
+ return value as LoadStep[];
59
+ }
60
+
61
+ /** Ranges from $0400 where the RAM changed and the CPU wrote since the last capture, joined across gaps below `join` bytes. */
62
+ export function changedBlocks(before: Uint8Array, after: Uint8Array, map: Uint8Array, join: number): Array<{ start: number; end: number; bytes: number }> {
63
+ const blocks: Array<{ start: number; end: number; bytes: number }> = [];
64
+ for (let address = FIRST_SEGMENT_ADDRESS; address < 0x10000; address++) {
65
+ if (before[address] === after[address] || (map[address]! & COVERAGE.ramWrite) === 0) continue;
66
+ const last = blocks.at(-1);
67
+ if (last !== undefined && address - last.end <= join) {
68
+ last.end = address;
69
+ last.bytes++;
70
+ } else blocks.push({ start: address, end: address, bytes: 1 });
71
+ }
72
+ return blocks;
73
+ }
74
+
75
+ const hex4 = (value: number) => value.toString(16).padStart(4, "0");
76
+
77
+ /**
78
+ * Runs the program and its steps. Each capture step writes <out>/<name>/:
79
+ * image.bin, image.json, entry_state.json, a PRG for each segment, and with
80
+ * `drive` the drive RAM as drive8.prg. Blocks below `minBlock` bytes are
81
+ * counted, not written: a game changes its variables all the time.
82
+ */
83
+ export async function loads(program: string, options: { out: string; steps: LoadStep[]; drive: boolean; minBlock: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
84
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
85
+ try {
86
+ await client.reset({ mode: "hard", run: false });
87
+ let previous: Buffer;
88
+ /** A PRG that autostart put into memory without the CPU: its bytes count as written in the first capture. */
89
+ let loadedProgram: Buffer | undefined;
90
+ if (program.toLowerCase().endsWith(".prg")) {
91
+ const { bytes } = readProjectFile(program);
92
+ if (!(await autostartLoaded(client, program, bytes))) return { output: { loaded: false, reason: `The program was not in memory after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
93
+ // The RAM before RUN, without the program, so the first capture holds the program too.
94
+ previous = await readRam(client, 0x0000, 0x10000);
95
+ const load = bytes[0]! | (bytes[1]! << 8);
96
+ previous.fill(0, load, Math.min(0x10000, load + bytes.length - 2));
97
+ loadedProgram = bytes;
98
+ await client.memmap({ action: "clear" });
99
+ await client.keyboard(textToPetscii("RUN\n"));
100
+ } else {
101
+ // A disk, tape or cartridge: VICE loads and starts it. The RAM test of the KERNAL puts back what it writes.
102
+ previous = await readRam(client, 0x0000, 0x10000);
103
+ await client.memmap({ action: "clear" });
104
+ await client.autostart({ path: program, index: 0, run: true });
105
+ }
106
+
107
+ const captures: Array<Record<string, unknown>> = [];
108
+ for (const [index, step] of options.steps.entries()) {
109
+ if (!("capture" in step)) {
110
+ const notReached = await runActionStep(client, step, parseC64Address);
111
+ if (notReached !== undefined && "runUntil" in step) {
112
+ const reason = `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
113
+ return { output: { loaded: false, reason, captures }, failed: true };
114
+ }
115
+ continue;
116
+ }
117
+ if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
118
+ const registers = await client.registersGet("c64");
119
+ const memory = await readRam(client, 0x0000, 0x10000);
120
+ const map = await readCoverage(client);
121
+ if (loadedProgram !== undefined) {
122
+ // The PRG itself is part of the first capture.
123
+ const load = loadedProgram[0]! | (loadedProgram[1]! << 8);
124
+ for (let address = load; address < Math.min(0x10000, load + loadedProgram.length - 2); address++) map[address]! |= COVERAGE.ramWrite;
125
+ loadedProgram = undefined;
126
+ }
127
+ const blocks = changedBlocks(previous, memory, map, 16);
128
+ const segments = blocks.filter((block) => block.end - block.start + 1 >= options.minBlock);
129
+ const small = blocks.filter((block) => !segments.includes(block)).reduce((sum, block) => sum + block.bytes, 0);
130
+ const state = await machineState(client);
131
+ const vectors = vectorsOf(state, memory);
132
+ const directory = `${options.out}/${step.capture}`;
133
+ const write = (name: string, bytes: Uint8Array) => writeProjectFile(`${directory}/${name}`, bytes, { what: "--out" });
134
+ write("image.bin", memory);
135
+ const files = segments.map((segment) => {
136
+ const name = `segment-${hex4(segment.start)}-${hex4(segment.end)}.prg`;
137
+ write(name, Buffer.concat([Buffer.from([segment.start & 0xff, segment.start >> 8]), memory.subarray(segment.start, segment.end + 1)]));
138
+ return name;
139
+ });
140
+ let drive: Record<string, unknown> | undefined;
141
+ if (options.drive) {
142
+ const ram = Buffer.from((await client.memoryRead({ address: 0x0000, size: DRIVE_RAM, space: "drive8", view: "cpu" })).data, "hex");
143
+ write("drive8.prg", Buffer.concat([Buffer.from([0x00, 0x00]), ram]));
144
+ drive = { file: "drive8.prg", pc: formatC64Address((await client.registersGet("drive8")).pc), space: "drive8" };
145
+ }
146
+ const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
147
+ write(
148
+ "image.json",
149
+ json({
150
+ capture: step.capture,
151
+ entry: formatC64Address(registers.pc),
152
+ segments: segments.map((segment, at) => ({ ...showRange(segment), file: files[at] })),
153
+ vectors,
154
+ image: "image.bin",
155
+ ...(drive === undefined ? {} : { drive }),
156
+ }),
157
+ );
158
+ write("entry_state.json", json(entryState(registers, state, vectors)));
159
+ captures.push({
160
+ capture: step.capture,
161
+ out: directory,
162
+ pc: formatC64Address(registers.pc),
163
+ segments: segments.map(showRange),
164
+ ...(small === 0 ? {} : { smallChanges: small }),
165
+ ...(drive === undefined ? {} : { drive: drive.file }),
166
+ });
167
+ previous = memory;
168
+ await client.memmap({ action: "clear" });
169
+ }
170
+ return { output: { loaded: true, captures }, failed: false };
171
+ } finally {
172
+ await client.close();
173
+ }
174
+ }
@@ -0,0 +1,68 @@
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
+ export interface MachineState {
18
+ port: Buffer;
19
+ pages: Buffer;
20
+ io: Record<keyof typeof IO_AREAS, Buffer>;
21
+ romVectors: Buffer;
22
+ }
23
+
24
+ /** Reads `size` bytes from `address` in the RAM view, in parts. */
25
+ export async function readRam(client: ViceSessionClient, address: number, size: number): Promise<Buffer> {
26
+ const parts: Buffer[] = [];
27
+ for (let at = address; at < address + size; at += MAX_READ) {
28
+ parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_READ, address + size - at), space: "c64", view: "ram" })).data, "hex"));
29
+ }
30
+ return Buffer.concat(parts);
31
+ }
32
+
33
+ /** The state the program finds at its entry: what a rebuild's loader must recreate. */
34
+ export async function machineState(client: ViceSessionClient): Promise<MachineState> {
35
+ const port = Buffer.from((await client.memoryRead({ address: 0x0000, size: 2, space: "c64", view: "cpu" })).data, "hex");
36
+ const pages = await readRam(client, 0x0000, 0x400);
37
+ const io = {} as Record<keyof typeof IO_AREAS, Buffer>;
38
+ 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");
39
+ const romVectors = Buffer.from((await client.memoryRead({ address: 0xfffa, size: 6, space: "c64", view: "cpu" })).data, "hex");
40
+ return { port, pages, io, romVectors };
41
+ }
42
+
43
+ const word = (bytes: Buffer, offset: number) => formatC64Address(bytes[offset]! | (bytes[offset + 1]! << 8));
44
+
45
+ /** The KERNAL vectors in page 3, the hardware vectors in RAM, and the ROM's own. */
46
+ export function vectorsOf(state: MachineState, memory: Buffer) {
47
+ return {
48
+ irq: word(state.pages, 0x314),
49
+ brk: word(state.pages, 0x316),
50
+ nmi: word(state.pages, 0x318),
51
+ ram: { nmi: word(memory, 0xfffa), reset: word(memory, 0xfffc), irq: word(memory, 0xfffe) },
52
+ rom: { nmi: word(state.romVectors, 0), reset: word(state.romVectors, 2), irq: word(state.romVectors, 4) },
53
+ };
54
+ }
55
+
56
+ /** The content of entry_state.json; `differsFromReset` only when the caller compared with a clean reset. */
57
+ export function entryState(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, differsFromReset?: Array<{ address: string; entry: number; reset: number }>) {
58
+ return {
59
+ registers: { pc: formatC64Address(registers.pc), a: registers.a, x: registers.x, y: registers.y, sp: registers.sp, flags: registers.flags },
60
+ cpuPort: { $00: state.port[0], $01: state.port[1] },
61
+ pages: Object.fromEntries([0, 1, 2, 3].map((page) => [formatC64Address(page << 8), state.pages.subarray(page << 8, (page + 1) << 8).toString("hex")])),
62
+ 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") }])),
63
+ vectors,
64
+ ...(differsFromReset === undefined ? {} : { differsFromReset }),
65
+ };
66
+ }
67
+
68
+ export const json = (value: unknown) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`);