c64bridge 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/data/embeddings_basic.json +1 -1
- package/data/embeddings_hardware.json +1 -1
- package/data/embeddings_mixed.json +1 -1
- package/data/embeddings_other.json +1 -1
- package/data/graphics/character-set.csv +64 -64
- package/data/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +144 -150
- package/dist/config.js +93 -39
- package/dist/device.js +252 -130
- package/dist/diagnostics.js +10 -2
- package/dist/knowledge.js +2 -1
- package/dist/mcp-server.js +12 -4
- package/dist/petscii.js +18 -15
- package/dist/petsciiArt.js +40 -14
- package/dist/rag/indexer.js +2 -1
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -275
- package/dist/tools/audio.js +97 -18
- package/dist/tools/debug.js +9 -9
- package/dist/tools/developer.js +7 -7
- package/dist/tools/errors.js +19 -1
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/hex.js +9 -0
- package/dist/tools/input.js +21 -18
- package/dist/tools/machineControl.js +6 -6
- package/dist/tools/memory.js +11 -10
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/background.js +6 -1
- package/dist/tools/meta/filesystem.js +37 -10
- package/dist/tools/meta/graphics.js +11 -16
- package/dist/tools/meta/memory.js +16 -22
- package/dist/tools/meta/program.js +28 -9
- package/dist/tools/pollValidator.js +30 -2
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +10 -9
- package/dist/tools/registry/disk.js +7 -5
- package/dist/tools/storage.js +21 -17
- package/dist/tools/streaming.js +2 -2
- package/dist/tools/translation/assembler.js +192 -57
- package/dist/tools/translation/basicTokenizer.js +44 -10
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/types.js +11 -4
- package/dist/tools/vice.js +1 -1
- package/dist/vicBitmap.js +16 -0
- package/dist/vice/mockServer.js +99 -8
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +34 -6
- package/mcp/schemas/c64_disk.schema.json +2 -2
- package/mcp/schemas/c64_input.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +2 -2
- package/mcp/schemas/c64_system.schema.json +6 -4
- package/mcp/schemas/c64_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +20 -14
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +113 -20
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- package/server.json +2 -2
|
@@ -9,13 +9,18 @@ import { Buffer } from "node:buffer";
|
|
|
9
9
|
import { asciiToPetscii, encodeStringWithNames } from "../../petscii.js";
|
|
10
10
|
const DEFAULT_START_ADDRESS = 0x0801;
|
|
11
11
|
const REM_TOKEN = 0x8f;
|
|
12
|
+
const DATA_TOKEN = 0x83;
|
|
13
|
+
const PRINT_TOKEN = 0x99;
|
|
14
|
+
// Highest line number the BASIC V2 line parser (LINGET) accepts; larger values give ?SYNTAX ERROR.
|
|
15
|
+
const MAX_LINE_NUMBER = 63999;
|
|
12
16
|
const TOKEN_TABLE = buildTokenTable();
|
|
13
17
|
export function basicToPrg(source, options) {
|
|
14
18
|
const startAddress = options?.startAddress ?? DEFAULT_START_ADDRESS;
|
|
15
19
|
const normalizedSource = normalize(source);
|
|
16
20
|
const lines = normalizedSource.split("\n");
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
// Like typing lines into the C64, lines are kept in ascending order, a later
|
|
22
|
+
// duplicate replaces the earlier line, and a bare line number deletes it.
|
|
23
|
+
const programLines = new Map();
|
|
19
24
|
for (const rawLine of lines) {
|
|
20
25
|
const trimmedRight = rawLine.replace(/\s+$/u, "");
|
|
21
26
|
if (trimmedRight === "") {
|
|
@@ -26,11 +31,20 @@ export function basicToPrg(source, options) {
|
|
|
26
31
|
throw new Error(`Expected line number at: "${rawLine}"`);
|
|
27
32
|
}
|
|
28
33
|
const lineNumber = Number.parseInt(match[1], 10);
|
|
29
|
-
if (!Number.isInteger(lineNumber) || lineNumber < 0 || lineNumber >
|
|
30
|
-
throw new Error(`Line number out of range: ${match[1]}`);
|
|
34
|
+
if (!Number.isInteger(lineNumber) || lineNumber < 0 || lineNumber > MAX_LINE_NUMBER) {
|
|
35
|
+
throw new Error(`Line number out of range: ${match[1]} (C64 BASIC accepts 0-${MAX_LINE_NUMBER})`);
|
|
31
36
|
}
|
|
32
37
|
const content = match[2] ?? "";
|
|
33
|
-
|
|
38
|
+
if (content === "") {
|
|
39
|
+
programLines.delete(lineNumber);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
programLines.set(lineNumber, tokenize(content));
|
|
43
|
+
}
|
|
44
|
+
const lineBuffers = [];
|
|
45
|
+
let currentAddress = startAddress;
|
|
46
|
+
for (const lineNumber of [...programLines.keys()].sort((a, b) => a - b)) {
|
|
47
|
+
const tokenised = programLines.get(lineNumber);
|
|
34
48
|
const lineLength = 2 + 2 + tokenised.length + 1;
|
|
35
49
|
const nextAddress = currentAddress + lineLength;
|
|
36
50
|
const lineBuffer = Buffer.alloc(lineLength);
|
|
@@ -56,8 +70,16 @@ function tokenize(content) {
|
|
|
56
70
|
let index = 0;
|
|
57
71
|
let inString = false;
|
|
58
72
|
let inRemark = false;
|
|
73
|
+
// As in the ROM CRUNCH routine, DATA items are copied literally until the
|
|
74
|
+
// next ':' outside quotes, and '?' outside strings, REM, and DATA becomes PRINT.
|
|
75
|
+
let inData = false;
|
|
59
76
|
while (index < content.length) {
|
|
60
|
-
if (!inString && !inRemark) {
|
|
77
|
+
if (!inString && !inRemark && !inData) {
|
|
78
|
+
if (upper[index] === "?") {
|
|
79
|
+
bytes.push(PRINT_TOKEN);
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
61
83
|
const token = matchToken(upper, index);
|
|
62
84
|
if (token) {
|
|
63
85
|
bytes.push(token.byte);
|
|
@@ -65,6 +87,9 @@ function tokenize(content) {
|
|
|
65
87
|
if (token.byte === REM_TOKEN) {
|
|
66
88
|
inRemark = true;
|
|
67
89
|
}
|
|
90
|
+
else if (token.byte === DATA_TOKEN) {
|
|
91
|
+
inData = true;
|
|
92
|
+
}
|
|
68
93
|
continue;
|
|
69
94
|
}
|
|
70
95
|
}
|
|
@@ -82,15 +107,24 @@ function tokenize(content) {
|
|
|
82
107
|
end += 1;
|
|
83
108
|
}
|
|
84
109
|
const segment = content.slice(start, end);
|
|
85
|
-
|
|
110
|
+
// Lowercase letters would print as graphics characters in the default
|
|
111
|
+
// character set, so string text is stored as the PETSCII letters.
|
|
112
|
+
const encoded = encodeStringWithNames(segment, { foldLowercase: true });
|
|
86
113
|
for (const b of encoded)
|
|
87
114
|
bytes.push(b);
|
|
88
115
|
index = end; // leave closing quote to be processed next iteration
|
|
89
116
|
}
|
|
90
117
|
continue;
|
|
91
118
|
}
|
|
92
|
-
|
|
119
|
+
// REM text folds only ASCII a-z, like string text (LIST would show them as
|
|
120
|
+
// graphics); full Unicode upper-casing could change length or leave Latin-1.
|
|
121
|
+
const plainChar = inRemark
|
|
122
|
+
? (char >= "a" && char <= "z" ? char.toUpperCase() : char)
|
|
123
|
+
: (!inString ? upper[index] : char) ?? char;
|
|
93
124
|
bytes.push(asciiToPetscii(plainChar));
|
|
125
|
+
if (inData && !inString && plainChar === ":") {
|
|
126
|
+
inData = false;
|
|
127
|
+
}
|
|
94
128
|
index += 1;
|
|
95
129
|
}
|
|
96
130
|
if (bytes.length === 0) {
|
|
@@ -112,7 +146,7 @@ function buildTokenTable() {
|
|
|
112
146
|
["END", 0x80],
|
|
113
147
|
["FOR", 0x81],
|
|
114
148
|
["NEXT", 0x82],
|
|
115
|
-
["DATA",
|
|
149
|
+
["DATA", DATA_TOKEN],
|
|
116
150
|
["INPUT#", 0x84],
|
|
117
151
|
["INPUT", 0x85],
|
|
118
152
|
["DIM", 0x86],
|
|
@@ -134,7 +168,7 @@ function buildTokenTable() {
|
|
|
134
168
|
["DEF", 0x96],
|
|
135
169
|
["POKE", 0x97],
|
|
136
170
|
["PRINT#", 0x98],
|
|
137
|
-
["PRINT",
|
|
171
|
+
["PRINT", PRINT_TOKEN],
|
|
138
172
|
["CONT", 0x9a],
|
|
139
173
|
["LIST", 0x9b],
|
|
140
174
|
["CLR", 0x9c],
|
|
@@ -55,7 +55,7 @@ const OPCODE_ROWS = [
|
|
|
55
55
|
// 0xE0-0xEF
|
|
56
56
|
[["CPX", M.Imm], ["SBC", M.IndX], ["NOP", M.Imm], ["INS", M.IndX], ["CPX", M.Zp], ["SBC", M.Zp], ["INC", M.Zp], ["INS", M.Zp], ["INX", M.Imp], ["SBC", M.Imm], ["NOP", M.Imp], ["SBC", M.Imm], ["CPX", M.Abs], ["SBC", M.Abs], ["INC", M.Abs], ["INS", M.Abs]],
|
|
57
57
|
// 0xF0-0xFF
|
|
58
|
-
[["BEQ", M.Rel], ["SBC", M.IndY], ["HLT", M.Imp], ["INS", M.IndY], ["NOP", M.
|
|
58
|
+
[["BEQ", M.Rel], ["SBC", M.IndY], ["HLT", M.Imp], ["INS", M.IndY], ["NOP", M.ZpX], ["SBC", M.ZpX], ["INC", M.ZpX], ["INS", M.ZpX], ["SED", M.Imp], ["SBC", M.AbsY], ["NOP", M.Imp], ["INS", M.AbsY], ["NOP", M.AbsX], ["SBC", M.AbsX], ["INC", M.AbsX], ["INS", M.AbsX]],
|
|
59
59
|
];
|
|
60
60
|
const ILLEGAL_MAP = [
|
|
61
61
|
0x989C, 0x9C9C, 0x888C, 0x9C9C,
|
package/dist/tools/types.js
CHANGED
|
@@ -104,7 +104,8 @@ export function createOperationDispatcher(toolName, handlers) {
|
|
|
104
104
|
throw new ToolValidationError(`${toolName} is a grouped tool and requires an ${OPERATION_DISCRIMINATOR} string to select an operation`, { path: `$.${OPERATION_DISCRIMINATOR}`, details: { allowed } });
|
|
105
105
|
}
|
|
106
106
|
const opKey = opValue;
|
|
107
|
-
|
|
107
|
+
// Own properties only: "constructor", "toString", etc. must not resolve to Object.prototype members.
|
|
108
|
+
const handler = hasOwnKey(handlers, opKey) ? handlers[opKey] : undefined;
|
|
108
109
|
if (!handler) {
|
|
109
110
|
throw new ToolValidationError(`${toolName} does not support ${OPERATION_DISCRIMINATOR} "${opValue}"`, { path: `$.${OPERATION_DISCRIMINATOR}`, details: { allowed } });
|
|
110
111
|
}
|
|
@@ -184,7 +185,7 @@ function resolveSupportedPlatforms(tool, args, defaultPlatforms) {
|
|
|
184
185
|
if (!operation) {
|
|
185
186
|
return basePlatforms;
|
|
186
187
|
}
|
|
187
|
-
const operationPlatforms = tool.operationPlatforms
|
|
188
|
+
const operationPlatforms = ownValue(tool.operationPlatforms, operation);
|
|
188
189
|
return operationPlatforms && operationPlatforms.length > 0 ? operationPlatforms : basePlatforms;
|
|
189
190
|
}
|
|
190
191
|
function resolveUnsupportedToolName(toolName, tool, args) {
|
|
@@ -192,11 +193,17 @@ function resolveUnsupportedToolName(toolName, tool, args) {
|
|
|
192
193
|
if (!operation) {
|
|
193
194
|
return toolName;
|
|
194
195
|
}
|
|
195
|
-
const mappedName = tool.operationToolNames
|
|
196
|
+
const mappedName = ownValue(tool.operationToolNames, operation);
|
|
196
197
|
if (mappedName) {
|
|
197
198
|
return mappedName;
|
|
198
199
|
}
|
|
199
|
-
return tool.operationPlatforms
|
|
200
|
+
return ownValue(tool.operationPlatforms, operation) ? operation : toolName;
|
|
201
|
+
}
|
|
202
|
+
function hasOwnKey(record, key) {
|
|
203
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
204
|
+
}
|
|
205
|
+
function ownValue(record, key) {
|
|
206
|
+
return record && hasOwnKey(record, key) ? record[key] : undefined;
|
|
200
207
|
}
|
|
201
208
|
function getSelectedOperation(args) {
|
|
202
209
|
if (typeof args !== "object" || args === null) {
|
package/dist/tools/vice.js
CHANGED
|
@@ -33,7 +33,7 @@ const resourceValueSchema = {
|
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
const resourceNameSchema = stringSchema({
|
|
36
|
-
description: "VICE resource name (example: SidEngine,
|
|
36
|
+
description: "VICE resource name (example: SidEngine, SidModel).",
|
|
37
37
|
minLength: 1,
|
|
38
38
|
maxLength: 64,
|
|
39
39
|
pattern: RESOURCE_NAME_PATTERN,
|
package/dist/vicBitmap.js
CHANGED
|
@@ -4,6 +4,8 @@ const BITMAP_BYTE_LENGTH = 8000;
|
|
|
4
4
|
const SCREEN_RAM_BYTE_LENGTH = 1000;
|
|
5
5
|
const COLOR_RAM_BYTE_LENGTH = 1000;
|
|
6
6
|
const COLOR_RAM_ADDRESS = 0xD800;
|
|
7
|
+
const CHAR_ROM_SHADOW_OFFSET = 0x1000;
|
|
8
|
+
const CHAR_ROM_SHADOW_LENGTH = 0x1000;
|
|
7
9
|
const VIC_PALETTE = [
|
|
8
10
|
[0x00, 0x00, 0x00],
|
|
9
11
|
[0xFF, 0xFF, 0xFF],
|
|
@@ -83,6 +85,17 @@ export function resolveVicBitmapMemoryLayout(bitmapAddress = 0x2000, screenAddre
|
|
|
83
85
|
if (rangesOverlap(bitmapAddress, BITMAP_BYTE_LENGTH, screenAddress, SCREEN_RAM_BYTE_LENGTH)) {
|
|
84
86
|
throw new Error("Bitmap RAM and screen RAM must not overlap");
|
|
85
87
|
}
|
|
88
|
+
// In banks 0 and 2 the VIC reads the character ROM, not RAM, at bank offsets $1000-$1FFF.
|
|
89
|
+
if (bank === 0 || bank === 2) {
|
|
90
|
+
const shadowStart = bankBase + CHAR_ROM_SHADOW_OFFSET;
|
|
91
|
+
const shadowRange = `$${formatHex(shadowStart)}-$${formatHex(shadowStart + CHAR_ROM_SHADOW_LENGTH - 1)}`;
|
|
92
|
+
if (rangesOverlap(bitmapOffset, BITMAP_BYTE_LENGTH, CHAR_ROM_SHADOW_OFFSET, CHAR_ROM_SHADOW_LENGTH)) {
|
|
93
|
+
throw new Error(`Bitmap RAM must not overlap the character ROM shadow at ${shadowRange} in VIC bank ${bank}`);
|
|
94
|
+
}
|
|
95
|
+
if (rangesOverlap(screenOffset, SCREEN_RAM_BYTE_LENGTH, CHAR_ROM_SHADOW_OFFSET, CHAR_ROM_SHADOW_LENGTH)) {
|
|
96
|
+
throw new Error(`Screen RAM must not overlap the character ROM shadow at ${shadowRange} in VIC bank ${bank}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
86
99
|
const screenNibble = ((screenOffset / 0x0400) & 0x0F) << 4;
|
|
87
100
|
const bitmapBit = bitmapOffset === 0x2000 ? 0x08 : 0x00;
|
|
88
101
|
return {
|
|
@@ -310,6 +323,9 @@ function validateAddress(address, length, label) {
|
|
|
310
323
|
throw new Error(`${label} range must fit within 64 KB address space`);
|
|
311
324
|
}
|
|
312
325
|
}
|
|
326
|
+
function formatHex(value) {
|
|
327
|
+
return value.toString(16).toUpperCase().padStart(4, "0");
|
|
328
|
+
}
|
|
313
329
|
function rangesOverlap(leftStart, leftLength, rightStart, rightLength) {
|
|
314
330
|
const leftEnd = leftStart + leftLength - 1;
|
|
315
331
|
const rightEnd = rightStart + rightLength - 1;
|
package/dist/vice/mockServer.js
CHANGED
|
@@ -40,10 +40,16 @@ export class ViceMockServer {
|
|
|
40
40
|
this.registerMetadata = buildDefaultRegisterMetadata();
|
|
41
41
|
this.registerValues = new Map();
|
|
42
42
|
this.resources = new Map();
|
|
43
|
+
/** Output lines of VICE's I/O simulation joyport device per port (0 = port 1). */
|
|
44
|
+
this.joyportOutput = [0, 0];
|
|
43
45
|
this.sockets = new Set();
|
|
46
|
+
/** Images attached with autostart, and drive CPU resets, as observed by tests. */
|
|
47
|
+
this.autostarts = [];
|
|
48
|
+
this.driveResets = [];
|
|
44
49
|
this.host = options?.host ?? "127.0.0.1";
|
|
45
50
|
this.requestedPort = options?.port;
|
|
46
51
|
this.resetState();
|
|
52
|
+
this.resetResources();
|
|
47
53
|
}
|
|
48
54
|
async start() {
|
|
49
55
|
if (this.server)
|
|
@@ -114,9 +120,26 @@ export class ViceMockServer {
|
|
|
114
120
|
switch (cmd) {
|
|
115
121
|
case 0x85: // info
|
|
116
122
|
return buildResponse(cmd, reqId);
|
|
117
|
-
case 0xCC: // reset
|
|
118
|
-
|
|
123
|
+
case 0xCC: { // reset: types 8-11 reset only that drive's CPU
|
|
124
|
+
const type = body[0] ?? 0;
|
|
125
|
+
if (type >= 8 && type <= 11) {
|
|
126
|
+
this.driveResets.push(type);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.resetState();
|
|
130
|
+
}
|
|
131
|
+
return buildResponse(cmd, reqId);
|
|
132
|
+
}
|
|
133
|
+
case 0xDD: { // autostart: power-cycles, attaches the image to drive 8 and loads (run=1 also runs)
|
|
134
|
+
const nameLength = body[3] ?? 0;
|
|
135
|
+
this.autostarts.push({
|
|
136
|
+
run: body[0] === 1,
|
|
137
|
+
fileIndex: body.readUInt16LE(1),
|
|
138
|
+
filename: body.subarray(4, 4 + nameLength).toString("utf8"),
|
|
139
|
+
});
|
|
140
|
+
this.renderAutoload(body[0] === 1);
|
|
119
141
|
return buildResponse(cmd, reqId);
|
|
142
|
+
}
|
|
120
143
|
case 0x11: { // checkpoint get
|
|
121
144
|
const id = body.readUInt32LE(0);
|
|
122
145
|
const checkpoint = this.checkpoints.get(id);
|
|
@@ -185,13 +208,17 @@ export class ViceMockServer {
|
|
|
185
208
|
case 0x01: { // mem get
|
|
186
209
|
const start = body.readUInt16LE(1);
|
|
187
210
|
const end = body.readUInt16LE(3);
|
|
188
|
-
|
|
211
|
+
if (start > end) {
|
|
212
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x81); // VICE rejects start > end with INVALID_PARAMETER.
|
|
213
|
+
}
|
|
214
|
+
const length = end - start + 1;
|
|
189
215
|
const payload = Buffer.alloc(2 + length);
|
|
190
|
-
payload.writeUInt16LE(length, 0);
|
|
216
|
+
payload.writeUInt16LE(length & 0xffff, 0); // Like VICE: a full 64 KB read reports ML = 0.
|
|
191
217
|
for (let i = 0; i < length; i += 1) {
|
|
192
218
|
payload[2 + i] = this.memory[(start + i) & 0xffff];
|
|
193
219
|
}
|
|
194
220
|
this.applyHeartbeatOverlay(start, length, payload);
|
|
221
|
+
this.applyJoyportOverlay(start, length, payload);
|
|
195
222
|
return buildResponse(cmd, reqId, payload);
|
|
196
223
|
}
|
|
197
224
|
case 0x02: { // mem set
|
|
@@ -246,7 +273,7 @@ export class ViceMockServer {
|
|
|
246
273
|
const name = body.subarray(1, 1 + nameLen).toString("utf8");
|
|
247
274
|
const value = this.resources.get(name);
|
|
248
275
|
if (value === undefined) {
|
|
249
|
-
return buildResponse(cmd, reqId, Buffer.
|
|
276
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x01); // VICE: OBJECT_MISSING for an unknown resource.
|
|
250
277
|
}
|
|
251
278
|
if (typeof value === "number") {
|
|
252
279
|
const buffer = Buffer.alloc(2 + 4);
|
|
@@ -266,12 +293,20 @@ export class ViceMockServer {
|
|
|
266
293
|
const isNumber = body[0] === 1;
|
|
267
294
|
const nameLen = body[1] ?? 0;
|
|
268
295
|
const name = body.subarray(2, 2 + nameLen).toString("utf8");
|
|
296
|
+
if (!this.resources.has(name)) {
|
|
297
|
+
// VICE: an unknown integer resource fails resources_set (CMD_FAILURE); an unknown string one is OBJECT_MISSING.
|
|
298
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), isNumber ? 0x8f : 0x01);
|
|
299
|
+
}
|
|
269
300
|
if (isNumber) {
|
|
270
301
|
const valueLen = body[2 + nameLen] ?? 0;
|
|
271
302
|
const valueBytes = body.subarray(3 + nameLen, 3 + nameLen + valueLen);
|
|
272
303
|
const padded = Buffer.alloc(4);
|
|
273
304
|
valueBytes.copy(padded, 0, 0, Math.min(4, valueBytes.length));
|
|
274
305
|
const value = padded.readInt32LE(0);
|
|
306
|
+
// VICE rejects joyport device ids it does not have (JOYPORT_MAX_DEVICES is 44 in 3.10).
|
|
307
|
+
if (/^JoyPort\dDevice$/.test(name) && (value < 0 || value >= 44)) {
|
|
308
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x8f);
|
|
309
|
+
}
|
|
275
310
|
this.resources.set(name, value);
|
|
276
311
|
}
|
|
277
312
|
else {
|
|
@@ -281,6 +316,18 @@ export class ViceMockServer {
|
|
|
281
316
|
}
|
|
282
317
|
return buildResponse(cmd, reqId);
|
|
283
318
|
}
|
|
319
|
+
case 0xA2: { // joyport set: like VICE, it only stores the I/O simulation device's output lines
|
|
320
|
+
const port = body.readUInt16LE(0);
|
|
321
|
+
const value = body.readUInt16LE(2);
|
|
322
|
+
if (port > 1) {
|
|
323
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x01);
|
|
324
|
+
}
|
|
325
|
+
if (value > 0xff) {
|
|
326
|
+
return buildResponse(cmd, reqId, Buffer.alloc(0), 0x81);
|
|
327
|
+
}
|
|
328
|
+
this.joyportOutput[port] = value & 0x1f;
|
|
329
|
+
return buildResponse(cmd, reqId);
|
|
330
|
+
}
|
|
284
331
|
case 0xAA: // exit monitor
|
|
285
332
|
return buildResponse(cmd, reqId);
|
|
286
333
|
case 0xBB: // quit
|
|
@@ -359,7 +406,9 @@ export class ViceMockServer {
|
|
|
359
406
|
return payload;
|
|
360
407
|
}
|
|
361
408
|
encodeDisplay() {
|
|
362
|
-
const pixels = Buffer.alloc(64
|
|
409
|
+
const pixels = Buffer.alloc(64);
|
|
410
|
+
for (let i = 0; i < pixels.length; i += 1)
|
|
411
|
+
pixels[i] = i & 0x0f;
|
|
363
412
|
const header = Buffer.alloc(17);
|
|
364
413
|
const pixelLength = Buffer.alloc(4);
|
|
365
414
|
header.writeUInt32LE(13, 0);
|
|
@@ -371,7 +420,9 @@ export class ViceMockServer {
|
|
|
371
420
|
header.writeUInt16LE(200, 14);
|
|
372
421
|
header[16] = 8;
|
|
373
422
|
pixelLength.writeUInt32LE(pixels.length, 0);
|
|
374
|
-
|
|
423
|
+
// VICE 3.9/3.10 send a body of 4 + FL + BL bytes, which drops the last 4 pixel bytes.
|
|
424
|
+
const full = Buffer.concat([header, pixelLength, pixels]);
|
|
425
|
+
return full.subarray(0, 4 + 13 + pixels.length);
|
|
375
426
|
}
|
|
376
427
|
applyRegisterWrites(body) {
|
|
377
428
|
if (body.length < 3) {
|
|
@@ -430,11 +481,51 @@ export class ViceMockServer {
|
|
|
430
481
|
for (const meta of this.registerMetadata) {
|
|
431
482
|
this.registerValues.set(meta.id, 0);
|
|
432
483
|
}
|
|
484
|
+
this.joyportOutput = [0, 0];
|
|
485
|
+
}
|
|
486
|
+
// Resources survive a machine reset in VICE. Only resources that exist in
|
|
487
|
+
// VICE 3.10 are known; others fail the way VICE fails (see below).
|
|
488
|
+
resetResources() {
|
|
433
489
|
this.resources = new Map([
|
|
434
|
-
["C64Model", 2],
|
|
435
490
|
["SidEngine", 1],
|
|
491
|
+
["JoyPort1Device", 1],
|
|
492
|
+
["JoyPort2Device", 1],
|
|
493
|
+
["Drive8Type", 1541],
|
|
494
|
+
["Drive9Type", 0],
|
|
495
|
+
["Drive10Type", 0],
|
|
496
|
+
["Drive11Type", 0],
|
|
497
|
+
["VICIIDoubleSize", 0],
|
|
498
|
+
["VICIIBorderMode", 0],
|
|
499
|
+
["VICIIFullscreen", 0],
|
|
500
|
+
["SoundVolume", 100],
|
|
501
|
+
["SidModel", 0],
|
|
502
|
+
["MachineVideoStandard", 1],
|
|
503
|
+
["KernalName", "kernal-901227-03.bin"],
|
|
436
504
|
]);
|
|
437
505
|
}
|
|
506
|
+
/** Lines a joyport device drives, as CIA1 sees them (active low); only the I/O simulation device (37) is modelled. */
|
|
507
|
+
joyportLines(port) {
|
|
508
|
+
return this.resources.get(`JoyPort${port}Device`) === 37 ? this.joyportOutput[port - 1] : 0xff;
|
|
509
|
+
}
|
|
510
|
+
// CIA1 port A ($DC00) reads joyport 2 and port B ($DC01) joyport 1, ANDed with the keyboard lines.
|
|
511
|
+
applyJoyportOverlay(start, length, payload) {
|
|
512
|
+
for (const [address, port] of [[0xdc00, 2], [0xdc01, 1]]) {
|
|
513
|
+
if (address >= start && address < start + length) {
|
|
514
|
+
payload[2 + address - start] = 0xff & this.joyportLines(port);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// What VICE's autoload leaves on screen after its power cycle: the typed LOAD and READY.
|
|
519
|
+
renderAutoload(run) {
|
|
520
|
+
// The power cycle's KERNAL reset clears $0200-$03FF (RAMTAS).
|
|
521
|
+
this.memory.fill(0x00, 0x0200, 0x0400);
|
|
522
|
+
this.memory.fill(0x20, SCREEN_BASE, SCREEN_BASE + 1000);
|
|
523
|
+
const lines = ['LOAD"*",8,1', "", "SEARCHING FOR *", "LOADING", "READY.", ...(run ? ["RUN"] : [])];
|
|
524
|
+
lines.forEach((line, row) => {
|
|
525
|
+
const codes = asciiToScreenCodes(line);
|
|
526
|
+
codes.copy(Buffer.from(this.memory.buffer, SCREEN_BASE + row * 40, codes.length));
|
|
527
|
+
});
|
|
528
|
+
}
|
|
438
529
|
renderHello() {
|
|
439
530
|
if (this.helloReady)
|
|
440
531
|
return;
|
package/dist/vice/readiness.js
CHANGED
|
@@ -126,15 +126,10 @@ export function asciiToScreenCodes(text) {
|
|
|
126
126
|
out.push(ch.charCodeAt(0) - 64);
|
|
127
127
|
else if (ch >= '0' && ch <= '9')
|
|
128
128
|
out.push(0x30 + (ch.charCodeAt(0) - 48));
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
else if (ch === ':')
|
|
134
|
-
out.push(0x3A);
|
|
135
|
-
else if (ch === '-')
|
|
136
|
-
out.push(0x2D);
|
|
137
|
-
// extend as needed; ignore others
|
|
129
|
+
// Space, digits and punctuation ($20-$3F) have the same screen codes as ASCII.
|
|
130
|
+
else if (ch >= ' ' && ch <= '?')
|
|
131
|
+
out.push(ch.charCodeAt(0));
|
|
132
|
+
// ignore others
|
|
138
133
|
}
|
|
139
134
|
return Buffer.from(out);
|
|
140
135
|
}
|
package/dist/vice/viceClient.js
CHANGED
|
@@ -159,6 +159,24 @@ export class ViceClient {
|
|
|
159
159
|
async resetSoft() { await this.send(0xCC, Buffer.from([0x00])); }
|
|
160
160
|
async resetHard() { await this.send(0xCC, Buffer.from([0x01])); }
|
|
161
161
|
async reset(type = 0) { await this.send(0xCC, Buffer.from([type])); }
|
|
162
|
+
/** Reset only the drive CPU of unit 8-11 (BM 0xCC reset types 8-11). */
|
|
163
|
+
async resetDrive(unit) { await this.send(0xCC, Buffer.from([unit])); }
|
|
164
|
+
/**
|
|
165
|
+
* Attach an image and load from it (BM 0xDD Autostart). With run=false VICE
|
|
166
|
+
* uses its autoload mode, which attaches to drive 8 and LOADs without RUN.
|
|
167
|
+
*/
|
|
168
|
+
async autostart(filename, options) {
|
|
169
|
+
const name = Buffer.from(filename, "utf8");
|
|
170
|
+
if (name.length === 0 || name.length > 255) {
|
|
171
|
+
throw new Error("Autostart file names must be 1-255 bytes");
|
|
172
|
+
}
|
|
173
|
+
const body = Buffer.alloc(4 + name.length);
|
|
174
|
+
body[0] = options.run ? 1 : 0;
|
|
175
|
+
body.writeUInt16LE(options.fileIndex ?? 0, 1);
|
|
176
|
+
body[3] = name.length;
|
|
177
|
+
name.copy(body, 4);
|
|
178
|
+
await this.send(0xDD, body);
|
|
179
|
+
}
|
|
162
180
|
/**
|
|
163
181
|
* Exit the emulator process (BM 0xBB Quit). The socket will be closed by VICE.
|
|
164
182
|
* Consumers should still call close() to dispose any local resources.
|
|
@@ -177,7 +195,9 @@ export class ViceClient {
|
|
|
177
195
|
body[5] = 0; // comp space
|
|
178
196
|
body.writeUInt16LE(0, 6); // bank
|
|
179
197
|
const frame = await this.send(0x01, body);
|
|
180
|
-
|
|
198
|
+
// ML is 16 bits wide, so VICE reports a full $0000-$FFFF read as 0; the body length is authoritative.
|
|
199
|
+
const declared = frame.readUInt16LE(12);
|
|
200
|
+
const len = Math.min(frame.length - 14, declared === 0 ? 0x10000 : declared);
|
|
181
201
|
return frame.subarray(14, 14 + len);
|
|
182
202
|
}
|
|
183
203
|
async memSet(start, payload) {
|
|
@@ -200,7 +220,7 @@ export class ViceClient {
|
|
|
200
220
|
/** Set the VICE joyport's active-low control byte (BM command 0xA2). */
|
|
201
221
|
async joyportSet(port, activeLowMask) {
|
|
202
222
|
const body = Buffer.alloc(4);
|
|
203
|
-
body.writeUInt16LE(port, 0);
|
|
223
|
+
body.writeUInt16LE(port - 1, 0); // VICE joyports are 0-based (JOYPORT_1 = 0, JOYPORT_2 = 1).
|
|
204
224
|
body.writeUInt16LE(activeLowMask & 0xff, 2);
|
|
205
225
|
await this.send(0xA2, body);
|
|
206
226
|
}
|
|
@@ -458,12 +478,20 @@ export class ViceClient {
|
|
|
458
478
|
const innerWidth = body.readUInt16LE(12);
|
|
459
479
|
const innerHeight = body.readUInt16LE(14);
|
|
460
480
|
const bitsPerPixel = body[16] ?? 8;
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
481
|
+
// FL counts only the DW..BP fields; the 4-byte BL field sits between them and the pixels.
|
|
482
|
+
const pixelLengthOffset = 4 + infoLength;
|
|
483
|
+
if (body.length < pixelLengthOffset + 4) {
|
|
484
|
+
throw new Error("Display response length mismatch");
|
|
485
|
+
}
|
|
486
|
+
const pixelLength = body.readUInt32LE(pixelLengthOffset);
|
|
487
|
+
const pixelStart = pixelLengthOffset + 4;
|
|
488
|
+
const received = body.length - pixelStart;
|
|
489
|
+
// VICE 3.9/3.10 size the body as 4 + FL + BL, omitting the last 4 pixel bytes; zero-fill only that gap.
|
|
490
|
+
if (received < pixelLength - 4) {
|
|
464
491
|
throw new Error("Display pixel buffer truncated");
|
|
465
492
|
}
|
|
466
|
-
const pixels = Buffer.
|
|
493
|
+
const pixels = Buffer.alloc(pixelLength);
|
|
494
|
+
body.copy(pixels, 0, pixelStart, pixelStart + Math.min(received, pixelLength));
|
|
467
495
|
return {
|
|
468
496
|
debugWidth,
|
|
469
497
|
debugHeight,
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"integer",
|
|
120
120
|
"null"
|
|
121
121
|
],
|
|
122
|
-
"description": "Track count (D64 supports 35
|
|
122
|
+
"description": "Track count (D64 supports 35 to 41; DNP requires explicit tracks).",
|
|
123
123
|
"minimum": 1,
|
|
124
124
|
"maximum": 255
|
|
125
125
|
},
|
|
@@ -421,7 +421,7 @@
|
|
|
421
421
|
"integer",
|
|
422
422
|
"null"
|
|
423
423
|
],
|
|
424
|
-
"description": "Track count (D64 supports 35
|
|
424
|
+
"description": "Track count (D64 supports 35 to 41; DNP requires explicit tracks).",
|
|
425
425
|
"minimum": 1,
|
|
426
426
|
"maximum": 255
|
|
427
427
|
},
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
"required": [
|
|
179
179
|
"op"
|
|
180
180
|
],
|
|
181
|
-
"description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input
|
|
181
|
+
"description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
|
|
182
182
|
"additionalProperties": false,
|
|
183
183
|
"x-c64bridge-operations": [
|
|
184
184
|
{
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
"op": "joystick",
|
|
277
|
-
"description": "Simulate joystick input. On C64U/U64 this uses machine:input
|
|
277
|
+
"description": "Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4.",
|
|
278
278
|
"required": [
|
|
279
279
|
"port",
|
|
280
280
|
"controls",
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
],
|
|
286
286
|
"inputSchema": {
|
|
287
287
|
"type": "object",
|
|
288
|
-
"description": "Simulate joystick input. On C64U/U64 this uses machine:input
|
|
288
|
+
"description": "Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4.",
|
|
289
289
|
"properties": {
|
|
290
290
|
"op": {
|
|
291
291
|
"const": "joystick"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"properties": {
|
|
61
61
|
"type": {
|
|
62
62
|
"type": "string",
|
|
63
|
-
"description": "Assertion type",
|
|
63
|
+
"description": "Assertion type; sid_silent needs the C64 Ultimate audio stream (c64u backend).",
|
|
64
64
|
"enum": [
|
|
65
65
|
"screen_contains",
|
|
66
66
|
"memory_equals",
|
|
@@ -462,7 +462,7 @@
|
|
|
462
462
|
"properties": {
|
|
463
463
|
"type": {
|
|
464
464
|
"type": "string",
|
|
465
|
-
"description": "Assertion type",
|
|
465
|
+
"description": "Assertion type; sid_silent needs the C64 Ultimate audio stream (c64u backend).",
|
|
466
466
|
"enum": [
|
|
467
467
|
"screen_contains",
|
|
468
468
|
"memory_equals",
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"name": {
|
|
24
24
|
"type": "string",
|
|
25
|
-
"description": "Unique task name",
|
|
26
|
-
"minLength": 1
|
|
25
|
+
"description": "Unique task name (letters, digits, spaces, '_', '-', '.'; must not start with '.' or a space).",
|
|
26
|
+
"minLength": 1,
|
|
27
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_. -]*$"
|
|
27
28
|
},
|
|
28
29
|
"operation": {
|
|
29
30
|
"type": "string",
|
|
@@ -264,8 +265,9 @@
|
|
|
264
265
|
"properties": {
|
|
265
266
|
"name": {
|
|
266
267
|
"type": "string",
|
|
267
|
-
"description": "Unique task name",
|
|
268
|
-
"minLength": 1
|
|
268
|
+
"description": "Unique task name (letters, digits, spaces, '_', '-', '.'; must not start with '.' or a space).",
|
|
269
|
+
"minLength": 1,
|
|
270
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_. -]*$"
|
|
269
271
|
},
|
|
270
272
|
"operation": {
|
|
271
273
|
"type": "string",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"name": {
|
|
13
13
|
"type": "string",
|
|
14
|
-
"description": "VICE resource name (example: SidEngine,
|
|
14
|
+
"description": "VICE resource name (example: SidEngine, SidModel).",
|
|
15
15
|
"minLength": 1,
|
|
16
16
|
"maxLength": 64,
|
|
17
17
|
"pattern": "^[A-Z][A-Za-z0-9_.-]{0,63}$"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"name": {
|
|
48
48
|
"type": "string",
|
|
49
|
-
"description": "VICE resource name (example: SidEngine,
|
|
49
|
+
"description": "VICE resource name (example: SidEngine, SidModel).",
|
|
50
50
|
"minLength": 1,
|
|
51
51
|
"maxLength": 64,
|
|
52
52
|
"pattern": "^[A-Z][A-Za-z0-9_.-]{0,63}$"
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"name": {
|
|
78
78
|
"type": "string",
|
|
79
|
-
"description": "VICE resource name (example: SidEngine,
|
|
79
|
+
"description": "VICE resource name (example: SidEngine, SidModel).",
|
|
80
80
|
"minLength": 1,
|
|
81
81
|
"maxLength": 64,
|
|
82
82
|
"pattern": "^[A-Z][A-Za-z0-9_.-]{0,63}$"
|