@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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 (162) 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 +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -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 +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -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-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,804 @@
1
+ // Translates C64-domain operations into binary-monitor commands. VICE quirks
2
+ // and register/bank id lookups live here; callers never see monitor details.
3
+
4
+ import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
5
+ import { type Comparison, type Condition, type BacktraceFrame, type HistoryEntry, type Instruction, type MemmapRange, type ProfileEntry, type JoystickState, type MemoryView, type RegisterValues, type Registers, type Space } from "../../protocol/vice.ts";
6
+ import { COVERAGE, formatC64Address, formatC64Byte } from "../../c64.ts";
7
+ import { decodeDisplay, decodePalette, visibleFrame, type IndexedFrame } from "./screen.ts";
8
+ import type { TextMonitor } from "./text-monitor.ts";
9
+ import type { CiaHidden } from "./chips.ts";
10
+ import {
11
+ BinaryMonitor,
12
+ Command,
13
+ decodeBanks,
14
+ decodeMemory,
15
+ decodeRegisters,
16
+ decodeRegistersAvailable,
17
+ memoryGetBody,
18
+ Memspace,
19
+ type RegisterInfo,
20
+ } from "./binary-monitor.ts";
21
+
22
+ const MEMSPACE: Record<Space, number> = { c64: Memspace.main, drive8: Memspace.drive8 };
23
+
24
+ /** The 6502 registers the domain needs, by VICE register name. */
25
+ const REGISTER_NAMES = { pc: "PC", a: "A", x: "X", y: "Y", sp: "SP", flags: "FL" } as const;
26
+ type RegisterKey = keyof typeof REGISTER_NAMES;
27
+ type RegisterIds = Record<RegisterKey, number>;
28
+
29
+ async function availableRegisters(monitor: BinaryMonitor, space: Space): Promise<RegisterInfo[]> {
30
+ return decodeRegistersAvailable((await monitor.request(Command.registersAvailable, Buffer.from([MEMSPACE[space]]))).body);
31
+ }
32
+
33
+ /** The id of the register `name` among the registers VICE reports for `space`. */
34
+ function registerId(available: RegisterInfo[], name: string, space: Space): number {
35
+ const register = available.find((candidate) => candidate.name === name);
36
+ if (register === undefined) throw new Error(`VICE reports no ${name} register for ${space}`);
37
+ return register.id;
38
+ }
39
+
40
+ function registerIds(available: RegisterInfo[], space: Space): RegisterIds {
41
+ const id = (key: RegisterKey) => registerId(available, REGISTER_NAMES[key], space);
42
+ return { pc: id("pc"), a: id("a"), x: id("x"), y: id("y"), sp: id("sp"), flags: id("flags") };
43
+ }
44
+
45
+ const OPERATORS: Record<Comparison, string> = { eq: "==", ne: "!=", lt: "<", lte: "<=", gt: ">", gte: ">=" };
46
+
47
+ /**
48
+ * Builds the VICE condition expression for a typed condition, for a
49
+ * checkpoint in `space`. VICE evaluates strictly left to right, so every
50
+ * comparison is parenthesized. Memory conditions work in the C64 space only.
51
+ */
52
+ export function conditionExpression(condition: Condition, space: Space): string {
53
+ const operator = (comparison: Comparison) => OPERATORS[comparison];
54
+ switch (condition.kind) {
55
+ case "register": {
56
+ const prefix = space === "drive8" ? "8:" : "";
57
+ return `(${prefix}${condition.register.toUpperCase()} ${operator(condition.operator)} ${formatC64Byte(condition.value)})`;
58
+ }
59
+ case "memory": {
60
+ if (condition.space !== "c64") {
61
+ throw new WireFailure("unsupported-in-space", "A memory condition can test only space c64 memory.");
62
+ }
63
+ return `(@${condition.view}:${formatC64Address(condition.address)} ${operator(condition.operator)} ${formatC64Byte(condition.value)})`;
64
+ }
65
+ case "raster": {
66
+ const line = `(RL == $${condition.line.toString(16)})`;
67
+ return condition.cycle === undefined ? line : `(${line} && (CY >= $${condition.cycle.toString(16)}))`;
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Lines a raster window spans, so a DMA stall cannot skip every instruction start in it. */
73
+ const RASTER_WINDOW_LINES = 4;
74
+
75
+ function rasterLine(line: number): string {
76
+ return `(RL == $${line.toString(16)})`;
77
+ }
78
+
79
+ /**
80
+ * True at the first instruction start at or after raster position
81
+ * (line, cycle): that line from the cycle on, or one of the next lines
82
+ * (wrapping at the end of the frame).
83
+ */
84
+ export function rasterWindowExpression(line: number, cycle: number, linesPerFrame: number): string {
85
+ const parts = [`(${rasterLine(line)} && (CY >= $${cycle.toString(16)}))`];
86
+ for (let offset = 1; offset < RASTER_WINDOW_LINES; offset++) parts.push(rasterLine((line + offset) % linesPerFrame));
87
+ return parts.join(" || ");
88
+ }
89
+
90
+ /** True anywhere in RASTER_WINDOW_LINES lines from `line` (wrapping). */
91
+ export function rasterLinesExpression(line: number, linesPerFrame: number): string {
92
+ const parts: string[] = [];
93
+ for (let offset = 0; offset < RASTER_WINDOW_LINES; offset++) parts.push(rasterLine((line + offset) % linesPerFrame));
94
+ return parts.join(" || ");
95
+ }
96
+
97
+ /** Whether a raster position lies inside the window rasterWindowExpression describes. */
98
+ export function inRasterWindow(position: { line: number; cycle: number }, line: number, cycle: number, linesPerFrame: number): boolean {
99
+ if (position.line === line) return position.cycle >= cycle;
100
+ const ahead = (position.line - line + linesPerFrame) % linesPerFrame;
101
+ return ahead > 0 && ahead < RASTER_WINDOW_LINES;
102
+ }
103
+
104
+ /** The 12 opcodes that jam an NMOS 6502/6510. */
105
+ export const JAM_OPCODES = new Set([0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2]);
106
+
107
+ const DIRECTION_BITS: Record<JoystickState["direction"], number> = {
108
+ center: 0,
109
+ up: 0x01,
110
+ down: 0x02,
111
+ left: 0x04,
112
+ right: 0x08,
113
+ "up-left": 0x05,
114
+ "up-right": 0x09,
115
+ "down-left": 0x06,
116
+ "down-right": 0x0a,
117
+ };
118
+
119
+ /** Control-port line levels as the CIA reads them: a pressed line is 0. */
120
+ export function joystickLines(state: JoystickState): number {
121
+ return 0x1f & ~(DIRECTION_BITS[state.direction] | (state.fire ? 0x10 : 0));
122
+ }
123
+
124
+ function hex4(value: number): string {
125
+ return value.toString(16).padStart(4, "0");
126
+ }
127
+
128
+ /**
129
+ * Parses monitor disassembly lines such as ".C:e5cf 85 CC STA $CC".
130
+ * Hex numbers in the text become lowercase. A line that wraps past $ffff ends the list.
131
+ * An answer with no instruction but other text is refused.
132
+ */
133
+ export function parseDisassembly(answer: string, start: number): Instruction[] {
134
+ const instructions: Instruction[] = [];
135
+ const unknown: string[] = [];
136
+ for (const line of answer.split("\n")) {
137
+ const match = /^\.(?:C|\d+):([0-9a-f]{4})\s+((?:[0-9a-f]{2} )*[0-9a-f]{2})\s+(.*?)\s*$/i.exec(line);
138
+ if (match === null) {
139
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
140
+ continue;
141
+ }
142
+ const address = Number.parseInt(match[1]!, 16);
143
+ if (address < start || (instructions.length > 0 && address <= instructions.at(-1)!.address)) break;
144
+ instructions.push({
145
+ address,
146
+ bytes: match[2]!.replace(/ /g, "").toLowerCase(),
147
+ text: match[3]!.replace(/\$([0-9A-Fa-f]+)/g, (_all, digits: string) => `$${digits.toLowerCase()}`),
148
+ });
149
+ }
150
+ if (instructions.length === 0 && unknown.length > 0) throw unknownForm("disassembly", unknown);
151
+ return instructions;
152
+ }
153
+
154
+ /**
155
+ * Parses monitor CPU history lines such as
156
+ * ".C:e5cd A5 C6 LDA $C6 A:00 X:00 Y:0a SP:f3 ..-...Z. 2535609".
157
+ * An answer with no entry but other text is refused.
158
+ */
159
+ export function parseHistory(answer: string): Array<Omit<HistoryEntry, "rasterLine" | "rasterCycle"> & { clock: bigint }> {
160
+ const entries = [];
161
+ const unknown: string[] = [];
162
+ for (const line of answer.split("\n")) {
163
+ const match =
164
+ /^\.(?:C|\d+):([0-9a-f]{4})\s+((?:[0-9a-f]{2} )*[0-9a-f]{2})\s+(.*?)\s+A:([0-9a-f]{2}) X:([0-9a-f]{2}) Y:([0-9a-f]{2}) SP:([0-9a-f]{2})\s+\S+\s+(\d+)\s*$/i.exec(
165
+ line,
166
+ );
167
+ if (match === null) {
168
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
169
+ continue;
170
+ }
171
+ entries.push({
172
+ address: Number.parseInt(match[1]!, 16),
173
+ bytes: match[2]!.replace(/ /g, "").toLowerCase(),
174
+ text: match[3]!.replace(/\$([0-9A-Fa-f]+)/g, (_all, digits: string) => `$${digits.toLowerCase()}`),
175
+ a: Number.parseInt(match[4]!, 16),
176
+ x: Number.parseInt(match[5]!, 16),
177
+ y: Number.parseInt(match[6]!, 16),
178
+ sp: Number.parseInt(match[7]!, 16),
179
+ clock: BigInt(match[8]!),
180
+ });
181
+ }
182
+ if (entries.length === 0 && unknown.length > 0) throw unknownForm("CPU history", unknown);
183
+ return entries;
184
+ }
185
+
186
+ /**
187
+ * Parses the addresses that the monitor's hunt prints, one per line; at most
188
+ * `limit` come back. An empty answer means no match. An answer with no
189
+ * address but other text is refused.
190
+ */
191
+ export function parseHunt(answer: string, limit: number): number[] {
192
+ const matches: number[] = [];
193
+ const unknown: string[] = [];
194
+ for (const line of answer.split("\n")) {
195
+ const match = /^([0-9a-f]{4})$/i.exec(line.trim());
196
+ if (match === null) {
197
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim())) unknown.push(line.trim());
198
+ continue;
199
+ }
200
+ if (matches.length < limit) matches.push(Number.parseInt(match[1]!, 16));
201
+ }
202
+ if (matches.length === 0 && unknown.length > 0) throw unknownForm("search result", unknown);
203
+ return matches;
204
+ }
205
+
206
+ /**
207
+ * Reconstructs the JSR call chain from the stack, most recent first. VICE's own
208
+ * backtrace keeps stale entries across resets and interrupts and can miss the
209
+ * current call, so the stack itself is the evidence: from SP+1 upward, a pushed
210
+ * return address R is accepted when the byte at R-2 is a JSR opcode; that JSR's
211
+ * target is the routine, and it returns to R+1. Interrupt frames are skipped.
212
+ * A best estimate: data on the stack can look like a return address.
213
+ */
214
+ export function backtraceFromStack(sp: number, memory: Uint8Array, depth: number): BacktraceFrame[] {
215
+ const frames: BacktraceFrame[] = [];
216
+ let at = sp + 1;
217
+ while (at <= 0xfe && frames.length < depth) {
218
+ const pushed = memory[0x0100 + at]! | (memory[0x0100 + at + 1]! << 8);
219
+ const jsr = (pushed - 2) & 0xffff;
220
+ if (memory[jsr] === 0x20) {
221
+ frames.push({ address: memory[(jsr + 1) & 0xffff]! | (memory[(jsr + 2) & 0xffff]! << 8), returnAddress: (pushed + 1) & 0xffff });
222
+ at += 2;
223
+ } else {
224
+ at += 1;
225
+ }
226
+ }
227
+ return frames;
228
+ }
229
+
230
+ /**
231
+ * Parses "profile flat" rows such as "36399 1.9% 19318 1.0% e9ff": total
232
+ * cycles, total share, self cycles, self share, routine. Rows that do not name
233
+ * a routine address (ROOT) are skipped.
234
+ */
235
+ export function parseProfile(answer: string): ProfileEntry[] {
236
+ const entries: ProfileEntry[] = [];
237
+ const unknown: string[] = [];
238
+ for (const line of answer.split("\n")) {
239
+ // Counts can carry the host locale's digit grouping ("1,123,200") and the percent a decimal comma.
240
+ // A profile of zero cycles, as after a window move restarts it, prints its percents as "-nan".
241
+ const row = new RegExp(`^\\s*(${COUNT})\\s+(?:[\\d.,]+|-?nan)%\\s+(${COUNT})\\s+([\\d.,]+|-?nan)%\\s+(\\S+)\\s*$`, "i").exec(line);
242
+ if (row === null) {
243
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim()) && !/^Total\s+%\s+Self\s+%$/.test(line.trim()) && !/^[-\s]+$/.test(line)) unknown.push(line.trim());
244
+ continue;
245
+ }
246
+ // A row named ROOT or by a label has no routine address.
247
+ if (!/^[0-9a-f]{4}$/i.test(row[4]!)) continue;
248
+ entries.push({ address: Number.parseInt(row[4]!, 16), totalCycles: digits(row[1]!), selfCycles: digits(row[2]!), percent: /nan/i.test(row[3]!) ? 0 : Number(row[3]!.replace(",", ".")) });
249
+ }
250
+ // An answer in another form must not read as "nothing ran".
251
+ if (entries.length === 0 && unknown.length > 0) throw unknownForm("profile", unknown);
252
+ return entries;
253
+ }
254
+
255
+ /** A count, perhaps grouped by thousands with a comma, a dot, an apostrophe or a (narrow) space. */
256
+ const COUNT = "\\d{1,3}(?:[,.'\\u00a0\\u202f ]\\d{3})+|\\d+";
257
+ const digits = (count: string) => count.replace(/\D/g, "");
258
+
259
+ function unknownForm(what: string, lines: string[]): WireFailure {
260
+ return new WireFailure("operation-failed", `VICE printed the ${what} in a form that c64-re-tools does not read: ${JSON.stringify(lines.slice(0, 3))}`);
261
+ }
262
+
263
+ /** The coverage byte (D40) of one memmap row's I/O, ROM and RAM columns, each "rwx" with "-" for no access. */
264
+ export function memmapFlags(io: string, rom: string, ram: string): number {
265
+ let flags = 0;
266
+ if (ram[0] === "r") flags |= COVERAGE.ramRead;
267
+ if (ram[1] === "w") flags |= COVERAGE.ramWrite;
268
+ if (ram[2] === "x") flags |= COVERAGE.ramExecute;
269
+ if (rom[0] === "r") flags |= COVERAGE.romRead;
270
+ if (rom[2] === "x") flags |= COVERAGE.romExecute;
271
+ if (io[0] === "r") flags |= COVERAGE.ioRead;
272
+ if (io[1] === "w") flags |= COVERAGE.ioWrite;
273
+ if (io[2] === "x") flags |= COVERAGE.ioExecute;
274
+ return flags;
275
+ }
276
+
277
+ /**
278
+ * Parses memmap rows such as "00c2: --- --- rw- (uninitialized read)": the I/O,
279
+ * ROM and RAM access flags of one address. Adjacent addresses with the same
280
+ * flags merge into one range; at most `maxRanges` come back. When more ranges
281
+ * follow, `next` is the first touched address that is not reported.
282
+ */
283
+ export function parseMemmap(answer: string, maxRanges: number): { ranges: MemmapRange[]; next?: number } {
284
+ const ranges: MemmapRange[] = [];
285
+ const unknown: string[] = [];
286
+ for (const line of answer.split("\n")) {
287
+ const match = /^([0-9a-f]{4}): (\S{3}) (\S{3}) (\S{3})/i.exec(line);
288
+ if (match === null) {
289
+ if (line.trim() !== "" && !CONDITION_LINE.test(line.trim()) && !/^addr:\s+IO\s+ROM\s+RAM$/.test(line.trim())) unknown.push(line.trim());
290
+ continue;
291
+ }
292
+ const address = Number.parseInt(match[1]!, 16);
293
+ const flags = memmapFlags(match[2]!, match[3]!, match[4]!);
294
+ if (flags === 0) continue;
295
+ const last = ranges.at(-1);
296
+ if (last !== undefined && last.end === address - 1 && last.flags === flags) {
297
+ last.end = address;
298
+ continue;
299
+ }
300
+ if (ranges.length >= maxRanges) return { ranges, next: address };
301
+ ranges.push({ start: address, end: address, flags });
302
+ }
303
+ if (ranges.length === 0 && unknown.length > 0) throw unknownForm("memory map", unknown);
304
+ return { ranges };
305
+ }
306
+
307
+ /**
308
+ * Lines VICE prints when a checkpoint stops the machine during a text command:
309
+ * "#1 (Stop on exec c000) 101/$065, 20/$14", then the instruction there.
310
+ */
311
+ const STOP_LINE = /^(?:#\d+ \((?:Stop on|Trace)\s|\.(?:C|\d+):[0-9a-f]{4}\s)/i;
312
+
313
+ /**
314
+ * The line VICE prints on the text monitor when the binary monitor sets a
315
+ * checkpoint condition. It can come late, in the answer of the next text
316
+ * command (seen on Windows).
317
+ */
318
+ const CONDITION_LINE = /^Setting checkpoint \d+ condition to: /;
319
+
320
+ /** The lines of a text command's answer that are not blank, not a stop line, not a condition line and not `known`. */
321
+ function otherLines(answer: string, known?: RegExp): string[] {
322
+ return answer
323
+ .split("\n")
324
+ .map((line) => line.trim())
325
+ .filter((line) => line !== "" && !STOP_LINE.test(line) && !CONDITION_LINE.test(line) && known?.test(line) !== true);
326
+ }
327
+
328
+ /** Refuses the answer of a text command that prints nothing, or only `known` lines, when it works. */
329
+ function expectQuiet(what: string, answer: string, known?: RegExp): void {
330
+ const other = otherLines(answer, known);
331
+ if (other.length > 0) throw unknownForm(what, other);
332
+ }
333
+
334
+ /** Quotes a host file path for a text-monitor command. Session scratch paths never contain quotes. */
335
+ function quoted(path: string): string {
336
+ if (/["\r\n]/.test(path)) throw new Error(`a monitor file path cannot contain quotes or line breaks: ${path}`);
337
+ return `"${path}"`;
338
+ }
339
+
340
+ export function statusRegisterFromFlags(flags: Registers["flags"]): number {
341
+ return (
342
+ (flags.n ? 0x80 : 0) |
343
+ (flags.v ? 0x40 : 0) |
344
+ 0x20 | // bit 5 always reads as 1 on the 6502
345
+ (flags.b ? 0x10 : 0) |
346
+ (flags.d ? 0x08 : 0) |
347
+ (flags.i ? 0x04 : 0) |
348
+ (flags.z ? 0x02 : 0) |
349
+ (flags.c ? 0x01 : 0)
350
+ );
351
+ }
352
+
353
+ export function flagsFromStatusRegister(value: number): Registers["flags"] {
354
+ return {
355
+ n: (value & 0x80) !== 0,
356
+ v: (value & 0x40) !== 0,
357
+ b: (value & 0x10) !== 0,
358
+ d: (value & 0x08) !== 0,
359
+ i: (value & 0x04) !== 0,
360
+ z: (value & 0x02) !== 0,
361
+ c: (value & 0x01) !== 0,
362
+ };
363
+ }
364
+
365
+ /** Generous bound for commands that run the machine (step/next): they answer only once it stops again. */
366
+ export const RUNNING_COMMAND_TIMEOUT_MS = 30_000;
367
+
368
+ /**
369
+ * Domain operations over one binary-monitor connection. Every method sends
370
+ * monitor commands, and any monitor command stops the machine; restoring the
371
+ * run state is the session's job.
372
+ */
373
+ export class ViceAdapter {
374
+ readonly #monitor: BinaryMonitor;
375
+ readonly #text: TextMonitor;
376
+ readonly #registerIds: Record<Space, RegisterIds>;
377
+ readonly #banks: Record<MemoryView | "rom" | "io", number>;
378
+ /** The C64's raster line and cycle registers. */
379
+ readonly #rasterIds: { line: number; cycle: number };
380
+ /** VICE numbers of the checkpoints set in drive 8's memory space. */
381
+ readonly #driveCheckpoints = new Set<number>();
382
+
383
+ private constructor(
384
+ monitor: BinaryMonitor,
385
+ text: TextMonitor,
386
+ registerIdsBySpace: Record<Space, RegisterIds>,
387
+ banks: Record<MemoryView | "rom" | "io", number>,
388
+ rasterIds: { line: number; cycle: number },
389
+ ) {
390
+ this.#monitor = monitor;
391
+ this.#text = text;
392
+ this.#registerIds = registerIdsBySpace;
393
+ this.#banks = banks;
394
+ this.#rasterIds = rasterIds;
395
+ }
396
+
397
+ /** Looks up the register and bank ids this VICE uses. Stops the machine. */
398
+ static async create(monitor: BinaryMonitor, text: TextMonitor): Promise<ViceAdapter> {
399
+ const banks = decodeBanks((await monitor.request(Command.banksAvailable)).body);
400
+ const bankId = (name: string) => {
401
+ const bank = banks.find((candidate) => candidate.name === name);
402
+ if (bank === undefined) throw new Error(`VICE reports no ${name} memory bank`);
403
+ return bank.id;
404
+ };
405
+ const c64Registers = await availableRegisters(monitor, "c64");
406
+ const driveRegisters = await availableRegisters(monitor, "drive8");
407
+ return new ViceAdapter(
408
+ monitor,
409
+ text,
410
+ { c64: registerIds(c64Registers, "c64"), drive8: registerIds(driveRegisters, "drive8") },
411
+ { cpu: bankId("cpu"), ram: bankId("ram"), rom: bankId("rom"), io: bankId("io") },
412
+ { line: registerId(c64Registers, "LIN", "c64"), cycle: registerId(c64Registers, "CYC", "c64") },
413
+ );
414
+ }
415
+
416
+ /** Reads memory as the selected CPU (view cpu) or as plain RAM (view ram, c64 only). Returns lowercase hex. */
417
+ async readMemory(options: { address: number; size: number; space: Space; view: MemoryView }): Promise<string> {
418
+ if (options.space === "drive8" && options.view === "ram") {
419
+ throw new WireFailure("unsupported-in-space", "view ram is available only in space c64; use view cpu for drive8.");
420
+ }
421
+ const response = await this.#monitor.request(
422
+ Command.memoryGet,
423
+ memoryGetBody({
424
+ start: options.address,
425
+ end: options.address + options.size - 1,
426
+ memspace: MEMSPACE[options.space],
427
+ bank: options.space === "c64" ? this.#banks[options.view] : 0,
428
+ }),
429
+ );
430
+ const bytes = decodeMemory(response.body);
431
+ if (bytes.length !== options.size) throw new Error(`VICE returned ${bytes.length} bytes for a ${options.size}-byte read`);
432
+ return bytes.toString("hex");
433
+ }
434
+
435
+ /**
436
+ * The timer latches and the interrupt mask of the CIA at `base`, from VICE's
437
+ * text-monitor dump: no register read gives them (D63).
438
+ */
439
+ async ciaHidden(base: 0xdc00 | 0xdd00): Promise<CiaHidden> {
440
+ const answer = await this.#computerText(`io ${hex4(base)}`);
441
+ const mask = /\(mask: ([0-9a-f]{2})\)/i.exec(answer);
442
+ const latchA = /Timer A: [0-9a-f]{4} \(latched ([0-9a-f]{4})\)/i.exec(answer);
443
+ const latchB = /Timer B: [0-9a-f]{4} \(latched ([0-9a-f]{4})\)/i.exec(answer);
444
+ if (mask === null || latchA === null || latchB === null) throw unknownForm("CIA dump", answer.split("\n"));
445
+ return { latchA: Number.parseInt(latchA[1]!, 16), latchB: Number.parseInt(latchB[1]!, 16), interruptMask: Number.parseInt(mask[1]!, 16) & 0x1f };
446
+ }
447
+
448
+ /** Reads C64 I/O registers through the I/O bank, whatever the CPU has banked in, without side effects. */
449
+ async readIo(start: number, size: number): Promise<Uint8Array> {
450
+ const response = await this.#monitor.request(
451
+ Command.memoryGet,
452
+ memoryGetBody({ start, end: start + size - 1, memspace: Memspace.main, bank: this.#banks.io }),
453
+ );
454
+ return decodeMemory(response.body);
455
+ }
456
+
457
+ /** Writes memory as the selected CPU (view cpu) or into plain RAM (view ram, c64 only). */
458
+ async writeMemory(options: { address: number; data: string; space: Space; view: MemoryView }): Promise<number> {
459
+ if (options.space === "drive8" && options.view === "ram") {
460
+ throw new WireFailure("unsupported-in-space", "view ram is available only in space c64; use view cpu for drive8.");
461
+ }
462
+ const bytes = Buffer.from(options.data, "hex");
463
+ const header = Buffer.alloc(8);
464
+ header[0] = 0; // no side effects
465
+ header.writeUInt16LE(options.address, 1);
466
+ header.writeUInt16LE(options.address + bytes.length - 1, 3);
467
+ header[5] = MEMSPACE[options.space];
468
+ header.writeUInt16LE(options.space === "c64" ? this.#banks[options.view] : 0, 6);
469
+ await this.#monitor.request(Command.memorySet, Buffer.concat([header, bytes]));
470
+ return bytes.length;
471
+ }
472
+
473
+ /** Sets any subset of the registers (flags merge into the current status register). Returns the full set. */
474
+ async writeRegisters(space: Space, values: RegisterValues): Promise<Registers> {
475
+ const current = await this.readRegisters(space);
476
+ const ids = this.#registerIds[space];
477
+ const items: Buffer[] = [];
478
+ const add = (key: RegisterKey, value: number) => {
479
+ const item = Buffer.alloc(4);
480
+ item[0] = 3; // item size after this byte
481
+ item[1] = ids[key];
482
+ item.writeUInt16LE(value, 2);
483
+ items.push(item);
484
+ };
485
+ if (values.pc !== undefined) add("pc", values.pc);
486
+ for (const key of ["a", "x", "y", "sp"] as const) if (values[key] !== undefined) add(key, values[key]);
487
+ if (values.flags !== undefined) add("flags", statusRegisterFromFlags({ ...current.flags, ...values.flags }));
488
+ const count = Buffer.alloc(2);
489
+ count.writeUInt16LE(items.length, 0);
490
+ await this.#monitor.request(Command.registersSet, Buffer.concat([Buffer.from([MEMSPACE[space]]), count, ...items]));
491
+ return this.readRegisters(space);
492
+ }
493
+
494
+ async readRegisters(space: Space): Promise<Registers> {
495
+ const values = decodeRegisters((await this.#monitor.request(Command.registersGet, Buffer.from([MEMSPACE[space]]))).body);
496
+ const ids = this.#registerIds[space];
497
+ const value = (key: RegisterKey) => {
498
+ const register = values.find((candidate) => candidate.id === ids[key]);
499
+ if (register === undefined) throw new Error(`VICE returned no ${REGISTER_NAMES[key]} register for ${space}`);
500
+ return register.value;
501
+ };
502
+ return {
503
+ pc: value("pc") & 0xffff,
504
+ a: value("a") & 0xff,
505
+ x: value("x") & 0xff,
506
+ y: value("y") & 0xff,
507
+ sp: value("sp") & 0xff,
508
+ flags: flagsFromStatusRegister(value("flags")),
509
+ };
510
+ }
511
+
512
+ /** The raster line and the cycle in it where the C64 stopped. */
513
+ async rasterPosition(): Promise<{ line: number; cycle: number }> {
514
+ const values = decodeRegisters((await this.#monitor.request(Command.registersGet, Buffer.from([Memspace.main]))).body);
515
+ const value = (id: number) => {
516
+ const register = values.find((candidate) => candidate.id === id);
517
+ if (register === undefined) throw new Error("VICE returned no raster registers");
518
+ return register.value;
519
+ };
520
+ return { line: value(this.#rasterIds.line), cycle: value(this.#rasterIds.cycle) };
521
+ }
522
+
523
+ /** Leaves the monitor so the machine runs again. */
524
+ async resume(): Promise<void> {
525
+ await this.#monitor.request(Command.exit);
526
+ }
527
+
528
+ /** Stops the machine (any monitor command does). */
529
+ async stop(): Promise<void> {
530
+ await this.#monitor.request(Command.ping);
531
+ }
532
+
533
+ /**
534
+ * Waits, with no time limit, until VICE answers a ping: VICE answers in
535
+ * order, so every binary command sent before it has run. Stops the machine.
536
+ */
537
+ async answered(): Promise<void> {
538
+ await this.#monitor.request(Command.ping, new Uint8Array(0), Infinity);
539
+ }
540
+
541
+ /**
542
+ * Runs a text command that acts on the monitor's default device, with the
543
+ * computer as that device. VICE makes drive 8 the default device when a
544
+ * drive checkpoint stops the machine, and commands such as
545
+ * load, d, hunt, mmsh and stopwatch then act on the drive instead.
546
+ */
547
+ async #computerText(command: string, timeoutMs?: number): Promise<string> {
548
+ await this.#text.command("dev c:");
549
+ return timeoutMs === undefined ? this.#text.command(command) : this.#text.command(command, timeoutMs);
550
+ }
551
+
552
+ /** Waits, with no time limit, until a harmless text command has run, so every text command sent before it has run. */
553
+ async drainText(): Promise<void> {
554
+ await this.#text.command("~ $0000", Infinity);
555
+ }
556
+
557
+ /** Executes `count` C64 instructions, with subroutine calls as one when `over`. Answers once stopped again. */
558
+ async step(count: number, over: boolean, timeoutMs = RUNNING_COMMAND_TIMEOUT_MS): Promise<void> {
559
+ const body = Buffer.alloc(3);
560
+ body[0] = over ? 1 : 0;
561
+ body.writeUInt16LE(count, 1);
562
+ await this.#monitor.request(Command.advanceInstructions, body, timeoutMs);
563
+ }
564
+
565
+ /** Runs until just after the next RTS/RTI. Answers at once; the machine stops later. */
566
+ async untilReturn(): Promise<void> {
567
+ await this.#monitor.request(Command.executeUntilReturn);
568
+ }
569
+
570
+ /** Resets the C64 (soft) or power-cycles it (hard). The machine runs afterwards. */
571
+ async reset(mode: "soft" | "hard"): Promise<void> {
572
+ await this.#monitor.request(Command.reset, Buffer.from([mode === "hard" ? 1 : 0]));
573
+ }
574
+
575
+ /** Where the CPU starts after a reset: the vector at $fffc in ROM. */
576
+ async resetVector(): Promise<number> {
577
+ const bytes = decodeMemory(
578
+ (await this.#monitor.request(Command.memoryGet, memoryGetBody({ start: 0xfffc, end: 0xfffd, memspace: Memspace.main, bank: this.#banks.rom }))).body,
579
+ );
580
+ return bytes.readUInt16LE(0);
581
+ }
582
+
583
+ /**
584
+ * Adds a stopping checkpoint over [start, end]: exec for a breakpoint;
585
+ * load/store for a watchpoint. Returns its VICE number. Never a VICE
586
+ * "temporary" checkpoint: in VICE, setting one resumes the machine (like
587
+ * the text monitor's until), whatever the manual says.
588
+ */
589
+ async addCheckpoint(options: { start: number; end: number; operation: number; space: Space; enabled?: boolean }): Promise<number> {
590
+ const body = Buffer.alloc(9);
591
+ body.writeUInt16LE(options.start, 0);
592
+ body.writeUInt16LE(options.end, 2);
593
+ body[4] = 1; // stop when hit
594
+ body[5] = options.enabled === false ? 0 : 1;
595
+ body[6] = options.operation;
596
+ body[7] = 0; // not temporary
597
+ body[8] = MEMSPACE[options.space];
598
+ const info = await this.#monitor.request(Command.checkpointSet, body);
599
+ const number = info.body.readUInt32LE(0);
600
+ if (options.space === "drive8") this.#driveCheckpoints.add(number);
601
+ return number;
602
+ }
603
+
604
+ /** True for a checkpoint in drive 8's memory space: its stop leaves the computer's CPU between two cycles of an instruction. */
605
+ isDriveCheckpoint(number: number): boolean {
606
+ return this.#driveCheckpoints.has(number);
607
+ }
608
+
609
+ async setCondition(number: number, expression: string): Promise<void> {
610
+ const text = Buffer.from(expression, "latin1");
611
+ if (text.length > 255) throw new Error("condition expression is too long");
612
+ const head = Buffer.alloc(5);
613
+ head.writeUInt32LE(number, 0);
614
+ head[4] = text.length;
615
+ await this.#monitor.request(Command.conditionSet, Buffer.concat([head, text]));
616
+ }
617
+
618
+ async toggleCheckpoint(number: number, enabled: boolean): Promise<void> {
619
+ const body = Buffer.alloc(5);
620
+ body.writeUInt32LE(number, 0);
621
+ body[4] = enabled ? 1 : 0;
622
+ await this.#monitor.request(Command.checkpointToggle, body);
623
+ }
624
+
625
+ async deleteCheckpoint(number: number): Promise<void> {
626
+ const body = Buffer.alloc(4);
627
+ body.writeUInt32LE(number, 0);
628
+ await this.#monitor.request(Command.checkpointDelete, body);
629
+ this.#driveCheckpoints.delete(number);
630
+ }
631
+
632
+ /**
633
+ * Loads a PRG file from the host file system into memory, where the CPU
634
+ * would write it, without reset or start. With `address`, the file's own
635
+ * load address is skipped. Returns where it went.
636
+ */
637
+ async loadProgram(file: string, address?: number): Promise<{ loadAddress: number; size: number }> {
638
+ const target = address === undefined ? "" : ` $${address.toString(16).padStart(4, "0")}`;
639
+ const answer = await this.#computerText(`load ${quoted(file)} 0${target}`);
640
+ const match = /from ([0-9A-Fa-f]{4}) to ([0-9A-Fa-f]{4}) \(([0-9A-Fa-f]+) bytes\)/.exec(answer);
641
+ if (match === null) throw new WireFailure("media-error", "The emulator could not load the program.");
642
+ return { loadAddress: Number.parseInt(match[1]!, 16), size: Number.parseInt(match[3]!, 16) };
643
+ }
644
+
645
+ /** Autostarts a program or image file from the host file system. The machine runs afterwards. */
646
+ async autostart(file: string, index: number, run: boolean): Promise<void> {
647
+ const name = Buffer.from(file, "utf8");
648
+ if (name.length > 255) throw new Error("autostart file path is too long");
649
+ const head = Buffer.alloc(4);
650
+ head[0] = run ? 1 : 0;
651
+ head.writeUInt16LE(index, 1);
652
+ head[3] = name.length;
653
+ await this.#monitor.request(Command.autostart, Buffer.concat([head, name]));
654
+ }
655
+
656
+ /** Detaches every cartridge. VICE powers the cartridge off; the C64 keeps running. */
657
+ async detachCartridge(): Promise<void> {
658
+ // Device 32 is the cartridge port; the monitor reads numbers as hex. VICE prints nothing when it detaches.
659
+ expectQuiet("cartridge detach answer", await this.#text.command("detach 20"));
660
+ }
661
+
662
+ /** Attaches a disk image file from the host file system to drive 8. */
663
+ async attachDisk(file: string): Promise<void> {
664
+ const answer = await this.#text.command(`attach ${quoted(file)} 8`);
665
+ // VICE prints nothing when the image attaches, and "Failed." when it does not.
666
+ const other = otherLines(answer);
667
+ if (other.includes("Failed.")) throw new WireFailure("media-error", "The emulator could not attach the disk image.");
668
+ if (other.length > 0) throw unknownForm("disk attach answer", other);
669
+ }
670
+
671
+ /** Queues PETSCII bytes in VICE's keyboard buffer feed, at most 255 per command. */
672
+ async feedKeyboard(bytes: Buffer): Promise<void> {
673
+ for (let offset = 0; offset < bytes.length; offset += 255) {
674
+ const chunk = bytes.subarray(offset, offset + 255);
675
+ await this.#monitor.request(Command.keyboardFeed, Buffer.concat([Buffer.from([chunk.length]), chunk]));
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Sets a control port's lines. The binary monitor drives VICE's "Joyport
681
+ * I/O simulation" device, which the launch puts in both control ports; it
682
+ * takes raw active-low line levels and 0-based port indexes.
683
+ */
684
+ async setJoystick(state: JoystickState): Promise<void> {
685
+ const body = Buffer.alloc(4);
686
+ body.writeUInt16LE(state.port - 1, 0);
687
+ body.writeUInt16LE(joystickLines(state), 2);
688
+ await this.#monitor.request(Command.joyportSet, body);
689
+ }
690
+
691
+ /** The last frame the VIC-II drew, visible area with borders, with VICE's palette. */
692
+ async captureFrame(standard: VideoStandard): Promise<{ frame: IndexedFrame; palette: Array<[number, number, number]> }> {
693
+ const display = decodeDisplay((await this.#monitor.request(Command.displayGet, Buffer.from([1, 0]))).body);
694
+ const palette = decodePalette((await this.#monitor.request(Command.paletteGet, Buffer.from([1]))).body);
695
+ return { frame: visibleFrame(display, standard), palette };
696
+ }
697
+
698
+ /**
699
+ * Runs text-monitor work with the monitor's default device and bank set to
700
+ * a space and view, then restores the defaults (device c:, bank cpu).
701
+ */
702
+ async #inTextContext<T>(space: Space, view: MemoryView, work: () => Promise<T>): Promise<T> {
703
+ if (space === "drive8" && view === "ram") {
704
+ throw new WireFailure("unsupported-in-space", "view ram is available only in space c64; use view cpu for drive8.");
705
+ }
706
+ if (space === "drive8") await this.#text.command("dev 8:");
707
+ else await this.#computerText(`bank ${view}`);
708
+ try {
709
+ return await work();
710
+ } finally {
711
+ if (space === "drive8") await this.#text.command("dev c:");
712
+ else await this.#text.command("bank cpu");
713
+ }
714
+ }
715
+
716
+ /** Finds a byte pattern (null = any byte) with the monitor's hunt. Returns match addresses, at most `limit`. */
717
+ async search(options: { start: number; end: number; pattern: Array<number | null>; space: Space; view: MemoryView; limit: number }): Promise<number[]> {
718
+ const tokens = options.pattern.map((token) => (token === null ? "xx" : token.toString(16).padStart(2, "0"))).join(" ");
719
+ const answer = await this.#inTextContext(options.space, options.view, () =>
720
+ this.#text.command(`hunt ${hex4(options.start)} ${hex4(options.end)} ${tokens}`, 30_000),
721
+ );
722
+ return parseHunt(answer, options.limit);
723
+ }
724
+
725
+ /** Disassembles `count` instructions from `address` with the monitor; stops at the end of memory. */
726
+ async disassemble(options: { address: number; count: number; space: Space; view: MemoryView }): Promise<Instruction[]> {
727
+ const end = Math.min(0xffff, options.address + options.count * 3 - 1);
728
+ const answer = await this.#inTextContext(options.space, options.view, () =>
729
+ this.#text.command(`d ${hex4(options.address)} ${hex4(end)}`),
730
+ );
731
+ return parseDisassembly(answer, options.address).slice(0, options.count);
732
+ }
733
+
734
+ /** The last `limit` instructions the CPU of a space executed, oldest first, with the clock each started at. */
735
+ async history(limit: number, space: Space): Promise<Array<Omit<HistoryEntry, "rasterLine" | "rasterCycle"> & { clock: bigint }>> {
736
+ const answer = await this.#text.command(`chis ${limit} ${space === "drive8" ? "8:" : "c:"}`);
737
+ // Some VICE builds print one more, older entry than asked for; the newest count.
738
+ return parseHistory(answer).slice(-limit);
739
+ }
740
+
741
+ /** The current CPU cycle count of the computer. */
742
+ async clock(): Promise<bigint> {
743
+ const answer = await this.#computerText("stopwatch");
744
+ const match = /Stopwatch:\s+(\d+)/.exec(answer);
745
+ if (match === null) throw unknownForm("stopwatch", answer.split("\n"));
746
+ return BigInt(match[1]!);
747
+ }
748
+
749
+ /** The CPU's view of all 64 KiB of a space at once, for whole-memory analyses. */
750
+ async readAll(space: Space): Promise<Uint8Array> {
751
+ const response = await this.#monitor.request(
752
+ Command.memoryGet,
753
+ memoryGetBody({ start: 0x0000, end: 0xffff, memspace: MEMSPACE[space], bank: space === "c64" ? this.#banks.cpu : 0 }),
754
+ );
755
+ const memory = decodeMemory(response.body);
756
+ if (memory.length !== 0x10000) throw new Error(`VICE returned ${memory.length} bytes for a 64 KiB read`);
757
+ return memory;
758
+ }
759
+
760
+ /** Saves the whole machine, attached disks included, to a host file. */
761
+ async saveSnapshot(file: string): Promise<void> {
762
+ const name = Buffer.from(file, "utf8");
763
+ if (name.length > 255) throw new Error("snapshot file path is too long");
764
+ // Byte 0: no ROMs (VICE has them). Byte 1: save disks, so a restore gets the disk state too.
765
+ await this.#monitor.request(Command.dump, Buffer.concat([Buffer.from([0, 1, name.length]), name]), 30_000);
766
+ }
767
+
768
+ /** Restores the machine from a snapshot file. The machine stays stopped. */
769
+ async restoreSnapshot(file: string): Promise<void> {
770
+ const name = Buffer.from(file, "utf8");
771
+ if (name.length > 255) throw new Error("snapshot file path is too long");
772
+ await this.#monitor.request(Command.undump, Buffer.concat([Buffer.from([name.length]), name]), 30_000);
773
+ }
774
+
775
+ async startProfiler(): Promise<void> {
776
+ expectQuiet("profiler answer", await this.#computerText("profile on"), /^Profiling (?:re)?started\.$/);
777
+ }
778
+
779
+ /** The routines with the most self time, from VICE's profiler. */
780
+ async profile(limit: number): Promise<ProfileEntry[]> {
781
+ return parseProfile(await this.#computerText(`profile flat ${limit}`)).slice(0, limit);
782
+ }
783
+
784
+ /** How the C64 CPU accessed [start, end] since the memmap was last cleared, as merged ranges. */
785
+ async memmap(start: number, end: number, maxRanges: number): Promise<{ ranges: MemmapRange[]; next?: number }> {
786
+ // The first argument is the access mask ("ioRWXrwx"); ff shows every kind of access.
787
+ return parseMemmap(await this.#computerText(`mmsh ff ${hex4(start)} ${hex4(end)}`, 60_000), maxRanges);
788
+ }
789
+
790
+ async clearMemmap(): Promise<void> {
791
+ expectQuiet("memory map answer", await this.#computerText("mmzap"));
792
+ }
793
+
794
+ async warp(): Promise<boolean> {
795
+ const answer = await this.#text.command("warp");
796
+ const match = /Warp mode is (on|off)/.exec(answer);
797
+ if (match === null) throw unknownForm("warp state", answer.split("\n"));
798
+ return match[1] === "on";
799
+ }
800
+
801
+ async setWarp(enabled: boolean): Promise<void> {
802
+ expectQuiet("warp answer", await this.#text.command(enabled ? "warp on" : "warp off"));
803
+ }
804
+ }