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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +19 -59
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +5 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  5. package/skills/c64-assembler/scripts/compare.ts +84 -0
  6. package/skills/c64-basic/SKILL.md +1 -2
  7. package/skills/c64-disk/SKILL.md +20 -6
  8. package/skills/c64-disk/scripts/containers.ts +124 -0
  9. package/skills/c64-disk/scripts/disk.ts +59 -5
  10. package/skills/c64-emulator/SKILL.md +11 -4
  11. package/skills/c64-knowledge/SKILL.md +53 -3
  12. package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
  13. package/skills/c64-memory-map/SKILL.md +4 -1
  14. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  15. package/skills/c64-provenance/SKILL.md +37 -9
  16. package/skills/c64-provenance/scripts/compare.ts +60 -29
  17. package/skills/c64-provenance/scripts/provenance.ts +248 -19
  18. package/skills/c64-provenance/scripts/runtime.ts +179 -0
  19. package/skills/c64-provenance/scripts/separate.ts +132 -0
  20. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  21. package/skills/c64-source/SKILL.md +119 -0
  22. package/skills/c64-source/scripts/generate.ts +402 -0
  23. package/skills/c64-source/scripts/loader.ts +320 -0
  24. package/skills/c64-source/scripts/opcodes.ts +61 -0
  25. package/skills/c64-source/scripts/source.ts +558 -0
  26. package/skills/c64-static-analysis/SKILL.md +27 -4
  27. package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
  28. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  29. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  30. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  31. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  32. package/skills/c64-testing/SKILL.md +27 -3
  33. package/skills/c64-testing/references/scenario-format.md +43 -3
  34. package/skills/c64-testing/scripts/run.ts +189 -0
  35. package/skills/c64-testing/scripts/scenario.ts +153 -135
  36. package/skills/c64-testing/scripts/test.ts +16 -6
  37. package/skills/c64-unpacker/SKILL.md +65 -14
  38. package/skills/c64-unpacker/scripts/evidence.ts +86 -21
  39. package/skills/c64-unpacker/scripts/loads.ts +174 -0
  40. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  41. package/skills/c64-unpacker/scripts/unpack.ts +302 -48
  42. package/src/c64.ts +91 -0
  43. package/src/cli/status.ts +3 -1
  44. package/src/host/main.ts +10 -25
  45. package/src/host/server.ts +8 -40
  46. package/src/host/vice/adapter.ts +25 -10
  47. package/src/host/vice/session.ts +64 -20
  48. package/src/host-client/autostart.ts +45 -0
  49. package/src/host-client/connect.ts +4 -5
  50. package/src/host-client/coverage.ts +59 -0
  51. package/src/host-client/steps.ts +170 -0
  52. package/src/host-client/transfer.ts +29 -3
  53. package/src/host-client/vice-session.ts +42 -13
  54. package/src/knowledge/history.ts +40 -13
  55. package/src/knowledge/import.ts +72 -56
  56. package/src/knowledge/read.ts +203 -52
  57. package/src/knowledge/schema.ts +94 -12
  58. package/src/knowledge/write.ts +432 -123
  59. package/src/mcp/main.ts +1 -4
  60. package/src/mcp/output.ts +32 -0
  61. package/src/mcp/server.ts +4 -2
  62. package/src/mcp/tools/debug.ts +110 -24
  63. package/src/mcp/tools/input.ts +1 -0
  64. package/src/mcp/tools/media.ts +22 -6
  65. package/src/mcp/tools/memory.ts +57 -10
  66. package/src/mcp/tools/video.ts +9 -3
  67. package/src/native/discover.ts +5 -2
  68. package/src/native/ghidra/index.ts +19 -3
  69. package/src/native/ghidra/scripts/C64Export.java +33 -1
  70. package/src/native/status.ts +15 -3
  71. package/src/native/types.ts +30 -1
  72. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  73. package/src/protocol/messages.ts +17 -10
  74. package/src/protocol/vice.ts +72 -18
  75. package/src/script.ts +3 -2
@@ -1,4 +1,3 @@
1
- import { createHash, timingSafeEqual } from "node:crypto";
2
1
  import { createServer, type Server, type Socket } from "node:net";
3
2
 
