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

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