@henols/c64-re-tools 1.0.0 → 2.0.0-rc.10

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +166 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +126 -0
  26. package/skills/c64-source/scripts/generate.ts +490 -0
  27. package/skills/c64-source/scripts/loader.ts +571 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +595 -0
  30. package/skills/c64-static-analysis/SKILL.md +119 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-testing/SKILL.md +103 -0
  37. package/skills/c64-testing/references/scenario-format.md +116 -0
  38. package/skills/c64-testing/scripts/checklist.ts +51 -0
  39. package/skills/c64-testing/scripts/run.ts +189 -0
  40. package/skills/c64-testing/scripts/scenario.ts +493 -0
  41. package/skills/c64-testing/scripts/test.ts +65 -0
  42. package/skills/c64-unpacker/SKILL.md +135 -0
  43. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  44. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  45. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  46. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  47. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  48. package/src/c64.ts +411 -0
  49. package/src/cli/ap-sdk.ts +44 -0
  50. package/src/cli/cleanup.ts +194 -0
  51. package/src/cli/main.ts +125 -0
  52. package/src/cli/status.ts +61 -0
  53. package/src/host/main.ts +134 -0
  54. package/src/host/server.ts +392 -0
  55. package/src/host/tools/c1541.ts +284 -0
  56. package/src/host/tools/index.ts +35 -0
  57. package/src/host/tools/petcat.ts +77 -0
  58. package/src/host/tools/status.ts +47 -0
  59. package/src/host/tools/version.ts +41 -0
  60. package/src/host/vice/adapter.ts +804 -0
  61. package/src/host/vice/binary-monitor.ts +389 -0
  62. package/src/host/vice/chips.ts +118 -0
  63. package/src/host/vice/connect.ts +34 -0
  64. package/src/host/vice/process.ts +413 -0
  65. package/src/host/vice/screen.ts +184 -0
  66. package/src/host/vice/session.ts +1426 -0
  67. package/src/host/vice/text-monitor.ts +223 -0
  68. package/src/host-client/autostart.ts +55 -0
  69. package/src/host-client/connect.ts +241 -0
  70. package/src/host-client/coverage.ts +63 -0
  71. package/src/host-client/steps.ts +170 -0
  72. package/src/host-client/tools.ts +89 -0
  73. package/src/host-client/transfer.ts +126 -0
  74. package/src/host-client/vice-session.ts +316 -0
  75. package/src/knowledge/database.ts +132 -0
  76. package/src/knowledge/history.ts +163 -0
  77. package/src/knowledge/import.ts +391 -0
  78. package/src/knowledge/read.ts +342 -0
  79. package/src/knowledge/schema.ts +229 -0
  80. package/src/knowledge/write.ts +663 -0
  81. package/src/mcp/main.ts +134 -0
  82. package/src/mcp/output.ts +32 -0
  83. package/src/mcp/server.ts +316 -0
  84. package/src/mcp/tools/debug.ts +327 -0
  85. package/src/mcp/tools/execution.ts +117 -0
  86. package/src/mcp/tools/input.ts +72 -0
  87. package/src/mcp/tools/machine.ts +100 -0
  88. package/src/mcp/tools/media.ts +129 -0
  89. package/src/mcp/tools/memory.ts +250 -0
  90. package/src/mcp/tools/video.ts +271 -0
  91. package/src/native/acme.ts +116 -0
  92. package/src/native/discover.ts +179 -0
  93. package/src/native/dxa.ts +166 -0
  94. package/src/native/ghidra/analyze.ts +86 -0
  95. package/src/native/ghidra/index.ts +173 -0
  96. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  97. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  98. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  99. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  100. package/src/native/ghidra/scripts/C64Export.java +274 -0
  101. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  102. package/src/native/local.ts +34 -0
  103. package/src/native/processes.ts +350 -0
  104. package/src/native/run.ts +172 -0
  105. package/src/native/staging.ts +83 -0
  106. package/src/native/status.ts +113 -0
  107. package/src/native/types.ts +257 -0
  108. package/src/native/watchdog.ts +78 -0
  109. package/src/package.ts +21 -0
  110. package/src/platform.ts +441 -0
  111. package/src/project.ts +39 -0
  112. package/src/protocol/framing.ts +132 -0
  113. package/src/protocol/messages.ts +259 -0
  114. package/src/protocol/tools.ts +266 -0
  115. package/src/protocol/vice.ts +1348 -0
  116. package/src/script.ts +60 -0
  117. package/src/time.ts +15 -0
  118. package/src/trace.ts +194 -0
  119. package/THIRD-PARTY-NOTICES.md +0 -26
  120. package/bin/cli.mjs +0 -283
  121. package/skills/acme-build/SKILL.md +0 -260
  122. package/skills/acme-build/scripts/acme.mjs +0 -341
  123. package/skills/acme-build/template.a +0 -53
  124. package/skills/c64-disk-access/SKILL.md +0 -159
  125. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  126. package/skills/c64-memory-mapping/SKILL.md +0 -631
  127. package/skills/c64-memory-mapping/memmap.json +0 -8800
  128. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  129. package/skills/c64-petcat/SKILL.md +0 -87
  130. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  131. package/skills/c64-program-recon/SKILL.md +0 -706
  132. package/skills/c64-program-recon/references/control-flow.md +0 -180
  133. package/skills/c64-program-recon/references/graphics.md +0 -79
  134. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  135. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  136. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  137. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  138. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  139. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  140. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  141. package/skills/c64-provenance-diff/SKILL.md +0 -292
  142. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  143. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  144. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  145. package/skills/c64-ram-capture/SKILL.md +0 -474
  146. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  147. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  148. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  149. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  150. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  151. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  152. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  153. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  154. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  155. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  156. package/skills/c64-ram-capture/transients/README.md +0 -137
  157. package/skills/routine-queue-walker/SKILL.md +0 -394
  158. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  159. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,1348 @@
