@henols/c64-re-tools 2.0.0-rc.6 → 2.0.0-rc.8

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 (55) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +1 -1
  3. package/skills/c64-basic/SKILL.md +2 -2
  4. package/skills/c64-basic/scripts/basic.ts +1 -1
  5. package/skills/c64-disk/SKILL.md +3 -3
  6. package/skills/c64-disk/scripts/disk.ts +2 -1
  7. package/skills/c64-emulator/SKILL.md +4 -1
  8. package/skills/c64-knowledge/SKILL.md +2 -2
  9. package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
  10. package/skills/c64-provenance/SKILL.md +10 -5
  11. package/skills/c64-provenance/scripts/compare.ts +35 -6
  12. package/skills/c64-provenance/scripts/provenance.ts +35 -15
  13. package/skills/c64-provenance/scripts/separate.ts +12 -4
  14. package/skills/c64-reverse-engineering/SKILL.md +2 -2
  15. package/skills/c64-source/SKILL.md +18 -11
  16. package/skills/c64-source/scripts/generate.ts +55 -10
  17. package/skills/c64-source/scripts/loader.ts +253 -6
  18. package/skills/c64-source/scripts/source.ts +46 -11
  19. package/skills/c64-static-analysis/SKILL.md +4 -4
  20. package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
  21. package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
  22. package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
  23. package/skills/c64-testing/SKILL.md +1 -1
  24. package/skills/c64-testing/references/scenario-format.md +1 -1
  25. package/skills/c64-testing/scripts/scenario.ts +9 -7
  26. package/skills/c64-unpacker/SKILL.md +11 -9
  27. package/skills/c64-unpacker/scripts/loads.ts +16 -4
  28. package/skills/c64-unpacker/scripts/machine.ts +13 -1
  29. package/skills/c64-unpacker/scripts/stages.ts +66 -3
  30. package/skills/c64-unpacker/scripts/unpack.ts +135 -42
  31. package/src/c64.ts +19 -5
  32. package/src/cli/status.ts +26 -3
  33. package/src/host/tools/c1541.ts +34 -5
  34. package/src/host/tools/petcat.ts +5 -2
  35. package/src/host/tools/status.ts +6 -3
  36. package/src/host/vice/adapter.ts +37 -9
  37. package/src/host/vice/chips.ts +11 -1
  38. package/src/host/vice/process.ts +7 -2
  39. package/src/host/vice/screen.ts +3 -2
  40. package/src/host/vice/session.ts +68 -15
  41. package/src/host-client/autostart.ts +11 -1
  42. package/src/host-client/connect.ts +10 -10
  43. package/src/host-client/coverage.ts +19 -10
  44. package/src/host-client/tools.ts +6 -6
  45. package/src/host-client/vice-session.ts +1 -1
  46. package/src/knowledge/schema.ts +7 -0
  47. package/src/knowledge/write.ts +5 -0
  48. package/src/mcp/tools/debug.ts +9 -5
  49. package/src/mcp/tools/machine.ts +4 -1
  50. package/src/mcp/tools/media.ts +7 -1
  51. package/src/mcp/tools/video.ts +7 -2
  52. package/src/native/discover.ts +2 -1
  53. package/src/platform.ts +3 -2
  54. package/src/protocol/tools.ts +19 -4
  55. package/src/protocol/vice.ts +56 -5
@@ -25,9 +25,9 @@ import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/nativ
25
25
  import { platformNames } from "#src/platform.ts";
26
26
  import { resolveProjectPath } from "#src/project.ts";
27
27
  import { runScript, UsageError } from "#src/script.ts";
28
- import { LOADER_JUMP, planLoader, readEntryState, simulate, type EntryState, type LoaderPlan } from "./loader.ts";
28
+ import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
29
29
  import { OPCODES } from "./opcodes.ts";
30
- import { instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
30
+ import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
31
31
 
32
32
  const USAGE = `generate.ts <image> --out <dir> [options]
33
33
 
@@ -36,7 +36,8 @@ const USAGE = `generate.ts <image> --out <dir> [options]
36
36
  --out <dir> the directory for the source files, relative to the project
37
37
  --entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
38
38
  --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): code
39
- that no run executed is marked and listed
39
+ that no run executed is marked and listed; <dir>/not_executed.json
40
+ has all of its ranges
40
41
  --space <space> the knowledge space of the image: c64 (the default), drive8, or
41
42
  bank0 to bank255; --coverage and --loader are for c64 only
42
43
  --loader also write loader.asm and loader.prg: the loader copies the
@@ -129,6 +130,8 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
129
130
  }
130
131
  const entryPoints = [...seeds].filter(inImage).sort((a, b) => a - b);
131
132
  if (entryPoints.length === 0) {
133
+ // An entry in no segment is a capture fault: the image is data only, and the result says entryOutside (R3-01).
134
+ if (image.entry !== undefined && !inImage(image.entry)) return new Map();
132
135
  throw new UsageError("Ghidra needs an entry point: give --entry, or record a routine or a code region in knowledge with the c64-knowledge skill");
133
136
  }
134
137
  const dataRanges = knowledge.regions
@@ -276,6 +279,30 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
276
279
  differ("sp", registers.sp, state.sp);
277
280
  const flags = (["n", "v", "d", "i", "z", "c"] as const).reduce((value, name, index) => value | (registers.flags[name] ? [0x80, 0x40, 0x08, 0x04, 0x02, 0x01][index]! : 0), 0);
278
281
  differ("flags", flags, state.flags & 0xcf);
