@henols/c64-re-tools 2.0.0-rc.9 → 2.0.0

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 (117) hide show
  1. package/README.md +97 -116
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +22 -6
  4. package/skills/c64-assembler/scripts/assemble.ts +42 -14
  5. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  6. package/skills/c64-basic/SKILL.md +10 -1
  7. package/skills/c64-basic/scripts/basic.ts +3 -3
  8. package/skills/c64-disk/SKILL.md +13 -4
  9. package/skills/c64-disk/scripts/disk.ts +7 -7
  10. package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
  11. package/skills/c64-docs/SKILL.md +138 -0
  12. package/skills/c64-docs/references/page-types.md +141 -0
  13. package/skills/c64-docs/references/style.md +43 -0
  14. package/skills/c64-docs/scripts/generate.ts +109 -0
  15. package/skills/c64-docs/scripts/lib/links.ts +167 -0
  16. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  17. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  18. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  19. package/skills/c64-docs/scripts/link.ts +239 -0
  20. package/skills/c64-emulator/SKILL.md +16 -5
  21. package/skills/c64-knowledge/SKILL.md +140 -18
  22. package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
  23. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  24. package/skills/c64-memory-map/SKILL.md +11 -2
  25. package/skills/c64-memory-map/scripts/memmap.ts +2 -2
  26. package/skills/c64-provenance/SKILL.md +66 -11
  27. package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
  28. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  29. package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
  30. package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
  31. package/skills/c64-provenance/scripts/provenance.ts +242 -40
  32. package/skills/c64-reverse-engineering/SKILL.md +51 -26
  33. package/skills/c64-source/SKILL.md +76 -28
  34. package/skills/c64-source/scripts/generate.ts +425 -127
  35. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
  36. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  37. package/skills/c64-static-analysis/SKILL.md +35 -18
  38. package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
  39. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
  40. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
  41. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
  42. package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
  43. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  44. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  45. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  46. package/skills/c64-testing/SKILL.md +36 -7
  47. package/skills/c64-testing/references/scenario-format.md +12 -8
  48. package/skills/c64-testing/scripts/checklist.ts +3 -3
  49. package/skills/c64-testing/scripts/diff.ts +167 -0
  50. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  51. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
  52. package/skills/c64-testing/scripts/run.ts +73 -21
  53. package/skills/c64-testing/scripts/test.ts +5 -5
  54. package/skills/c64-unpacker/SKILL.md +23 -9
  55. package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
  56. package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
  57. package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
  58. package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
  59. package/skills/c64-unpacker/scripts/unpack.ts +41 -24
  60. package/src/c64.ts +103 -5
  61. package/src/cli/cleanup.ts +1 -1
  62. package/src/cli/main.ts +7 -4
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +18 -4
  66. package/src/host/server.ts +10 -3
  67. package/src/host/tools/petcat.ts +1 -1
  68. package/src/host/tools/status.ts +1 -1
  69. package/src/host/vice/adapter.ts +4 -3
  70. package/src/host/vice/process.ts +2 -2
  71. package/src/host/vice/screen.ts +1 -42
  72. package/src/host/vice/session.ts +196 -23
  73. package/src/host-client/autostart.ts +1 -1
  74. package/src/host-client/connect.ts +1 -1
  75. package/src/host-client/coverage.ts +2 -29
  76. package/src/host-client/image.ts +66 -0
  77. package/src/host-client/steps.ts +9 -4
  78. package/src/host-client/tools.ts +1 -0
  79. package/src/host-client/transfer.ts +39 -11
  80. package/src/host-client/vice-session.ts +33 -16
  81. package/src/knowledge/conflicts.ts +161 -0
  82. package/src/knowledge/database.ts +150 -38
  83. package/src/knowledge/expression.ts +209 -0
  84. package/src/knowledge/graph.ts +104 -0
  85. package/src/knowledge/history.ts +70 -22
  86. package/src/knowledge/import.ts +64 -55
  87. package/src/knowledge/join.ts +211 -0
  88. package/src/knowledge/program.ts +266 -0
  89. package/src/knowledge/read.ts +258 -27
  90. package/src/knowledge/schema.ts +199 -75
  91. package/src/knowledge/text.ts +415 -0
  92. package/src/knowledge/write.ts +740 -57
  93. package/src/mcp/main.ts +1 -1
  94. package/src/mcp/output.ts +15 -8
  95. package/src/mcp/server.ts +3 -1
  96. package/src/mcp/tools/debug.ts +97 -16
  97. package/src/mcp/tools/execution.ts +38 -5
  98. package/src/mcp/tools/machine.ts +11 -2
  99. package/src/mcp/tools/media.ts +18 -13
  100. package/src/mcp/tools/memory.ts +17 -8
  101. package/src/mcp/tools/video.ts +9 -9
  102. package/src/native/discover.ts +2 -2
  103. package/src/native/ghidra/analyze.ts +2 -2
  104. package/src/native/ghidra/index.ts +1 -1
  105. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  106. package/src/native/ghidra/scripts/C64Export.java +1 -1
  107. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  108. package/src/native/types.ts +14 -8
  109. package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
  110. package/src/platform.ts +70 -7
  111. package/src/project.ts +180 -3
  112. package/src/protocol/messages.ts +1 -1
  113. package/src/protocol/tools.ts +2 -16
  114. package/src/protocol/vice.ts +124 -15
  115. package/src/script.ts +46 -7
  116. package/skills/c64-assembler/scripts/compare.ts +0 -84
  117. package/skills/c64-source/scripts/source.ts +0 -595
