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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -3,14 +3,17 @@
3
3
  // session starts headless and can move the machine into a VICE with a window
4
4
  // and back; breakpoints, watchpoints and the rest go along.
5
5
 
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";
6
+ import { isOneOf, WireFailure, type VideoStandard } from "../../protocol/messages.ts";
7
+ import { DISK_IMAGE_TYPES, type DiskFile, type DiskImageType } from "../../protocol/tools.ts";
8
+ 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 TraceHit, type TraceKind, type TraceResult, type PointStopReason, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
9
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
10
  import { tmpdir } from "node:os";
10
11
  import { join } from "node:path";
11
12
  import { setTimeout as sleep } from "node:timers/promises";
12
13
 
13
- import { formatC64Address } from "../../c64.ts";
14
+ import { formatC64Address, instructionLength } from "../../c64.ts";
15
+ import { encodePng, type IndexedFrame } from "../../graphics.ts";
16
+ import { instructionText, operandAddress } from "../../opcodes.ts";
14
17
  import type { ProcessSupervisor } from "../../native/processes.ts";
15
18
  import { trace } from "../../trace.ts";
16
19
  import type { ViceSessionFactory, ViceSessionHandle } from "../server.ts";
@@ -26,7 +29,7 @@ import {
26
29
  } from "./adapter.ts";
27
30
  import { decodeProgramCounter, MonitorConnectionError, MonitorError, MonitorTimeoutError, ResponseType, type MonitorResponse } from "./binary-monitor.ts";
28
31
  import { decodeCia, decodeSid, decodeSprite, decodeVicii } from "./chips.ts";
29
- import { compareFrames, differenceImage, encodePng, type IndexedFrame } from "./screen.ts";
32
+ import { compareFrames, differenceImage } from "./screen.ts";
30
33
  import { launchVice, startErrorLines, type ViceMode, type ViceProcess } from "./process.ts";
31
34
  import { TextMonitorError, TextMonitorTimeoutError } from "./text-monitor.ts";
32
35
 
@@ -74,6 +77,8 @@ const WINDOW_CLOSED =
74
77
  "Breakpoints and watchpoints are kept. Check the machine state before you continue.";
75
78
 
76
79
  /** Emulator state that a move to another VICE cannot carry. */
80
+ /** The first bytes of every VICE snapshot file. */
81
+ const VSF_MAGIC = Buffer.from("VICE Snapshot File\x1a", "latin1");
77
82
  const NOT_CARRIED = ["cpu history", "memory map", "profile", "keyboard input not yet typed"];
78
83
 
79
84
  /** Starts another VICE for this session; `log` also gets that launch's lines. */
@@ -119,6 +124,26 @@ export interface SessionOptions {
119
124
  launch?: ViceLauncher;
120
125
  /** Makes the host remove a path if it dies; returns the release. */
121
126
  ownPath?: (path: string) => () => void;
127
+ /** The known flaws of this VICE version (D56); keyboard and joystick results name them. */
128
+ flaws?: readonly string[];
129
+ /** Lists the entries of a disk image, so an autostart index is checked first (R2-08). Without it, VICE gets the index unchecked. */
130
+ directory?: DiskDirectory;
131
+ }
132
+
133
+ export type DiskDirectory = (type: DiskImageType, image: Buffer) => Promise<DiskFile[]>;
134
+
135
+ /**
136
+ * Where VICE keeps a staged file: an autostarted disk stays in drive 8, a tape
137
+ * in the datasette and a cartridge in the port, until the same place gets
138
+ * another file. Only a program is read once.
139
+ */
140
+ type StagedKind = "program" | "disk" | "tape" | "cartridge";
141
+
142
+ function stagedKind(type: string): StagedKind {
143
+ if (type === "crt") return "cartridge";
144
+ if (type === "tap" || type === "t64") return "tape";
145
+ if (type === "prg" || type === "p00") return "program";
146
+ return "disk";
122
147
  }
123
148
 
124
149
  export class ViceSession implements ViceSessionHandle {
@@ -128,9 +153,12 @@ export class ViceSession implements ViceSessionHandle {
128
153
  #mode: ViceMode = "headless";
129
154
  readonly #videoStandard: VideoStandard;
130
155
  readonly #log: (line: string) => void;
156
+ /** The known VICE flaws that keyboard and joystick input meet, as one text, or undefined. */
157
+ readonly #inputFlaw: string | undefined;
131
158
  readonly #untilReturnLimitMs: number;
132
159
  readonly #stepLimitMs: number;
133
160
  readonly #launch: ViceLauncher | undefined;
161
+ readonly #directory: DiskDirectory | undefined;
134
162
  /** Session files that outlive one VICE: snapshots, staged media, handovers. */
135
163
  readonly #workDir: string;
136
164
  readonly #releaseWorkDir: () => void;
@@ -157,6 +185,8 @@ export class ViceSession implements ViceSessionHandle {
157
185
  #driveStop = false;
158
186
  /** Counts stops caused by checkpoints or a CPU JAM rather than by a monitor command. */
159
187
  #haltCount = 0;
188
+ /** What stopped the machine last, when a user breakpoint or watchpoint or a JAM did; cleared when it runs again. */
189
+ #lastStop: { stopReason: PointStopReason; pointId?: number } | undefined;
160
190
  #queue: Promise<unknown> = Promise.resolve();
161
191
  #closed = false;
162
192
  #lost = false;
@@ -173,8 +203,10 @@ export class ViceSession implements ViceSessionHandle {
173
203
  #timingStart = 0n;
174
204
  /** Counts staged media files, for unique names. */
175
205
  #staged = 0;
206
+ /** True after an autostart of a cartridge, until an autostart of another file detaches it. */
207
+ #cartridge = false;
176
208
  /** The last staged file of each kind that VICE took; VICE can still read it. */
177
- readonly #stagedFiles = new Map<"autostart" | "disk", string>();
209
+ readonly #stagedFiles = new Map<StagedKind, string>();
178
210
  /** Session-local ids: one counter for breakpoints and watchpoints, from 1. */
179
211
  #nextPointId = 1;
180
212
  /**
@@ -196,6 +228,8 @@ export class ViceSession implements ViceSessionHandle {
196
228
  this.#untilReturnLimitMs = options.untilReturnLimitMs ?? UNTIL_RETURN_LIMIT_MS;
197
229
  this.#stepLimitMs = options.stepLimitMs ?? RUNNING_COMMAND_TIMEOUT_MS;
198
230
  this.#launch = options.launch;
231
+ this.#directory = options.directory;
232
+ this.#inputFlaw = options.flaws === undefined || options.flaws.length === 0 ? undefined : options.flaws.join(" ");
199
233
  this.#workDir = mkdtempSync(join(tmpdir(), "c64-re-tools-session-"));
200
234
  this.#releaseWorkDir = options.ownPath?.(this.#workDir) ?? (() => {});
201
235
  this.#watch(vice);
@@ -268,7 +302,7 @@ export class ViceSession implements ViceSessionHandle {
268
302
  session.#timingStart = await session.#adapter.clock();
269
303
  // c64_profile reports what ran since the session started.
270
304
  await session.#adapter.startProfiler();
271
- // The control-port lines start all pressed; release them before the C64 reads them.
305
+ // In VICE 3.10 and older the control-port lines start all pressed; release them before the C64 reads them.
272
306
  for (const joystick of session.#joysticks.values()) await session.#adapter.setJoystick(joystick);
273
307
  }),
274
308
  );
@@ -283,9 +317,10 @@ export class ViceSession implements ViceSessionHandle {
283
317
  op: O,
284
318
  params: ViceOperations[O]["params"],
285
319
  attachments: Buffer[] = [],
320
+ reply: Buffer[] = [],
286
321
  ): Promise<ViceOperations[O]["result"]> {
287
322
  // 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"]>;
323
+ return this.#enqueue(() => this.#run(op, params, attachments, reply), { whileHeld: op === "window" }) as Promise<ViceOperations[O]["result"]>;
289
324
  }
290
325
 
291
326
  async close(): Promise<void> {
@@ -303,10 +338,15 @@ export class ViceSession implements ViceSessionHandle {
303
338
  return this.#adapter;
304
339
  }
305
340
 
306
- async #run(op: ViceOperation, params: unknown, attachments: Buffer[]): Promise<unknown> {
341
+ async #run(op: ViceOperation, params: unknown, attachments: Buffer[], reply: Buffer[]): Promise<unknown> {
307
342
  switch (op) {
308
343
  case "status":
309
344
  return this.#status();
345
+ case "ioRead": {
346
+ const read = params as ViceOperations["ioRead"]["params"];
347
+ const bytes = await this.#observe(() => this.#machine.readIo(read.address, read.size));
348
+ return { address: read.address, data: Buffer.from(bytes).toString("hex") };
349
+ }
310
350
  case "memoryRead": {
311
351
  const read = params as ViceOperations["memoryRead"]["params"];
312
352
  const data = await this.#observe(() => this.#machine.readMemory(read));
@@ -345,10 +385,30 @@ export class ViceSession implements ViceSessionHandle {
345
385
  return this.#programLoad(params as ViceOperations["programLoad"]["params"], onlyAttachment(attachments));
346
386
  case "autostart": {
347
387
  const { type, index, run } = params as ViceOperations["autostart"]["params"];
348
- const file = this.#stage(onlyAttachment(attachments), type);
388
+ const bytes = onlyAttachment(attachments);
389
+ if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
390
+ const file = this.#stage(bytes, type);
391
+ // VICE keeps a cartridge through every reset and autostart, as a real C64 does.
392
+ if (type !== "crt" && this.#cartridge) {
393
+ await this.#observe(() => this.#machine.detachCartridge());
394
+ this.#cartridge = false;
395
+ this.#replaceStaged("cartridge", undefined);
396
+ }
397
+ // Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types (R3-05).
398
+ const centred: Array<1 | 2> = [];
399
+ for (const [port, joystick] of this.#joysticks) {
400
+ if (joystick.direction === "center" && !joystick.fire) continue;
401
+ const centre: JoystickState = { port, direction: "center", fire: false };
402
+ await this.#observe(() => this.#machine.setJoystick(centre));
403
+ this.#joysticks.set(port, centre);
404
+ centred.push(port);
405
+ }
349
406
  await this.#runningCommand(() => this.#machine.autostart(file, index, run));
350
- this.#replaceStaged("autostart", file);
351
- return { state: "running" };
407
+ if (type === "crt") this.#cartridge = true;
408
+ this.#replaceStaged(stagedKind(type), file);
409
+ // A reset sets the CPU clock back, so the stopwatch starts again.
410
+ this.#timingStart = 0n;
411
+ return { state: "running", ...(type === "tap" && this.#inputFlaw !== undefined ? { emulatorFlaw: this.#inputFlaw } : {}), ...(centred.length === 0 ? {} : { joysticksCentred: centred }) };
352
412
  }
353
413
  case "diskAttach": {
354
414
  const { type } = params as ViceOperations["diskAttach"]["params"];
@@ -362,7 +422,7 @@ export class ViceSession implements ViceSessionHandle {
362
422
  case "keyboard": {
363
423
  const bytes = Buffer.from((params as ViceOperations["keyboard"]["params"]).data, "hex");
364
424
  await this.#observe(() => this.#machine.feedKeyboard(bytes));
365
- return { queuedBytes: bytes.length };
425
+ return { queuedBytes: bytes.length, ...(this.#inputFlaw === undefined ? {} : { emulatorFlaw: this.#inputFlaw }) };
366
426
  }
367
427
  case "breakpoint":
368
428
  return this.#breakpoint(params as BreakpointParams);
@@ -394,8 +454,12 @@ export class ViceSession implements ViceSessionHandle {
394
454
  }
395
455
  case "snapshot":
396
456
  return this.#snapshot(params as ViceOperations["snapshot"]["params"]);
457
+ case "snapshotFile":
458
+ return this.#snapshotFile(params as ViceOperations["snapshotFile"]["params"], attachments, reply);
397
459
  case "cpuHistory":
398
460
  return this.#cpuHistory(params as ViceOperations["cpuHistory"]["params"]);
461
+ case "trace":
462
+ return this.#trace(params as ViceOperations["trace"]["params"]);
399
463
  case "backtrace": {
400
464
  const { depth, space } = params as ViceOperations["backtrace"]["params"];
401
465
  return this.#observe(async () => {
@@ -413,8 +477,12 @@ export class ViceSession implements ViceSessionHandle {
413
477
  return { cycles: (now - this.#timingStart).toString() };
414
478
  }
415
479
  case "profile": {
416
- const { limit } = params as ViceOperations["profile"]["params"];
417
- return { entries: await this.#observe(() => this.#machine.profile(limit)) };
480
+ const request = params as ViceOperations["profile"]["params"];
481
+ if (request.action === "clear") {
482
+ await this.#observe(() => this.#machine.startProfiler());
483
+ return { cleared: true };
484
+ }
485
+ return { entries: await this.#observe(() => this.#machine.profile(request.limit)) };
418
486
  }
419
487
  case "memmap": {
420
488
  const request = params as ViceOperations["memmap"]["params"];
@@ -422,7 +490,7 @@ export class ViceSession implements ViceSessionHandle {
422
490
  await this.#observe(() => this.#machine.clearMemmap());
423
491
  return { cleared: true };
424
492
  }
425
- return { ranges: await this.#observe(() => this.#machine.memmap(request.start, request.end, request.maxRanges)) };
493
+ return await this.#observe(() => this.#machine.memmap(request.start, request.end, request.maxRanges));
426
494
  }
427
495
  case "vicii":
428
496
  return this.#observe(() => this.#vicii());
@@ -442,7 +510,7 @@ export class ViceSession implements ViceSessionHandle {
442
510
  const joystick = params as JoystickState;
443
511
  await this.#observe(() => this.#machine.setJoystick(joystick));
444
512
  this.#joysticks.set(joystick.port, joystick);
445
- return joystick;
513
+ return { ...joystick, ...(this.#inputFlaw === undefined ? {} : { emulatorFlaw: this.#inputFlaw }) };
446
514
  }
447
515
  case "warp": {
448
516
  const { enabled } = params as ViceOperations["warp"]["params"];
@@ -458,8 +526,13 @@ export class ViceSession implements ViceSessionHandle {
458
526
  async #status(): Promise<MachineStatus> {
459
527
  const warp = await this.#observe(() => this.#machine.warp());
460
528
  // Read after #observe: it resumes a running machine, unless a breakpoint or a JAM stopped it meanwhile.
461
- const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window" };
462
- if (status.state === "stopped") status.pc = await this.#pc();
529
+ const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window", cartridge: this.#cartridge };
530
+ if (status.state === "stopped") {
531
+ status.pc = await this.#pc();
532
+ // VICE 3.9 sends no JAM event: the opcode at the pc tells.
533
+ const stop = this.#lastStop ?? ((await this.#jammedAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
534
+ if (stop !== undefined) Object.assign(status, stop);
535
+ }
463
536
  return status;
464
537
  }
465
538
 
@@ -485,8 +558,18 @@ export class ViceSession implements ViceSessionHandle {
485
558
  }
486
559
  if (params.action === "advance-frames") {
487
560
  if (params.count === undefined) throw new WireFailure("invalid-input", "advance-frames needs count, the number of frames");
488
- const outcome = await this.#runFrames(params.count);
489
- return { state: "stopped", pc: outcome.pc, advancedFrames: outcome.frames };
561
+ const raster = RASTER[this.#videoStandard];
562
+ if (params.anchor !== undefined && (params.anchor.line >= raster.lines || params.anchor.cycle >= raster.cycles)) {
563
+ throw new WireFailure("invalid-input", `The anchor is outside the frame: a ${this.#videoStandard} frame has lines 0-${raster.lines - 1} and cycles 0-${raster.cycles - 1}.`);
564
+ }
565
+ const outcome = await this.#runFrames(params.count, undefined, params.anchor);
566
+ return {
567
+ state: "stopped",
568
+ pc: outcome.pc,
569
+ advancedFrames: outcome.frames,
570
+ stopReason: outcome.end === "target" ? "frames" : outcome.end,
571
+ ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }),
572
+ };
490
573
  }
491
574
  await this.#stop();
492
575
  if (params.action === "until-return") {
@@ -516,6 +599,8 @@ export class ViceSession implements ViceSessionHandle {
516
599
 
517
600
  async #reset(params: { mode: "soft" | "hard"; run: boolean }): Promise<{ state: RunState; pc?: number }> {
518
601
  await this.#stop();
602
+ // A reset sets the CPU clock back, so the stopwatch starts again.
603
+ this.#timingStart = 0n;
519
604
  if (params.run) {
520
605
  await this.#runningCommand(() => this.#machine.reset(params.mode));
521
606
  return { state: "running" };
@@ -545,10 +630,11 @@ export class ViceSession implements ViceSessionHandle {
545
630
  * checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
546
631
  * The machine is stopped afterwards.
547
632
  */
548
- async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }): Promise<{ end: RunEnd; frames: number; pc: number }> {
633
+ async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8"; pointId?: number }> {
549
634
  await this.#stop();
550
635
  const lines = RASTER[this.#videoStandard].lines;
551
- const start = await this.#machine.rasterPosition();
636
+ // A frame ends where the raster passes the anchor; without one, where this run starts.
637
+ const start = anchor ?? (await this.#machine.rasterPosition());
552
638
  const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
553
639
  const ending = rasterWindowExpression(start.line, start.cycle, lines);
554
640
  const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
@@ -574,24 +660,139 @@ export class ViceSession implements ViceSessionHandle {
574
660
  }
575
661
  } else {
576
662
  frames++;
577
- if (frames >= limit) return { end: "frames", frames, pc: stop.pc };
663
+ if (frames >= limit) return { end: "frames", frames, pc: stop.pc, space: "c64" };
578
664
  await this.#machine.setCondition(clock, arming);
579
665
  }
580
666
  armingPhase = !armingPhase;
581
667
  continue;
582
668
  }
583
669
  if (end === "unknown") {
584
- if (await this.#jammedAt(stop.pc)) return { end: "jam", frames, pc: stop.pc };
670
+ if (await this.#jammedAt(stop.pc)) return { end: "jam", frames, pc: stop.pc, space: "c64" };
585
671
  throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this run.");
586
672
  }
587
- // A drive stop is completed before the result names the computer's pc.
588
- return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc };
673
+ // A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
674
+ const driveStop = this.#driveStop;
675
+ const pointId = end === "target" ? undefined : this.#userPoint(stop.hits)?.pointId;
676
+ return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64", ...(pointId === undefined ? {} : { pointId }) };
589
677
  }
590
678
  } finally {
591
679
  await this.#machine.deleteCheckpoint(clock);
592
680
  }
593
681
  }
594
682
 
683
+ /**
684
+ * A trace (D76): runs the machine for up to `frames` frames, counted as
685
+ * #runFrames counts them, with a stopping checkpoint for each point. At each
686
+ * hit the session records the instruction, its registers and the raster
687
+ * position, and runs on. It ends at the frame limit, at `maxHits` hits, or
688
+ * where a user point or a JAM stops the program. VICE also reports the hits
689
+ * of a checkpoint that does not stop, but without the PC, the registers or
690
+ * the raster position, so each point stops.
691
+ */
692
+ async #trace(params: ViceOperations["trace"]["params"]): Promise<TraceResult> {
693
+ await this.#stop();
694
+ const lines = RASTER[this.#videoStandard].lines;
695
+ const start = await this.#machine.rasterPosition();
696
+ const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
697
+ const ending = rasterWindowExpression(start.line, start.cycle, lines);
698
+ const operation: Record<TraceKind, number> = { exec: 0x04, read: 0x01, write: 0x02 };
699
+ const points: number[] = [];
700
+ const hits: TraceHit[] = [];
701
+ let frames = 0;
702
+ const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
703
+ try {
704
+ for (const point of params.points) points.push(await this.#machine.addCheckpoint({ start: point.start, end: point.end, operation: operation[point.kind], space: "c64" }));
705
+ await this.#machine.setCondition(clock, arming);
706
+ let armingPhase = true;
707
+ for (;;) {
708
+ const next = this.#nextStop(HALF_FRAME_LIMIT_MS);
709
+ await this.#resume();
710
+ const stop = await next;
711
+ if (stop === undefined) {
712
+ await this.#stop();
713
+ throw new WireFailure("operation-failed", "The emulator did not reach the next frame in time.");
714
+ }
715
+ const traced = points.flatMap((checkpoint, index) => (stop.hits.includes(checkpoint) ? [index] : []));
716
+ for (const index of traced) {
717
+ hits.push(await this.#traceHit(index, params.points[index]!.kind, frames));
718
+ if (hits.length >= params.maxHits) return { hits, frames, end: "hits", pc: stop.pc };
719
+ }
720
+ if (stop.hits.includes(clock)) {
721
+ if (armingPhase) await this.#machine.setCondition(clock, ending);
722
+ else {
723
+ frames++;
724
+ if (frames >= params.frames) return { hits, frames, end: "frames", pc: stop.pc };
725
+ await this.#machine.setCondition(clock, arming);
726
+ }
727
+ armingPhase = !armingPhase;
728
+ continue;
729
+ }
730
+ if (traced.length > 0) continue;
731
+ const point = this.#userPoint(stop.hits);
732
+ if (point !== undefined) return { hits, frames, end: point.stopReason, pc: stop.pc };
733
+ if (await this.#jammedAt(stop.pc)) return { hits, frames, end: "jam", pc: stop.pc };
734
+ throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this trace.");
735
+ }
736
+ } finally {
737
+ await this.#machine.deleteCheckpoint(clock);
738
+ for (const checkpoint of points) await this.#machine.deleteCheckpoint(checkpoint);
739
+ }
740
+ }
741
+
742
+ /**
743
+ * One hit of a trace point. An exec point stops before its instruction: the
744
+ * registers and the raster position are the ones of now. A read or write
745
+ * point stops after the instruction that made the access: the last entry of
746
+ * the CPU history is that instruction, with its registers and its clock.
747
+ */
748
+ async #traceHit(point: number, kind: TraceKind, frame: number): Promise<TraceHit> {
749
+ const position = await this.#machine.rasterPosition();
750
+ const byteAt = async (address: number) => Number.parseInt(await this.#machine.readMemory({ address, size: 1, space: "c64", view: "cpu" }), 16);
751
+ if (kind === "exec") {
752
+ const registers = await this.#machine.readRegisters("c64");
753
+ const bytes = [...Buffer.from(await this.#machine.readMemory({ address: registers.pc, size: 3, space: "c64", view: "cpu" }), "hex")];
754
+ const length = instructionLength(bytes[0]!);
755
+ return {
756
+ point,
757
+ pc: registers.pc,
758
+ bytes: Buffer.from(bytes.slice(0, length)).toString("hex"),
759
+ text: instructionText(bytes, registers.pc),
760
+ a: registers.a,
761
+ x: registers.x,
762
+ y: registers.y,
763
+ sp: registers.sp,
764
+ frame,
765
+ rasterLine: position.line,
766
+ rasterCycle: position.cycle,
767
+ };
768
+ }
769
+ const [entry] = await this.#machine.history(1, "c64");
770
+ if (entry === undefined) throw new WireFailure("operation-failed", "The emulator gave no CPU history at a trace hit.");
771
+ const now = await this.#machine.clock();
772
+ const { lines, cycles } = RASTER[this.#videoStandard];
773
+ const span = BigInt(lines * cycles);
774
+ const at = Number((((BigInt(position.line * cycles + position.cycle) - (now - entry.clock)) % span) + span) % span);
775
+ const bytes = [...Buffer.from(entry.bytes, "hex")];
776
+ // A zero-page pointer is read now: the instruction did not change it.
777
+ const pointer = new Map<number, number>();
778
+ for (const address of [bytes[1] ?? 0, ((bytes[1] ?? 0) + 1) & 0xff, ((bytes[1] ?? 0) + entry.x) & 0xff, ((bytes[1] ?? 0) + entry.x + 1) & 0xff]) pointer.set(address, await byteAt(address));
779
+ const address = operandAddress(bytes, entry.x, entry.y, (zp) => pointer.get(zp) ?? 0);
780
+ return {
781
+ point,
782
+ pc: entry.address,
783
+ bytes: entry.bytes,
784
+ text: instructionText(bytes, entry.address),
785
+ a: entry.a,
786
+ x: entry.x,
787
+ y: entry.y,
788
+ sp: entry.sp,
789
+ frame,
790
+ rasterLine: Math.floor(at / cycles),
791
+ rasterCycle: at % cycles,
792
+ ...(address === undefined ? {} : { address, value: await byteAt(address) }),
793
+ };
794
+ }
795
+
595
796
  /**
596
797
  * Completes a stop that a drive 8 checkpoint caused: the computer runs to
597
798
  * its next instruction boundary, where a computer checkpoint stops it, so
@@ -602,6 +803,8 @@ export class ViceSession implements ViceSessionHandle {
602
803
  async #completeDriveStop(): Promise<number | undefined> {
603
804
  if (!this.#driveStop) return undefined;
604
805
  this.#driveStop = false;
806
+ // The drive checkpoint stopped the machine; the stop at the boundary keeps that reason.
807
+ const reason = this.#lastStop;
605
808
  const boundary = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
606
809
  try {
607
810
  for (let attempt = 0; attempt < DRIVE_STOP_TRIES; attempt++) {
@@ -612,6 +815,7 @@ export class ViceSession implements ViceSessionHandle {
612
815
  // The drive can hit its checkpoint again before the computer gets to the next instruction.
613
816
  if (stop.hits.includes(boundary)) {
614
817
  this.#driveStop = false;
818
+ this.#lastStop = reason;
615
819
  trace().event("vice.drive-stop", { pc: stop.pc, resumes: attempt + 1 });
616
820
  return stop.pc;
617
821
  }
@@ -626,14 +830,20 @@ export class ViceSession implements ViceSessionHandle {
626
830
  /** Names what caused a stop during a frame-clocked run. */
627
831
  #stopCause(stop: Stop, clock: number, target: number | undefined): Exclude<RunEnd, "frames" | "jam"> | "clock" | "unknown" {
628
832
  if (target !== undefined && stop.hits.includes(target)) return "target";
629
- const isPoint = (registry: Map<number, { checkpoint: number }>) =>
630
- [...registry.values()].some((point) => stop.hits.includes(point.checkpoint));
631
- if (isPoint(this.#breakpoints)) return "breakpoint";
632
- if (isPoint(this.#watchpoints)) return "watchpoint";
833
+ const point = this.#userPoint(stop.hits);
834
+ if (point !== undefined) return point.stopReason;
633
835
  if (stop.hits.includes(clock)) return "clock";
634
836
  return "unknown";
635
837
  }
636
838
 
839
+ /** The user breakpoint, else the user watchpoint, among the hit VICE checkpoints. */
840
+ #userPoint(hits: number[]): { stopReason: "breakpoint" | "watchpoint"; pointId: number } | undefined {
841
+ for (const [stopReason, registry] of [["breakpoint", this.#breakpoints], ["watchpoint", this.#watchpoints]] as const) {
842
+ for (const [pointId, point] of registry) if (hits.includes(point.checkpoint)) return { stopReason, pointId };
843
+ }
844
+ return undefined;
845
+ }
846
+
637
847
  async #jammedAt(pc: number): Promise<boolean> {
638
848
  const opcode = await this.#machine.readMemory({ address: pc, size: 1, space: "c64", view: "cpu" });
639
849
  return JAM_OPCODES.has(Number.parseInt(opcode, 16));
@@ -677,9 +887,9 @@ export class ViceSession implements ViceSessionHandle {
677
887
  if (expression !== undefined) await this.#machine.setCondition(checkpoint, expression);
678
888
  const outcome = await this.#runFrames(params.timeoutFrames, { checkpoint, deferred });
679
889
  const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
680
- // A stop event carries the computer's PC; a drive target reports the drive's.
681
- const pc = target.kind !== "raster" && target.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
682
- return { reached: stopReason === "target", stopReason, state: "stopped", pc };
890
+ // A stop event carries the computer's PC; a stop in the drive reports the drive's (F-31).
891
+ const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
892
+ return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space, ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }) };
683
893
  } finally {
684
894
  await this.#machine.deleteCheckpoint(checkpoint);
685
895
  }
@@ -716,34 +926,44 @@ export class ViceSession implements ViceSessionHandle {
716
926
  async #changePoint<T extends { checkpoint: number; enabled: boolean }>(
717
927
  registry: Map<number, T>,
718
928
  kind: string,
719
- params: { action: "remove" | "enable" | "disable"; id: number },
929
+ params: { action: "enable" | "disable"; id: number },
720
930
  ): 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
- }
931
+ const point = this.#point(registry, kind, params.id);
932
+ const enabled = params.action === "enable";
933
+ await this.#observe(() => this.#machine.toggleCheckpoint(point.checkpoint, enabled));
934
+ point.enabled = enabled;
935
+ return point;
936
+ }
937
+
938
+ async #removePoint(registry: Map<number, { checkpoint: number }>, kind: string, id: number): Promise<PointRemoved> {
939
+ const point = this.#point(registry, kind, id);
940
+ await this.#observe(() => this.#machine.deleteCheckpoint(point.checkpoint));
941
+ registry.delete(id);
942
+ return { removed: true, id };
943
+ }
944
+
945
+ #point<T>(registry: Map<number, T>, kind: string, id: number): T {
946
+ const point = registry.get(id);
947
+ if (point === undefined) throw new WireFailure("not-found", `There is no ${kind} with id ${id}.`);
731
948
  return point;
732
949
  }
733
950
 
734
- async #breakpoint(params: BreakpointParams): Promise<Breakpoint | { breakpoints: Breakpoint[] }> {
951
+ async #breakpoint(params: BreakpointParams): Promise<Breakpoint | PointRemoved | { breakpoints: Breakpoint[] }> {
735
952
  const publicView = ({ checkpoint: _checkpoint, expression: _expression, ...point }: Breakpoint & { checkpoint: number; expression?: string }): Breakpoint =>
736
953
  point;
737
954
  if (params.action === "list") return { breakpoints: [...this.#breakpoints.values()].map(publicView) };
955
+ if (params.action === "remove") return this.#removePoint(this.#breakpoints, "breakpoint", params.id);
738
956
  if (params.action !== "add") return publicView(await this.#changePoint(this.#breakpoints, "breakpoint", params));
957
+ // A breakpoint over a range stops at the first instruction executed in it: exec, not a watch (D43).
739
958
  const added = await this.#addPoint({
740
- ...pointRange({ address: params.address }),
959
+ ...(params.size === undefined ? pointRange({ address: params.address }) : { start: params.address, end: params.address + params.size - 1, operation: 0x04 }),
741
960
  space: params.space,
742
961
  ...(params.condition === undefined ? {} : { condition: params.condition }),
743
962
  });
744
963
  const point = {
745
964
  id: this.#nextPointId++,
746
965
  address: params.address,
966
+ ...(params.size === undefined ? {} : { size: params.size }),
747
967
  space: params.space,
748
968
  enabled: true,
749
969
  ...(params.condition === undefined ? {} : { condition: params.condition }),
@@ -753,10 +973,11 @@ export class ViceSession implements ViceSessionHandle {
753
973
  return publicView(point);
754
974
  }
755
975
 
756
- async #watchpoint(params: WatchpointParams): Promise<Watchpoint | { watchpoints: Watchpoint[] }> {
976
+ async #watchpoint(params: WatchpointParams): Promise<Watchpoint | PointRemoved | { watchpoints: Watchpoint[] }> {
757
977
  const publicView = ({ checkpoint: _checkpoint, expression: _expression, ...point }: Watchpoint & { checkpoint: number; expression?: string }): Watchpoint =>
758
978
  point;
759
979
  if (params.action === "list") return { watchpoints: [...this.#watchpoints.values()].map(publicView) };
980
+ if (params.action === "remove") return this.#removePoint(this.#watchpoints, "watchpoint", params.id);
760
981
  if (params.action !== "add") return publicView(await this.#changePoint(this.#watchpoints, "watchpoint", params));
761
982
  const added = await this.#addPoint({
762
983
  ...pointRange({ address: params.address, size: params.size, access: params.access }),
@@ -790,14 +1011,21 @@ export class ViceSession implements ViceSessionHandle {
790
1011
  }
791
1012
 
792
1013
  /**
793
- * Records the staged file that VICE took for `kind`, and removes the file
794
- * it replaces: VICE does not read that one again. A file that VICE did not
795
- * take stays until the session closes, because a held VICE can still read it.
1014
+ * Records the staged file that VICE took for `kind`, or that it no longer
1015
+ * has, and removes the file it replaces: VICE does not read that one again.
1016
+ * A file that VICE did not take stays until the session closes, because a
1017
+ * held VICE can still read it. So does a file that Windows does not let go.
796
1018
  */
797
- #replaceStaged(kind: "autostart" | "disk", file: string): void {
1019
+ #replaceStaged(kind: StagedKind, file: string | undefined): void {
798
1020
  const previous = this.#stagedFiles.get(kind);
799
- this.#stagedFiles.set(kind, file);
800
- if (previous !== undefined) rmSync(previous, { force: true });
1021
+ if (file === undefined) this.#stagedFiles.delete(kind);
1022
+ else this.#stagedFiles.set(kind, file);
1023
+ if (previous === undefined || previous === file) return;
1024
+ try {
1025
+ rmSync(previous, { force: true });
1026
+ } catch {
1027
+ // The session directory goes when the session closes.
1028
+ }
801
1029
  }
802
1030
 
803
1031
  /** Loads a PRG without reset or start; finishes stopped. */
@@ -858,7 +1086,10 @@ export class ViceSession implements ViceSessionHandle {
858
1086
  async #cias(which: CiaSelection): Promise<CiaState[]> {
859
1087
  const ids: Array<1 | 2> = which === "both" ? [1, 2] : [Number(which) as 1 | 2];
860
1088
  const chips = [];
861
- for (const id of ids) chips.push(decodeCia(id, await this.#machine.readIo(id === 1 ? 0xdc00 : 0xdd00, 0x10)));
1089
+ for (const id of ids) {
1090
+ const base = id === 1 ? 0xdc00 : 0xdd00;
1091
+ chips.push(decodeCia(id, await this.#machine.readIo(base, 0x10), await this.#machine.ciaHidden(base)));
1092
+ }
862
1093
  return chips;
863
1094
  }
864
1095
 
@@ -910,15 +1141,43 @@ export class ViceSession implements ViceSessionHandle {
910
1141
  this.#snapshots.delete(params.name);
911
1142
  return { discarded: true };
912
1143
  }
1144
+ return this.#restoreFrom(existing);
1145
+ }
1146
+
1147
+ /** Restores the machine from a .vsf in the session directory; the CPU stays stopped. */
1148
+ async #restoreFrom(file: string): Promise<{ restored: true; state: "stopped" }> {
913
1149
  await this.#stop();
914
1150
  const elapsed = (await this.#machine.clock()) - this.#timingStart;
915
- await this.#machine.restoreSnapshot(existing);
1151
+ await this.#machine.restoreSnapshot(file);
916
1152
  this.#state = "stopped";
917
1153
  // The cycle clock jumped with the restore; the stopwatch keeps its reading, as a window move does.
918
1154
  this.#timingStart = (await this.#machine.clock()) - elapsed;
919
1155
  return { restored: true, state: "stopped" };
920
1156
  }
921
1157
 
1158
+ /** A snapshot as bytes (D41): export a named one into `reply`, or restore from the attached .vsf. */
1159
+ async #snapshotFile(params: ViceOperations["snapshotFile"]["params"], attachments: Buffer[], reply: Buffer[]): Promise<ViceOperations["snapshotFile"]["result"]> {
1160
+ if (params.action === "export") {
1161
+ const file = this.#snapshots.get(params.name);
1162
+ if (file === undefined) throw new WireFailure("not-found", `There is no snapshot named ${params.name}.`);
1163
+ const bytes = readFileSync(file);
1164
+ reply.push(bytes);
1165
+ return { exported: true, bytes: bytes.length };
1166
+ }
1167
+ const bytes = onlyAttachment(attachments);
1168
+ // Checked before the machine stops: VICE gives no reason for a file that is no snapshot (F-07).
1169
+ if (!bytes.subarray(0, VSF_MAGIC.length).equals(VSF_MAGIC)) throw new WireFailure("invalid-input", "The file is not a VICE snapshot file (.vsf). Give a file that c64_snapshot save wrote.");
1170
+ const file = this.#stage(bytes, "vsf");
1171
+ try {
1172
+ return await this.#restoreFrom(file);
1173
+ } catch (error) {
1174
+ if (error instanceof WireFailure) throw error;
1175
+ 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.");
1176
+ } finally {
1177
+ rmSync(file, { force: true });
1178
+ }
1179
+ }
1180
+
922
1181
  /**
923
1182
  * Moves the machine into a VICE of the other mode: a snapshot of this
924
1183
  * one, restored in a new one, with the user points, joysticks, warp and the
@@ -933,6 +1192,7 @@ export class ViceSession implements ViceSessionHandle {
933
1192
  if (this.#held !== undefined) throw new WireFailure("machine-unavailable", this.#heldWhy);
934
1193
  if (this.#launch === undefined) throw new WireFailure("operation-failed", "This session cannot start another emulator.");
935
1194
  const running = this.#state === "running";
1195
+ this.#log(mode === "window" ? "moving the machine to a VICE with a window" : "moving the machine back to a headless VICE");
936
1196
  await this.#stop();
937
1197
  const handover: Handover = {
938
1198
  file: join(this.#workDir, mode === "window" ? "window-open.vsf" : "window-close.vsf"),
@@ -1018,10 +1278,12 @@ export class ViceSession implements ViceSessionHandle {
1018
1278
  if (baseline === undefined) throw new WireFailure("not-found", `There is no baseline named ${params.baseline}.`);
1019
1279
  const current = await this.#observe(() => this.#machine.captureFrame(this.#videoStandard));
1020
1280
  const difference = compareFrames(baseline.frame, current.frame, params.mask);
1021
- const mismatchRatio = difference.comparedPixels === 0 ? 0 : difference.mismatchingPixels / difference.comparedPixels;
1281
+ if (difference.comparedPixels === 0) throw new WireFailure("invalid-input", "The mask covers the whole screen: nothing to compare.");
1282
+ const mismatchRatio = difference.mismatchingPixels / difference.comparedPixels;
1022
1283
  const result: ScreenComparison = {
1023
1284
  match: mismatchRatio <= params.maxMismatchRatio,
1024
1285
  mismatchingPixels: difference.mismatchingPixels,
1286
+ comparedPixels: difference.comparedPixels,
1025
1287
  mismatchRatio,
1026
1288
  };
1027
1289
  if (difference.bounds !== undefined) result.bounds = difference.bounds;
@@ -1076,6 +1338,7 @@ export class ViceSession implements ViceSessionHandle {
1076
1338
  */
1077
1339
  async #resumingCommand(send: () => Promise<void>): Promise<void> {
1078
1340
  this.#state = "running";
1341
+ this.#lastStop = undefined;
1079
1342
  try {
1080
1343
  await send();
1081
1344
  } catch (error) {
@@ -1251,10 +1514,12 @@ export class ViceSession implements ViceSessionHandle {
1251
1514
  const stop: Stop = { pc: decodeProgramCounter(event.body), hits: this.#checkpointHits };
1252
1515
  if (stop.hits.length > 0 || event.type === ResponseType.jam) this.#haltCount++;
1253
1516
  if (stop.hits.some((hit) => this.#adapter?.isDriveCheckpoint(hit))) this.#driveStop = true;
1517
+ this.#lastStop = event.type === ResponseType.jam ? { stopReason: "jam" } : this.#userPoint(stop.hits);
1254
1518
  this.#checkpointHits = [];
1255
1519
  this.#state = "stopped";
1256
1520
  this.#stopWaiters = this.#stopWaiters.filter((waiter) => !waiter(stop));
1257
1521
  } else if (event.type === ResponseType.resumed) {
1522
+ this.#lastStop = undefined;
1258
1523
  this.#state = "running";
1259
1524
  }
1260
1525
  }
@@ -1291,14 +1556,27 @@ async function setPoint(
1291
1556
  return checkpoint;
1292
1557
  }
1293
1558
 
1559
+ /**
1560
+ * Refuses an autostart index that selects no program: VICE numbers the
1561
+ * entries from 1, and the caller from 0, as the c64-disk directory list does.
1562
+ * A DEL entry holds no program (R2-08), and an index past the last entry selects nothing.
1563
+ */
1564
+ function checkDirectoryEntry(entries: DiskFile[], index: number): void {
1565
+ const entry = entries[index - 1];
1566
+ if (entry === undefined) throw new WireFailure("invalid-input", `The disk has ${entries.length} directory entries, so index ${index - 1} selects no entry. The c64-disk directory list gives the index of each entry.`);
1567
+ if (entry.type === "del") throw new WireFailure("invalid-input", `Directory entry ${index - 1} (${entry.name}) is a DEL entry and holds no program. Give the index of a PRG entry.`);
1568
+ }
1569
+
1294
1570
  /** The production factory: launch a headless VICE per session through the host's supervisor. */
1295
1571
  export function viceSessionFactory(options: {
1296
1572
  supervisor: ProcessSupervisor;
1297
1573
  env?: NodeJS.ProcessEnv;
1298
1574
  log?: (line: string) => void;
1575
+ /** The known flaws of the VICE version that the host checked at start. */
1576
+ flaws?: readonly string[];
1577
+ directory?: DiskDirectory;
1299
1578
  }): ViceSessionFactory {
1300
- const log = options.log ?? (() => {});
1301
- return async ({ videoStandard }) => {
1579
+ return async ({ videoStandard, log = options.log ?? (() => {}) }) => {
1302
1580
  const launch: ViceLauncher = (mode, launchLog) =>
1303
1581
  launchVice({
1304
1582
  videoStandard,
@@ -1311,6 +1589,11 @@ export function viceSessionFactory(options: {
1311
1589
  ...(options.env === undefined ? {} : { env: options.env }),
1312
1590
  });
1313
1591
  const vice = await launch("headless", () => {});
1314
- return ViceSession.start(vice, videoStandard, log, { launch, ownPath: (path) => options.supervisor.ownPath(path) });
1592
+ return ViceSession.start(vice, videoStandard, log, {
1593
+ launch,
1594
+ ownPath: (path) => options.supervisor.ownPath(path),
1595
+ ...(options.flaws === undefined ? {} : { flaws: options.flaws }),
1596
+ ...(options.directory === undefined ? {} : { directory: options.directory }),
1597
+ });
1315
1598
  };
1316
1599
  }