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.
Files changed (63) hide show
  1. package/README.md +2 -2
  2. package/data/embeddings_basic.json +1 -1
  3. package/data/embeddings_hardware.json +1 -1
  4. package/data/embeddings_mixed.json +1 -1
  5. package/data/embeddings_other.json +1 -1
  6. package/data/graphics/character-set.csv +64 -64
  7. package/data/sound/sidwave.md +113 -36
  8. package/data/sound/sidwave.schema.json +138 -28
  9. package/dist/.tsbuildinfo +1 -1
  10. package/dist/c64Client.js +144 -150
  11. package/dist/config.js +93 -39
  12. package/dist/device.js +252 -130
  13. package/dist/diagnostics.js +10 -2
  14. package/dist/knowledge.js +2 -1
  15. package/dist/mcp-server.js +12 -4
  16. package/dist/petscii.js +18 -15
  17. package/dist/petsciiArt.js +40 -14
  18. package/dist/rag/indexer.js +2 -1
  19. package/dist/sidwave.js +453 -61
  20. package/dist/sidwaveCompiler.js +1424 -275
  21. package/dist/tools/audio.js +97 -18
  22. package/dist/tools/debug.js +9 -9
  23. package/dist/tools/developer.js +7 -7
  24. package/dist/tools/errors.js +19 -1
  25. package/dist/tools/graphics.js +4 -4
  26. package/dist/tools/hex.js +9 -0
  27. package/dist/tools/input.js +21 -18
  28. package/dist/tools/machineControl.js +6 -6
  29. package/dist/tools/memory.js +11 -10
  30. package/dist/tools/meta/audio.js +8 -2
  31. package/dist/tools/meta/background.js +6 -1
  32. package/dist/tools/meta/filesystem.js +37 -10
  33. package/dist/tools/meta/graphics.js +11 -16
  34. package/dist/tools/meta/memory.js +16 -22
  35. package/dist/tools/meta/program.js +28 -9
  36. package/dist/tools/pollValidator.js +30 -2
  37. package/dist/tools/printer.js +4 -4
  38. package/dist/tools/programRunners.js +10 -9
  39. package/dist/tools/registry/disk.js +7 -5
  40. package/dist/tools/storage.js +21 -17
  41. package/dist/tools/streaming.js +2 -2
  42. package/dist/tools/translation/assembler.js +192 -57
  43. package/dist/tools/translation/basicTokenizer.js +44 -10
  44. package/dist/tools/translation/opcodeInventory.js +1 -1
  45. package/dist/tools/types.js +11 -4
  46. package/dist/tools/vice.js +1 -1
  47. package/dist/vicBitmap.js +16 -0
  48. package/dist/vice/mockServer.js +99 -8
  49. package/dist/vice/readiness.js +4 -9
  50. package/dist/vice/viceClient.js +34 -6
  51. package/mcp/schemas/c64_disk.schema.json +2 -2
  52. package/mcp/schemas/c64_input.schema.json +3 -3
  53. package/mcp/schemas/c64_program.schema.json +2 -2
  54. package/mcp/schemas/c64_system.schema.json +6 -4
  55. package/mcp/schemas/c64_vice.schema.json +3 -3
  56. package/mcp/server.json +1 -1
  57. package/mcp/tools.json +20 -14
  58. package/mcp.json +1 -1
  59. package/package.json +1 -1
  60. package/scripts/mockC64Server.mjs +113 -20
  61. package/scripts/run-coverage.mjs +31 -1
  62. package/scripts/update-readme.ts +3 -2
  63. 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
- const lineBuffers = [];
18
- let currentAddress = startAddress;
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 > 65535) {
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
- const tokenised = tokenize(content);
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
- const encoded = encodeStringWithNames(segment);
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
- const plainChar = (!inString && !inRemark ? upper[index] : char) ?? char;
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", 0x83],
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", 0x99],
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.ZpS], ["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]],
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,
@@ -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
- const handler = handlers[opKey];
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?.[operation];
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?.[operation];
196
+ const mappedName = ownValue(tool.operationToolNames, operation);
196
197
  if (mappedName) {
197
198
  return mappedName;
198
199
  }
199
- return tool.operationPlatforms?.[operation] ? operation : toolName;
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) {
@@ -33,7 +33,7 @@ const resourceValueSchema = {
33
33
  },
34
34
  };
35
35
  const resourceNameSchema = stringSchema({
36
- description: "VICE resource name (example: SidEngine, C64Model).",
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;
@@ -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
- this.resetState();
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
- const length = Math.max(0, end - start + 1);
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.from([0, 0]));
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, 0);
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
- return Buffer.concat([header, pixelLength, pixels]);
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;
@@ -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
- else if (ch === ' ')
130
- out.push(0x20);
131
- else if (ch === '.')
132
- out.push(0x2E);
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
  }
@@ -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
- const len = frame.readUInt16LE(12);
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
- const pixelLength = body.readUInt32LE(17);
462
- const pixelStart = 4 + infoLength;
463
- if (pixelStart + pixelLength > body.length) {
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.from(body.subarray(pixelStart, pixelStart + pixelLength));
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 or 40; DNP requires explicit tracks).",
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 or 40; DNP requires explicit tracks).",
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; VICE uses Binary Monitor Joyport Set. 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.",
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; VICE uses Binary Monitor Joyport Set.",
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; VICE uses Binary Monitor Joyport Set.",
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, C64Model).",
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, C64Model).",
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, C64Model).",
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}$"
package/mcp/server.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Stdio-based Model Context Protocol (MCP) server that bridges Commodore 64 Ultimate hardware with AI agents.",
5
5
  "transports": [
6
6
  "stdio"