@@ -1,4 +1,4 @@
1
- // The c64-testing run script (D45): runs steps in its own emulator, from a
1
+ // The c64-testing run script: runs steps in its own emulator, from a
2
2
  // program or a snapshot file, and writes what the steps ask for into a
3
3
  // directory: screens, coverage maps, snapshot files, and results.json with
4
4
  // the observations. One side, no comparison. Exit status: 0 when every step
@@ -6,20 +6,20 @@
6
6
 
7
7
  import { parseArgs } from "node:util";
8
8
 
9
- import { formatC64Address, parseC64Address } from "#src/c64.ts";
9
+ import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
10
10
  import { runProgram } from "#src/host-client/autostart.ts";
11
11
  import { readCoverage } from "#src/host-client/coverage.ts";
12
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, WireFailure } from "#src/host-client/tools.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
14
  import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
15
- import { ViceSessionClient } from "#src/host-client/vice-session.ts";
15
+ import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
16
16
  import { resolveProjectPath } from "#src/project.ts";
17
17
  import { runScript, UsageError } from "#src/script.ts";
18
- import { loadSymbols } from "./scenario.ts";
18
+ import { loadSymbols } from "./lib/scenario.ts";
19
19
 
20
20
  const USAGE = `run.ts <run.json> --out <dir>
21
21
 
22
- <run.json> is a run file relative to the project directory: where to start,
22
+ <run.json> is a run file relative to the project folder: where to start,
23
23
  optional symbols, and the steps. See references/scenario-format.md, section
24
24
  Runs. The files that the steps write, and results.json, go into <dir>. The
25
25
  result is one JSON object.`;
@@ -30,7 +30,8 @@ type RunStep =
30
30
  | { save: string }
31
31
  | { screen: string }
32
32
  | { coverage: string }
33
- | { observe: string; memory?: Array<string | { at: string; size?: number }>; registers?: boolean; vicii?: boolean; sprites?: number[] };
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[] };
34
35
 
