@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,241 @@
1
+ // The c64-testing run script (D45): runs steps in its own emulator, from a
2
+ // program or a snapshot file, and writes what the steps ask for into a
3
+ // directory: screens, coverage maps, snapshot files, and results.json with
4
+ // the observations. One side, no comparison. Exit status: 0 when every step
5
+ // ran, 1 when a step did not, 2 for a bad call.
6
+
7
+ import { parseArgs } from "node:util";
8
+
9
+ import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
10
+ import { runProgram } from "#src/host-client/autostart.ts";
11
+ import { readCoverage } from "#src/host-client/coverage.ts";
12
+ import { ACTION_STEP_KEYS, ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
13
+ import { MAX_MEMORY_READ, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, WireFailure, type TraceHit, type TraceKind } from "#src/host-client/tools.ts";
14
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
15
+ import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
16
+ import { resolveProjectPath } from "#src/project.ts";
17
+ import { runScript, UsageError } from "#src/script.ts";
18
+ import { loadSymbols } from "./lib/scenario.ts";
19
+
20
+ const USAGE = `run.ts <run.json> --out <dir>
21
+
22
+ <run.json> is a run file relative to the project folder: where to start,
23
+ optional symbols, and the steps. See references/scenario-format.md, section
24
+ Runs. The files that the steps write, and results.json, go into <dir>. The
25
+ result is one JSON object.`;
26
+
27
+ type RunStep =
28
+ | ActionStep
29
+ | { snapshot: string }
30
+ | { save: string }
31
+ | { screen: string }
32
+ | { coverage: string }
33
+ | { trace: string; points: Array<{ kind: TraceKind; at: string; end?: string }>; frames: number; maxHits?: number }
34
+ | { observe: string; memory?: "all" | Array<string | { at: string; size?: number; view?: "cpu" | "ram" }>; registers?: boolean; vicii?: boolean; sprites?: number[] };
35
+
36
+ interface Run {
37
+ name: string;
38
+ videoStandard?: "pal" | "ntsc";
39
+ start: { program: string; load?: boolean } | { snapshot: string };
40
+ symbols?: string;
41
+ steps: RunStep[];
42
+ }
43
+
44
+ const OWN_KINDS = ["snapshot", "save", "screen", "coverage", "observe", "trace"] as const;
45
+ const TRACE_KEYS = ["trace", "points", "frames", "maxHits"];
46
+ const OBSERVE_KEYS = ["observe", "memory", "registers", "vicii", "sprites"];
47
+ /** A name of a file that a step writes: letters, digits, dots, underscores or hyphens. */
48
+ const NAME = /^[A-Za-z0-9._-]{1,64}$/;
49
+
50
+ function readRun(path: string): Run {
51
+ let value: unknown;
52
+ try {
53
+ value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
54
+ } catch (error) {
55
+ if (error instanceof WireFailure) throw error;
56
+ throw new UsageError(`the run ${path} is not JSON: ${(error as Error).message}`);
57
+ }
58
+ const fail = (message: string): never => {
59
+ throw new UsageError(`run ${path}: ${message}`);
60
+ };
61
+ if (!isObject(value)) return fail("is not a JSON object");
62
+ if (typeof value.name !== "string" || value.name === "") fail("needs a name");
63
+ if (value.videoStandard !== undefined && value.videoStandard !== "pal" && value.videoStandard !== "ntsc") fail("videoStandard must be pal or ntsc");
64
+ const start = value.start;
65
+ if (!isObject(start) || (typeof start.program !== "string") === (typeof start.snapshot !== "string")) fail("start needs program or snapshot");
66
+ if (isObject(start) && start.load !== undefined && typeof start.load !== "boolean") fail("start.load must be true or false");
67
+ if (value.symbols !== undefined && typeof value.symbols !== "string") fail("symbols must be knowledge, none or a JSON file");
68
+ if (!Array.isArray(value.steps) || value.steps.length === 0) fail("needs steps");
69
+ const labels = new Set<string>();
70
+ (value.steps as unknown[]).forEach((step, index) => {
71
+ const at = `step ${index + 1}`;
72
+ if (!isObject(step)) return fail(`${at} must be an object`);
73
+ const kinds = "observe" in step ? ["observe"] : "trace" in step ? ["trace"] : Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key) || (OWN_KINDS as readonly string[]).includes(key));
74
+ if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...ACTION_STEP_KINDS, ...OWN_KINDS].join(", ")}`);
75
+ const kind = kinds[0]!;
76
+ const allowed = kind === "observe" ? OBSERVE_KEYS : kind === "trace" ? TRACE_KEYS : (ACTION_STEP_KEYS[kind] ?? [kind]);
77
+ for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
78
+ if ((ACTION_STEP_KINDS as readonly string[]).includes(kind)) return checkActionStep(kind, step, at, fail);
79
+ const name = step[kind];
80
+ if (kind === "snapshot") {
81
+ if (typeof name !== "string" || name === "") fail(`${at}: snapshot needs a project file`);
82
+ return;
83
+ }
84
+ if (typeof name !== "string" || !NAME.test(name)) fail(`${at}: ${kind} needs a name of 1 to 64 letters, digits, dots, underscores or hyphens`);
85
+ if (kind === "trace") {
86
+ if (labels.has(name as string)) fail(`${at} needs a new name`);
87
+ labels.add(name as string);
88
+ const points = step.points;
89
+ if (!Array.isArray(points) || points.length === 0 || points.length > MAX_TRACE_POINTS) return fail(`${at}: points is a list of 1 to ${MAX_TRACE_POINTS} objects with kind and at`);
90
+ for (const point of points as unknown[]) {
91
+ if (!isObject(point) || Object.keys(point).some((key) => !["kind", "at", "end"].includes(key))) return fail(`${at}: a point has kind, at and end, and nothing else`);
92
+ if (!(TRACE_KINDS as readonly unknown[]).includes(point.kind)) fail(`${at}: a point kind is exec, read or write`);
93
+ if (typeof point.at !== "string" || (point.end !== undefined && typeof point.end !== "string")) fail(`${at}: a point needs at, and end when it is a range, as names or $ addresses`);
94
+ }
95
+ if (!isInteger(step.frames, 1, MAX_TRACE_FRAMES)) fail(`${at}: frames must be a number from 1 to ${MAX_TRACE_FRAMES}`);
96
+ if (step.maxHits !== undefined && !isInteger(step.maxHits, 1, MAX_TRACE_HITS)) fail(`${at}: maxHits must be a number from 1 to ${MAX_TRACE_HITS}`);
97
+ return;
98
+ }
99
+ if (kind !== "observe") return;
100
+ if (labels.has(name as string)) fail(`${at} needs a new observation name`);
101
+ labels.add(name as string);
102
+ const memoryItem = (item: unknown) =>
103
+ typeof item === "string" ||
104
+ (isObject(item) &&
105
+ typeof item.at === "string" &&
106
+ Object.keys(item).every((key) => ["at", "size", "view"].includes(key)) &&
107
+ (item.size === undefined || isInteger(item.size, 1, 0x10000)) &&
108
+ (item.view === undefined || item.view === "cpu" || item.view === "ram"));
109
+ if (step.memory !== undefined && step.memory !== "all" && (!Array.isArray(step.memory) || !step.memory.every(memoryItem))) {
110
+ fail(`${at}: memory is "all", or a list of names, $ addresses, or objects with at, size (1 to 65536) and view (cpu or ram)`);
111
+ }
112
+ if (step.registers !== undefined && typeof step.registers !== "boolean") fail(`${at}: registers must be true or false`);
113
+ if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
114
+ if (step.sprites !== undefined && (!Array.isArray(step.sprites) || !step.sprites.every((sprite) => isInteger(sprite, 0, 7)))) fail(`${at}: sprites is a list of numbers from 0 to 7`);
115
+ });
116
+ return value as unknown as Run;
117
+ }
118
+
119
+ /** A trace hit with $ addresses, as a person reads it. */
120
+ function showHit(hit: TraceHit) {
121
+ return { ...hit, pc: formatC64Address(hit.pc), ...(hit.address === undefined ? {} : { address: formatC64Address(hit.address) }) };
122
+ }
123
+
124
+ async function runFile(path: string, out: string): Promise<{ output: Record<string, unknown>; failed: boolean }> {
125
+ const run = readRun(path);
126
+ const symbols = loadSymbols({ program: "", symbols: run.symbols ?? "none" });
127
+ const at = (location: string): number => {
128
+ if (location.startsWith("$")) return parseC64Address(location);
129
+ const address = symbols.get(location);
130
+ if (address === undefined) throw new UsageError(`the symbols have no ${location}`);
131
+ return address;
132
+ };
133
+ const files: string[] = [];
134
+ const write = (name: string, bytes: Uint8Array) => {
135
+ writeProjectFile(`${out}/${name}`, bytes, { what: "--out" });
136
+ files.push(name);
137
+ };
138
+ const observations: Record<string, unknown> = {};
139
+ const traces: Record<string, unknown> = {};
140
+ const client = await ViceSessionClient.open({ videoStandard: run.videoStandard ?? "pal" });
141
+ let done = 0;
142
+ let reason: string | undefined;
143
+ try {
144
+ await client.reset({ mode: "hard", run: false });
145
+ if ("snapshot" in run.start) {
146
+ await client.snapshotRestore(readProjectFile(run.start.snapshot).bytes);
147
+ } else if (run.start.load === true) {
148
+ await client.execution({ action: "advance-frames", count: 150, space: "c64" });
149
+ await client.programLoad({ path: run.start.program });
150
+ } else {
151
+ const failed = await runProgram(client, run.start.program, readProjectFile(run.start.program).bytes);
152
+ if (failed !== undefined) reason = failed === "not-loaded" ? "The program was not in memory after autostart." : "The program did not start after RUN; it needs a BASIC start line.";
153
+ }
154
+ await client.memmap({ action: "clear" });
155
+ for (const step of reason === undefined ? run.steps : []) {
156
+ if ("snapshot" in step) {
157
+ await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
158
+ } else if ("save" in step) {
159
+ await client.snapshot({ action: "save", name: "run-save" });
160
+ write(`${step.save}.vsf`, await client.snapshotExport("run-save"));
161
+ await client.snapshot({ action: "discard", name: "run-save" });
162
+ } else if ("screen" in step) {
163
+ write(`${step.screen}.png`, Buffer.from((await client.screenCapture()).png, "base64"));
164
+ } else if ("coverage" in step) {
165
+ write(`${step.coverage}.bin`, await readCoverage(client));
166
+ await client.memmap({ action: "clear" });
167
+ } else if ("trace" in step) {
168
+ // A trace (D76): every hit of the points while the program runs on, to <name>.json.
169
+ const points = step.points.map((point) => ({ kind: point.kind, start: at(point.at), end: at(point.end ?? point.at) }));
170
+ if (points.some((point) => point.end < point.start)) throw new UsageError(`the trace ${step.trace} has a point whose end is before its start`);
171
+ const traced = await client.trace({ points, frames: step.frames, maxHits: step.maxHits ?? MAX_TRACE_HITS });
172
+ write(`${step.trace}.json`, Buffer.from(`${JSON.stringify({ points: step.points, frames: traced.frames, end: traced.end, hits: traced.hits.map(showHit) }, null, 1)}\n`));
173
+ traces[step.trace] = { file: `${step.trace}.json`, hits: traced.hits.length, frames: traced.frames, end: traced.end };
174
+ if (traced.end !== "frames" && traced.end !== "hits") {
175
+ reason = `The trace ${step.trace} ended at a ${traced.end} at ${formatC64Address(traced.pc)}.`;
176
+ break;
177
+ }
178
+ } else if ("observe" in step) {
179
+ const seen: Record<string, unknown> = {};
180
+ if (step.memory === "all") {
181
+ // All 64 KiB of RAM, also under the ROMs and the I/O area, as one file (M23.9).
182
+ write(`${step.observe}.bin`, await readMemoryInParts(client, 0x0000, 0x10000, "ram"));
183
+ seen.memory = { file: `${step.observe}.bin` };
184
+ } else if (step.memory !== undefined) {
185
+ const memory: Record<string, string | { file: string }> = {};
186
+ for (const item of step.memory) {
187
+ const spec = typeof item === "string" ? { at: item } : item;
188
+ const size = spec.size ?? 1;
189
+ const bytes = await readMemoryInParts(client, at(spec.at), size, spec.view ?? "cpu");
190
+ // More than one read gives goes to a file: results.json stays small.
191
+ if (size <= MAX_MEMORY_READ) memory[spec.at] = bytes.toString("hex");
192
+ else {
193
+ const file = `${step.observe}-${spec.at.replace(/^\$/, "")}.bin`;
194
+ write(file, bytes);
195
+ memory[spec.at] = { file };
196
+ }
197
+ }
198
+ seen.memory = memory;
199
+ }
200
+ if (step.registers === true) {
201
+ const registers = await client.registersGet("c64");
202
+ seen.registers = { ...registers, pc: formatC64Address(registers.pc) };
203
+ }
204
+ if (step.vicii === true) seen.vicii = formatVicii(await client.vicii());
205
+ if (step.sprites !== undefined) seen.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
206
+ observations[step.observe] = seen;
207
+ } else {
208
+ const notReached = await runActionStep(client, step, at);
209
+ if (notReached !== undefined && "runUntil" in step) {
210
+ reason = `Step ${done + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
211
+ break;
212
+ }
213
+ }
214
+ done++;
215
+ }
216
+ const pc = (await client.registersGet("c64")).pc;
217
+ const results = { name: run.name, completed: reason === undefined, steps: run.steps.length, done, ...(reason === undefined ? {} : { reason }), pc: formatC64Address(pc), files: [...files], observations, ...(Object.keys(traces).length === 0 ? {} : { traces }) };
218
+ write("results.json", Buffer.from(`${JSON.stringify(results, null, 2)}\n`));
219
+ return { output: { ...results, files, observations: Object.keys(observations), out }, failed: reason !== undefined };
220
+ } finally {
221
+ await client.close();
222
+ }
223
+ }
224
+
225
+ await runScript(
226
+ async (argv) => {
227
+ const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { out: { type: "string" }, help: { type: "boolean" } } });
228
+ if (values.help) throw new UsageError("");
229
+ if (positionals.length !== 1) throw new UsageError("give one run file");
230
+ if (values.out === undefined) throw new UsageError("run needs --out <dir>");
231
+ try {
232
+ resolveProjectPath(values.out);
233
+ } catch {
234
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
235
+ }
236
+ const { output, failed } = await runFile(positionals[0]!, values.out);
237
+ if (failed) process.exitCode = 1;
238
+ return output;
239
+ },
240
+ { usage: USAGE },
241
+ );
@@ -6,22 +6,32 @@ import { parseArgs } from "node:util";
6
6
 