282
+ if (state.vic !== undefined) {
283
+ // The display registers and the raster line (D60): the loader waited for the line, so the jump comes in it or the next one.
284
+ const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
285
+ for (const { register, mask } of VIC_DISPLAY_REGISTERS) differ(formatC64Address(0xd000 + register), vic[register]! & mask, state.vic.registers[register]! & mask);
286
+ differ("the VIC bank", Buffer.from((await client.ioRead(0xdd00, 1)).data, "hex")[0]! & 0x03, state.vic.bank);
287
+ const line = rasterLine(vic);
288
+ const wanted = rasterLine(state.vic.registers);
289
+ if (line !== wanted && line !== wanted + 1) differences.push(`the raster line is ${line}, not ${wanted}`);
290
+ }
291
+ if (plan.cia !== undefined) {
292
+ // The CIA timers (D63): at the jump a running timer is 3 cycles, the JMP, before its value at the entry.
293
+ const chips = (await client.cia("both")).chips;
294
+ const word = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
295
+ const differWord = (what: string, got: number, wanted: number) => got !== wanted && differences.push(`${what} is ${word(got)}, not ${word(wanted)}`);
296
+ for (const timer of plan.cia.timers) {
297
+ const chip = chips[timer.cia - 1]!;
298
+ const name = `CIA ${timer.cia} timer ${timer.timer}`;
299
+ const [counter, latch, control] = timer.timer === "A" ? [chip.timerA, chip.latchA, chip.controlA] : [chip.timerB, chip.latchB, chip.controlB];
300
+ differ(`${name} control`, control, timer.control);
301
+ differWord(`${name} latch`, latch, timer.latch);
302
+ if (timer.counterAtJump !== undefined) differWord(`${name} counter`, counter, timer.counterAtJump);
303
+ }
304
+ plan.cia.masks.forEach((mask, index) => differ(`CIA ${index + 1} interrupt mask`, chips[index]!.interruptMask, mask));
305
+ }
279
306
  return { verified: differences.length === 0, differences: differences.slice(0, MAX_LISTED) };
280
307
  } finally {
281
308
  await client.close();
@@ -284,8 +311,10 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
284
311
 
285
312
  /** The first words of every file that the script writes. */
286
313
  const GENERATED_MARK = "; Generated by c64-source";
314
+ /** The file with every range of instructions that no run executed (D49). */
315
+ const NOT_EXECUTED_FILE = "not_executed.json";
287
316
 
288
- /** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg; other files stay. */
317
+ /** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg and not_executed.json; other files stay. */
289
318
  function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
290
319
  const directory = resolveProjectPath(out);
291
320
  if (!existsSync(directory)) return;
@@ -293,7 +322,7 @@ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
293
322
  if (!entry.isFile()) continue;
294
323
  const name = entry.name;
295
324
  const path = join(directory, name);
296
- if (name === "loader.prg") rmSync(path, { force: true });
325
+ if (name === "loader.prg" || name === NOT_EXECUTED_FILE) rmSync(path, { force: true });
297
326
  else if (name.endsWith(".asm") && !keep.has(name) && readFileSync(path, "utf8").startsWith(GENERATED_MARK)) rmSync(path, { force: true });
298
327
  }
299
328
  }
@@ -344,16 +373,18 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
344
373
  referenceTargets: knowledge.references.map((reference) => reference.to),
345
374
  ...(coverage === undefined ? {} : { coverage }),
346
375
  // The platform names are C64 names: drive code and banks get none.
347
- ...(space === DEFAULT_SPACE ? { platform: new Map(platformNames().map((item) => [item.address, item.name])) } : {}),
376
+ ...(space === DEFAULT_SPACE ? { platform: platformNames() } : {}),
348
377
  });
349
378
  const asBytes = await probe(laid);
350
379
  const header = [`Generated by c64-source from ${image.path}${space === DEFAULT_SPACE ? "" : ` in space ${space}`} and knowledge revision ${knowledge.revision}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
351
380
  const rendered = render(laid, asBytes, header);
352
381
  const files = [...rendered.files];
353
382
  let loader: LoaderPlan | undefined;
383
+ // An image without its entry has no program: the source is still written and checked, but the run fails (R2-02).
384
+ const missingEntry = entryOutsideImage(image.entry, image.segments);
354
385
  // A loader that cannot be planned does not stop the source: the source is written and checked, and the result says why.
355
- let loaderRefused: string | undefined;
356
- if (state !== undefined) {
386
+ let loaderRefused: string | undefined = state === undefined ? undefined : missingEntry;
387
+ if (state !== undefined && missingEntry === undefined) {
357
388
  const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
358
389
  try {
359
390
  loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
@@ -367,6 +398,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
367
398
  for (const [index, file] of files.entries()) if (!file.text.startsWith(GENERATED_MARK)) files[index] = { ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` };
368
399
  removeOldFiles(values.out, new Set(files.map((file) => file.name)));
369
400
  for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
401
+ const showRange = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
402
+ const inData = entriesInData(entries, knowledge.regions);
403
+ if (rendered.notExecuted !== undefined) {
404
+ const all = { image: image.path, revision: knowledge.revision, instructions: rendered.notExecuted.instructions, ranges: rendered.notExecuted.ranges.map(showRange) };
405
+ writeProjectFile(`${values.out}/${NOT_EXECUTED_FILE}`, Buffer.from(`${JSON.stringify(all, null, 2)}\n`, "utf8"), { what: "--out" });
406
+ }
370
407
 