35
36
  interface Run {
36
37
  name: string;
@@ -40,7 +41,8 @@ interface Run {
40
41
  steps: RunStep[];
41
42
  }
42
43
 
43
- const OWN_KINDS = ["snapshot", "save", "screen", "coverage", "observe"] as const;
44
+ const OWN_KINDS = ["snapshot", "save", "screen", "coverage", "observe", "trace"] as const;
45
+ const TRACE_KEYS = ["trace", "points", "frames", "maxHits"];
44
46
  const OBSERVE_KEYS = ["observe", "memory", "registers", "vicii", "sprites"];
45
47
  /** A name of a file that a step writes: letters, digits, dots, underscores or hyphens. */
46
48
  const NAME = /^[A-Za-z0-9._-]{1,64}$/;
@@ -68,10 +70,10 @@ function readRun(path: string): Run {
68
70
  (value.steps as unknown[]).forEach((step, index) => {
69
71
  const at = `step ${index + 1}`;
70
72
  if (!isObject(step)) return fail(`${at} must be an object`);
71
- const kinds = "observe" in step ? ["observe"] : Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key) || (OWN_KINDS as readonly string[]).includes(key));
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));
72
74
  if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...ACTION_STEP_KINDS, ...OWN_KINDS].join(", ")}`);
73
75
  const kind = kinds[0]!;
74
- const allowed = kind === "observe" ? OBSERVE_KEYS : (ACTION_STEP_KEYS[kind] ?? [kind]);
76
+ const allowed = kind === "observe" ? OBSERVE_KEYS : kind === "trace" ? TRACE_KEYS : (ACTION_STEP_KEYS[kind] ?? [kind]);
75
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`);
76
78
  if ((ACTION_STEP_KINDS as readonly string[]).includes(kind)) return checkActionStep(kind, step, at, fail);
77
79
  const name = step[kind];
@@ -80,11 +82,32 @@ function readRun(path: string): Run {
80
82
  return;
81
83
  }
82
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
+ }
83
99
  if (kind !== "observe") return;
84
100
  if (labels.has(name as string)) fail(`${at} needs a new observation name`);
85
101
  labels.add(name as string);
86
- if (step.memory !== undefined && (!Array.isArray(step.memory) || step.memory.some((item) => typeof item !== "string" && !(isObject(item) && typeof item.at === "string" && (item.size === undefined || isInteger(item.size, 1, MAX_MEMORY_READ)))))) {
87
- fail(`${at}: memory is a list of names, $ addresses, or objects with at and size`);
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)`);
88
111
  }
89
112
  if (step.registers !== undefined && typeof step.registers !== "boolean") fail(`${at}: registers must be true or false`);
90
113
  if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
@@ -93,6 +116,11 @@ function readRun(path: string): Run {
93
116
  return value as unknown as Run;
94
117
  }
95
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
+
96
124
  async function runFile(path: string, out: string): Promise<{ output: Record<string, unknown>; failed: boolean }> {
97
125
  const run = readRun(path);
98
126
  const symbols = loadSymbols({ program: "", symbols: run.symbols ?? "none" });
@@ -108,6 +136,7 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
108
136
  files.push(name);
109
137
  };
110
138
  const observations: Record<string, unknown> = {};
139
+ const traces: Record<string, unknown> = {};
111
140
  const client = await ViceSessionClient.open({ videoStandard: run.videoStandard ?? "pal" });
112
141
  let done = 0;
113
142
  let reason: string | undefined;
@@ -135,13 +164,36 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
135
164
  } else if ("coverage" in step) {
136
165
  write(`${step.coverage}.bin`, await readCoverage(client));
137
166
  await client.memmap({ action: "clear" });
167
+ } else if ("trace" in step) {
168
+ // A trace: 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
+ }
138
178
  } else if ("observe" in step) {
139
179
  const seen: Record<string, unknown> = {};
140
- if (step.memory !== undefined) {
141
- const memory: Record<string, string> = {};
180
+ if (step.memory === "all") {
181
+ // All 64 KiB of RAM, also under the ROMs and the I/O area, as one file.
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 }> = {};
142
186
  for (const item of step.memory) {
143
187
  const spec = typeof item === "string" ? { at: item } : item;
144
- memory[spec.at] = (await client.memoryRead({ address: at(spec.at), size: spec.size ?? 1, space: "c64", view: "cpu" })).data;
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
+ }
145
197
  }
146
198
  seen.memory = memory;
147
199
  }
@@ -149,8 +201,8 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
149
201
  const registers = await client.registersGet("c64");
150
202
  seen.registers = { ...registers, pc: formatC64Address(registers.pc) };
151
203
  }
152
- if (step.vicii === true) seen.vicii = await client.vicii();
153
- if (step.sprites !== undefined) seen.sprites = (await client.sprites(step.sprites)).sprites;
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);
154
206
  observations[step.observe] = seen;
155
207
  } else {
156
208
  const notReached = await runActionStep(client, step, at);
@@ -162,7 +214,7 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
162
214
  done++;
163
215
  }
164
216
  const pc = (await client.registersGet("c64")).pc;
165
- const results = { name: run.name, completed: reason === undefined, steps: run.steps.length, done, ...(reason === undefined ? {} : { reason }), pc: formatC64Address(pc), files: [...files], observations };
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 }) };
166
218
  write("results.json", Buffer.from(`${JSON.stringify(results, null, 2)}\n`));
167
219
  return { output: { ...results, files, observations: Object.keys(observations), out }, failed: reason !== undefined };
168
220
  } finally {
@@ -171,15 +223,15 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
171
223
  }
172
224
 
173
225
  await runScript(
174
- async () => {
175
- const { values, positionals } = parseArgs({ args: process.argv.slice(2), strict: true, allowPositionals: true, options: { out: { type: "string" }, help: { type: "boolean" } } });
226
+ async (argv) => {
227
+ const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { out: { type: "string" }, help: { type: "boolean" } } });
176
228
  if (values.help) throw new UsageError("");
177
229
  if (positionals.length !== 1) throw new UsageError("give one run file");
178
230
  if (values.out === undefined) throw new UsageError("run needs --out <dir>");
179
231
  try {
180
232
  resolveProjectPath(values.out);
181
233
  } catch {
182
- throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
234
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
183
235
  }
184
236
  const { output, failed } = await runFile(positionals[0]!, values.out);
185
237
  if (failed) process.exitCode = 1;
@@ -8,11 +8,11 @@ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
8
8
  import { WireFailure } from "#src/host-client/tools.ts";
9
9
  import { resolveProjectPath } from "#src/project.ts";
10
10
  import { runScript, UsageError } from "#src/script.ts";
11
- import { readScenario, runScenario, ScenarioError, type ScenarioResult } from "./scenario.ts";
11
+ import { readScenario, runScenario, ScenarioError, type ScenarioResult } from "./lib/scenario.ts";
12
12
 
13
13
  const USAGE = `test.ts <scenario.json> [--out <dir>]