7
7
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
8
8
  import { WireFailure } from "#src/host-client/tools.ts";
9
+ import { resolveProjectPath } from "#src/project.ts";
9
10
  import { runScript, UsageError } from "#src/script.ts";
10
- import { readScenario, runScenario, ScenarioError, type ScenarioResult } from "./scenario.ts";
11
+ import { readScenario, runScenario, ScenarioError, type ScenarioResult } from "./lib/scenario.ts";
11
12
 
12
- const USAGE = `test.ts <scenario.json>
13
+ const USAGE = `test.ts <scenario.json> [--out <dir>]
13
14
 
14
- <scenario.json> is a scenario file relative to the project directory. See
15
- references/scenario-format.md. The result is one JSON object.`;
15
+ <scenario.json> is a scenario file relative to the project folder. See
16
+ references/scenario-format.md. With --out, a FAIL writes both screens, the
17
+ difference image and memory.json with each differing byte into <dir>. The
18
+ result is one JSON object.`;
16
19
 
17
20
  const EXIT = { PASS: 0, FAIL: 1, INCONCLUSIVE: 3 } as const;
18
21
 
19
22
  async function run(argv: string[]): Promise<ScenarioResult> {
20
- const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { help: { type: "boolean" } } });
23
+ const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { out: { type: "string" }, help: { type: "boolean" } } });
21
24
  if (values.help) throw new UsageError("");