371
408
  const output: Record<string, unknown> = {
372
409
  out: values.out,
@@ -375,7 +412,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
375
412
  instructions: rendered.instructions,
376
413
  dataBytes: rendered.dataBytes,
377
414
  ...(rendered.undecodedBytes === 0 ? {} : { undecodedBytes: rendered.undecodedBytes }),
415
+ ...(missingEntry === undefined ? {} : { entryOutside: { entry: formatC64Address(image.entry!), reason: missingEntry } }),
378
416
  labels: { named: laid.namedLabels, generated: laid.generatedLabels },
417
+ // An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it (F-27).
418
+ ...(inData.length === 0
419
+ ? {}
420
+ : { entriesInData: inData.map(({ entry, region }) => ({ entry: formatC64Address(entry), region: { ...showRange(region), type: region.type }, origin: region.origin })) }),
379
421
  ...(rendered.asBytes.length === 0
380
422
  ? {}
381
423
  : { asBytes: { count: rendered.asBytes.length, listed: rendered.asBytes.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), instruction: item.text })) } }),
@@ -385,7 +427,8 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
385
427
  notExecuted: {
386
428
  instructions: rendered.notExecuted.instructions,
387
429
  ranges: rendered.notExecuted.ranges.length,
388
- listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
430
+ listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map(showRange),
431
+ all: `${values.out}/${NOT_EXECUTED_FILE}`,
389
432
  },
390
433
  }),
391
434
  ...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
@@ -400,7 +443,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
400
443
  output.check = checked;
401
444
  if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
402
445
  if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
403
- if (loader === undefined || state === undefined) return { output, failed: false };
446
+ if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
404
447
 
405
448
  const loaderBuilt = await assembleFiles(files, "loader.asm");
406
449
  if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
@@ -431,6 +474,8 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
431
474
  copier: formatC64Address(loader.copier),
432
475
  stackByte: formatC64Address(loader.stackByte),
433
476
  ...ran,
477
+ // What the loader cannot set as it was at the entry (D63).
478
+ ...(loader.cia === undefined || loader.cia.limits.length === 0 ? {} : { limits: loader.cia.limits }),
434
479
  };
435
480
  return { output, failed: !ran.verified };
436
481
  }
@@ -1,5 +1,7 @@
1
1
  // The loader of a generated source (D48): a PRG that copies each segment and
2
- // the entry state's pages $0002-$03ff into place, sets $00/$01, the stack
2
+ // the entry state's pages $0002-$03ff into place, sets the display registers
3
+ // of the VIC-II and the VIC bank, waits for the entry's raster line (D60),
4
+ // sets the CIA timers and interrupt masks (D63), sets $00/$01, the stack
3
5
  // pointer, A, X, Y and the flags, and jumps to the entry. The copies run
4
6
  // from a free page with SEI and all RAM visible, in an order where no copy
5
7
  // overwrites the packed bytes of a copy that has not run yet. The plan is
@@ -20,6 +22,41 @@ export interface EntryState {
20
22
  port: [number, number];
21
23
  /** $0000-$03ff at the entry. */
22
24
  pages: Uint8Array;
25
+ /** The VIC-II at the entry (D60): $d000-$d02e, and the VIC bank (bits 0-1 of $dd00). */
26
+ vic?: { registers: Uint8Array; bank: number };
27
+ /** CIA 1 and CIA 2 at the entry (D63). */
28
+ cia?: [EntryCia, EntryCia];
29
+ }
30
+
31
+ /** A CIA at the entry: $xx00-$xx0f as a read without side effects gives them, and what no read gives. */
32
+ export interface EntryCia {
33
+ registers: Uint8Array;
34
+ latchA: number;
35
+ latchB: number;
36
+ interruptMask: number;
37
+ }
38
+
39
+ /**
40
+ * The VIC-II registers that the loader sets (D60), each with the bits that a
41
+ * read gives back: not the raster, the light pen, the interrupt registers or
42
+ * the collisions. The raster line comes from a wait instead.
43
+ */
44
+ export const VIC_DISPLAY_REGISTERS: ReadonlyArray<{ register: number; mask: number }> = [
45
+ ...Array.from({ length: 0x11 }, (_, register) => ({ register, mask: 0xff })),
46
+ { register: 0x11, mask: 0x7f },
47
+ { register: 0x15, mask: 0xff },
48
+ { register: 0x16, mask: 0x3f },
49
+ { register: 0x17, mask: 0xff },
50
+ { register: 0x18, mask: 0xfe },
51
+ { register: 0x1b, mask: 0xff },
52
+ { register: 0x1c, mask: 0xff },
53
+ { register: 0x1d, mask: 0xff },
54
+ ...Array.from({ length: 0x0f }, (_, index) => ({ register: 0x20 + index, mask: 0x0f })),
55
+ ];
56
+
57
+ /** The raster line of the entry, from $d011 bit 7 and $d012 as the capture read them. */
58
+ export function rasterLine(registers: Uint8Array): number {
59
+ return ((registers[0x11]! & 0x80) << 1) | registers[0x12]!;
23
60
  }
24
61
 
25
62
  const FLAG_BITS: Record<string, number> = { n: 0x80, v: 0x40, b: 0x10, d: 0x08, i: 0x04, z: 0x02, c: 0x01 };
@@ -57,6 +94,8 @@ export function readEntryState(text: string, path: string): EntryState {
57
94
  flags,
58
95
  port: [byte(value.cpuPort?.$00, "cpuPort.$00"), byte(value.cpuPort?.$01, "cpuPort.$01")],
59
96
  pages,
97
+ ...readVic(value.io, fail),
98
+ ...readCia(value, fail),
60
99
  };
61
100
  }
62
101
 
@@ -86,6 +125,8 @@ export interface LoaderPlan {
86
125
  stackByte: number;
87
126
  end: number;
88
127
  text: string;
128
+ /** The CIA timers and masks that the loader sets, when the entry state has them (D63). */
129
+ cia?: CiaPlan;
89
130
  }