14
14
 
15
- <scenario.json> is a scenario file relative to the project directory. See
15
+ <scenario.json> is a scenario file relative to the project folder. See
16
16
  references/scenario-format.md. With --out, a FAIL writes both screens, the
17
17
  difference image and memory.json with each differing byte into <dir>. The
18
18
  result is one JSON object.`;
@@ -27,7 +27,7 @@ async function run(argv: string[]): Promise<ScenarioResult> {
27
27
  try {
28
28
  resolveProjectPath(values.out);
29
29
  } catch {
30
- throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
30
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
31
31
  }
32
32
  }
33
33
  try {
@@ -56,8 +56,8 @@ async function runScenarioFile(path: string, out: string | undefined): Promise<S
56
56
  }
57
57
 
58
58
  await runScript(
59
- async () => {
60
- const result = await run(process.argv.slice(2));
59
+ async (argv) => {
60
+ const result = await run(argv);
61
61
  process.exitCode = EXIT[result.result];
62
62
  return result;
63
63
  },
@@ -13,7 +13,7 @@ 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.
16
+ - The program: a PRG file, relative to the project folder.
17
17
  - For a capture: the address where the unpacked program starts, and the directory for the result.
18
18
 
19
19
  ## Tools / execution path
@@ -21,9 +21,9 @@ This skill does not change project knowledge.
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
25
- node <skill>/scripts/unpack.ts stages original/game.prg --steps input.json
26
- node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out parts --drive
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
27
27
  ```
28
28
 
29
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`.
@@ -31,8 +31,9 @@ node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out
31
31
  | Option | Meaning |
32
32
  |---|---|
33
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 unpacked/game.prg` | Where to write the result, relative to the project directory. |
34
+ | `--out .c64-re-tools/local/capture` | Where to write the result, relative to the project folder. |
35
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. |
36
37
  | `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
37
38
  | `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