22
25
  if (positionals.length !== 1) throw new UsageError("give one scenario file");
26
+ if (values.out !== undefined) {
27
+ try {
28
+ resolveProjectPath(values.out);
29
+ } catch {
30
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
31
+ }
32
+ }
23
33
  try {
24
- return await runScenarioFile(positionals[0]!);
34
+ return await runScenarioFile(positionals[0]!, values.out);
25
35
  } catch (error) {
26
36
  // A scenario or symbol file that is not usable is a bad call.
27
37
  if (error instanceof ScenarioError) throw new UsageError(error.message);
@@ -29,7 +39,7 @@ async function run(argv: string[]): Promise<ScenarioResult> {
29
39
  }
30
40
  }
31
41
 
32
- async function runScenarioFile(path: string): Promise<ScenarioResult> {
42
+ async function runScenarioFile(path: string, out: string | undefined): Promise<ScenarioResult> {
33
43
  const scenario = readScenario(path);
34
44
  let client: ViceSessionClient;
35
45
  try {
@@ -39,15 +49,15 @@ async function runScenarioFile(path: string): Promise<ScenarioResult> {
39
49
  return { result: "INCONCLUSIVE", scenario: scenario.name, reason: `no emulator: ${error.message}`, checkpoints: [], differences: [] };
40
50
  }
41
51
  try {
42
- return await runScenario(client, scenario);
52
+ return await runScenario(client, scenario, out === undefined ? {} : { out });
43
53
  } finally {
44
54
  await client.close();
45
55
  }
46
56
  }
47
57
 
48
58
  await runScript(
49
- async () => {
50
- const result = await run(process.argv.slice(2));
59
+ async (argv) => {
60
+ const result = await run(argv);
51
61
  process.exitCode = EXIT[result.result];
52
62
  return result;
53
63
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: c64-unpacker
3
- description: Use this skill to find out if a packer or a cruncher compressed a C64 program, and to get the unpacked program for analysis. It shows evidence of packing without a guess of the packer name. It runs the program in an emulator until the unpacked code starts, then writes the memory into the project. Do not use it to analyze the unpacked code.
3
+ description: Use this skill to find out if a packer or a cruncher compressed a C64 program, and to get the unpacked program for analysis. It shows evidence of packing without a guess of the packer name. It runs the program in an emulator until the unpacked code starts, then writes the memory into the project. It also captures each part of a multi-load program and the drive RAM. Do not use it to analyze the unpacked code.
4
4
  ---
5
5
 
6
6
  # c64-unpacker
@@ -13,46 +13,109 @@ This skill does not change project knowledge.
13
13
 
14
14
  ## Inputs
15
15
 
16
- - The program: a PRG file, relative to the project directory.
17
- - For a capture: the address where the unpacked program starts, and where to write the result.
16
+ - The program: a PRG file, relative to the project folder.
17
+ - For a capture: the address where the unpacked program starts, and the directory for the result.
18
18
 
19
19
  ## Tools / execution path
20
20
 
21
21
  ```
22
22
  node <skill>/scripts/unpack.ts inspect original/game.prg
23
23
  node <skill>/scripts/unpack.ts trace original/game.prg
24
- node <skill>/scripts/unpack.ts capture original/game.prg --until '$c000' --out unpacked/game.prg --range '$c000' '$cfff'
24
+ node <skill>/scripts/unpack.ts capture original/game.prg --until '$c000' --out .c64-re-tools/local/capture
25
+ node <skill>/scripts/unpack.ts stages original/game.prg --steps .c64-re-tools/local/input.json
26
+ node <skill>/scripts/unpack.ts loads original/game.d64 --steps .c64-re-tools/local/parts.json --out .c64-re-tools/local/parts --drive
25
27
  ```
26
28
 
27
- `inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address.
29
+ `inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). In `stages` and `capture`, a `frames` step and a `runUntil` step with only `at` count as run time. `stages` refuses a `runUntil` step with `raster` or `memory`.
28
30
 
29
31
  | Option | Meaning |
30
32
  |---|---|
31
- | `--until '$c000'` | The address where the unpacked program starts. |
32
- | `--out unpacked/game.prg` | Where to write the result, relative to the project directory. |
33
+ | `--until '$c000'` | The address where the unpacked program starts. The capture stops there after the steps. To capture a later arrival at the address, add a `runUntil` step to an address after the first arrival. |
34
+ | `--out .c64-re-tools/local/capture` | Where to write the result, relative to the project folder. |
33
35
  | `--range '$c000' '$cfff'` | Write this range as a PRG. Without it, the script writes all 64 KiB of RAM as a flat file. |
36
+ | `--exclude '$a000-$a9ff'` | `capture` only: remove this range from the segments. You can give it more than once. `image.json` lists the ranges in `excluded`. The entry must not be in the range. |
34
37
  | `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
35
38
  | `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
36
- | `--entry '$c000'` | `trace` only: start the program at this address. Without it, `trace` types RUN. A program without a BASIC start line needs it. |
39
+ | `--entry '$c000'` | `trace`, `stages` and `capture`: start the program at this address. Without it, the script types RUN. A file that is not a BASIC program at `$0801` needs it. |
40
+ | `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. The scripts start the program as a c64-testing scenario does. Thus the same steps in a scenario get the program to the entry at the same time. |
41
+ | `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
42
+ | `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
43
+ | `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
37
44
 
38
- The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object.
45
+ The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object. The script refuses an option that the command does not use.
46
+
47
+ A steps file has the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of the c64-testing skill. Addresses are `$` and four hex digits. For example, wait 50 frames and press the space bar:
48
+
49
+ ```json
50
+ [{ "frames": 50 }, { "type": " " }]
51
+ ```
52
+
53
+ A `frames` step runs 1 to 30000 frames. A `runUntil` step has `timeoutFrames` from 1 to 30000.
54
+
55
+ A `type` step puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. A step cannot press one specific key of the keyboard matrix. A wait for any key that reads all keyboard columns can end without a key.
56
+
57
+ ### Project folder
58
+
59
+ - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
60
+ - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
61
+ - Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
62
+ - Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
63
+ - Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
64
+ - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
39
65
 
40
66
  ## Workflow
41
67
 
42
- 1. Run `inspect`. Read `basicStart` and `packing`:
68
+ 1. Run `inspect`. Read `basicStart` and `packing`. `basicStart` gives the first SYS line and its address. When the SYS argument needs values at run time, `sys` is null and `expression` gives the argument. Then `trace`, `stages` and `capture` stop at the first instruction after the BASIC text:
43
69
  - `likely`: most of the bytes look compressed. Packed data has this property.
44
70
  - `unlikely`: the bytes look like code and data. A simple cruncher can also give this result.
45
- - `unclear`: the file is short, or the blocks do not give clear evidence.
71
+ - `unclear`: the file is short, or the blocks do not give clear evidence. Or the code at the SYS address decodes or moves bytes outside the program.
72
+
73
+ Also read the evidence that the script finds in the file. The script never names a packer from it:
74
+ - `basicText`: raw evidence from the BASIC lines, not a listing. A byte that is not a printable character shows as `{$xx}`. A packer or a crack often puts its name there. For a listing, use the c64-basic skill.
75
+ - `strings`: readable text near the start of the code.
76
+ - `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
77
+ - `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
46
78
  2. Run `trace`. It gives the evidence from the run. Read `packing`:
47
- - `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the code that runs now. Its `start` is usually the entry point of the unpacked program.
79
+ - `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the written code that runs at the end of the trace. When the trace ends in ROM code, for example in an interrupt, it is the written code that runs next. Its `start` is code that runs after the unpacking. It is not always the entry point of the program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
48
80
  - `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
49
81
  - `unlikely`: the program executed no code that it wrote. If the program waits for a key or a joystick, it can move code after that input. Then use the c64-emulator skill.
50
82
  3. Find the address where the unpacked program starts:
51
- - Use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
83
+ - Run `stages`. The `entry` of the last stage is usually the start of the program. A routine that a stage writes and calls is part of that stage when it returns to the stage.
84
+ - Or use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
52
85
  - Or use the c64-emulator skill: set a breakpoint after the unpack loop, or look at the last JMP of the unpacker.
53
- 4. Run `capture` with that address. Use `--range` for the area that holds the unpacked program, if you know it.
54
- 5. Make sure that the result is the application: the code at the start address must not be the unpacker again.
55
- 6. Give the result to the c64-static-analysis skill. For a flat 64 KiB file, use its `--flat64k` option.
86
+ - Disassemble the code at the address with the c64-emulator skill. Make sure that it is the start of the program. The start of a program often sets the stack pointer, the interrupts and the vectors.
87
+ - A program can be relocated and not packed. Then a short copier moves the code and jumps to it. The moved code is the last stage. Make sure that the `entry` of the last stage is not code that the program writes later. An example is a routine that the program makes at run time. Disassemble the entry to make sure.
88
+ 4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
89
+ - `image.bin`: all 64 KiB of RAM at the entry.
90
+ - `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
91
+ - One PRG for each segment.
92
+ - `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
93
+ 5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it. Then capture again with `--exclude` for that range. Thus the source and the loader do not include the range.
94
+ 6. Put the capture into the knowledge with `image <out>/image.json` of the c64-knowledge skill. The knowledge keeps the segments, the entry, the entry state and the stages. The other skills read them there. The files in `<out>` are not necessary after that.
95
+ 7. Use `--range` only when you know the area of the program. Then the capture writes that one segment.
96
+
97
+ ## Multi-load programs
98
+
99
+ A multi-load program loads more parts from the disk while it runs, for example one part for each level. Use `loads` to capture each part. The program can be a PRG, or a disk, tape or cartridge image that VICE starts.
100
+
101
+ 1. Write a steps file. Put a `capture` step after each point where a part is in memory, for example:
102
+
103
+ ```json
104
+ [
105
+ { "runUntil": { "at": "$080d" }, "timeoutFrames": 6000 },
106
+ { "capture": "main" },
107
+ { "type": " " },
108
+ { "runUntil": { "at": "$c000" }, "timeoutFrames": 6000 },
109
+ { "capture": "level1" }
110
+ ]
111
+ ```
112
+
113
+ 2. Run `loads` with the steps file and a directory in `--out`. Add `--drive` when the program has its own drive code, for example a fast loader.
114
+ 3. Each capture writes `<out>/<name>/` with `image.bin`, `image.json`, `entry_state.json` and a PRG for each segment. A segment is a block of RAM from `$0400` that the program changed after the capture before. The first capture also has the program itself.
115
+ 4. Put the part that you work on into the knowledge with `image <out>/<name>/image.json` of the c64-knowledge skill.
116
+ 5. With `--drive`, `drive8.prg` is the drive RAM at `$0000`-`$07ff`. Put it into the knowledge with `image <out>/<name>/drive8.prg --space drive8`. Then analyze it with c64-static-analysis and `--space drive8`.
117
+
118
+ VICE records no memory access map for the drive CPU. Thus no tool gives coverage of drive code.
56
119
 
57
120
  ## Knowledge
58
121
 
@@ -63,18 +126,22 @@ Do not record the unpacker in the project knowledge, unless the user asks to exa
63
126
 
64
127
  ## Result
65
128
 
66
- The packing evidence, or the unpacked program in the project with the address where it starts.
129
+ The packing evidence, the stages, or the unpacked program with its image, segments and entry state in a directory of the project.
130
+
131
+ For `loads`: `captures`, with the name, the PC, the segments and the drive file of each capture. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
67
132
 
68
- The exit status is 0 for a result. The exit status is 1 for `traced: false`, for `captured: false` and for an error. The exit status is 2 for a wrong argument.
133
+ The exit status is 0 for a result. The exit status is 1 for `traced: false`, for `captured: false`, for `loaded: false` and for an error. The exit status is 2 for a wrong argument.
69
134
 
70
135
  ## Failure and conflicts
71
136
 
72
137
  - `traced: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
73
- - `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames.
138
+ - `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames. If it stopped in a key wait, give `--steps` with a key.
139
+ - A step did not reach its target: the reason names the step. Change the step or give it more frames.
140
+ - `loaded: false` and no capture has a segment: the program did not load a part, or it did not start. `keyWait: true` on a capture tells that the C64 waited at READY. or at an INPUT statement. For a tape image, read `emulatorFlaw`. A known VICE flaw can stop a tape load. Tell the user to install the latest VICE.
74
141
  - Do not name a packer from weak evidence. The script never names one.
75
- - Some programs unpack in more than one stage. Make sure that the captured code is the application and not a second unpacker.
142
+ - Some programs unpack in more than one stage. Run `stages` to find each stage. Capture at the entry of the last stage. Make sure that it is the application and not a second unpacker.
76
143
  - `not-found` or `invalid-input`: a path or an argument is wrong. Read the message.
77
- - `operation-failed` with a message about the host runtime: the host runtime does not run. Tell the user to start the host runtime with `npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host`.
144
+ - `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 disk parser, disassembler or emulator script.
78
145
 
79
146
  ## Handoffs
80
147