90
131
 
91
132
  /** The label of the loader's jump to the entry, in ACME's symbol list. */
@@ -97,9 +138,14 @@ const STUB = 0x080d;
97
138
  const STUB_SIZE = 21;
98
139
  /** The copier code and its three variables, without the table. */
99
140
  const COPIER_CODE_SIZE = 167;
141
+ /** The VIC-II code of the copier (D60), without its two tables of one byte per register. */
142
+ const VIC_CODE_SIZE = 44;
143
+ const VIC_SIZE = VIC_CODE_SIZE + 2 * VIC_DISPLAY_REGISTERS.length;
144
+ /** The copier and the stub's move of it: two pages. */
145
+ const COPIER_LIMIT = 0x200;
100
146
  const COPY_ENTRY = 7;
101
- /** X indexes the table, so the table fits in 256 bytes. */
102
- export const MAX_COPIES = Math.floor((256 - 2) / COPY_ENTRY);
147
+ /** X indexes the table, so the table fits in 256 bytes, and the copier with the VIC-II code fits in two pages. */
148
+ export const MAX_COPIES = Math.min(Math.floor((256 - 2) / COPY_ENTRY), Math.floor((COPIER_LIMIT - COPIER_CODE_SIZE - VIC_SIZE - 2) / COPY_ENTRY));
103
149
  /** A loader that loads into $d000 or higher would write the I/O area. */
104
150
  const LOAD_LIMIT = 0xcfff;
105
151
  const COPIER_PAGES = [...Array.from({ length: 15 }, (_, index) => 0xc000 + index * 0x100), ...Array.from({ length: 0x9b }, (_, index) => 0x9e00 - index * 0x100)];