38
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. |
@@ -49,8 +50,19 @@ A steps file has the steps `write`, `registers`, `joystick`, `type`, `frames` an
49
50
  [{ "frames": 50 }, { "type": " " }]
50
51
  ```
51
52
 
53
+ A `frames` step runs 1 to 30000 frames. A `runUntil` step has `timeoutFrames` from 1 to 30000.
54
+
52
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.
53
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`.
65
+
54
66
  ## Workflow
55
67
 
56
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:
@@ -59,7 +71,7 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
59
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.
60
72
 
61
73
  Also read the evidence that the script finds in the file. The script never names a packer from it:
62
- - `basicText`: the BASIC lines as text. A packer or a crack often puts its name there.
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.
63
75
  - `strings`: readable text near the start of the code.
64
76
  - `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
65
77
  - `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
@@ -72,13 +84,14 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
72
84
  - Or use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
73
85
  - Or use the c64-emulator skill: set a breakpoint after the unpack loop, or look at the last JMP of the unpacker.
74
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.
75
88
  4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
76
89
  - `image.bin`: all 64 KiB of RAM at the entry.
77
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.
78
91
  - One PRG for each segment.
79
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.
80
- 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.
81
- 6. Give `image.bin` to the c64-static-analysis skill with its `--flat64k` option, or give it the segment PRGs.
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.
82
95
  7. Use `--range` only when you know the area of the program. Then the capture writes that one segment.
83
96
 
84
97
  ## Multi-load programs
@@ -99,7 +112,8 @@ A multi-load program loads more parts from the disk while it runs, for example o
99
112
 
