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.
- 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/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +65 -61
- package/dist/config.js +75 -38
- package/dist/device.js +122 -106
- package/dist/knowledge.js +2 -1
- package/dist/petscii.js +18 -15
- package/dist/petsciiArt.js +40 -14
- package/dist/rag/indexer.js +2 -1
- package/dist/sidwave.js +1 -1
- package/dist/sidwaveCompiler.js +3 -9
- package/dist/tools/audio.js +27 -6
- package/dist/tools/debug.js +9 -9
- package/dist/tools/errors.js +12 -1
- package/dist/tools/hex.js +9 -0
- package/dist/tools/input.js +13 -7
- package/dist/tools/memory.js +5 -4
- package/dist/tools/meta/background.js +6 -1
- package/dist/tools/meta/filesystem.js +37 -10
- package/dist/tools/meta/graphics.js +10 -15
- package/dist/tools/meta/memory.js +12 -18
- package/dist/tools/meta/program.js +19 -4
- package/dist/tools/pollValidator.js +30 -2
- package/dist/tools/programRunners.js +5 -4
- package/dist/tools/translation/assembler.js +150 -47
- package/dist/tools/translation/basicTokenizer.js +39 -9
- package/dist/tools/types.js +11 -4
- package/dist/vicBitmap.js +16 -0
- package/dist/vice/mockServer.js +7 -3
- package/dist/vice/viceClient.js +16 -6
- package/mcp/schemas/c64_system.schema.json +6 -4
- package/mcp/server.json +1 -1
- package/mcp/tools.json +6 -4
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +98 -19
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
530
|
-
// address without starting it
|
|
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
|
-
|
|
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:
|
|
569
|
-
sendJson(res, { result: "sidplay_attachment", bytes:
|
|
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.
|
|
11
|
+
"version": "1.0.5",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "c64bridge",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.5",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|