4
3
  import { encodeFrame, MessageReader } from "../protocol/framing.ts";
@@ -12,9 +11,10 @@ export interface ViceSessionHandle {
12
11
  /**
13
12
  * Runs one operation. The session serializes its own work. Throws
14
13
  * WireFailure for domain failures. `attachments` holds the bytes the
15
- * operation takes (see attachmentCount).
14
+ * operation takes (see attachmentCount); bytes the operation sends back go
15
+ * into `reply`.
16
16
  */
17
- handle<O extends ViceOperation>(op: O, params: ViceOperations[O]["params"], attachments?: Buffer[]): Promise<ViceOperations[O]["result"]>;
17
+ handle<O extends ViceOperation>(op: O, params: ViceOperations[O]["params"], attachments?: Buffer[], reply?: Buffer[]): Promise<ViceOperations[O]["result"]>;
18
18
  /** Stops the emulator and drops queued work. Safe to call more than once. */
19
19
  close(): Promise<void>;
20
20
  }
@@ -37,16 +37,8 @@ export interface HostServerOptions {
37
37
  createViceSession: ViceSessionFactory;
38
38
  /** Native-tool requests on "tool" connections; without it they are refused. */
39
39
  tools?: ToolDispatcher;
40
- /** Defaults to 127.0.0.1. More addresses listen on the same port, for example a container bridge. */
40
+ /** Defaults to 127.0.0.1; 0.0.0.0 listens on every address, so a container reaches it too. */
41
41
  host?: string;
42
- /** Extra addresses to listen on, on the same port as `host`. */
43
- extraHosts?: string[];
44
- /**
45
- * The shared secret. A connection that arrives on an address that is
46
- * not loopback must send it in its hello; without a token such a
47
- * connection is refused.
48
- */
49
- token?: string;
50
42
  /** 0 picks a free port. */
51
43
  port: number;
52
44
  /** How long a new connection may stay silent before it must send hello. */
@@ -67,10 +59,6 @@ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
67
59
  /** How long a refused client may keep its side of the connection open after the host has ended its side. */
68
60
  const REFUSED_CLOSE_MS = 1_000;
69
61
 
70
- function digest(text: string): Buffer {
71
- return createHash("sha256").update(text, "utf8").digest();
72
- }
73
-
74
62
  /** A caught value for the log, with its stack when it has one. */
75
63
  function describe(error: unknown): string {
76
64
  return error instanceof Error ? (error.stack ?? error.message) : String(error);
@@ -95,18 +83,6 @@ class Connection {
95
83
  #isTool = false;
96
84
  readonly #reader = new MessageReader();
97
85
 
98
- /** A connection on a loopback address needs no token; any other needs the host's token. */
99
- #checkToken(token: string | undefined): WireError | undefined {
100
- if (isLoopback(this.#socket.localAddress ?? "")) return undefined;
101
- const expected = this.#options.token;
102
- // Equal-length digests: the comparison takes the same time for any token, also one with multibyte characters.
103
- const matches = expected !== undefined && token !== undefined && timingSafeEqual(digest(token), digest(expected));
104
- if (matches) return undefined;
105
- return {
106
- code: "installation-incomplete",
107
- message: "The c64-re-tools host runtime refused this client: a connection from outside the host needs C64RT_HOST_TOKEN with the same value on both sides.",
108
- };
109
- }
110
86
  readonly #socket: Socket;
111
87
  readonly #options: HostServerOptions;
112
88
  readonly #log: (line: string) => void;
@@ -191,7 +167,7 @@ class Connection {
191
167
  if (this.#state === "handshake") {
192
168
  if (message.type !== "hello") throw new ProtocolError("first message is not hello");
193
169
  clearTimeout(this.#handshakeTimer);
194
- const mismatch = checkHello(message) ?? this.#checkToken(message.token);
170
+ const mismatch = checkHello(message);
195
171
  if (mismatch !== undefined) {
196
172
  this.#log(`refusing a client: ${mismatch.message}`);
197
173
  trace().event("connection.refused", { connection: this.#id, role: message.role, ...mismatch });
@@ -270,10 +246,10 @@ class Connection {
270
246
  const session = this.#session;
271
247
  if (session === undefined || !isOneOf(VICE_OPERATIONS, op)) throw new WireFailure("invalid-input", `unknown operation: ${op}`);
272
248
  const params = validateViceParams(op, request.params);
273
- if (attachments.length !== attachmentCount(op)) {
274
- throw new WireFailure("invalid-input", `${op} takes ${attachmentCount(op)} file attachment(s)`);
249
+ if (attachments.length !== attachmentCount(op, params)) {
250
+ throw new WireFailure("invalid-input", `${op} takes ${attachmentCount(op, params)} file attachment(s)`);
275
251
  }
276
- result = await session.handle(op, params, attachments);
252
+ result = await session.handle(op, params, attachments, replyAttachments);
277
253
  }
278
254
  this.#send({ type: "reply", id: request.id, result }, replyAttachments);
279
255
  record({ ok: true, result });
@@ -308,12 +284,6 @@ class Connection {
308
284
  }
309
285
  }
310
286
 
311
- /** True for 127.0.0.0/8 and ::1, also in their IPv4-mapped IPv6 form. */
312
- export function isLoopback(address: string): boolean {
313
- const plain = address.startsWith("::ffff:") ? address.slice(7) : address;
314
- return plain === "::1" || /^127\.\d+\.\d+\.\d+$/.test(plain);
315
- }
316
-
317
287
  /** A port as the user gives it: decimal digits only, 0 to 65535. Anything else gives undefined. */
318
288
  export function parsePort(text: string): number | undefined {
319
289
  if (!/^\d{1,5}$/.test(text)) return undefined;
@@ -386,8 +356,6 @@ export async function startHostServer(options: HostServerOptions): Promise<HostS
386
356
  let port: number;
387
357
  try {
388
358
  port = await listen(host, options.port);
389
- // The extra addresses share the port, so one endpoint setting reaches every address.
390
- for (const extra of options.extraHosts ?? []) await listen(extra, port);
391
359
  } catch (error) {
392
360
  await Promise.all(servers.map((server) => new Promise<void>((resolve) => server.close(() => resolve()))));
393
361
  throw error;
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
5
5
  import { type Comparison, type Condition, type BacktraceFrame, type HistoryEntry, type Instruction, type MemmapRange, type ProfileEntry, type JoystickState, type MemoryView, type RegisterValues, type Registers, type Space } from "../../protocol/vice.ts";
6
- import { formatC64Address, formatC64Byte } from "../../c64.ts";
6
+ import { COVERAGE, formatC64Address, formatC64Byte } from "../../c64.ts";
7
7
  import { decodeDisplay, decodePalette, visibleFrame, type IndexedFrame } from "./screen.ts";
8
8
  import type { TextMonitor } from "./text-monitor.ts";
9
9
  import {
@@ -258,12 +258,27 @@ function unknownForm(what: string, lines: string[]): WireFailure {
258
258
  return new WireFailure("operation-failed", `VICE printed the ${what} in a form that c64-re-tools does not read: ${JSON.stringify(lines.slice(0, 3))}`);
259
259
  }
260
260
 
261
+ /** The coverage byte (D40) of one memmap row's I/O, ROM and RAM columns, each "rwx" with "-" for no access. */
262
+ export function memmapFlags(io: string, rom: string, ram: string): number {
263
+ let flags = 0;
264
+ if (ram[0] === "r") flags |= COVERAGE.ramRead;
265
+ if (ram[1] === "w") flags |= COVERAGE.ramWrite;
266
+ if (ram[2] === "x") flags |= COVERAGE.ramExecute;
267
+ if (rom[0] === "r") flags |= COVERAGE.romRead;
268
+ if (rom[2] === "x") flags |= COVERAGE.romExecute;
269
+ if (io[0] === "r") flags |= COVERAGE.ioRead;
270
+ if (io[1] === "w") flags |= COVERAGE.ioWrite;
271
+ if (io[2] === "x") flags |= COVERAGE.ioExecute;
272
+ return flags;
273
+ }
274
+
261
275
  /**
262
276
  * Parses memmap rows such as "00c2: --- --- rw- (uninitialized read)": the I/O,
263
277
  * ROM and RAM access flags of one address. Adjacent addresses with the same
264
- * combined flags merge into one range; at most `maxRanges` come back.
278
+ * flags merge into one range; at most `maxRanges` come back. When more ranges
279
+ * follow, `next` is the first touched address that is not reported.
265
280
  */
266
- export function parseMemmap(answer: string, maxRanges: number): MemmapRange[] {
281
+ export function parseMemmap(answer: string, maxRanges: number): { ranges: MemmapRange[]; next?: number } {
267
282
  const ranges: MemmapRange[] = [];
268
283
  const unknown: string[] = [];
269
284
  for (const line of answer.split("\n")) {
@@ -273,18 +288,18 @@ export function parseMemmap(answer: string, maxRanges: number): MemmapRange[] {
273
288
  continue;
274
289
  }
275
290
  const address = Number.parseInt(match[1]!, 16);
276
- const flags = match[2]! + match[3]! + match[4]!;
277
- const access = { execute: flags.includes("x"), read: flags.includes("r"), write: flags.includes("w") };
291
+ const flags = memmapFlags(match[2]!, match[3]!, match[4]!);
292
+ if (flags === 0) continue;
278
293
  const last = ranges.at(-1);
279
- if (last !== undefined && last.end === address - 1 && last.execute === access.execute && last.read === access.read && last.write === access.write) {
294
+ if (last !== undefined && last.end === address - 1 && last.flags === flags) {
280
295
  last.end = address;
281
296
  continue;
282
297
  }
283
- if (ranges.length >= maxRanges) break;
284
- ranges.push({ start: address, end: address, ...access });
298
+ if (ranges.length >= maxRanges) return { ranges, next: address };
299
+ ranges.push({ start: address, end: address, flags });
285
300
  }
286
301
  if (ranges.length === 0 && unknown.length > 0) throw unknownForm("memory map", unknown);
287
- return ranges;
302
+ return { ranges };
288
303
  }
289
304
 
290
305
  /**
@@ -739,7 +754,7 @@ export class ViceAdapter {
739
754
  }
740
755
 
741
756
  /** How the C64 CPU accessed [start, end] since the memmap was last cleared, as merged ranges. */
742
- async memmap(start: number, end: number, maxRanges: number): Promise<MemmapRange[]> {
757
+ async memmap(start: number, end: number, maxRanges: number): Promise<{ ranges: MemmapRange[]; next?: number }> {
743
758
  // The first argument is the access mask ("ioRWXrwx"); ff shows every kind of access.
744
759
  return parseMemmap(await this.#computerText(`mmsh ff ${hex4(start)} ${hex4(end)}`, 60_000), maxRanges);
745
760
  }
@@ -4,8 +4,8 @@
4
4
  // and back; breakpoints, watchpoints and the rest go along.
5
5
 
6
6
  import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
7
- import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
8
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
+ import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
8
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { setTimeout as sleep } from "node:timers/promises";
@@ -283,9 +283,10 @@ export class ViceSession implements ViceSessionHandle {
283
283
  op: O,
284
284
  params: ViceOperations[O]["params"],
285
285
  attachments: Buffer[] = [],
286
+ reply: Buffer[] = [],
286
287
  ): Promise<ViceOperations[O]["result"]> {
287
288
  // A window request for the mode the session is in sends nothing to VICE, so a held VICE does not stop it.
288
- return this.#enqueue(() => this.#run(op, params, attachments), { whileHeld: op === "window" }) as Promise<ViceOperations[O]["result"]>;
289
+ return this.#enqueue(() => this.#run(op, params, attachments, reply), { whileHeld: op === "window" }) as Promise<ViceOperations[O]["result"]>;
289
290
  }
290
291
 
291
292
  async close(): Promise<void> {
@@ -303,10 +304,15 @@ export class ViceSession implements ViceSessionHandle {
303
304
  return this.#adapter;
304
305
  }
305
306
 
306
- async #run(op: ViceOperation, params: unknown, attachments: Buffer[]): Promise<unknown> {
307
+ async #run(op: ViceOperation, params: unknown, attachments: Buffer[], reply: Buffer[]): Promise<unknown> {
307
308
  switch (op) {
308
309
  case "status":
309
310
  return this.#status();
311
+ case "ioRead": {
312
+ const read = params as ViceOperations["ioRead"]["params"];
313
+ const bytes = await this.#observe(() => this.#machine.readIo(read.address, read.size));
314
+ return { address: read.address, data: Buffer.from(bytes).toString("hex") };
315
+ }
310
316
  case "memoryRead": {
311
317
  const read = params as ViceOperations["memoryRead"]["params"];
312
318
  const data = await this.#observe(() => this.#machine.readMemory(read));
@@ -394,6 +400,8 @@ export class ViceSession implements ViceSessionHandle {
394
400
  }
395
401
  case "snapshot":
396
402
  return this.#snapshot(params as ViceOperations["snapshot"]["params"]);
403
+ case "snapshotFile":
404
+ return this.#snapshotFile(params as ViceOperations["snapshotFile"]["params"], attachments, reply);
397
405
  case "cpuHistory":
398
406
  return this.#cpuHistory(params as ViceOperations["cpuHistory"]["params"]);
399
407
  case "backtrace": {
@@ -422,7 +430,7 @@ export class ViceSession implements ViceSessionHandle {
422
430
  await this.#observe(() => this.#machine.clearMemmap());
423
431
  return { cleared: true };
424
432
  }
425
- return { ranges: await this.#observe(() => this.#machine.memmap(request.start, request.end, request.maxRanges)) };
433
+ return await this.#observe(() => this.#machine.memmap(request.start, request.end, request.maxRanges));
426
434
  }
427
435
  case "vicii":
428
436
  return this.#observe(() => this.#vicii());
@@ -716,34 +724,44 @@ export class ViceSession implements ViceSessionHandle {
716
724
  async #changePoint<T extends { checkpoint: number; enabled: boolean }>(
717
725
  registry: Map<number, T>,
718
726
  kind: string,
719
- params: { action: "remove" | "enable" | "disable"; id: number },
727
+ params: { action: "enable" | "disable"; id: number },
720
728
  ): Promise<T> {
721
- const point = registry.get(params.id);
722
- if (point === undefined) throw new WireFailure("not-found", `There is no ${kind} with id ${params.id}.`);
723
- if (params.action === "remove") {
724
- await this.#observe(() => this.#machine.deleteCheckpoint(point.checkpoint));
725
- registry.delete(params.id);
726
- } else {
727
- const enabled = params.action === "enable";
728
- await this.#observe(() => this.#machine.toggleCheckpoint(point.checkpoint, enabled));
729
- point.enabled = enabled;
730
- }
729
+ const point = this.#point(registry, kind, params.id);
730
+ const enabled = params.action === "enable";
731
+ await this.#observe(() => this.#machine.toggleCheckpoint(point.checkpoint, enabled));
732
+ point.enabled = enabled;
731
733
  return point;
732
734
  }
733
735
 
734
- async #breakpoint(params: BreakpointParams): Promise<Breakpoint | { breakpoints: Breakpoint[] }> {
736
+ async #removePoint(registry: Map<number, { checkpoint: number }>, kind: string, id: number): Promise<PointRemoved> {
737
+ const point = this.#point(registry, kind, id);
738
+ await this.#observe(() => this.#machine.deleteCheckpoint(point.checkpoint));
739
+ registry.delete(id);
740
+ return { removed: true, id };
741
+ }
742
+
743
+ #point<T>(registry: Map<number, T>, kind: string, id: number): T {
744
+ const point = registry.get(id);
745
+ if (point === undefined) throw new WireFailure("not-found", `There is no ${kind} with id ${id}.`);
746
+ return point;
747
+ }
748
+
749
+ async #breakpoint(params: BreakpointParams): Promise<Breakpoint | PointRemoved | { breakpoints: Breakpoint[] }> {
735
750
  const publicView = ({ checkpoint: _checkpoint, expression: _expression, ...point }: Breakpoint & { checkpoint: number; expression?: string }): Breakpoint =>
736
751
  point;
737
752
  if (params.action === "list") return { breakpoints: [...this.#breakpoints.values()].map(publicView) };
753
+ if (params.action === "remove") return this.#removePoint(this.#breakpoints, "breakpoint", params.id);
738
754
  if (params.action !== "add") return publicView(await this.#changePoint(this.#breakpoints, "breakpoint", params));
755
+ // A breakpoint over a range stops at the first instruction executed in it: exec, not a watch (D43).
739
756
  const added = await this.#addPoint({
740
- ...pointRange({ address: params.address }),
757
+ ...(params.size === undefined ? pointRange({ address: params.address }) : { start: params.address, end: params.address + params.size - 1, operation: 0x04 }),
741
758
  space: params.space,
742
759
  ...(params.condition === undefined ? {} : { condition: params.condition }),
743
760
  });
744
761
  const point = {
745
762
  id: this.#nextPointId++,
746
763
  address: params.address,
764
+ ...(params.size === undefined ? {} : { size: params.size }),
747
765
  space: params.space,
748
766
  enabled: true,
749
767
  ...(params.condition === undefined ? {} : { condition: params.condition }),
@@ -753,10 +771,11 @@ export class ViceSession implements ViceSessionHandle {
753
771
  return publicView(point);
754
772
  }
755
773
 
756
- async #watchpoint(params: WatchpointParams): Promise<Watchpoint | { watchpoints: Watchpoint[] }> {
774
+ async #watchpoint(params: WatchpointParams): Promise<Watchpoint | PointRemoved | { watchpoints: Watchpoint[] }> {
757
775
  const publicView = ({ checkpoint: _checkpoint, expression: _expression, ...point }: Watchpoint & { checkpoint: number; expression?: string }): Watchpoint =>
758
776
  point;
759
777
  if (params.action === "list") return { watchpoints: [...this.#watchpoints.values()].map(publicView) };
778
+ if (params.action === "remove") return this.#removePoint(this.#watchpoints, "watchpoint", params.id);
760
779
  if (params.action !== "add") return publicView(await this.#changePoint(this.#watchpoints, "watchpoint", params));
761
780
  const added = await this.#addPoint({
762
781
  ...pointRange({ address: params.address, size: params.size, access: params.access }),
@@ -910,15 +929,40 @@ export class ViceSession implements ViceSessionHandle {
910
929
  this.#snapshots.delete(params.name);
911
930
  return { discarded: true };
912
931
  }
932
+ return this.#restoreFrom(existing);
933
+ }
934
+
935
+ /** Restores the machine from a .vsf in the session directory; the CPU stays stopped. */
936
+ async #restoreFrom(file: string): Promise<{ restored: true; state: "stopped" }> {
913
937
  await this.#stop();
914
938
  const elapsed = (await this.#machine.clock()) - this.#timingStart;
915
- await this.#machine.restoreSnapshot(existing);
939
+ await this.#machine.restoreSnapshot(file);
916
940
  this.#state = "stopped";
917
941
  // The cycle clock jumped with the restore; the stopwatch keeps its reading, as a window move does.
918
942
  this.#timingStart = (await this.#machine.clock()) - elapsed;
919
943
  return { restored: true, state: "stopped" };
920
944
  }
921
945
 
946
+ /** A snapshot as bytes (D41): export a named one into `reply`, or restore from the attached .vsf. */
947
+ async #snapshotFile(params: ViceOperations["snapshotFile"]["params"], attachments: Buffer[], reply: Buffer[]): Promise<ViceOperations["snapshotFile"]["result"]> {
948
+ if (params.action === "export") {
949
+ const file = this.#snapshots.get(params.name);
950
+ if (file === undefined) throw new WireFailure("not-found", `There is no snapshot named ${params.name}.`);
951
+ const bytes = readFileSync(file);
952
+ reply.push(bytes);
953
+ return { exported: true, bytes: bytes.length };
954
+ }
955
+ const file = this.#stage(onlyAttachment(attachments), "vsf");
956
+ try {
957
+ return await this.#restoreFrom(file);
958
+ } catch (error) {
959
+ if (error instanceof WireFailure) throw error;
960
+ throw new WireFailure("invalid-input", "The emulator did not restore this snapshot file. A snapshot file restores only in the emulator version and with the machine settings that saved it.");
961
+ } finally {
962
+ rmSync(file, { force: true });
963
+ }
964
+ }
965
+
922
966
  /**
923
967
  * Moves the machine into a VICE of the other mode: a snapshot of this
924
968
  * one, restored in a new one, with the user points, joysticks, warp and the
@@ -0,0 +1,45 @@
1
+ // Autostart without the autostart's own RUN: the caller decides when the
2
+ // program starts, so keys it types later never mix with VICE's typing.
3
+
4
+ import { textToPetscii } from "../c64.ts";
5
+ import type { ViceSessionClient } from "./vice-session.ts";
6
+
7
+ /** How often, and for how long, a load is checked. */
8
+ export const LOAD_POLL_FRAMES = 25;
9
+ export const LOAD_LIMIT_FRAMES = 1500;
10
+
11
+ /**
12
+ * Autostarts the PRG at `path` with run false and advances frames until its
13
+ * first bytes are at its load address. Returns false when they do not arrive
14
+ * within LOAD_LIMIT_FRAMES.
15
+ */
16
+ export async function autostartLoaded(client: ViceSessionClient, path: string, prg: Uint8Array): Promise<boolean> {
17
+ const load = prg[0]! | (prg[1]! << 8);
18
+ const head = Buffer.from(prg.subarray(2, Math.min(prg.length, 2 + 64))).toString("hex");
19
+ await client.autostart({ path, index: 0, run: false });
20
+ for (let frames = 0; frames < LOAD_LIMIT_FRAMES; frames += LOAD_POLL_FRAMES) {
21
+ await client.execution({ action: "advance-frames", count: LOAD_POLL_FRAMES, space: "c64" });
22
+ if ((await client.memoryRead({ address: load, size: head.length / 2, space: "c64", view: "ram" })).data === head) return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ /** CURLIN high byte: $ff while BASIC waits in direct mode, a line number's high byte while a program runs. */
28
+ const CURLIN_HIGH = 0x003a;
29
+ export const RUN_LIMIT_FRAMES = 300;
30
+
31
+ /**
32
+ * Autostart that returns only when the program runs: load without VICE's
33
+ * own RUN, type RUN, and wait until BASIC runs a program line. A key that a
34
+ * later step types then reaches the program, never the READY prompt. Returns
35
+ * why it did not start, or undefined when it runs.
36
+ */
37
+ export async function runProgram(client: ViceSessionClient, path: string, prg: Uint8Array): Promise<"not-loaded" | "not-started" | undefined> {
38
+ if (!(await autostartLoaded(client, path, prg))) return "not-loaded";
39
+ await client.keyboard(textToPetscii("RUN\n"));
40
+ const started = await client.runUntil({
41
+ target: { kind: "memory", address: CURLIN_HIGH, operator: "ne", value: 0xff, space: "c64", view: "ram" },
42
+ timeoutFrames: RUN_LIMIT_FRAMES,
43
+ });
44
+ return started.reached ? undefined : "not-started";
45
+ }
@@ -4,7 +4,7 @@ import { connect as connectTcp, type Socket } from "node:net";
4
4
 
5
5
  import { encodeFrame, MessageReader } from "../protocol/framing.ts";
6
6
  import { elapsedMs, trace } from "../trace.ts";
7
- import { DEFAULT_HOST_PORT, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, HOST_PROTOCOL_ID, HOST_PROTOCOL_VERSION, parseHostMessage, ProtocolError, WireFailure, type ErrorCode, type Hello, type HostMessage, type Ping, type Request, type Role, type VideoStandard } from "../protocol/messages.ts";
7
+ import { DEFAULT_HOST_PORT, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, HOST_PROTOCOL_ID, HOST_PROTOCOL_VERSION, parseHostMessage, ProtocolError, WireFailure, withStopAndAsk, type ErrorCode, type Hello, type HostMessage, type Ping, type Request, type Role, type VideoStandard } from "../protocol/messages.ts";
8
8
 
9
9
  export interface Endpoint {
10
10
  host: string;
@@ -40,7 +40,9 @@ function unreachable(role: Role): WireFailure {
40
40
  const code: ErrorCode = role === "vice-session" ? "machine-unavailable" : "operation-failed";
41
41
  return new WireFailure(
42
42
  code,
43
- "The c64-re-tools host runtime is not running or cannot be reached. Start it on the host (npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host) and try again.",
43
+ withStopAndAsk(
44
+ "The c64-re-tools host runtime is not running or cannot be reached. Start it on the host (npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host) and try again.",
45
+ ),
44
46
  );
45
47
  }
46
48
 
@@ -150,9 +152,6 @@ export class HostConnection {
150
152
  const connection = new HostConnection(socket);
151
153
  const hello: Hello = { type: "hello", protocol: HOST_PROTOCOL_ID, version: HOST_PROTOCOL_VERSION, role: options.role };
152
154
  if (options.videoStandard !== undefined) hello.videoStandard = options.videoStandard;
153
- // A host that listens beyond loopback checks this shared secret.
154
- const token = (options.env ?? process.env).C64RT_HOST_TOKEN;
155
- if (token !== undefined && token !== "") hello.token = token;
156
155
 
157
156
  const stop = () => void connection.close();
158
157
  signal?.addEventListener("abort", stop, { once: true });
@@ -0,0 +1,59 @@
1
+ // Reads the emulator's memory access map as coverage bytes (D40): one byte
2
+ // per address, RAM, ROM and I/O apart. The MCP tool and the skill scripts share it.
3
+
4
+ import { COVERAGE, type CodeRange } from "../c64.ts";
5
+ import { MAX_MEMMAP_RANGES, type MemmapRange } from "../protocol/vice.ts";
6
+ import { readProjectFile } from "./transfer.ts";
7
+ import type { ViceSessionClient } from "./vice-session.ts";
8
+
9
+ /** A full coverage file: offset n is address n (D40). */
10
+ export const FULL_COVERAGE = 0x10000;
11
+
12
+ /**
13
+ * Reads full coverage files from the project and joins them: an address has
14
+ * every flag that any run gave it. `fail` refuses a file of another size.
15
+ */
16
+ export function readCoverageFiles(paths: string[], fail: (message: string) => never): Uint8Array | undefined {
17
+ if (paths.length === 0) return undefined;
18
+ const joined = new Uint8Array(FULL_COVERAGE);
19
+ for (const path of paths) {
20
+ const { bytes } = readProjectFile(path);
21
+ if (bytes.length !== FULL_COVERAGE) {
22
+ fail(`--coverage ${path} has ${bytes.length} bytes; a full coverage file has 65536, one for each address. Write it with c64_memmap action read and out, without start and end.`);
23
+ }
24
+ for (let address = 0; address < FULL_COVERAGE; address++) joined[address]! |= bytes[address]!;
25
+ }
26
+ return joined;
27
+ }
28
+
29
+ /** The coverage bytes of start..end, read from the session in parts. */
30
+ export async function readCoverage(session: Pick<ViceSessionClient, "memmap">, start = 0x0000, end = 0xffff): Promise<Uint8Array> {
31
+ const map = new Uint8Array(end - start + 1);
32
+ for (let from: number | undefined = start; from !== undefined; ) {
33
+ const part: { ranges: MemmapRange[]; next?: number } = await session.memmap({ action: "read", start: from, end, maxRanges: MAX_MEMMAP_RANGES });
34
+ for (const range of part.ranges) map.fill(range.flags, range.start - start, range.end - start + 1);
35
+ from = part.next;
36
+ }
37
+ return map;
38
+ }
39
+
40
+ /** Runs of RAM that a program wrote, at least `minimum` bytes each, in a map from $0000. */
41
+ export function writtenBlocks(map: Uint8Array, minimum: number): CodeRange[] {
42
+ const blocks: CodeRange[] = [];
43
+ let start = -1;
44
+ for (let address = 0; address <= map.length; address++) {
45
+ const written = address < map.length && (map[address]! & COVERAGE.ramWrite) !== 0;
46
+ if (written && start < 0) start = address;
47
+ if (!written && start >= 0) {
48
+ if (address - start >= minimum) blocks.push({ start, end: address - 1 });
49
+ start = -1;
50
+ }
51
+ }
52
+ return blocks;
53
+ }
54
+
55
+ /** True when the CPU executed an instruction in RAM somewhere in the range. */
56
+ export function executedIn(map: Uint8Array, range: CodeRange): boolean {
57
+ for (let address = range.start; address <= range.end; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0) return true;
58
+ return false;
59
+ }