c64bridge 1.0.4 → 1.0.5

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 (41) hide show
  1. package/data/embeddings_basic.json +1 -1
  2. package/data/embeddings_hardware.json +1 -1
  3. package/data/embeddings_mixed.json +1 -1
  4. package/data/embeddings_other.json +1 -1
  5. package/data/graphics/character-set.csv +64 -64
  6. package/dist/.tsbuildinfo +1 -1
  7. package/dist/c64Client.js +65 -61
  8. package/dist/config.js +75 -38
  9. package/dist/device.js +122 -106
  10. package/dist/knowledge.js +2 -1
  11. package/dist/petscii.js +18 -15
  12. package/dist/petsciiArt.js +40 -14
  13. package/dist/rag/indexer.js +2 -1
  14. package/dist/sidwave.js +1 -1
  15. package/dist/sidwaveCompiler.js +3 -9
  16. package/dist/tools/audio.js +27 -6
  17. package/dist/tools/debug.js +9 -9
  18. package/dist/tools/errors.js +12 -1
  19. package/dist/tools/hex.js +9 -0
  20. package/dist/tools/input.js +13 -7
  21. package/dist/tools/memory.js +5 -4
  22. package/dist/tools/meta/background.js +6 -1
  23. package/dist/tools/meta/filesystem.js +37 -10
  24. package/dist/tools/meta/graphics.js +10 -15
  25. package/dist/tools/meta/memory.js +12 -18
  26. package/dist/tools/meta/program.js +19 -4
  27. package/dist/tools/pollValidator.js +30 -2
  28. package/dist/tools/programRunners.js +5 -4
  29. package/dist/tools/translation/assembler.js +150 -47
  30. package/dist/tools/translation/basicTokenizer.js +39 -9
  31. package/dist/tools/types.js +11 -4
  32. package/dist/vicBitmap.js +16 -0
  33. package/dist/vice/mockServer.js +7 -3
  34. package/dist/vice/viceClient.js +16 -6
  35. package/mcp/schemas/c64_system.schema.json +6 -4
  36. package/mcp/server.json +1 -1
  37. package/mcp/tools.json +6 -4
  38. package/mcp.json +1 -1
  39. package/package.json +1 -1
  40. package/scripts/mockC64Server.mjs +98 -19
  41. package/server.json +2 -2
@@ -250,6 +250,14 @@ function cancelHeartbeatOnWrite(state, address, length) {
250
250
  }
251
251
 