1
+ // The VICE-session operations of the Host Runtime wire: their vocabulary, shapes and validators.
2
+
3
+ import { invalid, isInteger, isObject, isOneOf, onlyFields, ProtocolError, VIDEO_STANDARDS, type VideoStandard } from "./messages.ts";
4
+
5
+ export const SPACES = ["c64", "drive8"] as const;
6
+ export type Space = (typeof SPACES)[number];
7
+
8
+ export const MEMORY_VIEWS = ["cpu", "ram"] as const;
9
+ export type MemoryView = (typeof MEMORY_VIEWS)[number];
10
+
11
+ export const RUN_STATES = ["running", "stopped"] as const;
12
+ export type RunState = (typeof RUN_STATES)[number];
13
+
14
+ /** Bounds of c64_memory_search, c64_memory_compare and c64_disassemble. */
15
+ export const MAX_SEARCH_PATTERN = 256;
16
+ export const MAX_SEARCH_RESULTS = 1000;
17
+ export const MAX_COMPARE_SIZE = 4096;
18
+ export const MAX_COMPARE_DIFFERENCES = 32;
19
+ export const MAX_DISASSEMBLE = 256;
20
+
21
+ export interface MemoryLocation {
22
+ address: number;
23
+ space: Space;
24
+ view: MemoryView;
25
+ }
26
+
27
+ export interface Instruction {
28
+ address: number;
29
+ /** The instruction's bytes, lowercase hex. */
30
+ bytes: string;
31
+ /** Mnemonic and operand, for example "LDA #$00". */
32
+ text: string;
33
+ }
34
+
35
+ /** Bounds of c64_memory_read and c64_memory_write. */
36
+ export const MAX_MEMORY_READ = 4096;
37
+ export const MAX_MEMORY_WRITE = 4096;
38
+
39
+ export const EXECUTION_ACTIONS = ["pause", "resume", "step", "next", "until-return", "advance-frames"] as const;
40
+ export type ExecutionAction = (typeof EXECUTION_ACTIONS)[number];
41
+ /** Bounds of c64_execution counts. */
42
+ export const MAX_EXECUTION_COUNT = 10_000;
43
+
44
+ /** File types each media operation accepts, by lowercase extension. */
45
+ export const AUTOSTART_TYPES = ["prg", "p00", "t64", "tap", "d64", "d71", "d81", "g64", "x64", "crt"] as const;
46
+ export const DISK_TYPES = ["d64", "d71", "d81", "g64", "x64"] as const;
47
+
48
+ /** Most bytes c64_keyboard queues in one call. */
49
+ export const MAX_KEYBOARD_BYTES = 1024;
50
+
51
+ export const JOYSTICK_DIRECTIONS = [
52
+ "center",
53
+ "up",
54
+ "down",
55
+ "left",
56
+ "right",
57
+ "up-left",
58
+ "up-right",
59
+ "down-left",
60
+ "down-right",
61
+ ] as const;
62
+ export type JoystickDirection = (typeof JOYSTICK_DIRECTIONS)[number];
63
+
64
+ export interface JoystickState {
65
+ port: 1 | 2;
66
+ direction: JoystickDirection;
67
+ fire: boolean;
68
+ }
69
+
70
+ // Typed conditions
71
+
72
+ export const COMPARISONS = ["eq", "ne", "lt", "lte", "gt", "gte"] as const;
73
+ export type Comparison = (typeof COMPARISONS)[number];
74
+ export const CONDITION_REGISTERS = ["a", "x", "y", "sp"] as const;
75
+
76
+ export type Condition =
77
+ | { kind: "register"; register: (typeof CONDITION_REGISTERS)[number]; operator: Comparison; value: number }
78
+ | { kind: "memory"; address: number; operator: Comparison; value: number; space: Space; view: MemoryView }
79
+ | { kind: "raster"; line: number; cycle?: number };
80
+
81
+ /** Raster lines and cycles per line of each video standard. */
82
+ export const RASTER: Record<VideoStandard, { lines: number; cycles: number }> = {
83
+ pal: { lines: 312, cycles: 63 },
84
+ ntsc: { lines: 263, cycles: 65 },
85
+ };
86
+
87
+ // Breakpoints and watchpoints
88
+
89
+ export const CHECKPOINT_ACTIONS = ["add", "remove", "enable", "disable", "list"] as const;
90
+ export const WATCH_ACCESS = ["read", "write", "read-write"] as const;
91
+ export type WatchAccess = (typeof WATCH_ACCESS)[number];
92
+ export const MAX_WATCH_SIZE = 256;
93
+
94
+ export interface Breakpoint {
95
+ id: number;
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;
99
+ space: Space;
100
+ enabled: boolean;
101
+ /** The condition given at add, when there was one. */
102
+ condition?: Condition;
103
+ }
104
+
105
+ export interface Watchpoint {
106
+ id: number;
107
+ address: number;
108
+ size: number;
109
+ access: WatchAccess;
110
+ space: Space;
111
+ enabled: boolean;
112
+ /** The condition given at add, when there was one. */
113
+ condition?: Condition;
114
+ }
115
+
116
+ export type BreakpointParams =
117
+ | { action: "add"; address: number; size?: number; space: Space; condition?: Condition }
118
+ | { action: "remove"; id: number }
119
+ | { action: "enable" | "disable"; id: number }
120
+ | { action: "list" };
121
+
122
+ export type WatchpointParams =
123
+ | { action: "add"; address: number; size: number; access: WatchAccess; space: Space; condition?: Condition }
124
+ | { action: "remove"; id: number }
125
+ | { action: "enable" | "disable"; id: number }
126
+ | { action: "list" };
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
+
134
+ // Chip state
135
+
136
+ export const VICII_MODES = ["text", "multicolor-text", "extended-color-text", "bitmap", "multicolor-bitmap", "invalid"] as const;
137
+ export type ViciiMode = (typeof VICII_MODES)[number];
138
+
139
+ export interface ViciiState {
140
+ rasterLine: number;
141
+ mode: ViciiMode;
142
+ screenAddress: number;
143
+ graphicsAddress: number;
144
+ scrollX: number;
145
+ scrollY: number;
146
+ borderColor: number;
147
+ backgroundColors: number[];
148
+ }
149
+
150
+ export interface SpriteState {
151
+ index: number;
152
+ x: number;
153
+ y: number;
154
+ enabled: boolean;
155
+ color: number;
156
+ multicolor: boolean;
157
+ expandX: boolean;
158
+ expandY: boolean;
159
+ behindBackground: boolean;
160
+ dataAddress: number;
161
+ }
162
+
163
+ export interface CiaState {
164
+ id: 1 | 2;
165
+ portA: number;
166
+ portB: number;
167
+ ddrA: number;
168
+ ddrB: number;
169
+ timerA: number;
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;
174
+ controlA: number;
175
+ controlB: number;
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;
179
+ tod: { hours: number; minutes: number; seconds: number; tenths: number };
180
+ }
181
+
182
+ export interface SidState {
183
+ voices: Array<{ index: number; frequency: number; pulseWidth: number; control: number; attackDecay: number; sustainRelease: number }>;
184
+ filter: { cutoff: number; resonance: number; routing: number; mode: number };
185
+ volume: number;
186
+ }
187
+
188
+ export const CIA_SELECTIONS = ["1", "2", "both"] as const;
189
+ export type CiaSelection = (typeof CIA_SELECTIONS)[number];
190
+
191
+ // History, backtrace and timing
192
+
193
+ export const MAX_HISTORY = 200;
194
+ export const MAX_BACKTRACE = 64;
195
+ export const TIMING_ACTIONS = ["start", "read"] as const;
196
+
197
+ /** c64_window: open moves the machine into a VICE with a window, close back into a headless one. */
198
+ export const WINDOW_ACTIONS = ["open", "close"] as const;
199
+
200
+ export interface WindowResult {
201
+ window: boolean;
202
+ state: RunState;
203
+ /** Emulator state a move cannot carry; empty when nothing moved. */
204
+ notCarried: string[];
205
+ }
206
+
207
+ export interface HistoryEntry {
208
+ address: number;
209
+ bytes: string;
210
+ text: string;
211
+ a: number;
212
+ x: number;
213
+ y: number;
214
+ sp: number;
215
+ /** C64 space only: the raster position when the instruction started. */
216
+ rasterLine?: number;
217
+ rasterCycle?: number;
218
+ }
219
+
220
+ export interface BacktraceFrame {
221
+ /** The routine's entry address. */
222
+ address: number;
223
+ /** Where the routine returns to; absent when it cannot be known (a reset or interrupt entry). */
224
+ returnAddress?: number;
225
+ }
226
+
227
+ // Profile and memmap
228
+
229
+ export const MAX_PROFILE = 100;
230
+ export const MAX_MEMMAP_RANGES = 1000;
231
+
232
+ export interface ProfileEntry {
233
+ address: number;
234
+ /** Decimal strings: cycle counts can outgrow a JSON number. */
235
+ totalCycles: string;
236
+ selfCycles: string;
237
+ /** Share of all profiled cycles spent in the routine itself. */
238
+ percent: number;
239
+ }
240
+
241
+ /** Addresses with the same access flags: the coverage byte of src/c64.ts (D40), RAM, ROM and I/O apart. */
242
+ export interface MemmapRange {
243
+ start: number;
244
+ end: number;
245
+ flags: number;
246
+ }
247
+
248
+ // Screen baselines and snapshots
249
+
250
+ /** 1-64 letters, digits, dots, underscores or hyphens. */
251
+ export const TRANSIENT_NAME = /^[A-Za-z0-9._-]{1,64}$/;
252
+ export const MAX_BASELINES = 64;
253
+ export const MAX_SNAPSHOTS = 64;
254
+ /** Most rectangles one screen comparison mask holds. */
255
+ export const MAX_MASK_RECTANGLES = 64;
256
+
257
+ export interface Rectangle {
258
+ x: number;
259
+ y: number;
260
+ width: number;
261
+ height: number;
262
+ }
263
+
264
+ /** The visible frame with normal borders that a screen capture holds, in pixels. */
265
+ export const FRAME_SIZE: Record<VideoStandard, { width: number; height: number }> = {
266
+ pal: { width: 384, height: 272 },
267
+ ntsc: { width: 384, height: 247 },
268
+ };
269
+
270
+ /** True when the mask rectangles leave no pixel of the frame to compare. */
271
+ export function maskCoversFrame(mask: readonly Rectangle[], standard: VideoStandard): boolean {
272
+ const { width, height } = FRAME_SIZE[standard];
273
+ const masked = new Uint8Array(width * height);
274
+ for (const rectangle of mask) {
275
+ for (let y = rectangle.y; y < Math.min(height, rectangle.y + rectangle.height); y++) {
276
+ if (rectangle.x < width) masked.fill(1, y * width + rectangle.x, y * width + Math.min(width, rectangle.x + rectangle.width));
277
+ }
278
+ }
279
+ return !masked.includes(0);
280
+ }
281
+
282
+ export interface ScreenComparison {
283
+ match: boolean;
284
+ mismatchingPixels: number;
285
+ /** Pixels compared: the frame minus the masked ones; always at least 1. */
286
+ comparedPixels: number;
287
+ mismatchRatio: number;
288
+ /** Absent when no pixel differs. */
289
+ bounds?: Rectangle;
290
+ /** Base64 PNG of the differences, when asked for. */
291
+ diffPng?: string;
292
+ }
293
+
294
+ // c64_observe
295
+
296
+ export const MAX_OBSERVE_RANGES = 16;
297
+ export const MAX_OBSERVE_BYTES = 4096;
298
+
299
+ export interface ObserveParams {
300
+ registers?: Space;
301
+ memory?: Array<{ address: number; size: number; space: Space; view: MemoryView }>;
302
+ vicii?: boolean;
303
+ /** Empty means all eight sprites. */
304
+ sprites?: number[];
305
+ cia?: CiaSelection;
306
+ sid?: boolean;
307
+ screen?: boolean;
308
+ timing?: boolean;
309
+ }
310
+
311
+ export interface ObserveResult {
312
+ registers?: Registers;
313
+ memory?: Array<{ address: number; data: string }>;
314
+ vicii?: ViciiState;
315
+ sprites?: SpriteState[];
316
+ cia?: CiaState[];
317
+ sid?: SidState;
318
+ timing?: { rasterLine: number; rasterCycle: number };
319
+ /** Base64 PNG of the last drawn frame. */
320
+ screen?: { width: number; height: number; png: string };
321
+ }
322
+
323
+ export const RESET_MODES = ["soft", "hard"] as const;
324
+ export type ResetMode = (typeof RESET_MODES)[number];
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // VICE-session operations. Addresses are integers 0..0xffff; bytes travel as
328
+ // lowercase hex. The MCP formats them into the public shapes.
329
+
330
+ export interface MachineStatus {
331
+ state: RunState;
332
+ videoStandard: VideoStandard;
333
+ warp: boolean;
334
+ /** True while the machine runs in a VICE with a window. */
335
+ window: boolean;
336
+ /** Present only while the C64 CPU is stopped. */
337
+ pc?: number;
338
+ /** True while a cartridge from an autostart is attached (D64). */
339
+ cartridge: boolean;
340
+ }
341
+
342
+ export interface MemoryReadParams {
343
+ address: number;
344
+ size: number;
345
+ space: Space;
346
+ view: MemoryView;
347
+ }
348
+
349
+ export interface MemoryReadResult {
350
+ address: number;
351
+ data: string;
352
+ }
353
+
354
+ export interface CpuFlags {
355
+ n: boolean;
356
+ v: boolean;
357
+ b: boolean;
358
+ d: boolean;
359
+ i: boolean;
360
+ z: boolean;
361
+ c: boolean;
362
+ }
363
+
364
+ export interface Registers {
365
+ pc: number;
366
+ a: number;
367
+ x: number;
368
+ y: number;
369
+ sp: number;
370
+ flags: CpuFlags;
371
+ }
372
+
373
+ export interface MemoryWriteParams {
374
+ address: number;
375
+ /** Lowercase hex, 1 to MAX_MEMORY_WRITE bytes. */
376
+ data: string;
377
+ space: Space;
378
+ view: MemoryView;
379
+ }
380
+
381
+ /** Any subset of the registers; flags may be a subset too. */
382
+ export interface RegisterValues {
383
+ pc?: number;
384
+ a?: number;
385
+ x?: number;
386
+ y?: number;
387
+ sp?: number;
388
+ flags?: Partial<CpuFlags>;
389
+ }
390
+
391
+ export interface ExecutionParams {
392
+ action: ExecutionAction;
393
+ /** step and next: defaults to 1; advance-frames: required. */
394
+ count?: number;
395
+ space: Space;
396
+ /**
397
+ * advance-frames only: the raster position that ends a frame, instead of the
398
+ * position where this call starts. Runs that stop between frames then keep
399
+ * the frames of one timeline (D62).
400
+ */
401
+ anchor?: { line: number; cycle: number };
402
+ }
403
+
404
+ export interface ExecutionResult {
405
+ state: RunState;
406
+ pc?: number;
407
+ executed?: number;
408
+ advancedFrames?: number;
409
+ }
410
+
411
+ /** c64_run_until targets. */
412
+ export type RunTarget =
413
+ | { kind: "address"; address: number; space: Space; condition?: Condition }
414
+ | { kind: "memory"; address: number; operator: Comparison; value: number; space: Space; view: MemoryView }
415
+ | { kind: "raster"; line: number; cycle?: number };
416
+
417
+ export const STOP_REASONS = ["target", "breakpoint", "watchpoint", "jam", "timeout"] as const;
418
+ export type StopReason = (typeof STOP_REASONS)[number];
419
+ export const MAX_TIMEOUT_FRAMES = 30_000;
420
+
421
+ export interface RunUntilResult {
422
+ reached: boolean;
423
+ stopReason: StopReason;
424
+ state: RunState;
425
+ /** The PC of the CPU in `space`: the CPU that stopped. */
426
+ pc: number;
427
+ space: Space;
428
+ }
429
+
430
+ export interface ViceOperations {
431
+ status: { params: Record<string, never>; result: MachineStatus };
432
+ memoryRead: { params: MemoryReadParams; result: MemoryReadResult };
433
+ /** The I/O registers at $d000-$dfff as the chips hold them, whatever the CPU banks in, without side effects (D43). */
434
+ ioRead: { params: { address: number; size: number }; result: MemoryReadResult };
435
+ registersGet: { params: { space: Space }; result: Registers };
436
+ memoryWrite: { params: MemoryWriteParams; result: { address: number; bytesWritten: number } };
437
+ /** `pattern`: bytes, with null as a wildcard. */
438
+ memorySearch: {
439
+ params: { start: number; end: number; pattern: Array<number | null>; space: Space; view: MemoryView; maxResults: number };
440
+ result: { matches: number[] };
441
+ };
442
+ memoryCompare: {
443
+ params: { left: MemoryLocation; right: MemoryLocation; size: number };
444
+ result: { equal: boolean; differentBytes: number; firstDifferences: Array<{ offset: number; left: number; right: number }> };
445
+ };
446
+ disassemble: { params: { address: number; count: number; space: Space; view: MemoryView }; result: { instructions: Instruction[] } };
447
+ registersSet: { params: { space: Space; values: RegisterValues }; result: Registers };
448
+ execution: { params: ExecutionParams; result: ExecutionResult };
449
+ runUntil: { params: { target: RunTarget; timeoutFrames: number }; result: RunUntilResult };
450
+ /** Attachment: the PRG bytes, load address first. */
451
+ programLoad: { params: { address?: number }; result: { state: RunState; loadAddress: number; size: number } };
452
+ /** 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. */
453
+ /**
454
+ * `emulatorFlaw`: for a tape image, a known flaw of the host's VICE version that a tape load meets (D56).
455
+ * `joysticksCentred`: the control ports that held a direction or fire and were centred first (R3-05).
456
+ */
457
+ autostart: { params: { type: string; index: number; run: boolean }; result: { state: RunState; emulatorFlaw?: string; joysticksCentred?: Array<1 | 2> } };
458
+ /** Attachment: the disk image bytes; `type` is the file's extension. */
459
+ diskAttach: { params: { type: string }; result: { attached: boolean } };
460
+ /** pc is the reset vector when run is false: the CPU stops there. */
461
+ reset: { params: { mode: ResetMode; run: boolean }; result: { state: RunState; pc?: number } };
462
+ /** `data`: PETSCII bytes as lowercase hex. */
463
+ /** `emulatorFlaw`: a known flaw of the host's VICE version that this input meets (D56). */
464
+ keyboard: { params: { data: string }; result: { queuedBytes: number; emulatorFlaw?: string } };
465
+ joystick: { params: JoystickState; result: JoystickState & { emulatorFlaw?: string } };
466
+ breakpoint: { params: BreakpointParams; result: Breakpoint | PointRemoved | { breakpoints: Breakpoint[] } };
467
+ watchpoint: { params: WatchpointParams; result: Watchpoint | PointRemoved | { watchpoints: Watchpoint[] } };
468
+ cpuHistory: { params: { limit: number; space: Space }; result: { entries: HistoryEntry[] } };
469
+ backtrace: { params: { depth: number; space: Space }; result: { frames: BacktraceFrame[] } };
470
+ /** `cycles` is a decimal string. */
471
+ timing: { params: { action: (typeof TIMING_ACTIONS)[number] }; result: { started: boolean } | { cycles: string } };
472
+ profile: { params: { limit: number }; result: { entries: ProfileEntry[] } };
473
+ memmap: {
474
+ params: { action: "read"; start: number; end: number; maxRanges: number } | { action: "clear" };
475
+ /** `next`: more ranges follow from this address; read again from there. */
476
+ result: { ranges: MemmapRange[]; next?: number } | { cleared: boolean };
477
+ };
478
+ vicii: { params: Record<string, never>; result: ViciiState };
479
+ /** `indexes`: unique sprite numbers 0-7, in the order to report them. */
480
+ sprites: { params: { indexes: number[] }; result: { sprites: SpriteState[] } };
481
+ cia: { params: { which: CiaSelection }; result: { chips: CiaState[] } };
482
+ sid: { params: Record<string, never>; result: SidState };
483
+ observe: { params: ObserveParams; result: ObserveResult };
484
+ /** The last frame the VIC-II drew, visible area with borders, as a base64 PNG. */
485
+ screenCapture: { params: { baseline?: string }; result: { width: number; height: number; png: string; baseline?: string } };
486
+ screenCompare: {
487
+ params: { baseline: string; maxMismatchRatio: number; mask: Rectangle[]; includeDiff: boolean };
488
+ result: ScreenComparison;
489
+ };
490
+ screenBaselines: { params: Record<string, never>; result: { baselines: string[] } };
491
+ snapshot: {
492
+ params: { action: "save" | "restore" | "discard"; name: string } | { action: "list" };
493
+ result: { saved: boolean; name: string } | { restored: boolean; state: RunState } | { snapshots: string[] } | { discarded: boolean };
494
+ };
495
+ /**
496
+ * Snapshot files (D41): export sends the bytes of a named snapshot back as
497
+ * one attachment; restore takes the bytes of a .vsf as one attachment.
498
+ */
499
+ snapshotFile: {
500
+ params: { action: "export"; name: string } | { action: "restore" };
501
+ result: { exported: boolean; bytes: number } | { restored: boolean; state: RunState };
502
+ };
503
+ screenDiscard: { params: { baseline: string }; result: { discarded: boolean } };
504
+ warp: { params: { enabled: boolean }; result: { enabled: boolean } };
505
+ window: { params: { action: (typeof WINDOW_ACTIONS)[number] }; result: WindowResult };
506
+ }
507
+ export type ViceOperation = keyof ViceOperations;
508
+ export const VICE_OPERATIONS = [
509
+ "status",
510
+ "memoryRead",
511
+ "registersGet",
512
+ "memoryWrite",
513
+ "memorySearch",
514
+ "memoryCompare",
515
+ "disassemble",
516
+ "registersSet",
517
+ "execution",
518
+ "runUntil",
519
+ "programLoad",
520
+ "autostart",
521
+ "diskAttach",
522
+ "reset",
523
+ "keyboard",
524
+ "joystick",
525
+ "breakpoint",
526
+ "watchpoint",
527
+ "screenCapture",
528
+ "screenCompare",
529
+ "screenBaselines",
530
+ "screenDiscard",
531
+ "snapshot",
532
+ "snapshotFile",
533
+ "ioRead",
534
+ "cpuHistory",
535
+ "backtrace",
536
+ "timing",
537
+ "profile",
538
+ "memmap",
539
+ "vicii",
540
+ "sprites",
541
+ "cia",
542
+ "sid",
543
+ "observe",
544
+ "warp",
545
+ "window",
546
+ ] as const satisfies readonly ViceOperation[];
547
+
548
+ /** How many attachments an operation takes; a snapshotFile restore takes the .vsf. */
549
+ export function attachmentCount(op: ViceOperation, params?: unknown): number {
550
+ if (op === "snapshotFile") return isObject(params) && params.action === "restore" ? 1 : 0;
551
+ return op === "programLoad" || op === "autostart" || op === "diskAttach" ? 1 : 0;
552
+ }
553
+
554
+ /** Validates request parameters on the host. Throws WireFailure(invalid-input). */
555
+ export function validateViceParams<O extends ViceOperation>(op: O, params: unknown): ViceOperations[O]["params"] {
556
+ if (!isObject(params)) invalid("parameters must be an object");
557
+ switch (op) {
558
+ case "status":
559
+ case "vicii":
560
+ case "sid":
561
+ case "screenBaselines":
562
+ onlyFields(params, []);
563
+ return {} as ViceOperations[O]["params"];
564
+ case "screenCapture": {
565
+ onlyFields(params, ["baseline"]);
566
+ if (params.baseline === undefined) return {} as ViceOperations[O]["params"];
567
+ return { baseline: validName(params.baseline, "baseline") } as ViceOperations[O]["params"];
568
+ }
569
+ case "screenDiscard":
570
+ onlyFields(params, ["baseline"]);
571
+ return { baseline: validName(params.baseline, "baseline") } as ViceOperations[O]["params"];
572
+ case "snapshot": {
573
+ onlyFields(params, params.action === "list" ? ["action"] : ["action", "name"]);
574
+ if (params.action === "list") return { action: "list" } as ViceOperations[O]["params"];
575
+ if (params.action !== "save" && params.action !== "restore" && params.action !== "discard") {
576
+ invalid("action must be save, restore, list or discard");
577
+ }
578
+ return { action: params.action, name: validName(params.name, "name") } as ViceOperations[O]["params"];
579
+ }
580
+ case "snapshotFile": {
581
+ if (params.action === "restore") {
582
+ onlyFields(params, ["action"]);
583
+ return { action: "restore" } as ViceOperations[O]["params"];
584
+ }
585
+ if (params.action !== "export") invalid("action must be export or restore");
586
+ onlyFields(params, ["action", "name"]);
587
+ return { action: "export", name: validName(params.name, "name") } as ViceOperations[O]["params"];
588
+ }
589
+ case "screenCompare": {
590
+ onlyFields(params, ["baseline", "maxMismatchRatio", "mask", "includeDiff"]);
591
+ const ratio = params.maxMismatchRatio;
592
+ if (typeof ratio !== "number" || !(ratio >= 0 && ratio <= 1)) invalid("maxMismatchRatio must be a number from 0 to 1");
593
+ if (!Array.isArray(params.mask) || params.mask.length > MAX_MASK_RECTANGLES) invalid(`mask must be a list of at most ${MAX_MASK_RECTANGLES} rectangles`);
594
+ const mask = params.mask.map((rectangle): Rectangle => {
595
+ if (!isObject(rectangle)) invalid("each mask entry must be a rectangle");
596
+ onlyFields(rectangle, ["x", "y", "width", "height"], "a mask rectangle");
597
+ const ok = isInteger(rectangle.x, 0, 4095) && isInteger(rectangle.y, 0, 4095) && isInteger(rectangle.width, 1, 4096) && isInteger(rectangle.height, 1, 4096);
598
+ if (!ok) invalid("a mask rectangle needs integer x, y, width and height, with width and height at least 1");
599
+ return { x: rectangle.x as number, y: rectangle.y as number, width: rectangle.width as number, height: rectangle.height as number };
600
+ });
601
+ if (typeof params.includeDiff !== "boolean") invalid("includeDiff must be true or false");
602
+ return { baseline: validName(params.baseline, "baseline"), maxMismatchRatio: ratio, mask, includeDiff: params.includeDiff } as ViceOperations[O]["params"];
603
+ }
604
+ case "cpuHistory": {
605
+ onlyFields(params, ["limit", "space"]);
606
+ if (!isInteger(params.limit, 1, MAX_HISTORY)) invalid(`limit must be an integer from 1 to ${MAX_HISTORY}`);
607
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
608
+ return { limit: params.limit, space: params.space } as ViceOperations[O]["params"];
609
+ }
610
+ case "backtrace": {
611
+ onlyFields(params, ["depth", "space"]);
612
+ if (!isInteger(params.depth, 1, MAX_BACKTRACE)) invalid(`depth must be an integer from 1 to ${MAX_BACKTRACE}`);
613
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
614
+ return { depth: params.depth, space: params.space } as ViceOperations[O]["params"];
615
+ }
616
+ case "timing": {
617
+ onlyFields(params, ["action"]);
618
+ if (!isOneOf(TIMING_ACTIONS, params.action)) invalid("action must be start or read");
619
+ return { action: params.action } as ViceOperations[O]["params"];
620
+ }
621
+ case "observe": {
622
+ onlyFields(params, ["registers", "memory", "vicii", "sprites", "cia", "sid", "screen", "timing"]);
623
+ const result: ObserveParams = {};
624
+ if (params.registers !== undefined) {
625
+ if (!isOneOf(SPACES, params.registers)) invalid("registers must be c64 or drive8");
626
+ result.registers = params.registers;
627
+ }
628
+ if (params.memory !== undefined) {
629
+ const ranges = params.memory;
630
+ if (!Array.isArray(ranges) || ranges.length === 0 || ranges.length > MAX_OBSERVE_RANGES) {
631
+ invalid(`memory must hold 1 to ${MAX_OBSERVE_RANGES} ranges`);
632
+ }
633
+ result.memory = ranges.map((range) => validateViceParams("memoryRead", range));
634
+ if (result.memory.reduce((sum, range) => sum + range.size, 0) > MAX_OBSERVE_BYTES) {
635
+ invalid(`the memory ranges must hold at most ${MAX_OBSERVE_BYTES} bytes together`);
636
+ }
637
+ }
638
+ for (const flag of ["vicii", "sid", "screen", "timing"] as const) {
639
+ if (params[flag] === undefined) continue;
640
+ if (typeof params[flag] !== "boolean") invalid(`${flag} must be true or false`);
641
+ if (params[flag]) result[flag] = true;
642
+ }
643
+ if (params.sprites !== undefined) result.sprites = validateViceParams("sprites", { indexes: params.sprites }).indexes;
644
+ if (params.cia !== undefined) result.cia = validateViceParams("cia", { which: params.cia }).which;
645
+ if (Object.keys(result).length === 0) invalid("ask for at least one observation");
646
+ return result as ViceOperations[O]["params"];
647
+ }
648
+ case "profile": {
649
+ onlyFields(params, ["limit"]);
650
+ if (!isInteger(params.limit, 1, MAX_PROFILE)) invalid(`limit must be an integer from 1 to ${MAX_PROFILE}`);
651
+ return { limit: params.limit } as ViceOperations[O]["params"];
652
+ }
653
+ case "memmap": {
654
+ onlyFields(params, params.action === "clear" ? ["action"] : ["action", "start", "end", "maxRanges"]);
655
+ if (params.action === "clear") return { action: "clear" } as ViceOperations[O]["params"];
656
+ if (params.action !== "read") invalid("action must be read or clear");
657
+ if (!isInteger(params.start, 0, 0xffff) || !isInteger(params.end, 0, 0xffff)) invalid("start and end must be integers from 0 to 65535");
658
+ if (params.end < params.start) invalid("end must not be before start");
659
+ if (!isInteger(params.maxRanges, 1, MAX_MEMMAP_RANGES)) invalid(`maxRanges must be an integer from 1 to ${MAX_MEMMAP_RANGES}`);
660
+ return { action: "read", start: params.start, end: params.end, maxRanges: params.maxRanges } as ViceOperations[O]["params"];
661
+ }
662
+ case "sprites": {
663
+ onlyFields(params, ["indexes"]);
664
+ const indexes = params.indexes;
665
+ if (!Array.isArray(indexes) || !indexes.every((index) => isInteger(index, 0, 7))) invalid("sprites holds sprite numbers 0 to 7");
666
+ if (new Set(indexes).size !== indexes.length) invalid("sprites holds each sprite number once");
667
+ return { indexes: indexes as number[] } as ViceOperations[O]["params"];
668
+ }
669
+ case "cia": {
670
+ onlyFields(params, ["which"]);
671
+ if (!isOneOf(CIA_SELECTIONS, params.which)) invalid("cia must be 1, 2 or both");
672
+ return { which: params.which } as ViceOperations[O]["params"];
673
+ }
674
+ case "memoryRead": {
675
+ onlyFields(params, ["address", "size", "space", "view"]);
676
+ if (!isInteger(params.address, 0, 0xffff)) invalid("address must be an integer from 0 to 65535");
677
+ if (!isInteger(params.size, 1, MAX_MEMORY_READ)) invalid(`size must be an integer from 1 to ${MAX_MEMORY_READ}`);
678
+ if (params.address + params.size > 0x10000) invalid("the range runs past $ffff");
679
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
680
+ if (!isOneOf(MEMORY_VIEWS, params.view)) invalid("view must be cpu or ram");
681
+ const result: MemoryReadParams = { address: params.address, size: params.size, space: params.space, view: params.view };
682
+ return result as ViceOperations[O]["params"];
683
+ }
684
+ case "ioRead": {
685
+ onlyFields(params, ["address", "size"]);
686
+ if (!isInteger(params.address, 0xd000, 0xdfff)) invalid("address must be in the I/O area $d000-$dfff");
687
+ if (!isInteger(params.size, 1, 0x1000) || params.address + params.size > 0xe000) invalid("the range must stay inside $d000-$dfff");
688
+ return { address: params.address, size: params.size } as ViceOperations[O]["params"];
689
+ }
690
+ case "registersGet": {
691
+ onlyFields(params, ["space"]);
692
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
693
+ return { space: params.space } as ViceOperations[O]["params"];
694
+ }
695
+ case "memoryWrite": {
696
+ onlyFields(params, ["address", "data", "space", "view"]);
697
+ if (!isInteger(params.address, 0, 0xffff)) invalid("address must be an integer from 0 to 65535");
698
+ if (!isHexData(params.data) || params.data.length === 0) invalid("data must be lowercase hex bytes");
699
+ const size = params.data.length / 2;
700
+ if (size > MAX_MEMORY_WRITE) invalid(`data must be at most ${MAX_MEMORY_WRITE} bytes`);
701
+ if (params.address + size > 0x10000) invalid("the range runs past $ffff");
702
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
703
+ if (!isOneOf(MEMORY_VIEWS, params.view)) invalid("view must be cpu or ram");
704
+ const result: MemoryWriteParams = { address: params.address, data: params.data, space: params.space, view: params.view };
705
+ return result as ViceOperations[O]["params"];
706
+ }
707
+ case "memorySearch": {
708
+ onlyFields(params, ["start", "end", "pattern", "space", "view", "maxResults"]);
709
+ if (!isInteger(params.start, 0, 0xffff) || !isInteger(params.end, 0, 0xffff)) invalid("start and end must be integers from 0 to 65535");
710
+ if (params.end < params.start) invalid("end must not be before start");
711
+ const pattern = params.pattern;
712
+ if (!Array.isArray(pattern) || pattern.length === 0 || pattern.length > MAX_SEARCH_PATTERN) {
713
+ invalid(`pattern must have 1 to ${MAX_SEARCH_PATTERN} bytes`);
714
+ }
715
+ if (!pattern.every((token) => token === null || isInteger(token, 0, 0xff))) invalid("pattern holds bytes 0 to 255 or null wildcards");
716
+ if (pattern.every((token) => token === null)) invalid("pattern needs at least one byte that is not a wildcard");
717
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
718
+ if (!isOneOf(MEMORY_VIEWS, params.view)) invalid("view must be cpu or ram");
719
+ if (!isInteger(params.maxResults, 1, MAX_SEARCH_RESULTS)) invalid(`maxResults must be an integer from 1 to ${MAX_SEARCH_RESULTS}`);
720
+ return {
721
+ start: params.start,
722
+ end: params.end,
723
+ pattern: pattern as Array<number | null>,
724
+ space: params.space,
725
+ view: params.view,
726
+ maxResults: params.maxResults,
727
+ } as ViceOperations[O]["params"];
728
+ }
729
+ case "memoryCompare": {
730
+ onlyFields(params, ["left", "right", "size"]);
731
+ if (!isInteger(params.size, 1, MAX_COMPARE_SIZE)) invalid(`size must be an integer from 1 to ${MAX_COMPARE_SIZE}`);
732
+ const location = (value: unknown, name: string): MemoryLocation => {
733
+ if (!isObject(value)) invalid(`${name} must be an object`);
734
+ onlyFields(value, ["address", "space", "view"], name);
735
+ if (!isInteger(value.address, 0, 0xffff)) invalid(`${name} address must be an integer from 0 to 65535`);
736
+ if ((value.address as number) + (params.size as number) > 0x10000) invalid(`the ${name} range runs past $ffff`);
737
+ if (!isOneOf(SPACES, value.space)) invalid(`${name} space must be c64 or drive8`);
738
+ if (!isOneOf(MEMORY_VIEWS, value.view)) invalid(`${name} view must be cpu or ram`);
739
+ return { address: value.address as number, space: value.space, view: value.view };
740
+ };
741
+ return { left: location(params.left, "left"), right: location(params.right, "right"), size: params.size } as ViceOperations[O]["params"];
742
+ }
743
+ case "disassemble": {
744
+ onlyFields(params, ["address", "count", "space", "view"]);
745
+ if (!isInteger(params.address, 0, 0xffff)) invalid("address must be an integer from 0 to 65535");
746
+ if (!isInteger(params.count, 1, MAX_DISASSEMBLE)) invalid(`count must be an integer from 1 to ${MAX_DISASSEMBLE}`);
747
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
748
+ if (!isOneOf(MEMORY_VIEWS, params.view)) invalid("view must be cpu or ram");
749
+ return { address: params.address, count: params.count, space: params.space, view: params.view } as ViceOperations[O]["params"];
750
+ }
751
+ case "registersSet": {
752
+ onlyFields(params, ["space", "values"]);
753
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
754
+ const values = params.values;
755
+ if (!isObject(values)) invalid("values must be an object");
756
+ const known = ["pc", "a", "x", "y", "sp", "flags"];
757
+ for (const key of Object.keys(values)) if (!known.includes(key)) invalid(`values has an unknown register: ${key}`);
758
+ if (Object.keys(values).length === 0) invalid("values must name at least one register");
759
+ const result: RegisterValues = {};
760
+ if (values.pc !== undefined) {
761
+ if (!isInteger(values.pc, 0, 0xffff)) invalid("pc must be an integer from 0 to 65535");
762
+ result.pc = values.pc;
763
+ }
764
+ for (const name of ["a", "x", "y", "sp"] as const) {
765
+ if (values[name] === undefined) continue;
766
+ if (!isInteger(values[name], 0, 0xff)) invalid(`${name} must be an integer from 0 to 255`);
767
+ result[name] = values[name];
768
+ }
769
+ if (values.flags !== undefined) {
770
+ const flags = values.flags;
771
+ if (!isObject(flags)) invalid("flags must be an object");
772
+ const set: Partial<CpuFlags> = {};
773
+ for (const [flag, value] of Object.entries(flags)) {
774
+ if (!["n", "v", "b", "d", "i", "z", "c"].includes(flag)) invalid(`flags has an unknown flag: ${flag}`);
775
+ if (typeof value !== "boolean") invalid(`flag ${flag} must be true or false`);
776
+ set[flag as keyof CpuFlags] = value;
777
+ }
778
+ result.flags = set;
779
+ }
780
+ return { space: params.space, values: result } as ViceOperations[O]["params"];
781
+ }
782
+ case "execution": {
783
+ onlyFields(params, ["action", "space", "count", "anchor"]);
784
+ if (!isOneOf(EXECUTION_ACTIONS, params.action)) invalid(`action must be one of ${EXECUTION_ACTIONS.join(", ")}`);
785
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
786
+ const counted = params.action === "step" || params.action === "next" || params.action === "advance-frames";
787
+ if (params.count !== undefined) {
788
+ if (!counted) invalid(`count is not used with action ${params.action}`);
789
+ if (!isInteger(params.count, 1, MAX_EXECUTION_COUNT)) invalid(`count must be an integer from 1 to ${MAX_EXECUTION_COUNT}`);
790
+ } else if (params.action === "advance-frames") {
791
+ invalid("advance-frames needs count, the number of frames");
792
+ }
793
+ const result: ExecutionParams = { action: params.action, space: params.space };
794
+ if (counted) result.count = (params.count as number | undefined) ?? 1;
795
+ if (params.anchor !== undefined) {
796
+ if (params.action !== "advance-frames") invalid(`anchor is not used with action ${params.action}`);
797
+ const anchor = params.anchor;
798
+ 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");
799
+ onlyFields(anchor as Record<string, unknown>, ["line", "cycle"]);
800
+ result.anchor = { line: anchor.line as number, cycle: anchor.cycle as number };
801
+ }
802
+ return result as ViceOperations[O]["params"];
803
+ }
804
+ case "runUntil": {
805
+ onlyFields(params, ["target", "timeoutFrames"]);
806
+ if (!isInteger(params.timeoutFrames, 1, MAX_TIMEOUT_FRAMES)) invalid(`timeoutFrames must be an integer from 1 to ${MAX_TIMEOUT_FRAMES}`);
807
+ return { target: validateRunTarget(params.target), timeoutFrames: params.timeoutFrames } as ViceOperations[O]["params"];
808
+ }
809
+ case "programLoad": {
810
+ onlyFields(params, ["address"]);
811
+ if (params.address !== undefined && !isInteger(params.address, 0, 0xffff)) invalid("address must be an integer from 0 to 65535");
812
+ return (params.address === undefined ? {} : { address: params.address }) as ViceOperations[O]["params"];
813
+ }
814
+ case "autostart": {
815
+ onlyFields(params, ["type", "index", "run"]);
816
+ if (!isOneOf(AUTOSTART_TYPES, params.type)) invalid(`the file must be one of: ${AUTOSTART_TYPES.map((type) => `.${type}`).join(", ")}`);
817
+ if (!isInteger(params.index, 0, 0xffff)) invalid("index must be an integer from 0 to 65535");
818
+ if (typeof params.run !== "boolean") invalid("run must be true or false");
819
+ return { type: params.type, index: params.index, run: params.run } as ViceOperations[O]["params"];
820
+ }
821
+ case "diskAttach": {
822
+ onlyFields(params, ["type"]);
823
+ if (!isOneOf(DISK_TYPES, params.type)) invalid(`the disk image must be one of: ${DISK_TYPES.map((type) => `.${type}`).join(", ")}`);
824
+ return { type: params.type } as ViceOperations[O]["params"];
825
+ }
826
+ case "keyboard": {
827
+ onlyFields(params, ["data"]);
828
+ if (!isHexData(params.data) || params.data.length === 0) invalid("data must be PETSCII bytes as lowercase hex");
829
+ if (params.data.length / 2 > MAX_KEYBOARD_BYTES) invalid(`at most ${MAX_KEYBOARD_BYTES} bytes can be queued at once`);
830
+ // VICE feeds a C string, so a zero byte would end the text early.
831
+ if (/^(?:..)*?00/.test(params.data)) invalid("PETSCII byte 0 cannot be typed");
832
+ return { data: params.data } as ViceOperations[O]["params"];
833
+ }
834
+ case "joystick": {
835
+ onlyFields(params, ["port", "direction", "fire"]);
836
+ if (params.port !== 1 && params.port !== 2) invalid("port must be 1 or 2");
837
+ if (!isOneOf(JOYSTICK_DIRECTIONS, params.direction)) invalid(`direction must be one of ${JOYSTICK_DIRECTIONS.join(", ")}`);
838
+ if (typeof params.fire !== "boolean") invalid("fire must be true or false");
839
+ return { port: params.port, direction: params.direction, fire: params.fire } as ViceOperations[O]["params"];
840
+ }
841
+ case "breakpoint":
842
+ case "watchpoint": {
843
+ if (!isOneOf(CHECKPOINT_ACTIONS, params.action)) invalid(`action must be one of ${CHECKPOINT_ACTIONS.join(", ")}`);
844
+ if (params.action === "list") {
845
+ onlyFields(params, ["action"]);
846
+ return { action: "list" } as ViceOperations[O]["params"];
847
+ }
848
+ if (params.action !== "add") {
849
+ onlyFields(params, ["action", "id"]);
850
+ if (!isInteger(params.id, 1, 0xffff_ffff)) invalid("id must be a positive integer");
851
+ return { action: params.action, id: params.id } as ViceOperations[O]["params"];
852
+ }
853
+ onlyFields(params, op === "watchpoint" ? ["action", "address", "space", "size", "access", "condition"] : ["action", "address", "space", "size", "condition"]);
854
+ if (!isInteger(params.address, 0, 0xffff)) invalid("address must be an integer from 0 to 65535");
855
+ if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
856
+ const added: Record<string, unknown> = { action: "add", address: params.address, space: params.space };
857
+ if (op === "watchpoint") {
858
+ if (!isInteger(params.size, 1, MAX_WATCH_SIZE)) invalid(`size must be an integer from 1 to ${MAX_WATCH_SIZE}`);
859
+ if (params.address + params.size > 0x10000) invalid("the range runs past $ffff");
860
+ if (!isOneOf(WATCH_ACCESS, params.access)) invalid("access must be read, write or read-write");
861
+ added.size = params.size;
862
+ added.access = params.access;
863
+ } else if (params.size !== undefined) {
864
+ if (!isInteger(params.size, 1, 0x10000) || params.address + params.size > 0x10000) invalid("size must keep the range inside $0000-$ffff");
865
+ added.size = params.size;
866
+ }
867
+ if (params.condition !== undefined) added.condition = validateCondition(params.condition);
868
+ return added as ViceOperations[O]["params"];
869
+ }
870
+ case "reset": {
871
+ onlyFields(params, ["mode", "run"]);
872
+ if (!isOneOf(RESET_MODES, params.mode)) invalid("mode must be soft or hard");
873
+ if (typeof params.run !== "boolean") invalid("run must be true or false");
874
+ return { mode: params.mode, run: params.run } as ViceOperations[O]["params"];
875
+ }
876
+ case "warp": {
877
+ onlyFields(params, ["enabled"]);
878
+ if (typeof params.enabled !== "boolean") invalid("enabled must be true or false");
879
+ return { enabled: params.enabled } as ViceOperations[O]["params"];
880
+ }
881
+ case "window": {
882
+ onlyFields(params, ["action"]);
883
+ if (!isOneOf(WINDOW_ACTIONS, params.action)) invalid("action must be open or close");
884
+ return { action: params.action } as ViceOperations[O]["params"];
885
+ }
886
+ }
887
+ return invalid(`unknown operation: ${String(op)}`);
888
+ }
889
+
890
+ /**
891
+ * Validates a typed condition. Raster bounds here cover both video
892
+ * standards; the session checks them against its own standard.
893
+ */
894
+ export function validateCondition(value: unknown): Condition {
895
+ if (!isObject(value)) invalid("condition must be an object");
896
+ const byte = (field: unknown, name: string) => {
897
+ if (!isInteger(field, 0, 0xff)) invalid(`condition ${name} must be an integer from 0 to 255`);
898
+ return field;
899
+ };
900
+ switch (value.kind) {
901
+ case "register": {
902
+ onlyFields(value, ["kind", "register", "operator", "value"], "the condition");
903
+ if (!isOneOf(CONDITION_REGISTERS, value.register)) invalid("condition register must be a, x, y or sp");
904
+ if (!isOneOf(COMPARISONS, value.operator)) invalid(`condition operator must be one of ${COMPARISONS.join(", ")}`);
905
+ return { kind: "register", register: value.register, operator: value.operator, value: byte(value.value, "value") };
906
+ }
907
+ case "memory": {
908
+ onlyFields(value, ["kind", "address", "operator", "value", "space", "view"], "the condition");
909
+ if (!isInteger(value.address, 0, 0xffff)) invalid("condition address must be an integer from 0 to 65535");
910
+ if (!isOneOf(COMPARISONS, value.operator)) invalid(`condition operator must be one of ${COMPARISONS.join(", ")}`);
911
+ if (!isOneOf(SPACES, value.space)) invalid("condition space must be c64 or drive8");
912
+ if (!isOneOf(MEMORY_VIEWS, value.view)) invalid("condition view must be cpu or ram");
913
+ return {
914
+ kind: "memory",
915
+ address: value.address,
916
+ operator: value.operator,
917
+ value: byte(value.value, "value"),
918
+ space: value.space,
919
+ view: value.view,
920
+ };
921
+ }
922
+ case "raster": {
923
+ onlyFields(value, ["kind", "line", "cycle"], "the condition");
924
+ if (!isInteger(value.line, 0, RASTER.pal.lines - 1)) invalid("condition line must be a raster line number");
925
+ if (value.cycle !== undefined && !isInteger(value.cycle, 0, RASTER.ntsc.cycles - 1)) {
926
+ invalid("condition cycle must be a cycle number in the raster line");
927
+ }
928
+ return value.cycle === undefined ? { kind: "raster", line: value.line } : { kind: "raster", line: value.line, cycle: value.cycle };
929
+ }
930
+ }
931
+ return invalid("condition kind must be register, memory or raster");
932
+ }
933
+
934
+ function validateRunTarget(value: unknown): RunTarget {
935
+ if (!isObject(value)) invalid("target must be an object");
936
+ switch (value.kind) {
937
+ case "address": {
938
+ onlyFields(value, ["kind", "address", "space", "condition"], "the target");
939
+ if (!isInteger(value.address, 0, 0xffff)) invalid("target address must be an integer from 0 to 65535");
940
+ if (!isOneOf(SPACES, value.space)) invalid("target space must be c64 or drive8");
941
+ const target: RunTarget = { kind: "address", address: value.address, space: value.space };
942
+ if (value.condition !== undefined) target.condition = validateCondition(value.condition);
943
+ return target;
944
+ }
945
+ case "memory": {
946
+ onlyFields(value, ["kind", "address", "operator", "value", "space", "view"], "the target");
947
+ const { kind: _kind, ...condition } = validateCondition({ ...value, kind: "memory" }) as Extract<Condition, { kind: "memory" }>;
948
+ return { kind: "memory", ...condition };
949
+ }
950
+ case "raster": {
951
+ onlyFields(value, ["kind", "line", "cycle"], "the target");
952
+ const raster = validateCondition(value) as Extract<Condition, { kind: "raster" }>;
953
+ return raster;
954
+ }
955
+ }
956
+ return invalid("target kind must be address, memory or raster");
957
+ }
958
+
959
+ function isRemoved(value: unknown): value is PointRemoved {
960
+ return isObject(value) && value.removed === true && isInteger(value.id, 1, 0xffff_ffff) && Object.keys(value).length === 2;
961
+ }
962
+
963
+ function isBreakpoint(value: unknown): value is Breakpoint {
964
+ return (
965
+ isObject(value) &&
966
+ isInteger(value.id, 1, 0xffff_ffff) &&
967
+ isInteger(value.address, 0, 0xffff) &&
968
+ (value.size === undefined || isInteger(value.size, 1, 0x10000)) &&
969
+ isOneOf(SPACES, value.space) &&
970
+ typeof value.enabled === "boolean" &&
971
+ (value.condition === undefined || isCondition(value.condition))
972
+ );
973
+ }
974
+
975
+ function isCondition(value: unknown): value is Condition {
976
+ try {
977
+ validateCondition(value);
978
+ return true;
979
+ } catch {
980
+ return false;
981
+ }
982
+ }
983
+
984
+ function isWatchpoint(value: unknown): value is Watchpoint {
985
+ return isBreakpoint(value) && isObject(value) && isInteger(value.size, 1, MAX_WATCH_SIZE) && isOneOf(WATCH_ACCESS, value.access);
986
+ }
987
+
988
+ function validName(value: unknown, what: string): string {
989
+ if (typeof value !== "string" || !TRANSIENT_NAME.test(value)) {
990
+ invalid(`${what} must be 1 to 64 letters, digits, dots, underscores or hyphens`);
991
+ }
992
+ return value;
993
+ }
994
+
995
+ function isHexData(value: unknown): value is string {
996
+ return typeof value === "string" && /^(?:[0-9a-f]{2})*$/.test(value);
997
+ }
998
+
999
+ function isFlags(value: unknown): value is CpuFlags {
1000
+ return isObject(value) && (["n", "v", "b", "d", "i", "z", "c"] as const).every((flag) => typeof value[flag] === "boolean");
1001
+ }
1002
+
1003
+ /** Validates a reply result on the client. Throws ProtocolError on a malformed result. */
1004
+ export function validateViceResult<O extends ViceOperation>(op: O, value: unknown): ViceOperations[O]["result"] {
1005
+ if (!isObject(value)) throw new ProtocolError(`${op} result is not an object`);
1006
+ switch (op) {
1007
+ case "status": {
1008
+ const ok =
1009
+ isOneOf(RUN_STATES, value.state) &&
1010
+ isOneOf(VIDEO_STANDARDS, value.videoStandard) &&
1011
+ typeof value.warp === "boolean" &&
1012
+ typeof value.window === "boolean" &&
1013
+ (value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
1014
+ typeof value.cartridge === "boolean";
1015
+ if (!ok) throw new ProtocolError("status result is malformed");
1016
+ break;
1017
+ }
1018
+ case "memoryRead":
1019
+ case "ioRead": {
1020
+ if (!isInteger(value.address, 0, 0xffff) || !isHexData(value.data)) {
1021
+ throw new ProtocolError(`${op} result is malformed`);
1022
+ }
1023
+ break;
1024
+ }
1025
+ case "registersGet": {
1026
+ const ok =
1027
+ isInteger(value.pc, 0, 0xffff) &&
1028
+ (["a", "x", "y", "sp"] as const).every((name) => isInteger(value[name], 0, 0xff)) &&
1029
+ isFlags(value.flags);
1030
+ if (!ok) throw new ProtocolError("registersGet result is malformed");
1031
+ break;
1032
+ }
1033
+ case "memoryWrite": {
1034
+ if (!isInteger(value.address, 0, 0xffff) || !isInteger(value.bytesWritten, 1, MAX_MEMORY_WRITE)) {
1035
+ throw new ProtocolError("memoryWrite result is malformed");
1036
+ }
1037
+ break;
1038
+ }
1039
+ case "registersSet": {
1040
+ return validateViceResult("registersGet", value) as unknown as ViceOperations[O]["result"];
1041
+ }
1042
+ case "memorySearch": {
1043
+ if (!Array.isArray(value.matches) || !value.matches.every((match) => isInteger(match, 0, 0xffff))) {
1044
+ throw new ProtocolError("memorySearch result is malformed");
1045
+ }
1046
+ break;
1047
+ }
1048
+ case "memoryCompare": {
1049
+ const ok =
1050
+ typeof value.equal === "boolean" &&
1051
+ isInteger(value.differentBytes, 0, MAX_COMPARE_SIZE) &&
1052
+ Array.isArray(value.firstDifferences) &&
1053
+ value.firstDifferences.length <= MAX_COMPARE_DIFFERENCES &&
1054
+ value.firstDifferences.every(
1055
+ (difference) =>
1056
+ isObject(difference) &&
1057
+ isInteger(difference.offset, 0, MAX_COMPARE_SIZE - 1) &&
1058
+ isInteger(difference.left, 0, 0xff) &&
1059
+ isInteger(difference.right, 0, 0xff),
1060
+ );
1061
+ if (!ok) throw new ProtocolError("memoryCompare result is malformed");
1062
+ break;
1063
+ }
1064
+ case "disassemble": {
1065
+ const ok =
1066
+ Array.isArray(value.instructions) &&
1067
+ value.instructions.every(
1068
+ (instruction) =>
1069
+ isObject(instruction) &&
1070
+ isInteger(instruction.address, 0, 0xffff) &&
1071
+ isHexData(instruction.bytes) &&
1072
+ typeof instruction.text === "string",
1073
+ );
1074
+ if (!ok) throw new ProtocolError("disassemble result is malformed");
1075
+ break;
1076
+ }
1077
+ case "runUntil": {
1078
+ const ok =
1079
+ typeof value.reached === "boolean" &&
1080
+ isOneOf(STOP_REASONS, value.stopReason) &&
1081
+ isOneOf(RUN_STATES, value.state) &&
1082
+ isInteger(value.pc, 0, 0xffff) &&
1083
+ isOneOf(SPACES, value.space);
1084
+ if (!ok) throw new ProtocolError("runUntil result is malformed");
1085
+ break;
1086
+ }
1087
+ case "execution": {
1088
+ const ok =
1089
+ isOneOf(RUN_STATES, value.state) &&
1090
+ (value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
1091
+ (value.executed === undefined || isInteger(value.executed, 0, MAX_EXECUTION_COUNT)) &&
1092
+ (value.advancedFrames === undefined || isInteger(value.advancedFrames, 0, MAX_EXECUTION_COUNT));
1093
+ if (!ok) throw new ProtocolError("execution result is malformed");
1094
+ break;
1095
+ }
1096
+ case "reset":
1097
+ case "autostart": {
1098
+ if (!isOneOf(RUN_STATES, value.state)) throw new ProtocolError(`${op} result is malformed`);
1099
+ if (op === "reset" && value.pc !== undefined && !isInteger(value.pc, 0, 0xffff)) throw new ProtocolError("reset result is malformed");
1100
+ if (op === "autostart" && !(value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string")) throw new ProtocolError("autostart result is malformed");
1101
+ if (op === "autostart" && !(value.joysticksCentred === undefined || (Array.isArray(value.joysticksCentred) && value.joysticksCentred.every((port) => port === 1 || port === 2)))) {
1102
+ throw new ProtocolError("autostart result is malformed");
1103
+ }
1104
+ break;
1105
+ }
1106
+ case "programLoad": {
1107
+ const ok = isOneOf(RUN_STATES, value.state) && isInteger(value.loadAddress, 0, 0xffff) && isInteger(value.size, 1, 0x10000);
1108
+ if (!ok) throw new ProtocolError("programLoad result is malformed");
1109
+ break;
1110
+ }
1111
+ case "diskAttach": {
1112
+ if (typeof value.attached !== "boolean") throw new ProtocolError("diskAttach result is malformed");
1113
+ break;
1114
+ }
1115
+ case "keyboard": {
1116
+ if (!isInteger(value.queuedBytes, 1, MAX_KEYBOARD_BYTES) || !(value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string")) throw new ProtocolError("keyboard result is malformed");
1117
+ break;
1118
+ }
1119
+ case "breakpoint": {
1120
+ const ok = Array.isArray(value.breakpoints) ? value.breakpoints.every(isBreakpoint) : isRemoved(value) || isBreakpoint(value);
1121
+ if (!ok) throw new ProtocolError("breakpoint result is malformed");
1122
+ break;
1123
+ }
1124
+ case "watchpoint": {
1125
+ const ok = Array.isArray(value.watchpoints) ? value.watchpoints.every(isWatchpoint) : isRemoved(value) || isWatchpoint(value);
1126
+ if (!ok) throw new ProtocolError("watchpoint result is malformed");
1127
+ break;
1128
+ }
1129
+ case "screenCompare": {
1130
+ const bounds = value.bounds;
1131
+ const ok =
1132
+ typeof value.match === "boolean" &&
1133
+ isInteger(value.mismatchingPixels, 0, 1 << 20) &&
1134
+ isInteger(value.comparedPixels, 1, 1 << 20) &&
1135
+ (value.mismatchingPixels as number) <= (value.comparedPixels as number) &&
1136
+ typeof value.mismatchRatio === "number" &&
1137
+ value.mismatchRatio >= 0 &&
1138
+ value.mismatchRatio <= 1 &&
1139
+ (bounds === undefined ||
1140
+ (isObject(bounds) && (["x", "y", "width", "height"] as const).every((field) => isInteger(bounds[field], 0, 4096)))) &&
1141
+ (value.diffPng === undefined || (typeof value.diffPng === "string" && /^[A-Za-z0-9+/]+={0,2}$/.test(value.diffPng)));
1142
+ if (!ok) throw new ProtocolError("screenCompare result is malformed");
1143
+ break;
1144
+ }
1145
+ case "screenBaselines": {
1146
+ if (!Array.isArray(value.baselines) || !value.baselines.every((name) => typeof name === "string")) {
1147
+ throw new ProtocolError("screenBaselines result is malformed");
1148
+ }
1149
+ break;
1150
+ }
1151
+ case "screenDiscard": {
1152
+ if (value.discarded !== true) throw new ProtocolError("screenDiscard result is malformed");
1153
+ break;
1154
+ }
1155
+ case "snapshot": {
1156
+ const ok =
1157
+ (value.saved === true && typeof value.name === "string") ||
1158
+ (value.restored === true && isOneOf(RUN_STATES, value.state)) ||
1159
+ (Array.isArray(value.snapshots) && value.snapshots.every((name) => typeof name === "string")) ||
1160
+ value.discarded === true;
1161
+ if (!ok) throw new ProtocolError("snapshot result is malformed");
1162
+ break;
1163
+ }
1164
+ case "snapshotFile": {
1165
+ const ok = (value.exported === true && isInteger(value.bytes, 1, 0xffff_ffff)) || (value.restored === true && isOneOf(RUN_STATES, value.state));
1166
+ if (!ok) throw new ProtocolError("snapshotFile result is malformed");
1167
+ break;
1168
+ }
1169
+ case "cpuHistory": {
1170
+ const ok =
1171
+ Array.isArray(value.entries) &&
1172
+ value.entries.every(
1173
+ (entry) =>
1174
+ isObject(entry) &&
1175
+ isInteger(entry.address, 0, 0xffff) &&
1176
+ isHexData(entry.bytes) &&
1177
+ typeof entry.text === "string" &&
1178
+ (["a", "x", "y", "sp"] as const).every((name) => isInteger(entry[name], 0, 0xff)) &&
1179
+ (entry.rasterLine === undefined || isInteger(entry.rasterLine, 0, 511)) &&
1180
+ (entry.rasterCycle === undefined || isInteger(entry.rasterCycle, 0, 127)),
1181
+ );
1182
+ if (!ok) throw new ProtocolError("cpuHistory result is malformed");
1183
+ break;
1184
+ }
1185
+ case "backtrace": {
1186
+ const ok =
1187
+ Array.isArray(value.frames) &&
1188
+ value.frames.every(
1189
+ (frame) =>
1190
+ isObject(frame) && isInteger(frame.address, 0, 0xffff) && (frame.returnAddress === undefined || isInteger(frame.returnAddress, 0, 0xffff)),
1191
+ );
1192
+ if (!ok) throw new ProtocolError("backtrace result is malformed");
1193
+ break;
1194
+ }
1195
+ case "timing": {
1196
+ const ok = "cycles" in value ? typeof value.cycles === "string" && /^\d+$/.test(value.cycles) : value.started === true;
1197
+ if (!ok) throw new ProtocolError("timing result is malformed");
1198
+ break;
1199
+ }
1200
+ case "profile": {
1201
+ const ok =
1202
+ Array.isArray(value.entries) &&
1203
+ value.entries.every(
1204
+ (entry) =>
1205
+ isObject(entry) &&
1206
+ isInteger(entry.address, 0, 0xffff) &&
1207
+ typeof entry.totalCycles === "string" &&
1208
+ /^\d+$/.test(entry.totalCycles) &&
1209
+ typeof entry.selfCycles === "string" &&
1210
+ /^\d+$/.test(entry.selfCycles) &&
1211
+ typeof entry.percent === "number",
1212
+ );
1213
+ if (!ok) throw new ProtocolError("profile result is malformed");
1214
+ break;
1215
+ }
1216
+ case "memmap": {
1217
+ const isRange = (range: unknown) =>
1218
+ isObject(range) &&
1219
+ isInteger(range.start, 0, 0xffff) &&
1220
+ isInteger(range.end, 0, 0xffff) &&
1221
+ isInteger(range.flags, 1, 0xff);
1222
+ const ok = Array.isArray(value.ranges)
1223
+ ? value.ranges.every(isRange) && (value.next === undefined || isInteger(value.next, 0, 0xffff))
1224
+ : value.cleared === true;
1225
+ if (!ok) throw new ProtocolError("memmap result is malformed");
1226
+ break;
1227
+ }
1228
+ case "observe": {
1229
+ // Each present section must have the shape of the single-purpose result.
1230
+ if (value.registers !== undefined) validateViceResult("registersGet", value.registers);
1231
+ if (value.memory !== undefined) {
1232
+ const ok = Array.isArray(value.memory) && value.memory.every((range) => isObject(range) && isInteger(range.address, 0, 0xffff) && isHexData(range.data));
1233
+ if (!ok) throw new ProtocolError("observe memory is malformed");
1234
+ }
1235
+ if (value.vicii !== undefined) validateViceResult("vicii", value.vicii);
1236
+ if (value.sprites !== undefined) validateViceResult("sprites", { sprites: value.sprites });
1237
+ if (value.cia !== undefined) validateViceResult("cia", { chips: value.cia });
1238
+ if (value.sid !== undefined) validateViceResult("sid", value.sid);
1239
+ if (value.timing !== undefined) {
1240
+ const timing = value.timing;
1241
+ if (!isObject(timing) || !isInteger(timing.rasterLine, 0, 511) || !isInteger(timing.rasterCycle, 0, 127)) {
1242
+ throw new ProtocolError("observe timing is malformed");
1243
+ }
1244
+ }
1245
+ if (value.screen !== undefined) validateViceResult("screenCapture", value.screen);
1246
+ break;
1247
+ }
1248
+ case "vicii": {
1249
+ const ok =
1250
+ isInteger(value.rasterLine, 0, 511) &&
1251
+ isOneOf(VICII_MODES, value.mode) &&
1252
+ isInteger(value.screenAddress, 0, 0xffff) &&
1253
+ isInteger(value.graphicsAddress, 0, 0xffff) &&
1254
+ isInteger(value.scrollX, 0, 7) &&
1255
+ isInteger(value.scrollY, 0, 7) &&
1256
+ isInteger(value.borderColor, 0, 15) &&
1257
+ Array.isArray(value.backgroundColors) &&
1258
+ value.backgroundColors.length === 4 &&
1259
+ value.backgroundColors.every((color) => isInteger(color, 0, 15));
1260
+ if (!ok) throw new ProtocolError("vicii result is malformed");
1261
+ break;
1262
+ }
1263
+ case "sprites": {
1264
+ const isSprite = (sprite: unknown) =>
1265
+ isObject(sprite) &&
1266
+ isInteger(sprite.index, 0, 7) &&
1267
+ isInteger(sprite.x, 0, 511) &&
1268
+ isInteger(sprite.y, 0, 255) &&
1269
+ isInteger(sprite.color, 0, 15) &&
1270
+ isInteger(sprite.dataAddress, 0, 0xffff) &&
1271
+ (["enabled", "multicolor", "expandX", "expandY", "behindBackground"] as const).every((flag) => typeof sprite[flag] === "boolean");
1272
+ if (!Array.isArray(value.sprites) || !value.sprites.every(isSprite)) throw new ProtocolError("sprites result is malformed");
1273
+ break;
1274
+ }
1275
+ case "cia": {
1276
+ const isCia = (chip: unknown) =>
1277
+ isObject(chip) &&
1278
+ (chip.id === 1 || chip.id === 2) &&
1279
+ (["portA", "portB", "ddrA", "ddrB", "controlA", "controlB", "interruptStatus"] as const).every((name) => isInteger(chip[name], 0, 0xff)) &&
1280
+ isInteger(chip.interruptMask, 0, 0x1f) &&
1281
+ (["timerA", "timerB", "latchA", "latchB"] as const).every((name) => isInteger(chip[name], 0, 0xffff)) &&
1282
+ isObject(chip.tod) &&
1283
+ isInteger(chip.tod.hours, 0, 23) &&
1284
+ isInteger(chip.tod.minutes, 0, 99) &&
1285
+ isInteger(chip.tod.seconds, 0, 99) &&
1286
+ isInteger(chip.tod.tenths, 0, 15);
1287
+ if (!Array.isArray(value.chips) || !value.chips.every(isCia)) throw new ProtocolError("cia result is malformed");
1288
+ break;
1289
+ }
1290
+ case "sid": {
1291
+ const voices = value.voices;
1292
+ const ok =
1293
+ Array.isArray(voices) &&
1294
+ voices.length === 3 &&
1295
+ voices.every(
1296
+ (voice) =>
1297
+ isObject(voice) &&
1298
+ isInteger(voice.index, 1, 3) &&
1299
+ isInteger(voice.frequency, 0, 0xffff) &&
1300
+ isInteger(voice.pulseWidth, 0, 0x0fff) &&
1301
+ (["control", "attackDecay", "sustainRelease"] as const).every((name) => isInteger(voice[name], 0, 0xff)),
1302
+ ) &&
1303
+ isObject(value.filter) &&
1304
+ isInteger(value.filter.cutoff, 0, 0x7ff) &&
1305
+ isInteger(value.filter.resonance, 0, 15) &&
1306
+ isInteger(value.filter.routing, 0, 15) &&
1307
+ isInteger(value.filter.mode, 0, 15) &&
1308
+ isInteger(value.volume, 0, 15);
1309
+ if (!ok) throw new ProtocolError("sid result is malformed");
1310
+ break;
1311
+ }
1312
+ case "screenCapture": {
1313
+ const ok =
1314
+ isInteger(value.width, 1, 1024) &&
1315
+ isInteger(value.height, 1, 1024) &&
1316
+ typeof value.png === "string" &&
1317
+ /^[A-Za-z0-9+/]+={0,2}$/.test(value.png) &&
1318
+ (value.baseline === undefined || (typeof value.baseline === "string" && TRANSIENT_NAME.test(value.baseline)));
1319
+ if (!ok) throw new ProtocolError("screenCapture result is malformed");
1320
+ break;
1321
+ }
1322
+ case "joystick": {
1323
+ const ok =
1324
+ (value.port === 1 || value.port === 2) &&
1325
+ isOneOf(JOYSTICK_DIRECTIONS, value.direction) &&
1326
+ typeof value.fire === "boolean" &&
1327
+ (value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string");
1328
+ if (!ok) throw new ProtocolError("joystick result is malformed");
1329
+ break;
1330
+ }
1331
+ case "warp": {
1332
+ if (typeof value.enabled !== "boolean") throw new ProtocolError("warp result is malformed");
1333
+ break;
1334
+ }
1335
+ case "window": {
1336
+ const ok =
1337
+ typeof value.window === "boolean" &&
1338
+ isOneOf(RUN_STATES, value.state) &&
1339
+ Array.isArray(value.notCarried) &&
1340
+ value.notCarried.every((entry) => typeof entry === "string");
1341
+ if (!ok) throw new ProtocolError("window result is malformed");
1342
+ break;
1343
+ }
1344
+ default:
1345
+ throw new ProtocolError(`unknown operation: ${String(op)}`);
1346
+ }
1347
+ return value as unknown as ViceOperations[O]["result"];
1348
+ }