c64bridge 0.7.4 → 0.8.0
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/.github/prompts/README.md +14 -4
- package/.github/prompts/assembly-routine-session.prompt.md +9 -9
- package/.github/prompts/basic-program-session.prompt.md +11 -9
- package/.github/prompts/drive-management-session.prompt.md +9 -9
- package/.github/prompts/hello-world-session.prompt.md +17 -0
- package/.github/prompts/petscii-art-session.prompt.md +9 -9
- package/.github/prompts/play-classic-game.prompt.md +9 -9
- package/.github/prompts/pr-converge.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +9 -9
- package/AGENTS.md +59 -60
- package/README.md +550 -191
- package/data/assembly/assembly-spec.md +290 -1
- package/data/context/bootstrap.md +26 -2
- package/data/context/fast-paths.md +44 -0
- package/data/graphics/petscii-style-guide.md +6 -2
- package/data/graphics/sprite-charset-best-practices.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +990 -26
- package/dist/config.js +5 -1
- package/dist/device.js +680 -57
- package/dist/diagnostics.js +371 -0
- package/dist/mcp/metadata.js +39 -0
- package/dist/mcp-server.js +170 -20
- package/dist/platform.js +10 -2
- package/dist/prompts/registry.js +166 -309
- package/dist/rag/init.js +37 -1
- package/dist/rag/knowledgeIndex.js +18 -6
- package/dist/rag/rateLimiter.js +2 -2
- package/dist/streamCapture.js +135 -0
- package/dist/tools/audio.js +3 -1
- package/dist/tools/debug.js +470 -0
- package/dist/tools/developer.js +10 -4
- package/dist/tools/graphics.js +183 -17
- package/dist/tools/machineControl.js +11 -5
- package/dist/tools/memory.js +19 -10
- package/dist/tools/meta/audio.js +240 -1
- package/dist/tools/meta/config.js +20 -28
- package/dist/tools/meta/configInventory.js +104 -0
- package/dist/tools/meta/diagnostics.js +45 -2
- package/dist/tools/meta/graphics.js +14 -6
- package/dist/tools/meta/index.js +1 -0
- package/dist/tools/meta/memory.js +20 -6
- package/dist/tools/meta/program.js +473 -0
- package/dist/tools/pollValidator.js +4 -2
- package/dist/tools/programRunners.js +8 -4
- package/dist/tools/registry/config.js +17 -0
- package/dist/tools/registry/disk.js +7 -0
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/graphics.js +111 -27
- package/dist/tools/registry/index.js +6 -0
- package/dist/tools/registry/memory.js +1 -0
- package/dist/tools/registry/platform.js +121 -0
- package/dist/tools/registry/program.js +26 -8
- package/dist/tools/registry/sound.js +96 -0
- package/dist/tools/registry/system.js +15 -2
- package/dist/tools/storage.js +1 -0
- package/dist/tools/streaming.js +1 -0
- package/dist/tools/types.js +38 -4
- package/dist/tools/vice.js +160 -0
- package/dist/vicBitmap.js +317 -0
- package/dist/vice/mockServer.js +439 -0
- package/dist/vice/process.js +310 -0
- package/dist/vice/readiness.js +18 -2
- package/dist/vice/viceClient.js +382 -22
- package/doc/developer.md +62 -24
- package/doc/img/backend-switch/hello-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice.png +0 -0
- package/doc/img/backend-switch/metadata.json +50 -0
- package/doc/plans/vice-backend-switch/plan.md +486 -0
- package/doc/plans/vice-backend-switch-2/plan.md +48 -0
- package/doc/plans/vice-support/notes.md +1 -0
- package/doc/plans/vice-support/plan.md +5 -0
- package/doc/plans/vice-support/rollout.md +23 -17
- package/doc/reviews/vice-support-review.md +185 -0
- package/doc/troubleshooting-mcp.md +35 -6
- package/doc/vice/support-matrix.md +54 -0
- package/mcp/prompts.json +292 -0
- package/mcp/protocol-examples.json +46 -0
- package/mcp/resources.json +940 -0
- package/mcp/schemas/c64_config.schema.json +350 -0
- package/mcp/schemas/c64_debug.schema.json +380 -0
- package/mcp/schemas/c64_disk.schema.json +278 -0
- package/mcp/schemas/c64_drive.schema.json +120 -0
- package/mcp/schemas/c64_extract.schema.json +302 -0
- package/mcp/schemas/c64_graphics.schema.json +333 -0
- package/mcp/schemas/c64_memory.schema.json +155 -0
- package/mcp/schemas/c64_printer.schema.json +218 -0
- package/mcp/schemas/c64_program.schema.json +391 -0
- package/mcp/schemas/c64_rag.schema.json +67 -0
- package/mcp/schemas/c64_select_backend.schema.json +32 -0
- package/mcp/schemas/c64_sound.schema.json +644 -0
- package/mcp/schemas/c64_stream.schema.json +61 -0
- package/mcp/schemas/c64_system.schema.json +227 -0
- package/mcp/schemas/c64_vice.schema.json +63 -0
- package/mcp/server.json +15 -0
- package/mcp/tools.json +4517 -0
- package/mcp.json +83 -9
- package/package.json +8 -3
- package/scripts/capture-backend-switch-screenshots.mjs +276 -0
- package/scripts/generate-mcp-interface.ts +289 -0
- package/scripts/invoke-bun.mjs +67 -21
- package/scripts/mockC64Server.mjs +162 -6
- package/scripts/run-coverage.mjs +284 -0
- package/scripts/run-tests.mjs +95 -39
- package/scripts/run-tests.ts +378 -73
- package/scripts/update-readme.ts +147 -13
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { asciiToScreenCodes } from "./readiness.js";
|
|
3
|
+
const SCREEN_BASE = 0x0400;
|
|
4
|
+
const SCREEN_SIZE = 40 * 25;
|
|
5
|
+
function buildDefaultRegisterMetadata() {
|
|
6
|
+
return Object.freeze([
|
|
7
|
+
{ id: 0, name: "PC", bits: 16 },
|
|
8
|
+
{ id: 1, name: "A", bits: 8 },
|
|
9
|
+
{ id: 2, name: "X", bits: 8 },
|
|
10
|
+
{ id: 3, name: "Y", bits: 8 },
|
|
11
|
+
{ id: 4, name: "SP", bits: 8 },
|
|
12
|
+
{ id: 5, name: "SR", bits: 8 },
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
function buildResponse(cmd, reqId, body = Buffer.alloc(0), err = 0) {
|
|
16
|
+
const header = Buffer.alloc(12 + body.length);
|
|
17
|
+
header[0] = 0x02;
|
|
18
|
+
header[1] = 0x02;
|
|
19
|
+
header.writeUInt32LE(body.length, 2);
|
|
20
|
+
header[6] = cmd;
|
|
21
|
+
header[7] = err;
|
|
22
|
+
header.writeUInt32LE(reqId >>> 0, 8);
|
|
23
|
+
if (body.length > 0) {
|
|
24
|
+
body.copy(header, 12);
|
|
25
|
+
}
|
|
26
|
+
return header;
|
|
27
|
+
}
|
|
28
|
+
export class ViceMockServer {
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.server = null;
|
|
31
|
+
this.memory = new Uint8Array(0x10000);
|
|
32
|
+
this.helloReady = false;
|
|
33
|
+
this.checkpoints = new Map();
|
|
34
|
+
this.nextCheckpointId = 1;
|
|
35
|
+
this.registerMetadata = buildDefaultRegisterMetadata();
|
|
36
|
+
this.registerValues = new Map();
|
|
37
|
+
this.resources = new Map();
|
|
38
|
+
this.sockets = new Set();
|
|
39
|
+
this.host = options?.host ?? "127.0.0.1";
|
|
40
|
+
this.requestedPort = options?.port;
|
|
41
|
+
this.resetState();
|
|
42
|
+
}
|
|
43
|
+
async start() {
|
|
44
|
+
if (this.server)
|
|
45
|
+
throw new Error("ViceMockServer already started");
|
|
46
|
+
await new Promise((resolve, reject) => {
|
|
47
|
+
this.server = net.createServer((socket) => this.handleSocket(socket));
|
|
48
|
+
const onError = (error) => {
|
|
49
|
+
this.server?.off("listening", onListening);
|
|
50
|
+
reject(error);
|
|
51
|
+
};
|
|
52
|
+
const onListening = () => {
|
|
53
|
+
this.server?.off("error", onError);
|
|
54
|
+
resolve();
|
|
55
|
+
};
|
|
56
|
+
this.server.once("error", onError);
|
|
57
|
+
this.server.listen(this.requestedPort ?? 0, this.host, () => resolve());
|
|
58
|
+
});
|
|
59
|
+
const address = this.server.address();
|
|
60
|
+
if (!address || typeof address === "string")
|
|
61
|
+
throw new Error("Failed to determine mock server port");
|
|
62
|
+
return { port: address.port };
|
|
63
|
+
}
|
|
64
|
+
async stop() {
|
|
65
|
+
if (!this.server)
|
|
66
|
+
return;
|
|
67
|
+
for (const socket of this.sockets) {
|
|
68
|
+
socket.destroy();
|
|
69
|
+
}
|
|
70
|
+
this.sockets.clear();
|
|
71
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
72
|
+
this.server = null;
|
|
73
|
+
}
|
|
74
|
+
handleSocket(socket) {
|
|
75
|
+
this.sockets.add(socket);
|
|
76
|
+
let buffer = Buffer.alloc(0);
|
|
77
|
+
socket.once("close", () => {
|
|
78
|
+
this.sockets.delete(socket);
|
|
79
|
+
buffer = Buffer.alloc(0);
|
|
80
|
+
});
|
|
81
|
+
socket.on("data", (chunk) => {
|
|
82
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
83
|
+
buffer = Buffer.concat([buffer, data]);
|
|
84
|
+
buffer = this.processBuffer(buffer, socket);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
processBuffer(buffer, socket) {
|
|
88
|
+
while (buffer.length >= 11) {
|
|
89
|
+
if (buffer[0] !== 0x02 || buffer[1] !== 0x02) {
|
|
90
|
+
buffer = buffer.slice(1);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const bodyLen = buffer.readUInt32LE(2);
|
|
94
|
+
const frameLen = 11 + bodyLen;
|
|
95
|
+
if (buffer.length < frameLen)
|
|
96
|
+
break;
|
|
97
|
+
const reqId = buffer.readUInt32LE(6);
|
|
98
|
+
const cmd = buffer[10];
|
|
99
|
+
const body = buffer.slice(11, frameLen);
|
|
100
|
+
buffer = buffer.slice(frameLen);
|
|
101
|
+
const response = this.handleCommand(cmd, reqId, body, socket);
|
|
102
|
+
if (response) {
|
|
103
|
+
socket.write(response);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return buffer;
|
|
107
|
+
}
|
|
108
|
+
handleCommand(cmd, reqId, body, socket) {
|
|
109
|
+
switch (cmd) {
|
|
110
|
+
case 0x85: // info
|
|
111
|
+
return buildResponse(cmd, reqId);
|
|
112
|
+
case 0xCC: // reset
|
|
113
|
+
this.resetState();
|
|
114
|
+
return buildResponse(cmd, reqId);
|
|
115
|
+
case 0x11: { // checkpoint get
|
|
116
|
+
const id = body.readUInt32LE(0);
|
|
117
|
+
const checkpoint = this.checkpoints.get(id);
|
|
118
|
+
if (!checkpoint) {
|
|
119
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x83);
|
|
120
|
+
}
|
|
121
|
+
return buildResponse(cmd, reqId, this.encodeCheckpoint(checkpoint));
|
|
122
|
+
}
|
|
123
|
+
case 0x12: { // checkpoint create
|
|
124
|
+
const start = body.readUInt16LE(0);
|
|
125
|
+
const end = body.readUInt16LE(2);
|
|
126
|
+
const stopOnHit = body[4] === 1;
|
|
127
|
+
const enabled = body[5] === 1;
|
|
128
|
+
const mask = body[6] ?? 0x04;
|
|
129
|
+
const temporary = body[7] === 1;
|
|
130
|
+
const memspace = (body[8] ?? 0) & 0xff;
|
|
131
|
+
const id = this.nextCheckpointId++;
|
|
132
|
+
const checkpoint = {
|
|
133
|
+
id,
|
|
134
|
+
start,
|
|
135
|
+
end,
|
|
136
|
+
stopOnHit,
|
|
137
|
+
enabled,
|
|
138
|
+
temporary,
|
|
139
|
+
load: (mask & 0x01) !== 0,
|
|
140
|
+
store: (mask & 0x02) !== 0,
|
|
141
|
+
execute: (mask & 0x04) !== 0,
|
|
142
|
+
hitCount: 0,
|
|
143
|
+
ignoreCount: 0,
|
|
144
|
+
hasCondition: false,
|
|
145
|
+
memspace,
|
|
146
|
+
};
|
|
147
|
+
this.checkpoints.set(id, checkpoint);
|
|
148
|
+
return buildResponse(0x11, reqId, this.encodeCheckpoint(checkpoint));
|
|
149
|
+
}
|
|
150
|
+
case 0x13: { // checkpoint delete
|
|
151
|
+
const id = body.readUInt32LE(0);
|
|
152
|
+
this.checkpoints.delete(id);
|
|
153
|
+
return buildResponse(cmd, reqId);
|
|
154
|
+
}
|
|
155
|
+
case 0x14: { // checkpoint list
|
|
156
|
+
for (const checkpoint of this.checkpoints.values()) {
|
|
157
|
+
socket.write(buildResponse(0x11, reqId, this.encodeCheckpoint(checkpoint)));
|
|
158
|
+
}
|
|
159
|
+
return buildResponse(cmd, reqId);
|
|
160
|
+
}
|
|
161
|
+
case 0x15: { // checkpoint toggle
|
|
162
|
+
const id = body.readUInt32LE(0);
|
|
163
|
+
const enabled = body[4] === 1;
|
|
164
|
+
const checkpoint = this.checkpoints.get(id);
|
|
165
|
+
if (!checkpoint) {
|
|
166
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x83);
|
|
167
|
+
}
|
|
168
|
+
checkpoint.enabled = enabled;
|
|
169
|
+
return buildResponse(cmd, reqId);
|
|
170
|
+
}
|
|
171
|
+
case 0x22: { // checkpoint condition
|
|
172
|
+
const id = body.readUInt32LE(0);
|
|
173
|
+
const checkpoint = this.checkpoints.get(id);
|
|
174
|
+
if (!checkpoint) {
|
|
175
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x83);
|
|
176
|
+
}
|
|
177
|
+
checkpoint.hasCondition = (body[4] ?? 0) > 0;
|
|
178
|
+
return buildResponse(cmd, reqId);
|
|
179
|
+
}
|
|
180
|
+
case 0x01: { // mem get
|
|
181
|
+
const start = body.readUInt16LE(1);
|
|
182
|
+
const end = body.readUInt16LE(3);
|
|
183
|
+
const length = Math.max(0, end - start + 1);
|
|
184
|
+
const payload = Buffer.alloc(2 + length);
|
|
185
|
+
payload.writeUInt16LE(length, 0);
|
|
186
|
+
for (let i = 0; i < length; i += 1) {
|
|
187
|
+
payload[2 + i] = this.memory[(start + i) & 0xffff];
|
|
188
|
+
}
|
|
189
|
+
return buildResponse(cmd, reqId, payload);
|
|
190
|
+
}
|
|
191
|
+
case 0x02: { // mem set
|
|
192
|
+
const start = body.readUInt16LE(1);
|
|
193
|
+
const end = body.readUInt16LE(3);
|
|
194
|
+
const payload = body.subarray(8);
|
|
195
|
+
for (let i = 0; i < payload.length && start + i <= 0xffff; i += 1) {
|
|
196
|
+
this.memory[start + i] = payload[i];
|
|
197
|
+
}
|
|
198
|
+
if (start <= SCREEN_BASE && end >= SCREEN_BASE) {
|
|
199
|
+
this.helloReady = false;
|
|
200
|
+
}
|
|
201
|
+
return buildResponse(cmd, reqId);
|
|
202
|
+
}
|
|
203
|
+
case 0x72: { // keyboard feed
|
|
204
|
+
const len = body[0] ?? 0;
|
|
205
|
+
const text = body.subarray(1, 1 + len).toString("ascii").toUpperCase();
|
|
206
|
+
if (text.includes("RUN")) {
|
|
207
|
+
this.renderHello();
|
|
208
|
+
}
|
|
209
|
+
return buildResponse(cmd, reqId);
|
|
210
|
+
}
|
|
211
|
+
case 0x31: { // registers get
|
|
212
|
+
const payload = this.encodeRegisterValues();
|
|
213
|
+
return buildResponse(cmd, reqId, payload);
|
|
214
|
+
}
|
|
215
|
+
case 0x32: { // registers set
|
|
216
|
+
this.applyRegisterWrites(body);
|
|
217
|
+
const payload = this.encodeRegisterValues();
|
|
218
|
+
return buildResponse(0x31, reqId, payload);
|
|
219
|
+
}
|
|
220
|
+
case 0x71: // step instructions
|
|
221
|
+
return buildResponse(cmd, reqId);
|
|
222
|
+
case 0x73: // step return
|
|
223
|
+
return buildResponse(cmd, reqId);
|
|
224
|
+
case 0x83: { // registers metadata
|
|
225
|
+
const payload = this.encodeRegisterMetadata();
|
|
226
|
+
return buildResponse(cmd, reqId, payload);
|
|
227
|
+
}
|
|
228
|
+
case 0x84: { // display get
|
|
229
|
+
const payload = this.encodeDisplay();
|
|
230
|
+
return buildResponse(cmd, reqId, payload);
|
|
231
|
+
}
|
|
232
|
+
case 0x51: { // resource get
|
|
233
|
+
const nameLen = body[0] ?? 0;
|
|
234
|
+
const name = body.subarray(1, 1 + nameLen).toString("utf8");
|
|
235
|
+
const value = this.resources.get(name);
|
|
236
|
+
if (value === undefined) {
|
|
237
|
+
return buildResponse(cmd, reqId, Buffer.from([0, 0]));
|
|
238
|
+
}
|
|
239
|
+
if (typeof value === "number") {
|
|
240
|
+
const buffer = Buffer.alloc(2 + 4);
|
|
241
|
+
buffer[0] = 1;
|
|
242
|
+
buffer[1] = 4;
|
|
243
|
+
buffer.writeInt32LE(value, 2);
|
|
244
|
+
return buildResponse(cmd, reqId, buffer);
|
|
245
|
+
}
|
|
246
|
+
const strBytes = Buffer.from(value, "utf8");
|
|
247
|
+
const buffer = Buffer.alloc(2 + strBytes.length);
|
|
248
|
+
buffer[0] = 0;
|
|
249
|
+
buffer[1] = strBytes.length & 0xff;
|
|
250
|
+
strBytes.copy(buffer, 2);
|
|
251
|
+
return buildResponse(cmd, reqId, buffer);
|
|
252
|
+
}
|
|
253
|
+
case 0x52: { // resource set
|
|
254
|
+
const isNumber = body[0] === 1;
|
|
255
|
+
const nameLen = body[1] ?? 0;
|
|
256
|
+
const name = body.subarray(2, 2 + nameLen).toString("utf8");
|
|
257
|
+
if (isNumber) {
|
|
258
|
+
const valueLen = body[2 + nameLen] ?? 0;
|
|
259
|
+
const valueBytes = body.subarray(3 + nameLen, 3 + nameLen + valueLen);
|
|
260
|
+
const padded = Buffer.alloc(4);
|
|
261
|
+
valueBytes.copy(padded, 0, 0, Math.min(4, valueBytes.length));
|
|
262
|
+
const value = padded.readInt32LE(0);
|
|
263
|
+
this.resources.set(name, value);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const valueLen = body[2 + nameLen] ?? 0;
|
|
267
|
+
const value = body.subarray(3 + nameLen, 3 + nameLen + valueLen).toString("utf8");
|
|
268
|
+
this.resources.set(name, value);
|
|
269
|
+
}
|
|
270
|
+
return buildResponse(cmd, reqId);
|
|
271
|
+
}
|
|
272
|
+
case 0xAA: // exit monitor
|
|
273
|
+
return buildResponse(cmd, reqId);
|
|
274
|
+
case 0xBB: // quit
|
|
275
|
+
queueMicrotask(() => socket.destroy());
|
|
276
|
+
return buildResponse(cmd, reqId);
|
|
277
|
+
default:
|
|
278
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x83);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
encodeCheckpoint(checkpoint) {
|
|
282
|
+
const buffer = Buffer.alloc(23);
|
|
283
|
+
buffer.writeUInt32LE(checkpoint.id, 0);
|
|
284
|
+
buffer[4] = checkpoint.hitCount > 0 ? 1 : 0;
|
|
285
|
+
buffer.writeUInt16LE(checkpoint.start & 0xffff, 5);
|
|
286
|
+
buffer.writeUInt16LE(checkpoint.end & 0xffff, 7);
|
|
287
|
+
buffer[9] = checkpoint.stopOnHit ? 1 : 0;
|
|
288
|
+
buffer[10] = checkpoint.enabled ? 1 : 0;
|
|
289
|
+
let mask = 0;
|
|
290
|
+
if (checkpoint.load)
|
|
291
|
+
mask |= 0x01;
|
|
292
|
+
if (checkpoint.store)
|
|
293
|
+
mask |= 0x02;
|
|
294
|
+
if (checkpoint.execute)
|
|
295
|
+
mask |= 0x04;
|
|
296
|
+
buffer[11] = mask;
|
|
297
|
+
buffer[12] = checkpoint.temporary ? 1 : 0;
|
|
298
|
+
buffer.writeUInt32LE(checkpoint.hitCount >>> 0, 13);
|
|
299
|
+
buffer.writeUInt32LE(checkpoint.ignoreCount >>> 0, 17);
|
|
300
|
+
buffer[21] = checkpoint.hasCondition ? 1 : 0;
|
|
301
|
+
buffer[22] = checkpoint.memspace & 0xff;
|
|
302
|
+
return buffer;
|
|
303
|
+
}
|
|
304
|
+
encodeRegisterValues() {
|
|
305
|
+
const entries = [];
|
|
306
|
+
for (const meta of this.registerMetadata) {
|
|
307
|
+
const size = Math.max(1, Math.ceil(Math.min(meta.bits, 16) / 8));
|
|
308
|
+
const value = this.registerValues.get(meta.id) ?? 0;
|
|
309
|
+
const entryLength = 2 + size;
|
|
310
|
+
const entry = Buffer.alloc(entryLength);
|
|
311
|
+
entry[0] = size + 1;
|
|
312
|
+
entry[1] = meta.id & 0xff;
|
|
313
|
+
entry.writeUIntLE(value >>> 0, 2, size);
|
|
314
|
+
entries.push(entry);
|
|
315
|
+
}
|
|
316
|
+
const payloadLength = 2 + entries.reduce((sum, entry) => sum + entry.length, 0);
|
|
317
|
+
const payload = Buffer.alloc(payloadLength);
|
|
318
|
+
payload.writeUInt16LE(entries.length, 0);
|
|
319
|
+
let offset = 2;
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
entry.copy(payload, offset);
|
|
322
|
+
offset += entry.length;
|
|
323
|
+
}
|
|
324
|
+
return payload;
|
|
325
|
+
}
|
|
326
|
+
encodeRegisterMetadata() {
|
|
327
|
+
const entries = [];
|
|
328
|
+
for (const meta of this.registerMetadata) {
|
|
329
|
+
const nameBytes = Buffer.from(meta.name, "utf8");
|
|
330
|
+
const entryLength = 4 + nameBytes.length;
|
|
331
|
+
const entry = Buffer.alloc(entryLength);
|
|
332
|
+
entry[0] = 3 + nameBytes.length;
|
|
333
|
+
entry[1] = meta.id & 0xff;
|
|
334
|
+
entry[2] = meta.bits & 0xff;
|
|
335
|
+
entry[3] = nameBytes.length & 0xff;
|
|
336
|
+
nameBytes.copy(entry, 4);
|
|
337
|
+
entries.push(entry);
|
|
338
|
+
}
|
|
339
|
+
const payloadLength = 2 + entries.reduce((sum, entry) => sum + entry.length, 0);
|
|
340
|
+
const payload = Buffer.alloc(payloadLength);
|
|
341
|
+
payload.writeUInt16LE(entries.length, 0);
|
|
342
|
+
let offset = 2;
|
|
343
|
+
for (const entry of entries) {
|
|
344
|
+
entry.copy(payload, offset);
|
|
345
|
+
offset += entry.length;
|
|
346
|
+
}
|
|
347
|
+
return payload;
|
|
348
|
+
}
|
|
349
|
+
encodeDisplay() {
|
|
350
|
+
const pixels = Buffer.alloc(64, 0);
|
|
351
|
+
const header = Buffer.alloc(17);
|
|
352
|
+
const pixelLength = Buffer.alloc(4);
|
|
353
|
+
header.writeUInt32LE(13, 0);
|
|
354
|
+
header.writeUInt16LE(320, 4);
|
|
355
|
+
header.writeUInt16LE(200, 6);
|
|
356
|
+
header.writeUInt16LE(0, 8);
|
|
357
|
+
header.writeUInt16LE(0, 10);
|
|
358
|
+
header.writeUInt16LE(320, 12);
|
|
359
|
+
header.writeUInt16LE(200, 14);
|
|
360
|
+
header[16] = 8;
|
|
361
|
+
pixelLength.writeUInt32LE(pixels.length, 0);
|
|
362
|
+
return Buffer.concat([header, pixelLength, pixels]);
|
|
363
|
+
}
|
|
364
|
+
applyRegisterWrites(body) {
|
|
365
|
+
if (body.length < 3) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const count = body.readUInt16LE(1);
|
|
369
|
+
let offset = 3;
|
|
370
|
+
for (let index = 0; index < count && offset < body.length; index += 1) {
|
|
371
|
+
const entrySize = body[offset] ?? 0;
|
|
372
|
+
const entryLength = entrySize + 1;
|
|
373
|
+
if (entryLength < 3 || offset + entryLength > body.length) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
const id = body[offset + 1] ?? 0;
|
|
377
|
+
const valueBytes = Math.max(0, entryLength - 2);
|
|
378
|
+
let value = 0;
|
|
379
|
+
if (valueBytes > 0) {
|
|
380
|
+
value = body.readUIntLE(offset + 2, Math.min(valueBytes, 4));
|
|
381
|
+
}
|
|
382
|
+
const meta = this.registerMetadata.find((m) => m.id === id);
|
|
383
|
+
if (meta) {
|
|
384
|
+
const mask = meta.bits >= 31 ? 0xffffffff : (1 << meta.bits) - 1;
|
|
385
|
+
this.registerValues.set(id, value & mask);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
this.registerValues.set(id, value);
|
|
389
|
+
}
|
|
390
|
+
offset += entryLength;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
resetState() {
|
|
394
|
+
this.memory.fill(0);
|
|
395
|
+
// Fill screen with spaces and READY.
|
|
396
|
+
this.memory.fill(0x20, SCREEN_BASE, SCREEN_BASE + SCREEN_SIZE);
|
|
397
|
+
const ready = asciiToScreenCodes("READY.");
|
|
398
|
+
ready.copy(Buffer.from(this.memory.buffer, SCREEN_BASE, ready.length));
|
|
399
|
+
// Initialise BASIC zero-page pointers so waitForBasicReady() succeeds
|
|
400
|
+
// immediately instead of spinning for 20 s on all-zero memory.
|
|
401
|
+
// $002B-$002C TXTTAB = $0801 (start of BASIC text)
|
|
402
|
+
// $002D-$002E VARTAB = $0803 (start of variables)
|
|
403
|
+
// $002F-$0030 ARYTAB = $0803 (start of arrays)
|
|
404
|
+
// $0031-$0032 STREND = $0803 (end of BASIC storage)
|
|
405
|
+
this.memory[0x2B] = 0x01;
|
|
406
|
+
this.memory[0x2C] = 0x08;
|
|
407
|
+
this.memory[0x2D] = 0x03;
|
|
408
|
+
this.memory[0x2E] = 0x08;
|
|
409
|
+
this.memory[0x2F] = 0x03;
|
|
410
|
+
this.memory[0x30] = 0x08;
|
|
411
|
+
this.memory[0x31] = 0x03;
|
|
412
|
+
this.memory[0x32] = 0x08;
|
|
413
|
+
this.helloReady = false;
|
|
414
|
+
this.checkpoints.clear();
|
|
415
|
+
this.nextCheckpointId = 1;
|
|
416
|
+
this.registerValues.clear();
|
|
417
|
+
for (const meta of this.registerMetadata) {
|
|
418
|
+
this.registerValues.set(meta.id, 0);
|
|
419
|
+
}
|
|
420
|
+
this.resources = new Map([
|
|
421
|
+
["C64Model", 2],
|
|
422
|
+
["SidEngine", 1],
|
|
423
|
+
]);
|
|
424
|
+
}
|
|
425
|
+
renderHello() {
|
|
426
|
+
if (this.helloReady)
|
|
427
|
+
return;
|
|
428
|
+
const row = 10;
|
|
429
|
+
const offset = SCREEN_BASE + row * 40;
|
|
430
|
+
const hello = asciiToScreenCodes("HELLO");
|
|
431
|
+
hello.copy(Buffer.from(this.memory.buffer, offset, hello.length));
|
|
432
|
+
this.helloReady = true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
export async function startViceMockServer(options) {
|
|
436
|
+
const server = new ViceMockServer(options);
|
|
437
|
+
const { port } = await server.start();
|
|
438
|
+
return Object.assign(server, { port });
|
|
439
|
+
}
|