100
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.
101
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.
102
- 4. With `--drive`, `drive8.prg` is the drive RAM at `$0000`-`$07ff`. Analyze it with c64-static-analysis and `--space drive8`, and record its knowledge in space `drive8`.
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`.
103
117
 
104
118
  VICE records no memory access map for the drive CPU. Thus no tool gives coverage of drive code.
105
119
 
@@ -45,7 +45,7 @@ export interface PackingEvidence {
45
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
46
  basicStart: { line: number; sys: string | null; expression?: string } | null;
47
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). */
48
+ /** The BASIC lines as text: a packer or a crack often puts its banner there. */
49
49
  basicText: string[];
50
50
  /** Runs of readable PETSCII near the start of the code, raw. */
51
51
  strings: string[];
@@ -1,4 +1,4 @@
1
- // The loads command of c64-unpacker (D53): one capture for each part of a
1
+ // The loads command of c64-unpacker: one capture for each part of a
2
2
  // multi-load program. The steps file has action steps and capture steps; a
3
3
  // capture writes what the C64 side changed since the previous capture as an
4
4
  // image with its entry state, and with --drive the drive RAM as a PRG for
@@ -66,7 +66,7 @@ export function readLoadSteps(path: string): LoadStep[] {
66
66
  * Ranges from $0400 that the CPU wrote since the last capture, joined across
67
67
  * gaps of up to `join` bytes, each with at least one changed byte. With
68
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).
69
+ * that was there (zeros over the zeros of VICE's start pattern).
70
70
  * Without it, the KERNAL RAM test wrote all of RAM after the map was cleared,
71
71
  * and only the changed bytes count.
72
72
  */
@@ -102,7 +102,7 @@ export async function loads(program: string, options: { out: string; steps: Load
102
102
  let loadedProgram: Buffer | undefined;
103
103
  /** False while the map holds the writes of the KERNAL RAM test. */
104
104
  let writesKnown = true;
105
- /** A known flaw of the host's VICE version that a tape load meets (D56). */
105
+ /** A known flaw of the host's VICE version that a tape load meets. */
106
106
  let emulatorFlaw: string | undefined;
107
107
  if (program.toLowerCase().endsWith(".prg")) {
108
108
  const { bytes } = readProjectFile(program);
@@ -113,7 +113,7 @@ export async function loads(program: string, options: { out: string; steps: Load
113
113
  previous.fill(0, load, Math.min(0x10000, load + bytes.length - 2));
114
114
  loadedProgram = bytes;
115
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).
116
+ // RUN as c64-testing types it: the steps start at the same time as a scenario's.
117
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
118
  } else {
119
119
  // A disk, tape or cartridge: VICE loads and starts it. The RAM test of the KERNAL puts back what it writes.
@@ -1,4 +1,4 @@
1
- // The machine state that a capture writes (D43): RAM, the CPU port, pages
1
+ // The machine state that a capture writes: RAM, the CPU port, pages
2
2
  // 0-3, the chip registers read without side effects, and the vectors. The
3
3
  // capture and loads commands of the unpacker share it.
4
4
 
@@ -9,12 +9,12 @@ type Registers = Awaited<ReturnType<ViceSessionClient["registersGet"]>>;
9
9
 
10
10
  const MAX_READ = 4096;
11
11
 
12
- /** The I/O areas of the entry state, read without side effects (D43). */
12
+ /** The I/O areas of the entry state, read without side effects. */
13
13
  export const IO_AREAS = { vic: [0xd000, 0x2f], sid: [0xd400, 0x1d], cia1: [0xdc00, 0x10], cia2: [0xdd00, 0x10] } as const;
14
14
  /** Frames from a hard reset to the READY prompt, with margin. */
15
15
  export const READY_FRAMES = 200;
16
16
 
17
- /** What no register read of a CIA gives (D63). */
17
+ /** What no register read of a CIA gives. */
18
18
  export interface CiaHidden {
19
19
  latchA: number;
20
20
  latchB: number;
@@ -70,7 +70,7 @@ export function entryState(registers: Registers, state: MachineState, vectors: R
70
70
  cpuPort: { $00: state.port[0], $01: state.port[1] },
71
71
  pages: Object.fromEntries([0, 1, 2, 3].map((page) => [formatC64Address(page << 8), state.pages.subarray(page << 8, (page + 1) << 8).toString("hex")])),
72
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).
73
+ // The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them.
74
74
  cia: state.cia,
75
75
  vectors,
76
76
  ...(differsFromReset === undefined ? {} : { differsFromReset }),
@@ -1,11 +1,11 @@
1
- // The stage rule of the c64-unpacker script (D43, F-17). The memory map keeps
1
+ // The stage rule of the c64-unpacker script. The memory map keeps
2
2
  // no order: a written and executed byte can be code that ran first and was
3
3
  // written over later, as a decruncher does to its own start code. So a stage
4
4
  // starts only where the CPU executes an opcode byte that the current stage
5
5
  // wrote before the CPU got there. Code that the stage executed before it wrote
6
6
  // it is the stage's own code: it stops the run once, and then a write watch
7
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,
8
+ // to the stage, is the stage's own code too: a packer hands off by JMP,
9
9
  // RTS or RTI and never runs its own code again.
10
10
 
11
11
  import { COVERAGE } from "#src/c64.ts";
@@ -71,7 +71,7 @@ export function inRanges(ranges: readonly Range[], address: number): boolean {
71
71
  }
72
72
 
73
73
  /**
74
- * Where a call returns to the stage (D59): the return address on top of the
74
+ * Where a call returns to the stage: the return address on top of the
75
75
  * stack, when a JSR that the stage executed pushed it. `stack` is page 1.
76
76
  * Returns the address of the instruction after that JSR.
77
77
  */
@@ -100,10 +100,10 @@ export function executedIn(map: Uint8Array, block: Range): Range[] {
100
100
 
101
101
  /**
102
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
103
+ * leftovers, joined into ranges that do not overlap or touch. A block
104
104
  * is a leftover when it holds the entry of an intermediate stage that started
105
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
106
+ * at that entry. Code that a stage ran before a later stage wrote over
107
107
  * it never drops a block, and the block that holds the capture entry is always
108
108
  * a segment. The filter comes before the join: a join first would drop the
109
109
  * program with a leftover. `writtenBy` is the last stage that wrote into the
@@ -128,3 +128,15 @@ export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: re
128
128
  }
129
129
  return segments;
130
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
+ }