@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/protocol/vice.ts
CHANGED
|
@@ -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"
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
(
|
|
1115
|
-
const ok = Array.isArray(value.ranges)
|
|
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.
|
|
1172
|
-
isInteger(chip
|
|
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 =
|
|
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
|
}
|