252
252
  function seedReadyPrompt(state) {
253
+ // After a reset the KERNAL clears screen RAM to spaces and BASIC sets up an
254
+ // empty program: TXTTAB=$0801, VARTAB/ARYTAB/STREND=$0803, FRETOP/MEMSIZ=$A000
255
+ // (as read from an Ultimate 64 after machine:reset).
256
+ state.memory.fill(toScreenCode(" "), 0x0400, 0x0400 + 1000);
257
+ state.memory.set([0x01, 0x08, 0x03, 0x08, 0x03, 0x08, 0x03, 0x08, 0x00, 0xa0], 0x2b);
258
+ state.memory.set([0x00, 0xa0], 0x37);
259
+ state.memory.set([0x00, 0x00], 0x0801);
260
+
253
261
  const text = "READY.";
254
262
  const buffer = new Uint8Array(text.length + 1);
255
263
  for (let i = 0; i < text.length; i += 1) {
@@ -259,6 +267,26 @@ function seedReadyPrompt(state) {
259
267
  state.memory.set(buffer, 0x0400);
260
268
  }
261
269
 
270
+ /**
271
+ * BASIC LINKPRG ($A533): walk the line chain from $0801 and rewrite each link
272
+ * pointer to the byte after the line's terminating zero. Stops at a zero link
273
+ * high byte, as the ROM does; the step limit only guards the mock.
274
+ */
275
+ function relinkBasicProgram(memory) {
276
+ let line = 0x0801;
277
+ for (let steps = 0; steps < 0x10000 && line < 0xfffb; steps += 1) {
278
+ if (memory[line + 1] === 0) return;
279
+ let y = 4;
280
+ do {
281
+ y += 1;
282
+ } while (line + y < 0xffff && memory[line + y] !== 0);
283
+ const next = line + y + 1;
284
+ memory[line] = next & 0xff;
285
+ memory[line + 1] = (next >> 8) & 0xff;
286
+ line = next;
287
+ }
288
+ }
289
+
262
290
  export async function startMockC64Server(options = {}) {
263
291
  const state = createInitialState();
264
292
  const streamRuntime = createStreamRuntime();
@@ -269,9 +297,14 @@ export async function startMockC64Server(options = {}) {
269
297
  // seed memory with READY prompt at $0400 using screen codes
270
298
  seedReadyPrompt(state);
271
299
 
300
+ // Firmware drive ids (route_drives.h DriveToSubsys): a and b are the
301
+ // emulated drives, softiec the IEC file system; anything else is rejected.
302
+ const FIRMWARE_DRIVE_BUS_IDS = { a: 8, b: 9, softiec: 11 };
303
+
272
304
  function ensureDriveState(id) {
273
305
  if (!state.drives[id]) {
274
306
  state.drives[id] = {
307
+ busId: FIRMWARE_DRIVE_BUS_IDS[id] ?? null,
275
308
  mountedImage: null,
276
309
  mode: "1541",
277
310
  power: "off",
@@ -282,14 +315,20 @@ export async function startMockC64Server(options = {}) {
282
315
  return state.drives[id];
283
316
  }
284
317
 
285
- ensureDriveState("drive8");
318
+ function seedDrives() {
319
+ ensureDriveState("a");
320
+ ensureDriveState("b");
321
+ ensureDriveState("softiec");
322
+ }
323
+
324
+ seedDrives();
286
325
 
287
326
  function resetState() {
288
327
  stopAllStreams();
289
328
  const fresh = createInitialState();
290
329
  Object.assign(state, fresh);
291
330
  seedReadyPrompt(state);
292
- ensureDriveState("drive8");
331
+ seedDrives();
293
332
  }
294
333
 
295
334
  function stopAllStreams() {
@@ -401,18 +440,32 @@ export async function startMockC64Server(options = {}) {
401
440
  }
402
441
 
403
442
  if (method === "GET" && url === "/v1/drives") {
404
- const drives = {};
405
- for (const [driveId, driveState] of Object.entries(state.drives)) {
406
- drives[driveId] = {
407
- enabled: driveState.power !== "off",
408
- power: driveState.power,
409
- mode: driveState.mode,
410
- // Mirror the real firmware's DriveInfo shape (image_path is a
411
- // plain string), not the internal mount-params object.
412
- image_path: driveState.mountedImage?.image ?? null,
413
- type: driveState.mountedImage?.type ?? driveState.mode ?? null,
443
+ // Firmware shape (route_drives.cc drive_info, IecInterface::info): a list
444
+ // of single-key objects. Power is `enabled`; the mounted image is split
445
+ // into its directory (image_path) and file name (image_file). The soft
446
+ // IEC drive is listed as "IEC Drive" although its route id is softiec,
447
+ // and the printer is listed but is not a drive.
448
+ const splitImage = (image) => {
449
+ if (!image) return { image_file: "", image_path: "" };
450
+ const slash = image.lastIndexOf("/");
451
+ return { image_file: image.slice(slash + 1), image_path: slash > 0 ? image.slice(0, slash) : "/" };
452
+ };
453
+ const drives = ["a", "b"].map((driveId) => {
454
+ const driveState = state.drives[driveId];
455
+ return {
456
+ [driveId]: {
457
+ enabled: driveState.power !== "off",
458
+ bus_id: driveState.busId,
459
+ type: driveState.mode,
460
+ rom: `${driveState.mode}.rom`,
461
+ ...splitImage(driveState.mountedImage?.image),
462
+ },
414
463
  };
415
- }
464
+ });
465
+ drives.push(
466
+ { "IEC Drive": { bus_id: state.drives.softiec.busId, enabled: state.drives.softiec.power !== "off", type: "DOS emulation", last_error: "00, OK, 00, 00", partitions: [{ id: 1, path: "/Temp/" }] } },
467
+ { "Printer Emulation": { bus_id: 4, enabled: false } },
468
+ );
416
469
  sendJson(res, { drives });
417
470
  return;
418
471
  }
@@ -526,11 +579,20 @@ export async function startMockC64Server(options = {}) {
526
579
  state.lastLoadedPrg = prg;
527
580
  state.loadCount += 1;
528
581
 
529
- // Mirror real firmware: place the PRG body at its embedded load
530
- // address without starting it (no RUN, no BASIC pointer changes).
582
+ // Mirror Ultimate 64 firmware 3.15: the PRG body is placed at its
583
+ // embedded load address without starting it, and, as after a BASIC
584
+ // LOAD, VARTAB/ARYTAB/STREND are set to the end address and the BASIC
585
+ // program at $0801 is relinked (which rewrites raw code at $0801).
531
586
  if (prg.length >= 2) {
532
587
  const loadAddress = prg.readUInt16LE(0);
533
- state.memory.set(prg.subarray(2), loadAddress);
588
+ const body = prg.subarray(2);
589
+ state.memory.set(body.subarray(0, 0x10000 - loadAddress), loadAddress);
590
+ const endAddress = Math.min(0xffff, loadAddress + body.length);
591
+ for (const pointer of [0x2d, 0x2f, 0x31]) {
592
+ state.memory[pointer] = endAddress & 0xff;
593
+ state.memory[pointer + 1] = endAddress >> 8;
594
+ }
595
+ relinkBasicProgram(state.memory);
534
596
  }
535
597
 
536
598
  sendJson(res, { result: "ok", bytes: prg.length });
@@ -564,9 +626,22 @@ export async function startMockC64Server(options = {}) {
564
626
  }
565
627
 
566
628
  const attachment = Buffer.concat(chunks);
629
+ // Like the firmware, a multipart upload is a list of files in part
630
+ // order: the SID first, optional song lengths second. A text field
631
+ // (for example a JSON-serialised Buffer) is not a SID upload.
632
+ const contentType = String(req.headers["content-type"] ?? "");
633
+ const files = contentType.startsWith("multipart/form-data")
634
+ ? [...(await new Response(attachment, { headers: { "content-type": contentType } }).formData()).values()]
635
+ : null;
636
+ if (files && (files.length === 0 || files.some((part) => typeof part === "string"))) {
637
+ sendJson(res, { errors: ["Upload of file failed."] }, 400);
638
+ return;
639
+ }
640
+ const sid = files ? Buffer.from(await files[0].arrayBuffer()) : attachment;
641
+ const songlengths = files?.[1] ? Buffer.from(await files[1].arrayBuffer()) : null;
567
642
  state.sidAttachmentCount += 1;
568
- state.lastSidAttachment = { songnr, bytes: attachment.length };
569
- sendJson(res, { result: "sidplay_attachment", bytes: attachment.length, songnr });
643
+ state.lastSidAttachment = { songnr, bytes: sid.length, sid, songlengths };
644
+ sendJson(res, { result: "sidplay_attachment", bytes: sid.length, songnr });
570
645
  return;
571
646
  }
572
647
  }
@@ -816,8 +891,12 @@ export async function startMockC64Server(options = {}) {
816
891
  if (routeUrl) {
817
892
  const match = /^\/v1\/drives\/([^:]+):(mount|remove|reset|on|off|load_rom|set_mode)$/.exec(routeUrl.pathname);
818
893
  if (match) {
819
- const driveId = decodeURIComponent(match[1]);
894
+ const driveId = decodeURIComponent(match[1]).toLowerCase();
820
895
  const action = match[2];
896
+ if (!Object.hasOwn(FIRMWARE_DRIVE_BUS_IDS, driveId)) {
897
+ sendJson(res, { errors: [`Invalid Drive '${decodeURIComponent(match[1])}'`] }, 400);
898
+ return;
899
+ }
821
900
  const driveState = ensureDriveState(driveId);
822
901
 
823
902
  const respond = (payload) => {
package/server.json CHANGED
@@ -8,12 +8,12 @@
8
8
  "source": "github"
9
9
  },
10
10
  "websiteUrl": "https://github.com/chrisgleissner/c64bridge",
11
- "version": "1.0.4",
11
+ "version": "1.0.5",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "c64bridge",
16
- "version": "1.0.4",
16
+ "version": "1.0.5",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },