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

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 +137 -0
  14. package/skills/c64-docs/references/page-types.md +113 -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 +193 -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 +89 -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 -60
  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 +63 -0
  81. package/src/host-client/image.ts +66 -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 +203 -32
  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 +137 -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 -8
  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
@@ -94,6 +94,8 @@ export const MAX_WATCH_SIZE = 256;
94
94
  export interface Breakpoint {
95
95
  id: number;
96
96
  address: number;
97
+ /** A range from address: the CPU stops at the first instruction it executes in it (D43). One address when not given. */
98
+ size?: number;
97
99
  space: Space;
98
100
  enabled: boolean;
99
101
  /** The condition given at add, when there was one. */
@@ -112,15 +114,23 @@ export interface Watchpoint {
112
114
  }
113
115
 
114
116
  export type BreakpointParams =
115
- | { action: "add"; address: number; space: Space; condition?: Condition }
116
- | { action: "remove" | "enable" | "disable"; id: number }
117
+ | { action: "add"; address: number; size?: number; space: Space; condition?: Condition }
118
+ | { action: "remove"; id: number }
119
+ | { action: "enable" | "disable"; id: number }
117
120
  | { action: "list" };
118
121
 
119
122
  export type WatchpointParams =
120
123
  | { action: "add"; address: number; size: number; access: WatchAccess; space: Space; condition?: Condition }
121
- | { action: "remove" | "enable" | "disable"; id: number }
124
+ | { action: "remove"; id: number }
125
+ | { action: "enable" | "disable"; id: number }
122
126
  | { action: "list" };
123
127
 
128
+ /** The result of remove: the point is gone, so only its id comes back. */
129
+ export interface PointRemoved {
130
+ removed: true;
131
+ id: number;
132
+ }
133
+
124
134
  // Chip state
125
135
 
126
136
  export const VICII_MODES = ["text", "multicolor-text", "extended-color-text", "bitmap", "multicolor-bitmap", "invalid"] as const;
@@ -158,9 +168,14 @@ export interface CiaState {
158
168
  ddrB: number;
159
169
  timerA: number;
160
170
  timerB: number;
171
+ /** The values that a timer loads at an underflow: write-only on the chip, from VICE's dump (D63). */
172
+ latchA: number;
173
+ latchB: number;
161
174
  controlA: number;
162
175
  controlB: number;
163
176
  interruptStatus: number;
177
+ /** The interrupt sources that are on: bits 0-4 of a write to $xd0d with bit 7 set. */
178
+ interruptMask: number;
164
179
  tod: { hours: number; minutes: number; seconds: number; tenths: number };
165
180
  }
166
181
 
@@ -202,6 +217,50 @@ export interface HistoryEntry {
202
217
  rasterCycle?: number;
203
218
  }
204
219
 
220
+ // Traces (D76): checkpoints that record each hit and let the program run on.
221
+
222
+ export const TRACE_KINDS = ["exec", "read", "write"] as const;
223
+ export type TraceKind = (typeof TRACE_KINDS)[number];
224
+ export const MAX_TRACE_POINTS = 8;
225
+ export const MAX_TRACE_FRAMES = 3000;
226
+ export const MAX_TRACE_HITS = 20_000;
227
+ export const TRACE_ENDS = ["frames", "hits", "breakpoint", "watchpoint", "jam"] as const;
228
+
229
+ export interface TracePoint {
230
+ kind: TraceKind;
231
+ start: number;
232
+ end: number;
233
+ }
234
+
235
+ /** One hit of a trace: the instruction that executed or made the access, the registers before it, and where the raster was. */
236
+ export interface TraceHit {
237
+ /** The index of the point in the request. */
238
+ point: number;
239
+ /** The address of the instruction. */
240
+ pc: number;
241
+ bytes: string;
242
+ text: string;
243
+ a: number;
244
+ x: number;
245
+ y: number;
246
+ sp: number;
247
+ /** The frame of the trace, from 0, and the raster position when the instruction started. */
248
+ frame: number;
249
+ rasterLine: number;
250
+ rasterCycle: number;
251
+ /** read and write: the address that the instruction used, when its mode tells it, and the byte there after the access. */
252
+ address?: number;
253
+ value?: number;
254
+ }
255
+
256
+ export interface TraceResult {
257
+ hits: TraceHit[];
258
+ frames: number;
259
+ /** frames: the frame limit; hits: the hit limit; breakpoint, watchpoint or jam: the program stopped there. */
260
+ end: (typeof TRACE_ENDS)[number];
261
+ pc: number;
262
+ }
263
+
205
264
  export interface BacktraceFrame {
206
265
  /** The routine's entry address. */
207
266
  address: number;
@@ -223,12 +282,11 @@ export interface ProfileEntry {
223
282
  percent: number;
224
283
  }
225
284
 
285
+ /** Addresses with the same access flags: the coverage byte of src/c64.ts (D40), RAM, ROM and I/O apart. */
226
286
  export interface MemmapRange {
227
287
  start: number;
228
288
  end: number;
229
- execute: boolean;
230
- read: boolean;
231
- write: boolean;
289
+ flags: number;
232
290
  }
233
291
 
234
292
  // Screen baselines and snapshots
@@ -247,9 +305,29 @@ export interface Rectangle {
247
305
  height: number;
248
306
  }
249
307
 
308
+ /** The visible frame with normal borders that a screen capture holds, in pixels. */
309
+ export const FRAME_SIZE: Record<VideoStandard, { width: number; height: number }> = {
310
+ pal: { width: 384, height: 272 },
311
+ ntsc: { width: 384, height: 247 },
312
+ };
313
+
314
+ /** True when the mask rectangles leave no pixel of the frame to compare. */
315
+ export function maskCoversFrame(mask: readonly Rectangle[], standard: VideoStandard): boolean {
316
+ const { width, height } = FRAME_SIZE[standard];
317
+ const masked = new Uint8Array(width * height);
318
+ for (const rectangle of mask) {
319
+ for (let y = rectangle.y; y < Math.min(height, rectangle.y + rectangle.height); y++) {
320
+ if (rectangle.x < width) masked.fill(1, y * width + rectangle.x, y * width + Math.min(width, rectangle.x + rectangle.width));
321
+ }
322
+ }
323
+ return !masked.includes(0);
324
+ }
325
+
250
326
  export interface ScreenComparison {
251
327
  match: boolean;
252
328
  mismatchingPixels: number;
329
+ /** Pixels compared: the frame minus the masked ones; always at least 1. */
330
+ comparedPixels: number;
253
331
  mismatchRatio: number;
254
332
  /** Absent when no pixel differs. */
255
333
  bounds?: Rectangle;
@@ -301,8 +379,18 @@ export interface MachineStatus {
301
379
  window: boolean;
302
380
  /** Present only while the C64 CPU is stopped. */
303
381
  pc?: number;
382
+ /** True while a cartridge from an autostart is attached (D64). */
383
+ cartridge: boolean;
384
+ /** Present only while stopped by a user breakpoint or watchpoint, or by a JAM. */
385
+ stopReason?: PointStopReason;
386
+ /** The id of the breakpoint or watchpoint that stopped the machine. */
387
+ pointId?: number;
304
388
  }
305
389
 
390
+ /** What stopped the machine outside a request that stops it. */
391
+ export const POINT_STOP_REASONS = ["breakpoint", "watchpoint", "jam"] as const;
392
+ export type PointStopReason = (typeof POINT_STOP_REASONS)[number];
393
+
306
394
  export interface MemoryReadParams {
307
395
  address: number;
308
396
  size: number;
@@ -357,6 +445,12 @@ export interface ExecutionParams {
357
445
  /** step and next: defaults to 1; advance-frames: required. */
358
446
  count?: number;
359
447
  space: Space;
448
+ /**
449
+ * advance-frames only: the raster position that ends a frame, instead of the
450
+ * position where this call starts. Runs that stop between frames then keep
451
+ * the frames of one timeline (D62).
452
+ */
453
+ anchor?: { line: number; cycle: number };
360
454
  }
361
455
 
362
456
  export interface ExecutionResult {
@@ -364,6 +458,10 @@ export interface ExecutionResult {
364
458
  pc?: number;
365
459
  executed?: number;
366
460
  advancedFrames?: number;
461
+ /** advance-frames only: frames when all frames ran, else what stopped the run early. */
462
+ stopReason?: "frames" | PointStopReason;
463
+ /** The id of the breakpoint or watchpoint that stopped the run. */
464
+ pointId?: number;
367
465
  }
368
466
 
369
467
  /** c64_run_until targets. */
@@ -380,12 +478,18 @@ export interface RunUntilResult {
380
478
  reached: boolean;
381
479
  stopReason: StopReason;
382
480
  state: RunState;
481
+ /** The PC of the CPU in `space`: the CPU that stopped. */
383
482
  pc: number;
483
+ space: Space;
484
+ /** The id of the breakpoint or watchpoint that stopped the run. */
485
+ pointId?: number;
384
486
  }
385
487
 
386
488
  export interface ViceOperations {
387
489
  status: { params: Record<string, never>; result: MachineStatus };
388
490
  memoryRead: { params: MemoryReadParams; result: MemoryReadResult };
491
+ /** The I/O registers at $d000-$dfff as the chips hold them, whatever the CPU banks in, without side effects (D43). */
492
+ ioRead: { params: { address: number; size: number }; result: MemoryReadResult };
389
493
  registersGet: { params: { space: Space }; result: Registers };
390
494
  memoryWrite: { params: MemoryWriteParams; result: { address: number; bytesWritten: number } };
391
495
  /** `pattern`: bytes, with null as a wildcard. */
@@ -403,25 +507,33 @@ export interface ViceOperations {
403
507
  runUntil: { params: { target: RunTarget; timeoutFrames: number }; result: RunUntilResult };
404
508
  /** Attachment: the PRG bytes, load address first. */
405
509
  programLoad: { params: { address?: number }; result: { state: RunState; loadAddress: number; size: number } };
406
- /** Attachment: the program or image bytes; `type` is the file's extension. */
407
- autostart: { params: { type: string; index: number; run: boolean }; result: { state: RunState } };
510
+ /** Attachment: the program or image bytes; `type` is the file's extension. `index` is VICE's file number: 0 loads "*", 1 is the first directory entry. */
511
+ /**
512
+ * `emulatorFlaw`: for a tape image, a known flaw of the host's VICE version that a tape load meets (D56).
513
+ * `joysticksCentred`: the control ports that held a direction or fire and were centred first (R3-05).
514
+ */
515
+ autostart: { params: { type: string; index: number; run: boolean }; result: { state: RunState; emulatorFlaw?: string; joysticksCentred?: Array<1 | 2> } };
408
516
  /** Attachment: the disk image bytes; `type` is the file's extension. */
409
517
  diskAttach: { params: { type: string }; result: { attached: boolean } };
410
518
  /** pc is the reset vector when run is false: the CPU stops there. */
411
519
  reset: { params: { mode: ResetMode; run: boolean }; result: { state: RunState; pc?: number } };
412
520
  /** `data`: PETSCII bytes as lowercase hex. */
413
- keyboard: { params: { data: string }; result: { queuedBytes: number } };
414
- joystick: { params: JoystickState; result: JoystickState };
415
- breakpoint: { params: BreakpointParams; result: Breakpoint | { breakpoints: Breakpoint[] } };
416
- watchpoint: { params: WatchpointParams; result: Watchpoint | { watchpoints: Watchpoint[] } };
521
+ /** `emulatorFlaw`: a known flaw of the host's VICE version that this input meets (D56). */
522
+ keyboard: { params: { data: string }; result: { queuedBytes: number; emulatorFlaw?: string } };
523
+ joystick: { params: JoystickState; result: JoystickState & { emulatorFlaw?: string } };
524
+ breakpoint: { params: BreakpointParams; result: Breakpoint | PointRemoved | { breakpoints: Breakpoint[] } };
525
+ watchpoint: { params: WatchpointParams; result: Watchpoint | PointRemoved | { watchpoints: Watchpoint[] } };
417
526
  cpuHistory: { params: { limit: number; space: Space }; result: { entries: HistoryEntry[] } };
527
+ trace: { params: { points: TracePoint[]; frames: number; maxHits: number }; result: TraceResult };
418
528
  backtrace: { params: { depth: number; space: Space }; result: { frames: BacktraceFrame[] } };
419
529
  /** `cycles` is a decimal string. */
420
530
  timing: { params: { action: (typeof TIMING_ACTIONS)[number] }; result: { started: boolean } | { cycles: string } };
421
- profile: { params: { limit: number }; result: { entries: ProfileEntry[] } };
531
+ /** clear starts the profiler again from zero. */
532
+ profile: { params: { action: "read"; limit: number } | { action: "clear" }; result: { entries: ProfileEntry[] } | { cleared: boolean } };
422
533
  memmap: {
423
534
  params: { action: "read"; start: number; end: number; maxRanges: number } | { action: "clear" };
424
- result: { ranges: MemmapRange[] } | { cleared: boolean };
535
+ /** `next`: more ranges follow from this address; read again from there. */
536
+ result: { ranges: MemmapRange[]; next?: number } | { cleared: boolean };
425
537
  };
426
538
  vicii: { params: Record<string, never>; result: ViciiState };
427
539
  /** `indexes`: unique sprite numbers 0-7, in the order to report them. */
@@ -440,6 +552,14 @@ export interface ViceOperations {
440
552
  params: { action: "save" | "restore" | "discard"; name: string } | { action: "list" };
441
553
  result: { saved: boolean; name: string } | { restored: boolean; state: RunState } | { snapshots: string[] } | { discarded: boolean };
442
554
  };
555
+ /**
556
+ * Snapshot files (D41): export sends the bytes of a named snapshot back as
557
+ * one attachment; restore takes the bytes of a .vsf as one attachment.
558
+ */
559
+ snapshotFile: {
560
+ params: { action: "export"; name: string } | { action: "restore" };
561
+ result: { exported: boolean; bytes: number } | { restored: boolean; state: RunState };
562
+ };
443
563
  screenDiscard: { params: { baseline: string }; result: { discarded: boolean } };
444
564
  warp: { params: { enabled: boolean }; result: { enabled: boolean } };
445
565
  window: { params: { action: (typeof WINDOW_ACTIONS)[number] }; result: WindowResult };
@@ -469,7 +589,10 @@ export const VICE_OPERATIONS = [
469
589
  "screenBaselines",
470
590
  "screenDiscard",
471
591
  "snapshot",
592
+ "snapshotFile",
593
+ "ioRead",
472
594
  "cpuHistory",
595
+ "trace",
473
596
  "backtrace",
474
597
  "timing",
475
598
  "profile",
@@ -483,8 +606,9 @@ export const VICE_OPERATIONS = [
483
606
  "window",
484
607
  ] as const satisfies readonly ViceOperation[];
485
608
 
486
- /** How many attachments each operation takes. */
487
- export function attachmentCount(op: ViceOperation): number {
609
+ /** How many attachments an operation takes; a snapshotFile restore takes the .vsf. */
610
+ export function attachmentCount(op: ViceOperation, params?: unknown): number {
611
+ if (op === "snapshotFile") return isObject(params) && params.action === "restore" ? 1 : 0;
488
612
  return op === "programLoad" || op === "autostart" || op === "diskAttach" ? 1 : 0;
489
613
  }
490
614
 
@@ -514,6 +638,15 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
514
638
  }
515
639
  return { action: params.action, name: validName(params.name, "name") } as ViceOperations[O]["params"];
516
640
  }
641
+ case "snapshotFile": {
642
+ if (params.action === "restore") {
643
+ onlyFields(params, ["action"]);
644
+ return { action: "restore" } as ViceOperations[O]["params"];
645
+ }
646
+ if (params.action !== "export") invalid("action must be export or restore");
647
+ onlyFields(params, ["action", "name"]);
648
+ return { action: "export", name: validName(params.name, "name") } as ViceOperations[O]["params"];
649
+ }
517
650
  case "screenCompare": {
518
651
  onlyFields(params, ["baseline", "maxMismatchRatio", "mask", "includeDiff"]);
519
652
  const ratio = params.maxMismatchRatio;
@@ -535,6 +668,20 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
535
668
  if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
536
669
  return { limit: params.limit, space: params.space } as ViceOperations[O]["params"];
537
670
  }
671
+ case "trace": {
672
+ onlyFields(params, ["points", "frames", "maxHits"]);
673
+ if (!Array.isArray(params.points) || params.points.length === 0 || params.points.length > MAX_TRACE_POINTS) invalid(`points must be a list of 1 to ${MAX_TRACE_POINTS} points`);
674
+ const points = (params.points as unknown[]).map((point) => {
675
+ if (!isObject(point)) return invalid("a point is an object with kind, start and end");
676
+ onlyFields(point, ["kind", "start", "end"]);
677
+ if (!isOneOf(TRACE_KINDS, point.kind)) invalid("a point kind must be exec, read or write");
678
+ if (!isInteger(point.start, 0, 0xffff) || !isInteger(point.end, point.start as number, 0xffff)) invalid("a point needs start and end from $0000 to $ffff, the end not before the start");
679
+ return { kind: point.kind, start: point.start, end: point.end } as TracePoint;
680
+ });
681
+ if (!isInteger(params.frames, 1, MAX_TRACE_FRAMES)) invalid(`frames must be an integer from 1 to ${MAX_TRACE_FRAMES}`);
682
+ if (!isInteger(params.maxHits, 1, MAX_TRACE_HITS)) invalid(`maxHits must be an integer from 1 to ${MAX_TRACE_HITS}`);
683
+ return { points, frames: params.frames, maxHits: params.maxHits } as ViceOperations[O]["params"];
684
+ }
538
685
  case "backtrace": {
539
686
  onlyFields(params, ["depth", "space"]);
540
687
  if (!isInteger(params.depth, 1, MAX_BACKTRACE)) invalid(`depth must be an integer from 1 to ${MAX_BACKTRACE}`);
@@ -574,9 +721,11 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
574
721
  return result as ViceOperations[O]["params"];
575
722
  }
576
723
  case "profile": {
577
- onlyFields(params, ["limit"]);
724
+ onlyFields(params, params.action === "clear" ? ["action"] : ["action", "limit"]);
725
+ if (params.action === "clear") return { action: "clear" } as ViceOperations[O]["params"];
726
+ if (params.action !== "read") invalid("action must be read or clear");
578
727
  if (!isInteger(params.limit, 1, MAX_PROFILE)) invalid(`limit must be an integer from 1 to ${MAX_PROFILE}`);
579
- return { limit: params.limit } as ViceOperations[O]["params"];
728
+ return { action: "read", limit: params.limit } as ViceOperations[O]["params"];
580
729
  }
581
730
  case "memmap": {
582
731
  onlyFields(params, params.action === "clear" ? ["action"] : ["action", "start", "end", "maxRanges"]);
@@ -609,6 +758,12 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
609
758
  const result: MemoryReadParams = { address: params.address, size: params.size, space: params.space, view: params.view };
610
759
  return result as ViceOperations[O]["params"];
611
760
  }
761
+ case "ioRead": {
762
+ onlyFields(params, ["address", "size"]);
763
+ if (!isInteger(params.address, 0xd000, 0xdfff)) invalid("address must be in the I/O area $d000-$dfff");
764
+ if (!isInteger(params.size, 1, 0x1000) || params.address + params.size > 0xe000) invalid("the range must stay inside $d000-$dfff");
765
+ return { address: params.address, size: params.size } as ViceOperations[O]["params"];
766
+ }
612
767
  case "registersGet": {
613
768
  onlyFields(params, ["space"]);
614
769
  if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
@@ -702,7 +857,7 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
702
857
  return { space: params.space, values: result } as ViceOperations[O]["params"];
703
858
  }
704
859
  case "execution": {
705
- onlyFields(params, ["action", "space", "count"]);
860
+ onlyFields(params, ["action", "space", "count", "anchor"]);
706
861
  if (!isOneOf(EXECUTION_ACTIONS, params.action)) invalid(`action must be one of ${EXECUTION_ACTIONS.join(", ")}`);
707
862
  if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
708
863
  const counted = params.action === "step" || params.action === "next" || params.action === "advance-frames";
@@ -714,6 +869,13 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
714
869
  }
715
870
  const result: ExecutionParams = { action: params.action, space: params.space };
716
871
  if (counted) result.count = (params.count as number | undefined) ?? 1;
872
+ if (params.anchor !== undefined) {
873
+ if (params.action !== "advance-frames") invalid(`anchor is not used with action ${params.action}`);
874
+ const anchor = params.anchor;
875
+ if (!isObject(anchor) || !isInteger(anchor.line, 0, 511) || !isInteger(anchor.cycle, 0, 127)) invalid("anchor needs line from 0 to 511 and cycle from 0 to 127");
876
+ onlyFields(anchor as Record<string, unknown>, ["line", "cycle"]);
877
+ result.anchor = { line: anchor.line as number, cycle: anchor.cycle as number };
878
+ }
717
879
  return result as ViceOperations[O]["params"];
718
880
  }
719
881
  case "runUntil": {
@@ -765,7 +927,7 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
765
927
  if (!isInteger(params.id, 1, 0xffff_ffff)) invalid("id must be a positive integer");
766
928
  return { action: params.action, id: params.id } as ViceOperations[O]["params"];
767
929
  }
768
- onlyFields(params, op === "watchpoint" ? ["action", "address", "space", "size", "access", "condition"] : ["action", "address", "space", "condition"]);
930
+ onlyFields(params, op === "watchpoint" ? ["action", "address", "space", "size", "access", "condition"] : ["action", "address", "space", "size", "condition"]);
769
931
  if (!isInteger(params.address, 0, 0xffff)) invalid("address must be an integer from 0 to 65535");
770
932
  if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
771
933
  const added: Record<string, unknown> = { action: "add", address: params.address, space: params.space };
@@ -775,6 +937,9 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
775
937
  if (!isOneOf(WATCH_ACCESS, params.access)) invalid("access must be read, write or read-write");
776
938
  added.size = params.size;
777
939
  added.access = params.access;
940
+ } else if (params.size !== undefined) {
941
+ if (!isInteger(params.size, 1, 0x10000) || params.address + params.size > 0x10000) invalid("size must keep the range inside $0000-$ffff");
942
+ added.size = params.size;
778
943
  }
779
944
  if (params.condition !== undefined) added.condition = validateCondition(params.condition);
780
945
  return added as ViceOperations[O]["params"];
@@ -868,11 +1033,16 @@ function validateRunTarget(value: unknown): RunTarget {
868
1033
  return invalid("target kind must be address, memory or raster");
869
1034
  }
870
1035
 
1036
+ function isRemoved(value: unknown): value is PointRemoved {
1037
+ return isObject(value) && value.removed === true && isInteger(value.id, 1, 0xffff_ffff) && Object.keys(value).length === 2;
1038
+ }
1039
+
871
1040
  function isBreakpoint(value: unknown): value is Breakpoint {
872
1041
  return (
873
1042
  isObject(value) &&
874
1043
  isInteger(value.id, 1, 0xffff_ffff) &&
875
1044
  isInteger(value.address, 0, 0xffff) &&
1045
+ (value.size === undefined || isInteger(value.size, 1, 0x10000)) &&
876
1046
  isOneOf(SPACES, value.space) &&
877
1047
  typeof value.enabled === "boolean" &&
878
1048
  (value.condition === undefined || isCondition(value.condition))
@@ -917,13 +1087,17 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
917
1087
  isOneOf(VIDEO_STANDARDS, value.videoStandard) &&
918
1088
  typeof value.warp === "boolean" &&
919
1089
  typeof value.window === "boolean" &&
920
- (value.pc === undefined || isInteger(value.pc, 0, 0xffff));
1090
+ (value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
1091
+ typeof value.cartridge === "boolean" &&
1092
+ (value.stopReason === undefined || isOneOf(POINT_STOP_REASONS, value.stopReason)) &&
1093
+ (value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
921
1094
  if (!ok) throw new ProtocolError("status result is malformed");
922
1095
  break;
923
1096
  }
924
- case "memoryRead": {
1097
+ case "memoryRead":
1098
+ case "ioRead": {
925
1099
  if (!isInteger(value.address, 0, 0xffff) || !isHexData(value.data)) {
926
- throw new ProtocolError("memoryRead result is malformed");
1100
+ throw new ProtocolError(`${op} result is malformed`);
927
1101
  }
928
1102
  break;
929
1103
  }
@@ -984,7 +1158,9 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
984
1158
  typeof value.reached === "boolean" &&
985
1159
  isOneOf(STOP_REASONS, value.stopReason) &&
986
1160
  isOneOf(RUN_STATES, value.state) &&
987
- isInteger(value.pc, 0, 0xffff);
1161
+ isInteger(value.pc, 0, 0xffff) &&
1162
+ isOneOf(SPACES, value.space) &&
1163
+ (value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
988
1164
  if (!ok) throw new ProtocolError("runUntil result is malformed");
989
1165
  break;
990
1166
  }
@@ -993,7 +1169,9 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
993
1169
  isOneOf(RUN_STATES, value.state) &&
994
1170
  (value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
995
1171
  (value.executed === undefined || isInteger(value.executed, 0, MAX_EXECUTION_COUNT)) &&
996
- (value.advancedFrames === undefined || isInteger(value.advancedFrames, 0, MAX_EXECUTION_COUNT));
1172
+ (value.advancedFrames === undefined || isInteger(value.advancedFrames, 0, MAX_EXECUTION_COUNT)) &&
1173
+ (value.stopReason === undefined || value.stopReason === "frames" || isOneOf(POINT_STOP_REASONS, value.stopReason)) &&
1174
+ (value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
997
1175
  if (!ok) throw new ProtocolError("execution result is malformed");
998
1176
  break;
999
1177
  }
@@ -1001,6 +1179,10 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1001
1179
  case "autostart": {
1002
1180
  if (!isOneOf(RUN_STATES, value.state)) throw new ProtocolError(`${op} result is malformed`);
1003
1181
  if (op === "reset" && value.pc !== undefined && !isInteger(value.pc, 0, 0xffff)) throw new ProtocolError("reset result is malformed");
1182
+ if (op === "autostart" && !(value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string")) throw new ProtocolError("autostart result is malformed");
1183
+ if (op === "autostart" && !(value.joysticksCentred === undefined || (Array.isArray(value.joysticksCentred) && value.joysticksCentred.every((port) => port === 1 || port === 2)))) {
1184
+ throw new ProtocolError("autostart result is malformed");
1185
+ }
1004
1186
  break;
1005
1187
  }
1006
1188
  case "programLoad": {
@@ -1013,16 +1195,16 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1013
1195
  break;
1014
1196
  }
1015
1197
  case "keyboard": {
1016
- if (!isInteger(value.queuedBytes, 1, MAX_KEYBOARD_BYTES)) throw new ProtocolError("keyboard result is malformed");
1198
+ if (!isInteger(value.queuedBytes, 1, MAX_KEYBOARD_BYTES) || !(value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string")) throw new ProtocolError("keyboard result is malformed");
1017
1199
  break;
1018
1200
  }
1019
1201
  case "breakpoint": {
1020
- const ok = Array.isArray(value.breakpoints) ? value.breakpoints.every(isBreakpoint) : isBreakpoint(value);
1202
+ const ok = Array.isArray(value.breakpoints) ? value.breakpoints.every(isBreakpoint) : isRemoved(value) || isBreakpoint(value);
1021
1203
  if (!ok) throw new ProtocolError("breakpoint result is malformed");
1022
1204
  break;
1023
1205
  }
1024
1206
  case "watchpoint": {
1025
- const ok = Array.isArray(value.watchpoints) ? value.watchpoints.every(isWatchpoint) : isWatchpoint(value);
1207
+ const ok = Array.isArray(value.watchpoints) ? value.watchpoints.every(isWatchpoint) : isRemoved(value) || isWatchpoint(value);
1026
1208
  if (!ok) throw new ProtocolError("watchpoint result is malformed");
1027
1209
  break;
1028
1210
  }
@@ -1031,6 +1213,8 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1031
1213
  const ok =
1032
1214
  typeof value.match === "boolean" &&
1033
1215
  isInteger(value.mismatchingPixels, 0, 1 << 20) &&
1216
+ isInteger(value.comparedPixels, 1, 1 << 20) &&
1217
+ (value.mismatchingPixels as number) <= (value.comparedPixels as number) &&
1034
1218
  typeof value.mismatchRatio === "number" &&
1035
1219
  value.mismatchRatio >= 0 &&
1036
1220
  value.mismatchRatio <= 1 &&
@@ -1059,6 +1243,11 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1059
1243
  if (!ok) throw new ProtocolError("snapshot result is malformed");
1060
1244
  break;
1061
1245
  }
1246
+ case "snapshotFile": {
1247
+ const ok = (value.exported === true && isInteger(value.bytes, 1, 0xffff_ffff)) || (value.restored === true && isOneOf(RUN_STATES, value.state));
1248
+ if (!ok) throw new ProtocolError("snapshotFile result is malformed");
1249
+ break;
1250
+ }
1062
1251
  case "cpuHistory": {
1063
1252
  const ok =
1064
1253
  Array.isArray(value.entries) &&
@@ -1075,6 +1264,29 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1075
1264
  if (!ok) throw new ProtocolError("cpuHistory result is malformed");
1076
1265
  break;
1077
1266
  }
1267
+ case "trace": {
1268
+ const ok =
1269
+ Array.isArray(value.hits) &&
1270
+ isInteger(value.frames, 0, MAX_TRACE_FRAMES) &&
1271
+ isOneOf(TRACE_ENDS, value.end) &&
1272
+ isInteger(value.pc, 0, 0xffff) &&
1273
+ value.hits.every(
1274
+ (hit) =>
1275
+ isObject(hit) &&
1276
+ isInteger(hit.point, 0, MAX_TRACE_POINTS - 1) &&
1277
+ isInteger(hit.pc, 0, 0xffff) &&
1278
+ isHexData(hit.bytes) &&
1279
+ typeof hit.text === "string" &&
1280
+ (["a", "x", "y", "sp"] as const).every((name) => isInteger(hit[name], 0, 0xff)) &&
1281
+ isInteger(hit.frame, 0, MAX_TRACE_FRAMES) &&
1282
+ isInteger(hit.rasterLine, 0, 511) &&
1283
+ isInteger(hit.rasterCycle, 0, 127) &&
1284
+ (hit.address === undefined || isInteger(hit.address, 0, 0xffff)) &&
1285
+ (hit.value === undefined || isInteger(hit.value, 0, 0xff)),
1286
+ );
1287
+ if (!ok) throw new ProtocolError("trace result is malformed");
1288
+ break;
1289
+ }
1078
1290
  case "backtrace": {
1079
1291
  const ok =
1080
1292
  Array.isArray(value.frames) &&
@@ -1091,6 +1303,10 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1091
1303
  break;
1092
1304
  }
1093
1305
  case "profile": {
1306
+ if ("cleared" in value) {
1307
+ if (value.cleared !== true) throw new ProtocolError("profile result is malformed");
1308
+ break;
1309
+ }
1094
1310
  const ok =
1095
1311
  Array.isArray(value.entries) &&
1096
1312
  value.entries.every(
@@ -1111,8 +1327,10 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1111
1327
  isObject(range) &&
1112
1328
  isInteger(range.start, 0, 0xffff) &&
1113
1329
  isInteger(range.end, 0, 0xffff) &&
1114
- (["execute", "read", "write"] as const).every((flag) => typeof range[flag] === "boolean");
1115
- const ok = Array.isArray(value.ranges) ? value.ranges.every(isRange) : value.cleared === true;
1330
+ isInteger(range.flags, 1, 0xff);
1331
+ const ok = Array.isArray(value.ranges)
1332
+ ? value.ranges.every(isRange) && (value.next === undefined || isInteger(value.next, 0, 0xffff))
1333
+ : value.cleared === true;
1116
1334
  if (!ok) throw new ProtocolError("memmap result is malformed");
1117
1335
  break;
1118
1336
  }
@@ -1168,8 +1386,8 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1168
1386
  isObject(chip) &&
1169
1387
  (chip.id === 1 || chip.id === 2) &&
1170
1388
  (["portA", "portB", "ddrA", "ddrB", "controlA", "controlB", "interruptStatus"] as const).every((name) => isInteger(chip[name], 0, 0xff)) &&
1171
- isInteger(chip.timerA, 0, 0xffff) &&
1172
- isInteger(chip.timerB, 0, 0xffff) &&
1389
+ isInteger(chip.interruptMask, 0, 0x1f) &&
1390
+ (["timerA", "timerB", "latchA", "latchB"] as const).every((name) => isInteger(chip[name], 0, 0xffff)) &&
1173
1391
  isObject(chip.tod) &&
1174
1392
  isInteger(chip.tod.hours, 0, 23) &&
1175
1393
  isInteger(chip.tod.minutes, 0, 99) &&
@@ -1211,7 +1429,11 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1211
1429
  break;
1212
1430
  }
1213
1431
  case "joystick": {
1214
- const ok = (value.port === 1 || value.port === 2) && isOneOf(JOYSTICK_DIRECTIONS, value.direction) && typeof value.fire === "boolean";
1432
+ const ok =
1433
+ (value.port === 1 || value.port === 2) &&
1434
+ isOneOf(JOYSTICK_DIRECTIONS, value.direction) &&
1435
+ typeof value.fire === "boolean" &&
1436
+ (value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string");
1215
1437
  if (!ok) throw new ProtocolError("joystick result is malformed");
1216
1438
  break;
1217
1439
  }