@henols/c64-re-tools 1.1.0 → 2.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +166 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +126 -0
- package/skills/c64-source/scripts/generate.ts +490 -0
- package/skills/c64-source/scripts/loader.ts +571 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +595 -0
- package/skills/c64-static-analysis/SKILL.md +119 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +163 -0
- package/src/knowledge/import.ts +391 -0
- package/src/knowledge/read.ts +342 -0
- package/src/knowledge/schema.ts +229 -0
- package/src/knowledge/write.ts +663 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// VICE binary remote monitor client (VICE manual, chapter "Binary monitor",
|
|
2
|
+
// API version 2). Owns framing, request/response correlation and body
|
|
3
|
+
// decoding. Nothing outside src/host/vice sees these bytes.
|
|
4
|
+
|
|
5
|
+
import type { Socket } from "node:net";
|
|
6
|
+
|
|
7
|
+
import { connectWithTimeout } from "./connect.ts";
|
|
8
|
+
|
|
9
|
+
export const STX = 0x02;
|
|
10
|
+
export const API_VERSION = 0x02;
|
|
11
|
+
/** Request id VICE uses for responses it sends unprompted (events). */
|
|
12
|
+
export const EVENT_REQUEST_ID = 0xffff_ffff;
|
|
13
|
+
|
|
14
|
+
const COMMAND_HEADER_BYTES = 11;
|
|
15
|
+
const RESPONSE_HEADER_BYTES = 12;
|
|
16
|
+
|
|
17
|
+
/** Command type bytes. */
|
|
18
|
+
export const Command = {
|
|
19
|
+
memoryGet: 0x01,
|
|
20
|
+
memorySet: 0x02,
|
|
21
|
+
checkpointGet: 0x11,
|
|
22
|
+
checkpointSet: 0x12,
|
|
23
|
+
checkpointDelete: 0x13,
|
|
24
|
+
checkpointList: 0x14,
|
|
25
|
+
checkpointToggle: 0x15,
|
|
26
|
+
conditionSet: 0x22,
|
|
27
|
+
registersGet: 0x31,
|
|
28
|
+
registersSet: 0x32,
|
|
29
|
+
dump: 0x41,
|
|
30
|
+
undump: 0x42,
|
|
31
|
+
resourceGet: 0x51,
|
|
32
|
+
resourceSet: 0x52,
|
|
33
|
+
advanceInstructions: 0x71,
|
|
34
|
+
keyboardFeed: 0x72,
|
|
35
|
+
executeUntilReturn: 0x73,
|
|
36
|
+
ping: 0x81,
|
|
37
|
+
banksAvailable: 0x82,
|
|
38
|
+
registersAvailable: 0x83,
|
|
39
|
+
displayGet: 0x84,
|
|
40
|
+
viceInfo: 0x85,
|
|
41
|
+
paletteGet: 0x91,
|
|
42
|
+
joyportSet: 0xa2,
|
|
43
|
+
userportSet: 0xb2,
|
|
44
|
+
exit: 0xaa,
|
|
45
|
+
quit: 0xbb,
|
|
46
|
+
reset: 0xcc,
|
|
47
|
+
autostart: 0xdd,
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
/** Response types that are not simply the command byte. */
|
|
51
|
+
const TERMINAL_RESPONSE: Partial<Record<number, number>> = {
|
|
52
|
+
[Command.checkpointGet]: 0x11,
|
|
53
|
+
[Command.checkpointSet]: 0x11,
|
|
54
|
+
[Command.registersSet]: 0x31,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Response types VICE sends as events (request id EVENT_REQUEST_ID). */
|
|
58
|
+
export const ResponseType = {
|
|
59
|
+
invalid: 0x00,
|
|
60
|
+
checkpointInfo: 0x11,
|
|
61
|
+
registerInfo: 0x31,
|
|
62
|
+
jam: 0x61,
|
|
63
|
+
stopped: 0x62,
|
|
64
|
+
resumed: 0x63,
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
/** The memspace byte. */
|
|
68
|
+
export const Memspace = {
|
|
69
|
+
main: 0x00,
|
|
70
|
+
drive8: 0x01,
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
export interface MonitorResponse {
|
|
74
|
+
type: number;
|
|
75
|
+
error: number;
|
|
76
|
+
requestId: number;
|
|
77
|
+
body: Buffer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** VICE answered a command with a non-zero error code. */
|
|
81
|
+
export class MonitorError extends Error {
|
|
82
|
+
override name = "MonitorError";
|
|
83
|
+
readonly command: number;
|
|
84
|
+
readonly errorCode: number;
|
|
85
|
+
|
|
86
|
+
constructor(command: number, errorCode: number) {
|
|
87
|
+
super(`VICE refused monitor command 0x${command.toString(16)} with error 0x${errorCode.toString(16)}`);
|
|
88
|
+
this.command = command;
|
|
89
|
+
this.errorCode = errorCode;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The monitor connection broke, timed out or sent bytes that are not the protocol. */
|
|
94
|
+
export class MonitorConnectionError extends Error {
|
|
95
|
+
override name = "MonitorConnectionError";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A command got no answer in time, but the connection is open. The command
|
|
100
|
+
* still waits in VICE and runs once VICE goes on (its own pause, a dialog).
|
|
101
|
+
*/
|
|
102
|
+
export class MonitorTimeoutError extends MonitorConnectionError {
|
|
103
|
+
override name = "MonitorTimeoutError";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function encodeCommand(requestId: number, command: number, body: Uint8Array = new Uint8Array(0)): Buffer {
|
|
107
|
+
const frame = Buffer.alloc(COMMAND_HEADER_BYTES + body.length);
|
|
108
|
+
frame[0] = STX;
|
|
109
|
+
frame[1] = API_VERSION;
|
|
110
|
+
frame.writeUInt32LE(body.length, 2);
|
|
111
|
+
frame.writeUInt32LE(requestId, 6);
|
|
112
|
+
frame[10] = command;
|
|
113
|
+
frame.set(body, COMMAND_HEADER_BYTES);
|
|
114
|
+
return frame;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Splits the response stream into responses. Throws MonitorConnectionError on a bad header. */
|
|
118
|
+
export class ResponseDecoder {
|
|
119
|
+
#pending: Buffer = Buffer.alloc(0);
|
|
120
|
+
|
|
121
|
+
push(chunk: Buffer): MonitorResponse[] {
|
|
122
|
+
this.#pending = this.#pending.length === 0 ? chunk : Buffer.concat([this.#pending, chunk]);
|
|
123
|
+
const responses: MonitorResponse[] = [];
|
|
124
|
+
while (this.#pending.length >= RESPONSE_HEADER_BYTES) {
|
|
125
|
+
if (this.#pending[0] !== STX) throw new MonitorConnectionError("response does not start with STX");
|
|
126
|
+
if (this.#pending[1] !== API_VERSION) {
|
|
127
|
+
throw new MonitorConnectionError(`response has API version ${this.#pending[1]}, expected ${API_VERSION}`);
|
|
128
|
+
}
|
|
129
|
+
const length = this.#pending.readUInt32LE(2);
|
|
130
|
+
if (this.#pending.length < RESPONSE_HEADER_BYTES + length) break;
|
|
131
|
+
responses.push({
|
|
132
|
+
type: this.#pending[6]!,
|
|
133
|
+
error: this.#pending[7]!,
|
|
134
|
+
requestId: this.#pending.readUInt32LE(8),
|
|
135
|
+
// Copy so a retained body never pins the whole receive buffer.
|
|
136
|
+
body: Buffer.from(this.#pending.subarray(RESPONSE_HEADER_BYTES, RESPONSE_HEADER_BYTES + length)),
|
|
137
|
+
});
|
|
138
|
+
this.#pending = this.#pending.subarray(RESPONSE_HEADER_BYTES + length);
|
|
139
|
+
}
|
|
140
|
+
return responses;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Body codecs
|
|
146
|
+
|
|
147
|
+
export function memoryGetBody(options: { start: number; end: number; memspace: number; bank: number; sideEffects?: boolean }): Buffer {
|
|
148
|
+
const body = Buffer.alloc(8);
|
|
149
|
+
body[0] = options.sideEffects ? 1 : 0;
|
|
150
|
+
body.writeUInt16LE(options.start, 1);
|
|
151
|
+
body.writeUInt16LE(options.end, 3);
|
|
152
|
+
body[5] = options.memspace;
|
|
153
|
+
body.writeUInt16LE(options.bank, 6);
|
|
154
|
+
return body;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The bytes of a memory-get response. The length field is 0 for a full 64 KiB read, so trust the body. */
|
|
158
|
+
export function decodeMemory(body: Buffer): Buffer {
|
|
159
|
+
if (body.length < 2) throw new MonitorConnectionError("memory response is too short");
|
|
160
|
+
return body.subarray(2);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface RegisterValue {
|
|
164
|
+
id: number;
|
|
165
|
+
value: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function decodeRegisters(body: Buffer): RegisterValue[] {
|
|
169
|
+
const count = body.readUInt16LE(0);
|
|
170
|
+
const registers: RegisterValue[] = [];
|
|
171
|
+
let offset = 2;
|
|
172
|
+
for (let index = 0; index < count; index++) {
|
|
173
|
+
const size = body[offset]!;
|
|
174
|
+
registers.push({ id: body[offset + 1]!, value: body.readUInt16LE(offset + 2) });
|
|
175
|
+
offset += size + 1;
|
|
176
|
+
}
|
|
177
|
+
return registers;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface RegisterInfo {
|
|
181
|
+
id: number;
|
|
182
|
+
bits: number;
|
|
183
|
+
name: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function decodeRegistersAvailable(body: Buffer): RegisterInfo[] {
|
|
187
|
+
const count = body.readUInt16LE(0);
|
|
188
|
+
const registers: RegisterInfo[] = [];
|
|
189
|
+
let offset = 2;
|
|
190
|
+
for (let index = 0; index < count; index++) {
|
|
191
|
+
const size = body[offset]!;
|
|
192
|
+
const nameLength = body[offset + 3]!;
|
|
193
|
+
registers.push({
|
|
194
|
+
id: body[offset + 1]!,
|
|
195
|
+
bits: body[offset + 2]!,
|
|
196
|
+
name: body.toString("latin1", offset + 4, offset + 4 + nameLength),
|
|
197
|
+
});
|
|
198
|
+
offset += size + 1;
|
|
199
|
+
}
|
|
200
|
+
return registers;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface BankInfo {
|
|
204
|
+
id: number;
|
|
205
|
+
name: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function decodeBanks(body: Buffer): BankInfo[] {
|
|
209
|
+
const count = body.readUInt16LE(0);
|
|
210
|
+
const banks: BankInfo[] = [];
|
|
211
|
+
let offset = 2;
|
|
212
|
+
for (let index = 0; index < count; index++) {
|
|
213
|
+
const size = body[offset]!;
|
|
214
|
+
const nameLength = body[offset + 3]!;
|
|
215
|
+
banks.push({ id: body.readUInt16LE(offset + 1), name: body.toString("latin1", offset + 4, offset + 4 + nameLength) });
|
|
216
|
+
offset += size + 1;
|
|
217
|
+
}
|
|
218
|
+
return banks;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The program counter carried by stopped, resumed and JAM events. */
|
|
222
|
+
export function decodeProgramCounter(body: Buffer): number {
|
|
223
|
+
return body.readUInt16LE(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Connection
|
|
228
|
+
|
|
229
|
+
interface Pending {
|
|
230
|
+
command: number;
|
|
231
|
+
terminalType: number;
|
|
232
|
+
extras: MonitorResponse[];
|
|
233
|
+
resolve: (response: MonitorResponse & { extras: MonitorResponse[] }) => void;
|
|
234
|
+
reject: (error: Error) => void;
|
|
235
|
+
timer: NodeJS.Timeout | undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
239
|
+
/**
|
|
240
|
+
* While an earlier command is overdue, later ones are still sent (VICE runs
|
|
241
|
+
* them in order once it goes on, so cleanup lands) but waited for only this long.
|
|
242
|
+
*/
|
|
243
|
+
export const OVERDUE_REQUEST_TIMEOUT_MS = 1_000;
|
|
244
|
+
|
|
245
|
+
/** One binary-monitor connection with request correlation and an event stream. */
|
|
246
|
+
export class BinaryMonitor {
|
|
247
|
+
/** Settles once the connection is gone, with the reason when it was not closed by us. */
|
|
248
|
+
readonly closed: Promise<Error | undefined>;
|
|
249
|
+
readonly #socket: Socket;
|
|
250
|
+
readonly #decoder = new ResponseDecoder();
|
|
251
|
+
readonly #pending = new Map<number, Pending>();
|
|
252
|
+
readonly #eventListeners = new Set<(event: MonitorResponse) => void>();
|
|
253
|
+
#nextId = 1;
|
|
254
|
+
#failure: Error | undefined;
|
|
255
|
+
#closedByUs = false;
|
|
256
|
+
/** The time limit of a request that names none. */
|
|
257
|
+
defaultTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
258
|
+
/** The time limit of a request while an earlier one is overdue (see OVERDUE_REQUEST_TIMEOUT_MS). */
|
|
259
|
+
overdueTimeoutMs = OVERDUE_REQUEST_TIMEOUT_MS;
|
|
260
|
+
/** Requests that timed out and whose late answer has not come yet. */
|
|
261
|
+
readonly #overdue = new Set<number>();
|
|
262
|
+
|
|
263
|
+
private constructor(socket: Socket) {
|
|
264
|
+
this.#socket = socket;
|
|
265
|
+
socket.setNoDelay(true);
|
|
266
|
+
this.closed = new Promise((resolve) => {
|
|
267
|
+
socket.on("close", () => {
|
|
268
|
+
const reason = this.#closedByUs ? undefined : (this.#failure ?? new MonitorConnectionError("VICE closed the monitor connection"));
|
|
269
|
+
this.#rejectAll(reason ?? new MonitorConnectionError("monitor connection closed"));
|
|
270
|
+
resolve(reason);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
socket.on("error", (error) => {
|
|
274
|
+
this.#failure ??= new MonitorConnectionError(`monitor connection failed: ${error.message}`);
|
|
275
|
+
});
|
|
276
|
+
socket.on("data", (chunk) => this.#receive(chunk));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Opens a connection. Rejects with MonitorConnectionError when nothing listens. */
|
|
280
|
+
static connect(port: number, host = "127.0.0.1", timeoutMs = 2_000): Promise<BinaryMonitor> {
|
|
281
|
+
return connectWithTimeout({
|
|
282
|
+
port,
|
|
283
|
+
host,
|
|
284
|
+
timeoutMs,
|
|
285
|
+
wrap: (socket) => new BinaryMonitor(socket),
|
|
286
|
+
failure: (message) => new MonitorConnectionError(`monitor ${message}`),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Subscribes to events (responses with request id EVENT_REQUEST_ID). Returns an unsubscribe function. */
|
|
291
|
+
onEvent(listener: (event: MonitorResponse) => void): () => void {
|
|
292
|
+
this.#eventListeners.add(listener);
|
|
293
|
+
return () => this.#eventListeners.delete(listener);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Sends one command and resolves with its terminal response. Responses
|
|
298
|
+
* carrying the same request id before the terminal one (checkpoint list)
|
|
299
|
+
* are returned in `extras`. Rejects with MonitorError on a VICE error code,
|
|
300
|
+
* and with MonitorTimeoutError after `timeoutMs` (Infinity: no limit).
|
|
301
|
+
*/
|
|
302
|
+
request(
|
|
303
|
+
command: number,
|
|
304
|
+
body: Uint8Array = new Uint8Array(0),
|
|
305
|
+
timeoutMs = this.defaultTimeoutMs,
|
|
306
|
+
): Promise<MonitorResponse & { extras: MonitorResponse[] }> {
|
|
307
|
+
if (this.#socket.destroyed) return Promise.reject(new MonitorConnectionError("monitor connection is closed"));
|
|
308
|
+
const requestId = this.#nextId;
|
|
309
|
+
this.#nextId = this.#nextId >= EVENT_REQUEST_ID - 1 ? 1 : this.#nextId + 1;
|
|
310
|
+
const limit = this.#overdue.size > 0 && timeoutMs !== Infinity ? Math.min(timeoutMs, this.overdueTimeoutMs) : timeoutMs;
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
const timer =
|
|
313
|
+
limit === Infinity
|
|
314
|
+
? undefined
|
|
315
|
+
: setTimeout(() => {
|
|
316
|
+
this.#pending.delete(requestId);
|
|
317
|
+
this.#overdue.add(requestId);
|
|
318
|
+
reject(new MonitorTimeoutError(`monitor command 0x${command.toString(16)} got no response in ${limit} ms`));
|
|
319
|
+
}, limit);
|
|
320
|
+
this.#pending.set(requestId, {
|
|
321
|
+
command,
|
|
322
|
+
terminalType: TERMINAL_RESPONSE[command] ?? command,
|
|
323
|
+
extras: [],
|
|
324
|
+
resolve,
|
|
325
|
+
reject,
|
|
326
|
+
timer,
|
|
327
|
+
});
|
|
328
|
+
this.#socket.write(encodeCommand(requestId, command, body));
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Closes the connection. Pending requests reject. */
|
|
333
|
+
close(): Promise<void> {
|
|
334
|
+
this.#closedByUs = true;
|
|
335
|
+
this.#socket.destroy();
|
|
336
|
+
return this.closed.then(() => {});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#receive(chunk: Buffer): void {
|
|
340
|
+
let responses: MonitorResponse[];
|
|
341
|
+
try {
|
|
342
|
+
responses = this.#decoder.push(chunk);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this.#failure = error as Error;
|
|
345
|
+
this.#socket.destroy();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
for (const response of responses) {
|
|
349
|
+
if (response.requestId === EVENT_REQUEST_ID) {
|
|
350
|
+
for (const listener of this.#eventListeners) listener(response);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const pending = this.#pending.get(response.requestId);
|
|
354
|
+
if (pending === undefined) {
|
|
355
|
+
// A late answer to a timed-out request: VICE went on.
|
|
356
|
+
this.#overdue.delete(response.requestId);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (response.error !== 0) {
|
|
360
|
+
this.#settle(response.requestId, pending);
|
|
361
|
+
pending.reject(new MonitorError(pending.command, response.error));
|
|
362
|
+
} else if (response.type === pending.terminalType) {
|
|
363
|
+
this.#settle(response.requestId, pending);
|
|
364
|
+
pending.resolve({ ...response, extras: pending.extras });
|
|
365
|
+
} else if (pending.command === Command.checkpointList && response.type === ResponseType.checkpointInfo) {
|
|
366
|
+
pending.extras.push(response);
|
|
367
|
+
} else {
|
|
368
|
+
this.#settle(response.requestId, pending);
|
|
369
|
+
pending.reject(
|
|
370
|
+
new MonitorConnectionError(
|
|
371
|
+
`monitor command 0x${pending.command.toString(16)} got a response of type 0x${response.type.toString(16)} instead of 0x${pending.terminalType.toString(16)}`,
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#settle(requestId: number, pending: Pending): void {
|
|
379
|
+
clearTimeout(pending.timer);
|
|
380
|
+
this.#pending.delete(requestId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#rejectAll(error: Error): void {
|
|
384
|
+
for (const [requestId, pending] of this.#pending) {
|
|
385
|
+
this.#settle(requestId, pending);
|
|
386
|
+
pending.reject(error);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Decodes C64 chip registers (VIC-II, CIA, SID) into the protocol's chip state shapes.
|
|
2
|
+
// Input bytes are register values as VICE's monitor reads them from the I/O
|
|
3
|
+
// bank without side effects; the SID's write-only registers read back as
|
|
4
|
+
// last written.
|
|
5
|
+
|
|
6
|
+
import { d018Offsets, vicBankBase } from "../../c64.ts";
|
|
7
|
+
import type { CiaState, SidState, SpriteState, ViciiMode, ViciiState } from "../../protocol/vice.ts";
|
|
8
|
+
|
|
9
|
+
export function viciiMode(d011: number, d016: number): ViciiMode {
|
|
10
|
+
const ecm = (d011 & 0x40) !== 0;
|
|
11
|
+
const bmm = (d011 & 0x20) !== 0;
|
|
12
|
+
const mcm = (d016 & 0x10) !== 0;
|
|
13
|
+
if (!ecm && !bmm) return mcm ? "multicolor-text" : "text";
|
|
14
|
+
if (!ecm && bmm) return mcm ? "multicolor-bitmap" : "bitmap";
|
|
15
|
+
if (ecm && !bmm && !mcm) return "extended-color-text";
|
|
16
|
+
return "invalid";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `vic` holds $d000-$d02e; `cia2` holds $dd00-$dd0f. */
|
|
20
|
+
export function decodeVicii(vic: Uint8Array, cia2: Uint8Array): ViciiState {
|
|
21
|
+
const d011 = vic[0x11]!;
|
|
22
|
+
const d016 = vic[0x16]!;
|
|
23
|
+
const offsets = d018Offsets(vic[0x18]!);
|
|
24
|
+
const mode = viciiMode(d011, d016);
|
|
25
|
+
const base = vicBankBase(cia2[0x00]!, cia2[0x02]!);
|
|
26
|
+
const bitmap = mode === "bitmap" || mode === "multicolor-bitmap";
|
|
27
|
+
return {
|
|
28
|
+
rasterLine: ((d011 & 0x80) << 1) | vic[0x12]!,
|
|
29
|
+
mode,
|
|
30
|
+
screenAddress: base + offsets.screen,
|
|
31
|
+
graphicsAddress: base + (bitmap ? offsets.bitmap : offsets.characters),
|
|
32
|
+
scrollX: d016 & 0x07,
|
|
33
|
+
scrollY: d011 & 0x07,
|
|
34
|
+
borderColor: vic[0x20]! & 0x0f,
|
|
35
|
+
backgroundColors: [vic[0x21]! & 0x0f, vic[0x22]! & 0x0f, vic[0x23]! & 0x0f, vic[0x24]! & 0x0f],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `pointers` holds the eight sprite pointer bytes at screen + $3f8. */
|
|
40
|
+
export function decodeSprite(index: number, vic: Uint8Array, cia2: Uint8Array, pointers: Uint8Array): SpriteState {
|
|
41
|
+
const bit = (register: number) => (vic[register]! & (1 << index)) !== 0;
|
|
42
|
+
return {
|
|
43
|
+
index,
|
|
44
|
+
x: vic[index * 2]! + (bit(0x10) ? 256 : 0),
|
|
45
|
+
y: vic[index * 2 + 1]!,
|
|
46
|
+
enabled: bit(0x15),
|
|
47
|
+
color: vic[0x27 + index]! & 0x0f,
|
|
48
|
+
multicolor: bit(0x1c),
|
|
49
|
+
expandX: bit(0x1d),
|
|
50
|
+
expandY: bit(0x17),
|
|
51
|
+
behindBackground: bit(0x1b),
|
|
52
|
+
dataAddress: vicBankBase(cia2[0x00]!, cia2[0x02]!) + pointers[index]! * 64,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bcd(value: number): number {
|
|
57
|
+
return (value >> 4) * 10 + (value & 0x0f);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The parts of a CIA that no register read gives: the timer latches and the interrupt mask. */
|
|
61
|
+
export interface CiaHidden {
|
|
62
|
+
latchA: number;
|
|
63
|
+
latchB: number;
|
|
64
|
+
interruptMask: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `registers` holds $dc00-$dc0f or $dd00-$dd0f. TOD hours become 0-23. */
|
|
68
|
+
export function decodeCia(id: 1 | 2, registers: Uint8Array, hidden: CiaHidden): CiaState {
|
|
69
|
+
const word = (low: number) => registers[low]! | (registers[low + 1]! << 8);
|
|
70
|
+
const hourRegister = registers[0x0b]!;
|
|
71
|
+
const pm = (hourRegister & 0x80) !== 0;
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
portA: registers[0x00]!,
|
|
75
|
+
portB: registers[0x01]!,
|
|
76
|
+
ddrA: registers[0x02]!,
|
|
77
|
+
ddrB: registers[0x03]!,
|
|
78
|
+
timerA: word(0x04),
|
|
79
|
+
timerB: word(0x06),
|
|
80
|
+
latchA: hidden.latchA,
|
|
81
|
+
latchB: hidden.latchB,
|
|
82
|
+
controlA: registers[0x0e]!,
|
|
83
|
+
controlB: registers[0x0f]!,
|
|
84
|
+
interruptStatus: registers[0x0d]!,
|
|
85
|
+
interruptMask: hidden.interruptMask,
|
|
86
|
+
tod: {
|
|
87
|
+
hours: (bcd(hourRegister & 0x1f) % 12) + (pm ? 12 : 0),
|
|
88
|
+
minutes: bcd(registers[0x0a]! & 0x7f),
|
|
89
|
+
seconds: bcd(registers[0x09]! & 0x7f),
|
|
90
|
+
tenths: registers[0x08]! & 0x0f,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `registers` holds $d400-$d418. */
|
|
96
|
+
export function decodeSid(registers: Uint8Array): SidState {
|
|
97
|
+
const voices = [0, 1, 2].map((voice) => {
|
|
98
|
+
const at = voice * 7;
|
|
99
|
+
return {
|
|
100
|
+
index: voice + 1,
|
|
101
|
+
frequency: registers[at]! | (registers[at + 1]! << 8),
|
|
102
|
+
pulseWidth: registers[at + 2]! | ((registers[at + 3]! & 0x0f) << 8),
|
|
103
|
+
control: registers[at + 4]!,
|
|
104
|
+
attackDecay: registers[at + 5]!,
|
|
105
|
+
sustainRelease: registers[at + 6]!,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
voices,
|
|
110
|
+
filter: {
|
|
111
|
+
cutoff: (registers[0x15]! & 0x07) | (registers[0x16]! << 3),
|
|
112
|
+
resonance: registers[0x17]! >> 4,
|
|
113
|
+
routing: registers[0x17]! & 0x0f,
|
|
114
|
+
mode: registers[0x18]! >> 4,
|
|
115
|
+
},
|
|
116
|
+
volume: registers[0x18]! & 0x0f,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The TCP connect that both VICE monitor clients share.
|
|
2
|
+
|
|
3
|
+
import { connect, type Socket } from "node:net";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Connects to a VICE monitor port and wraps the socket with `wrap` at once.
|
|
7
|
+
* Rejects with the error that `failure` makes from a message when the
|
|
8
|
+
* connection fails or takes longer than `timeoutMs`.
|
|
9
|
+
*/
|
|
10
|
+
export function connectWithTimeout<T>(options: {
|
|
11
|
+
port: number;
|
|
12
|
+
host: string;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
wrap: (socket: Socket) => T;
|
|
15
|
+
failure: (message: string) => Error;
|
|
16
|
+
}): Promise<T> {
|
|
17
|
+
const { port, host, timeoutMs, wrap, failure } = options;
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const socket = connect({ host, port });
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
socket.destroy();
|
|
22
|
+
reject(failure(`connect to port ${port} timed out`));
|
|
23
|
+
}, timeoutMs);
|
|
24
|
+
socket.once("connect", () => {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
socket.removeAllListeners("error");
|
|
27
|
+
resolve(wrap(socket));
|
|
28
|
+
});
|
|
29
|
+
socket.once("error", (error) => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
reject(failure(`connect to port ${port} failed: ${error.message}`));
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|