@@ -147,6 +193,161 @@ export function simulate(memory: Int16Array, copies: Copy[], expected: (address:
147
193
  return undefined;
148
194
  }
149
195
 
196
+ /** The VIC-II and its bank from the io part of an entry state, when it has them. */
197
+ function readVic(io: Record<string, any> | undefined, fail: (what: string) => never): { vic: NonNullable<EntryState["vic"]> } | Record<string, never> {
198
+ if (io?.vic === undefined) return {};
199
+ const vic = io.vic.bytes;
200
+ const cia2 = io.cia2?.bytes;
201
+ if (typeof vic !== "string" || !/^[0-9a-f]{94}$/i.test(vic)) return fail("io.vic.bytes is not 47 bytes of hex");
202
+ if (typeof cia2 !== "string" || !/^[0-9a-f]{2}/i.test(cia2)) return fail("io.cia2.bytes is missing");
203
+ return { vic: { registers: Buffer.from(vic, "hex"), bank: Number.parseInt(cia2.slice(0, 2), 16) & 0x03 } };
204
+ }
205
+
206
+ function readCia(value: Record<string, any>, fail: (what: string) => never): { cia: [EntryCia, EntryCia] } | Record<string, never> {
207
+ if (value.cia === undefined) return {};
208
+ const chip = (name: "cia1" | "cia2"): EntryCia => {
209
+ const bytes = value.io?.[name]?.bytes;
210
+ if (typeof bytes !== "string" || !/^[0-9a-f]{32}$/i.test(bytes)) return fail(`io.${name}.bytes is not 16 bytes of hex`);
211
+ const hidden = value.cia[name];
212
+ const number = (item: unknown, what: string, max: number) => (Number.isInteger(item) && (item as number) >= 0 && (item as number) <= max ? (item as number) : fail(`cia.${name}.${what} is not a number from 0 to ${max}`));
213
+ return { registers: Buffer.from(bytes, "hex"), latchA: number(hidden?.latchA, "latchA", 0xffff), latchB: number(hidden?.latchB, "latchB", 0xffff), interruptMask: number(hidden?.interruptMask, "interruptMask", 0x1f) };
214
+ };
215
+ return { cia: [chip("cia1"), chip("cia2")] };
216
+ }
217
+
218
+ /** One CIA timer that the loader sets, and what the check of the loader expects of it at the jump. */
219
+ export interface CiaTimerPlan {
220
+ cia: 1 | 2;
221
+ timer: "A" | "B";
222
+ /** The control register as a read gives it: without bit 4, the force load. */
223
+ control: number;
224
+ latch: number;
225
+ /** The counter at the jump, 3 cycles before the entry; undefined when the loader cannot set it. */
226
+ counterAtJump?: number;
227
+ }
228
+
229
+ export interface CiaPlan {
230
+ timers: CiaTimerPlan[];
231
+ masks: [number, number];
232
+ /** What the loader cannot set as it was at the entry. */
233
+ limits: string[];
234
+ }
235
+
236
+ /**
237
+ * The CIA timers as the KERNAL leaves them when RUN starts the loader, on a
238
+ * PAL C64: only CIA 1 timer A runs, for the 60 Hz interrupt. A timer that the
239
+ * entry has in this state is not written. The loader check reads every timer.
240
+ */
241
+ const KERNAL_TIMERS: ReadonlyArray<{ control: number; latch: number; counter?: number }> = [
242
+ { control: 0x01, latch: 0x4025 },
243
+ { control: 0x08, latch: 0xffff, counter: 0xffff },
244
+ { control: 0x08, latch: 0xffff, counter: 0xffff },
245
+ { control: 0x08, latch: 0xffff, counter: 0xffff },
246
+ ];
247
+ const KERNAL_MASKS: readonly [number, number] = [0x01, 0x00];
248
+ /** Cycles from the end of the CIA part to the entry: $00, $01, the stack pointer, the flags, A, X, Y, PLP and JMP. */
249
+ const TAIL_CYCLES = 33;
250
+ /** Cycles after the write that starts a CIA timer in which it does not count yet, measured in VICE. */
251
+ const CIA_START_DELAY = 1;
252
+ /** Cycles of one raster line of a PAL C64. */
253
+ const LINE_CYCLES = 63;
254
+ const PAL_LINES = 312;
255
+
256
+ interface Op {
257
+ text: string;
258
+ bytes: number;
259
+ cycles: number;
260
+ }
261
+
262
+ /**
263
+ * The copier's CIA part (D63), after the raster wait: the timers and
264
+ * interrupt masks of the entry state. A running timer is stopped, loaded
265
+ * with its counter at the entry plus the cycles to the entry, started, and
266
+ * given its latch, so it counts to the entry's value just at the entry.
267
+ * Then the interrupt flags that the copy left are read away.
268
+ */
269
+ export function ciaSection(state: EntryState): { ops: Op[]; plan: CiaPlan } | undefined {
270
+ if (state.cia === undefined) return undefined;
271
+ const lda = (value: number): Op => ({ text: `lda #${hex2(value)}`, bytes: 2, cycles: 2 });
272
+ const sta = (address: number): Op => ({ text: `sta ${hex4(address)}`, bytes: 3, cycles: 4 });
273
+ const limits: string[] = [];
274
+ const timers: CiaTimerPlan[] = [];
275
+ const disable: Op[] = [];
276
+ const loads: Array<Op | ((cycles: number[]) => Op)> = [];
277
+ const starts: Op[] = [];
278
+ const latches: Op[] = [];
279
+ const acks: Op[] = [];
280
+ const masks: Op[] = [];
281
+ /** For each running timer: its index in starts, and how its load value follows from the cycles after its start. */
282
+ const running: Array<{ start: number; counter: number; latch: number; system: boolean; name: string; plan: CiaTimerPlan }> = [];
283
+ state.cia.forEach((chip, index) => {
284
+ const id = (index + 1) as 1 | 2;
285
+ const base = index === 0 ? 0xdc00 : 0xdd00;
286
+ let touched = false;
287
+ for (const timer of ["A", "B"] as const) {
288
+ const at = timer === "A" ? 0x04 : 0x06;
289
+ const controlAt = base + (timer === "A" ? 0x0e : 0x0f);
290
+ const control = chip.registers[timer === "A" ? 0x0e : 0x0f]! & 0xef;
291
+ const counter = chip.registers[at]! | (chip.registers[at + 1]! << 8);
292
+ const latch = timer === "A" ? chip.latchA : chip.latchB;
293
+ const name = `CIA ${id} timer ${timer}`;
294
+ const plan: CiaTimerPlan = { cia: id, timer, control, latch, counterAtJump: counter };
295
+ timers.push(plan);
296
+ const kernal = KERNAL_TIMERS[index * 2 + (timer === "A" ? 0 : 1)]!;
297
+ const runs = (control & 0x01) !== 0;
298
+ if (!runs && control === kernal.control && latch === kernal.latch && counter === kernal.counter) continue;
299
+ touched = true;
300
+ // Stopped, a write of the high latch byte also loads the counter.
301
+ loads.push(lda(control & 0xee), sta(controlAt));
302
+ if (runs) {
303
+ // Timer A counts system cycles without bit 5, timer B without bits 5 and 6; other inputs do not count in the loader.
304
+ const system = timer === "A" ? (control & 0x20) === 0 : (control & 0x60) === 0;
305
+ const slot = running.length;
306
+ running.push({ start: starts.length, counter, latch, system, name, plan });
307
+ loads.push((values) => lda(values[slot]! & 0xff), sta(base + at), (values) => lda(values[slot]! >> 8), sta(base + at + 1));
308
+ starts.push(lda(control), sta(controlAt));
309
+ latches.push(lda(latch & 0xff), sta(base + at), lda(latch >> 8), sta(base + at + 1));
310
+ if (system) plan.counterAtJump = counter + 3;
311
+ continue;
312
+ }
313
+ loads.push(lda(counter & 0xff), sta(base + at), lda(counter >> 8), sta(base + at + 1));
314
+ if (latch === counter) continue;
315
+ if (latch >> 8 === counter >> 8) {
316
+ loads.push(lda(latch & 0xff), sta(base + at));
317
+ continue;
318
+ }
319
+ // The high byte loads the counter of a stopped timer too.
320
+ loads.push(lda(latch & 0xff), sta(base + at), lda(latch >> 8), sta(base + at + 1));
321
+ plan.counterAtJump = latch;
322
+ limits.push(`${name} is stopped with the counter $${counter.toString(16).padStart(4, "0")} and the latch $${latch.toString(16).padStart(4, "0")}: a stopped CIA timer loads its latch, so the counter is the latch.`);
323
+ }
324
+ const mask = chip.interruptMask;
325
+ if (mask !== KERNAL_MASKS[index]) {
326
+ disable.push(lda(0x7f), sta(base + 0x0d));
327
+ if (mask !== 0) masks.push(lda(0x80 | mask), sta(base + 0x0d));
328
+ touched = true;
329
+ }
330
+ if (touched) acks.push({ text: `lda ${hex4(base + 0x0d)}`, bytes: 3, cycles: 4 });
331
+ const flags = chip.registers[0x0d]! & 0x1f;
332
+ if (flags !== 0) limits.push(`CIA ${id} has the interrupt flags $${flags.toString(16).padStart(2, "0")} at the entry: only an event sets a flag, so the loader cannot.`);
333
+ });
334
+ if (loads.length === 0 && disable.length === 0) return { ops: [], plan: { timers, masks: [state.cia[0].interruptMask, state.cia[1].interruptMask], limits } };
335
+ // The cycles from each start write to the entry: what follows it here, and the tail.
336
+ const after = [...starts, ...latches, ...acks, ...masks];
337
+ const values = running.map((item) => {
338
+ const followers = after.slice(item.start + 2).reduce((sum, op) => sum + op.cycles, 0);
339
+ if (!item.system) return item.counter;
340
+ const load = item.counter + followers + TAIL_CYCLES - CIA_START_DELAY;
341
+ if (load <= 0xffff) return load;
342
+ limits.push(`${item.name} counts from $${item.counter.toString(16).padStart(4, "0")} at the entry: the loader cannot start it so near $ffff without an underflow, so the counter differs.`);
343
+ delete item.plan.counterAtJump;
344
+ return item.counter;
345
+ });
346
+ const pre: Op[] = state.vic === undefined ? [lda(0x35), { text: "sta $01", bytes: 2, cycles: 3 }] : [];
347
+ const ops = [...pre, ...disable, ...loads.map((op) => (typeof op === "function" ? op(values) : op)), ...after];
348
+ return { ops, plan: { timers, masks: [state.cia[0].interruptMask, state.cia[1].interruptMask], limits } };
349
+ }
350
+
150
351
  export interface LoaderInput {
151
352
  /** The segments with the files that hold them, in address order. */
152
353
  segments: Array<Range & { files: string[] }>;
@@ -179,9 +380,13 @@ export function planLoader(input: LoaderInput): LoaderPlan {
179
380
  }
180
381
  at += block.range.end - block.range.start + 1;
181
382
  }
182
- if (plain.length > MAX_COPIES) throw new WireFailure("invalid-input", `The loader needs ${plain.length} copies; it can do at most ${MAX_COPIES}. Join segments in the unpacker capture (--range) or write the loader by hand.`);
383
+ const cia = ciaSection(state);
384
+ const ciaSize = cia === undefined ? 0 : cia.ops.reduce((sum, op) => sum + op.bytes, 0);
385
+ // The CIA part takes room from the copy table in the copier's two pages.
386
+ const maxCopies = Math.min(MAX_COPIES, Math.floor((COPIER_LIMIT - COPIER_CODE_SIZE - (state.vic === undefined ? 0 : VIC_SIZE) - ciaSize - 2) / COPY_ENTRY));
387
+ if (plain.length > maxCopies) throw new WireFailure("invalid-input", `The loader needs ${plain.length} copies; it can do at most ${maxCopies}. Join segments in the unpacker capture (--range) or write the loader by hand.`);
183
388
  const copierImage = at;
184
- const copierSize = COPIER_CODE_SIZE + COPY_ENTRY * plain.length + 2;
389
+ const copierSize = COPIER_CODE_SIZE + (state.vic === undefined ? 0 : VIC_SIZE) + ciaSize + COPY_ENTRY * plain.length + 2;
185
390
  const end = copierImage + copierSize - 1;
186
391
  if (end > LOAD_LIMIT) throw new WireFailure("invalid-input", `The loader would end at ${hex4(end)}, after ${hex4(LOAD_LIMIT)}: loading it would write the I/O area. The image is too large for one loader file.`);
187
392
 
@@ -286,7 +491,11 @@ export function planLoader(input: LoaderInput): LoaderPlan {
286
491
  " adc #7",
287
492
  " tax",
288
493
  " jmp .next",
289
- `.done lda #${hex2(port0)}`,
494
+ ".done",
495
+ // The CIA part runs after the raster wait: the wait ends its whole lines earlier, so the entry comes in its line.
496
+ ...vicLines(state, Math.floor((cia?.ops.reduce((sum, op) => sum + op.cycles, 0) ?? 0) / LINE_CYCLES)),
497
+ ...(cia === undefined ? [] : cia.ops.map((op) => ` ${op.text}`)),
498
+ ` lda #${hex2(port0)}`,
290
499
  " sta $00",
291
500
  ` lda #${hex2(port1)}`,
292
501
  " sta $01",
@@ -303,6 +512,12 @@ export function planLoader(input: LoaderInput): LoaderPlan {
303
512
  ".count !word 0",
304
513
  ".index !byte 0",
305
514
  ".back !byte 0",
515
+ ...(state.vic === undefined
516
+ ? []
517
+ : [
518
+ `.vicreg !byte ${VIC_DISPLAY_REGISTERS.map((item) => hex2(item.register)).join(",")}`,
519
+ `.vicval !byte ${VIC_DISPLAY_REGISTERS.map((item) => hex2(state.vic!.registers[item.register]!)).join(",")}`,
520
+ ]),
306
521
  ".table",
307
522
  );
308
523
  for (const copy of copies) {
@@ -320,5 +535,37 @@ export function planLoader(input: LoaderInput): LoaderPlan {
320
535
  stackByte,
321
536
  end,
322
537
  text: `${lines.join("\n")}\n`,
538
+ ...(cia === undefined ? {} : { cia: cia.plan }),
323
539
  };
324
540
  }
541
+
542
+ /**
543
+ * The copier's VIC-II part (D60): with the I/O area visible it sets the
544
+ * display registers and the VIC bank of the entry state, then waits for the
545
+ * entry's raster line, so the program starts in the same phase of the frame.
546
+ */
547
+ function vicLines(state: EntryState, earlier: number): string[] {
548
+ if (state.vic === undefined) return [];
549
+ const line = (rasterLine(state.vic.registers) - earlier + PAL_LINES) % PAL_LINES;
550
+ return [
551
+ " lda #$35",
552
+ " sta $01",
553
+ ` ldx #${hex2(VIC_DISPLAY_REGISTERS.length - 1)}`,
554
+ ".vic ldy .vicreg,x",
555
+ " lda .vicval,x",
556
+ " sta $d000,y",
557
+ " dex",
558
+ " bpl .vic",
559
+ " lda $dd00",
560
+ " and #$fc",
561
+ ` ora #${hex2(state.vic.bank)}`,
562
+ " sta $dd00",
563
+ `.raster lda $d012`,
564
+ ` cmp #${hex2(line & 0xff)}`,
565
+ " bne .raster",
566
+ " lda $d011",
567
+ " and #$80",
568
+ ` cmp #${hex2((line >> 1) & 0x80)}`,
569
+ " bne .raster",
570
+ ];
571
+ }
@@ -7,7 +7,8 @@
7
7
 
8
8
  import { COVERAGE, formatC64Address } from "#src/c64.ts";
9
9
  import { WireFailure } from "#src/host-client/tools.ts";
10
- import type { CommentPlacement, RegionType } from "#src/knowledge/schema.ts";
10
+ import { ANALYZERS } from "#src/knowledge/import.ts";
11
+ import { isGeneratedFile, type CommentPlacement, type Origin, type RegionType } from "#src/knowledge/schema.ts";
11
12
  import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, type Mode } from "./opcodes.ts";
12
13
 
13
14
  export interface Range {
@@ -23,7 +24,8 @@ export interface SourceInput {
23
24
  entry?: number;
24
25
  /** Where Ghidra decoded an instruction, with its length. */
25
26
  instructions: ReadonlyMap<number, number>;
26
- symbols: ReadonlyArray<{ address: number; name: string }>;
27
+ /** Without an origin, a symbol counts as a user or LLM name. */
28
+ symbols: ReadonlyArray<{ address: number; name: string; origin?: Origin }>;
27
29
  regions: ReadonlyArray<{ start: number; end: number; type: RegionType; element?: number }>;
28
30
  comments: ReadonlyArray<{ address: number; placement: CommentPlacement; text: string }>;
29
31
  modules: ReadonlyArray<{ start: number; file: string; title?: string }>;
@@ -31,13 +33,10 @@ export interface SourceInput {
31
33
  referenceTargets: readonly number[];
32
34
  /** Joined coverage files (D40): an instruction that no run executed is marked (D49). */
33
35
  coverage?: Uint8Array;
34
- /** Platform names (D52): the name of an address that knowledge does not name. */
35
- platform?: ReadonlyMap<number, string>;
36
+ /** Platform names (D52): the name of an address that knowledge does not name, and the last address of its entry. */
37
+ platform?: ReadonlyArray<{ address: number; end: number; name: string }>;
36
38
  }
37
39
 
38
- /** Files that the generator writes itself; a module cannot have these names. */
39
- export const GENERATED_FILES = ["main.asm", "loader.asm", "constants.asm"] as const;
40
-
41
40
  const CODE_TYPES: readonly RegionType[] = ["code", "jumps", "dual-use"];
42
41
  const BYTES_PER_LINE = 8;
43
42
  const WORDS_PER_LINE = 4;
@@ -106,6 +105,24 @@ function operandValue(address: number, opcode: number, byteAt: (address: number)
106
105
  return low;
107
106
  }
108
107
 
108
+ /**
109
+ * Each entry point inside a data region of the knowledge, with that region.
110
+ * A data region wins over an entry (D44, D48): Ghidra does not decode there,
111
+ * and the source has data there. A code region in the knowledge changes that (F-27).
112
+ */
113
+ export function entriesInData<R extends { start: number; end: number; type: RegionType }>(entries: readonly number[], regions: readonly R[]): Array<{ entry: number; region: R }> {
114
+ return entries.flatMap((entry) => {
115
+ const region = regions.find((row) => entry >= row.start && entry <= row.end && !CODE_TYPES.includes(row.type));
116
+ return region === undefined ? [] : [{ entry, region }];
117
+ });
118
+ }
119
+
120
+ /** Why an image without its entry fails: the image does not hold the program (R2-02). Undefined when the entry is in a segment, or the image has no entry. */
121
+ export function entryOutsideImage(entry: number | undefined, segments: readonly Range[]): string | undefined {
122
+ if (entry === undefined || segments.some((segment) => entry >= segment.start && entry <= segment.end)) return undefined;
123
+ return `The entry ${hex4(entry)} is in no segment of the image: the image does not hold the program. Capture it again with c64-unpacker loads or capture.`;
124
+ }
125
+
109
126
  /** Splits the segments into pieces at module starts and lays out instructions and data. */
110
127
  export function layout(input: SourceInput): Layout {
111
128
  const inImage = (address: number) => input.segments.some((segment) => address >= segment.start && address <= segment.end);
@@ -116,7 +133,7 @@ export function layout(input: SourceInput): Layout {
116
133
  };
117
134
 
118
135
  for (const module of input.modules) {
119
- if ((GENERATED_FILES as readonly string[]).includes(module.file)) throw new WireFailure("invalid-input", `The module at ${hex4(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
136
+ if (isGeneratedFile(module.file)) throw new WireFailure("invalid-input", `The module at ${hex4(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
120
137
  }
121
138
  const modules = [...input.modules].sort((a, b) => a.start - b.start);
122
139
  const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
@@ -181,13 +198,23 @@ export function layout(input: SourceInput): Layout {
181
198
  const wanted = new Set<number>();
182
199
  const want = (address: number) => inImage(address) && wanted.add(address);
183
200
  const outside = new Set<number>();
184
- const symbols = new Map(input.symbols.map((symbol) => [symbol.address, symbol.name]));
185
201
  // A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
186
202
  const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
187
- const platform = new Map([...(input.platform ?? [])].filter(([, name]) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())));
203
+ const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())).map(({ address, name }) => [address, name]));
204
+ // An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins (D61).
205
+ const isAnalyzer = (origin: Origin | undefined) => (ANALYZERS as readonly (Origin | undefined)[]).includes(origin);
206
+ const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
207
+ /** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry (F-20). */
208
+ const insideEntry = new Map<number, number>();
188
209
  const use = (address: number) => {
189
210
  if (inImage(address)) wanted.add(address);
190
211
  else if (symbols.has(address) || platform.has(address)) outside.add(address);
212
+ else {
213
+ const entry = input.platform?.find((row) => address > row.address && address <= row.end && !inImage(row.address) && (symbols.has(row.address) || platform.has(row.address)));
214
+ if (entry === undefined) return;
215
+ outside.add(entry.address);
216
+ insideEntry.set(address, entry.address);
217
+ }
191
218
  };
192
219
  for (const piece of pieces) {
193
220
  for (const item of piece.items) {
@@ -226,6 +253,8 @@ export function layout(input: SourceInput): Layout {
226
253
  names.set(address, name);
227
254
  generatedLabels++;
228
255
  }
256
+ // The high byte of a vector is NMINV+1. A zero-page entry keeps zero-page instructions: its constant has two digits.
257
+ for (const [address, start] of insideEntry) names.set(address, `${names.get(start)!}+${address - start}`);
229
258
  return {
230
259
  pieces,
231
260
  names,
@@ -490,6 +519,8 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
490
519
  const out: string[] = [`; ${piece.title}`, `; ${hex4(piece.start)}-${hex4(piece.end)}`, ""];
491
520
  /** True while the instructions before have not executed: they form one range. */
492
521
  let running = false;
522
+ /** The line of the mark of the open range of instructions that did not execute. */
523
+ let markLine = 0;
493
524
  const head = (address: number) => {
494
525
  for (const text of lineComments.get(address) ?? []) out.push(`; ${text}`.trimEnd());
495
526
  const label = layout.names.get(address);
@@ -503,8 +534,12 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
503
534
  if (running) notExecuted.ranges.at(-1)!.end = item.address + item.length - 1;
504
535
  else {
505
536
  notExecuted.ranges.push({ start: item.address, end: item.address + item.length - 1 });
506
- out.push("; not executed");
537
+ markLine = out.length;
538
+ out.push("");
507
539
  }
540
+ // The mark gives the range, so a reader finds it in the full list (D49).
541
+ const range = notExecuted.ranges.at(-1)!;
542
+ out[markLine] = `; not executed ${hex4(range.start)}-${hex4(range.end)}`;
508
543
  notExecuted.instructions++;
509
544
  }
510
545
  running = !executed;
@@ -31,7 +31,7 @@ node <skill>/scripts/analyze.ts extracted/game.prg --entry '$080d' --decompile '
31
31
  | `--listing analysis/game.lst` | DXA only: write the disassembly listing to this project file. |
32
32
  | `--decompile '$0818'` | Ghidra only: decompile the routine at this address. Use it again for more routines, at most 32. |
33
33
  | `--flat64k` | The file is 64 KiB of memory from `$0000`, not a PRG. |
34
- | `--max-listed 20` | List at most this number of labels or functions. The default is 50. Use 0 to list none. |
34
+ | `--max-listed 20` | List at most this number of labels, functions or candidates. The default is 50. Use 0 to list none. |
35
35
  | `--instructions analysis/instructions.json` | Ghidra only: write every instruction to this project file as JSON. Each has `address`, `bytes`, `mnemonic`, `mode` and `operand` in 6502 syntax. A branch, jump or call also has its `target`. With `--coverage`, `executed` tells if it ran. |
36
36
  | `--space drive8` | The knowledge space of the image: `drive8` for code from the 1541 drive, or `bank0` to `bank255` for a cartridge bank. The default is `c64`. The seeds come from that space, and the findings go into it. |
37
37
  | `--coverage coverage/attract.bin` | A coverage file of 64 KiB from `c64_memmap` (action `read` with `out`). Use it again for more runs. The executed code becomes entry points. |
@@ -63,9 +63,9 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
63
63
  - `changes` counts the symbols, regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`. `suppressed` counts the findings that the analysis did not write, because a removal blocks that place.
64
64
  - DXA: `regions` lists the code and data regions. `labels` gives the number of names that DXA found (`count`) and lists the first names (`listed`).
65
65
  - Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
66
- - If a list is not complete, `rest` gives the c64-knowledge command that lists all of them.
66
+ - If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
67
67
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
68
- - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding and opcodes that the program wrote and then executed. The script does not write those. Look at them and decide.
68
+ - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
69
69
  - With `--coverage`, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
70
70
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
71
71
 
@@ -108,7 +108,7 @@ The exit status is 0 for a result, also with conflicts. The exit status is 1 for
108
108
  - `invalid-input` with a message about an entry point: no entry point is inside the program for Ghidra. Give one with `--entry`.
109
109
  - `stale-revision`: the knowledge changed during the analysis. The script recorded nothing. Run the analysis again.
110
110
  - `operation-failed` with a message about an incomplete result or listing: the analyzer did not give a complete result. The script recorded nothing. Tell the user.
111
- - `installation-incomplete`, or 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.
111
+ - `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.
112
112
  - `notDecompiled`: Ghidra did not decompile these routines. Each entry has the reason. If the reason says that the decompiler did not start, tell the user the remedy in the reason. On macOS, Ghidra has no native decompiler until the user builds it: `./gradlew buildNatives` in the `support/gradle` directory of Ghidra.
113
113
 
114
114
  ## Handoffs