c64bridge 1.0.5 → 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/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +83 -93
- package/dist/config.js +21 -4
- package/dist/device.js +130 -24
- package/dist/diagnostics.js +10 -2
- package/dist/mcp-server.js +12 -4
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -269
- package/dist/tools/audio.js +70 -12
- package/dist/tools/developer.js +7 -7
- package/dist/tools/errors.js +7 -0
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +8 -11
- package/dist/tools/machineControl.js +6 -6
- package/dist/tools/memory.js +6 -6
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/graphics.js +1 -1
- package/dist/tools/meta/memory.js +4 -4
- package/dist/tools/meta/program.js +12 -8
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +8 -8
- 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 +44 -12
- package/dist/tools/translation/basicTokenizer.js +5 -1
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/vice.js +1 -1
- package/dist/vice/mockServer.js +92 -5
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +18 -0
- 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_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +14 -10
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +15 -1
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- package/server.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { assemblyToPrgDetailed, AssemblyError
|
|
2
|
+
import { assemblyToPrgDetailed, AssemblyError } from "./translation/assembler.js";
|
|
3
3
|
import { basicToPrg } from "./translation/basicTokenizer.js";
|
|
4
4
|
import { defineToolModule, OPERATION_DISCRIMINATOR, } from "./types.js";
|
|
5
5
|
import { booleanSchema, objectSchema, optionalSchema, stringSchema } from "./schema.js";
|
|
@@ -267,7 +267,7 @@ async function executeUploadRunBasic(rawArgs, ctx) {
|
|
|
267
267
|
const runBasic = async (source) => ctx.client.uploadAndRunBasic(source);
|
|
268
268
|
let result = await runBasic(activeProgram);
|
|
269
269
|
if (!result.success) {
|
|
270
|
-
throw new ToolExecutionError("C64
|
|
270
|
+
throw new ToolExecutionError("C64 backend reported failure while running BASIC program", {
|
|
271
271
|
details: extractFailureDetails(result.details),
|
|
272
272
|
});
|
|
273
273
|
}
|
|
@@ -442,13 +442,13 @@ async function executeUploadRunAsm(rawArgs, ctx) {
|
|
|
442
442
|
// Assemble locally to expose structured metadata and capture symbols
|
|
443
443
|
const asmResult = assemblyToPrgDetailed(parsed.program);
|
|
444
444
|
const prg = asmResult.prg;
|
|
445
|
-
const entryAddress =
|
|
445
|
+
const entryAddress = asmResult.entryAddress;
|
|
446
446
|
if (ctx.platform.id === "vice") {
|
|
447
447
|
setViceSymbols(asmResult.symbols.entries());
|
|
448
448
|
}
|
|
449
449
|
const result = await ctx.client.uploadAndRunAsm(parsed.program);
|
|
450
450
|
if (!result.success) {
|
|
451
|
-
return toolErrorResult(new ToolExecutionError("C64
|
|
451
|
+
return toolErrorResult(new ToolExecutionError("C64 backend reported failure while running assembly program", {
|
|
452
452
|
details: extractFailureDetails(result.details),
|
|
453
453
|
}));
|
|
454
454
|
}
|
|
@@ -516,7 +516,7 @@ async function executeLoadPrg(rawArgs, ctx) {
|
|
|
516
516
|
ctx.logger.info("Loading PRG file", { path: parsed.path });
|
|
517
517
|
const result = await ctx.client.loadPrgFile(parsed.path);
|
|
518
518
|
if (!result.success) {
|
|
519
|
-
throw new ToolExecutionError("C64
|
|
519
|
+
throw new ToolExecutionError("C64 backend reported failure while loading PRG", {
|
|
520
520
|
details: extractFailureDetails(result.details),
|
|
521
521
|
});
|
|
522
522
|
}
|
|
@@ -566,7 +566,7 @@ async function executeRunPrg(rawArgs, ctx) {
|
|
|
566
566
|
}
|
|
567
567
|
const result = await ctx.client.runPrgFile(parsed.path);
|
|
568
568
|
if (!result.success) {
|
|
569
|
-
throw new ToolExecutionError("C64
|
|
569
|
+
throw new ToolExecutionError("C64 backend reported failure while running PRG", {
|
|
570
570
|
details: extractFailureDetails(result.details),
|
|
571
571
|
});
|
|
572
572
|
}
|
|
@@ -601,7 +601,7 @@ async function executeRunCrt(rawArgs, ctx) {
|
|
|
601
601
|
ctx.logger.info("Running CRT file", { path: parsed.path });
|
|
602
602
|
const result = await ctx.client.runCrtFile(parsed.path);
|
|
603
603
|
if (!result.success) {
|
|
604
|
-
throw new ToolExecutionError("C64
|
|
604
|
+
throw new ToolExecutionError("C64 backend reported failure while running CRT", {
|
|
605
605
|
details: extractFailureDetails(result.details),
|
|
606
606
|
});
|
|
607
607
|
}
|
|
@@ -773,7 +773,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
773
773
|
ctx.logger.info("Running CRT file", { path: parsed.path });
|
|
774
774
|
const result = await ctx.client.runCrtFile(parsed.path);
|
|
775
775
|
if (!result.success) {
|
|
776
|
-
throw new ToolExecutionError("C64
|
|
776
|
+
throw new ToolExecutionError("C64 backend reported failure while running CRT", {
|
|
777
777
|
details: extractFailureDetails(result.details),
|
|
778
778
|
});
|
|
779
779
|
}
|
|
@@ -76,7 +76,7 @@ const createImageArgsSchema = objectSchema({
|
|
|
76
76
|
maxLength: 16,
|
|
77
77
|
})),
|
|
78
78
|
tracks: optionalSchema(numberSchema({
|
|
79
|
-
description: "Track count (D64 supports 35
|
|
79
|
+
description: "Track count (D64 supports 35 to 41; DNP requires explicit tracks).",
|
|
80
80
|
integer: true,
|
|
81
81
|
minimum: 1,
|
|
82
82
|
maximum: 255,
|
|
@@ -210,10 +210,10 @@ const diskOperations = [
|
|
|
210
210
|
const { path, diskname, tracks } = parsed;
|
|
211
211
|
switch (format) {
|
|
212
212
|
case "d64":
|
|
213
|
-
if (tracks !== undefined && tracks
|
|
214
|
-
throw new ToolValidationError("D64 images support 35
|
|
213
|
+
if (tracks !== undefined && (tracks < 35 || tracks > 41)) {
|
|
214
|
+
throw new ToolValidationError("D64 images support 35 to 41 tracks", {
|
|
215
215
|
path: "$.tracks",
|
|
216
|
-
details: {
|
|
216
|
+
details: { minimum: 35, maximum: 41, received: tracks },
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
219
|
return storageModule.invoke("create_d64", { path, tracks, diskname }, ctx);
|
|
@@ -274,8 +274,10 @@ export const diskModuleGroup = defineToolModule({
|
|
|
274
274
|
variants: diskOperations.map((operation) => operation.schema),
|
|
275
275
|
}),
|
|
276
276
|
tags: ["storage", "drive", "grouped"],
|
|
277
|
-
|
|
277
|
+
// The VICE Binary Monitor has no command to detach a disk image, so unmount is Ultimate-only.
|
|
278
|
+
operationPlatforms: { unmount: ["c64u", "u2"], file_info: ["c64u", "u2"], create_image: ["c64u", "u2"], find_and_run: ["c64u", "u2"] },
|
|
278
279
|
operationToolNames: {
|
|
280
|
+
unmount: "drive_remove",
|
|
279
281
|
file_info: "file_info",
|
|
280
282
|
create_image: "create_image",
|
|
281
283
|
find_and_run: "find_and_run_program_by_name",
|
package/dist/tools/storage.js
CHANGED
|
@@ -97,10 +97,10 @@ const createD64ArgsSchema = objectSchema({
|
|
|
97
97
|
properties: {
|
|
98
98
|
path: diskImagePathSchema,
|
|
99
99
|
tracks: optionalSchema(numberSchema({
|
|
100
|
-
description: "Track count
|
|
100
|
+
description: "Track count: 35 for a standard disk, up to 41 for the extended area (firmware limit).",
|
|
101
101
|
integer: true,
|
|
102
102
|
minimum: 35,
|
|
103
|
-
maximum:
|
|
103
|
+
maximum: 41,
|
|
104
104
|
})),
|
|
105
105
|
diskname: diskNameSchema,
|
|
106
106
|
},
|
|
@@ -175,7 +175,7 @@ export const storageModule = defineToolModule({
|
|
|
175
175
|
drivesListArgsSchema.parse(args ?? {});
|
|
176
176
|
ctx.logger.info("Listing Ultimate drives");
|
|
177
177
|
const details = await ctx.client.drivesList();
|
|
178
|
-
return textResult("Retrieved
|
|
178
|
+
return textResult("Retrieved drive status.", {
|
|
179
179
|
success: true,
|
|
180
180
|
drives: details,
|
|
181
181
|
});
|
|
@@ -221,14 +221,16 @@ export const storageModule = defineToolModule({
|
|
|
221
221
|
mode: parsed.mode,
|
|
222
222
|
});
|
|
223
223
|
if (!result.success) {
|
|
224
|
-
throw new ToolExecutionError("C64
|
|
224
|
+
throw new ToolExecutionError("C64 backend reported failure while mounting disk image", {
|
|
225
225
|
details: normaliseFailure(result.details),
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
228
|
const details = toRecord(result.details) ?? {};
|
|
229
229
|
const finalMode = typeof details.mode === "string" ? details.mode : parsed.mode ?? null;
|
|
230
230
|
const finalType = typeof details.type === "string" ? details.type : parsed.type ?? null;
|
|
231
|
-
|
|
231
|
+
// Backend notes (for example VICE's autoload resetting the machine) belong in the visible text.
|
|
232
|
+
const note = typeof details.note === "string" ? ` ${details.note}` : "";
|
|
233
|
+
return textResult(`Mounted ${parsed.image} on ${parsed.drive}.${note}`, {
|
|
232
234
|
success: true,
|
|
233
235
|
drive: parsed.drive,
|
|
234
236
|
image: parsed.image,
|
|
@@ -269,7 +271,7 @@ export const storageModule = defineToolModule({
|
|
|
269
271
|
ctx.logger.info("Removing disk image", { drive: parsed.drive });
|
|
270
272
|
const result = await ctx.client.driveRemove(parsed.drive);
|
|
271
273
|
if (!result.success) {
|
|
272
|
-
throw new ToolExecutionError("C64
|
|
274
|
+
throw new ToolExecutionError("C64 backend reported failure while removing disk image", {
|
|
273
275
|
details: normaliseFailure(result.details),
|
|
274
276
|
});
|
|
275
277
|
}
|
|
@@ -311,7 +313,7 @@ export const storageModule = defineToolModule({
|
|
|
311
313
|
ctx.logger.info("Resetting drive", { drive: parsed.drive });
|
|
312
314
|
const result = await ctx.client.driveReset(parsed.drive);
|
|
313
315
|
if (!result.success) {
|
|
314
|
-
throw new ToolExecutionError("C64
|
|
316
|
+
throw new ToolExecutionError("C64 backend reported failure while resetting drive", {
|
|
315
317
|
details: normaliseFailure(result.details),
|
|
316
318
|
});
|
|
317
319
|
}
|
|
@@ -353,7 +355,7 @@ export const storageModule = defineToolModule({
|
|
|
353
355
|
ctx.logger.info("Powering on drive", { drive: parsed.drive });
|
|
354
356
|
const result = await ctx.client.driveOn(parsed.drive);
|
|
355
357
|
if (!result.success) {
|
|
356
|
-
throw new ToolExecutionError("C64
|
|
358
|
+
throw new ToolExecutionError("C64 backend reported failure while powering on drive", {
|
|
357
359
|
details: normaliseFailure(result.details),
|
|
358
360
|
});
|
|
359
361
|
}
|
|
@@ -396,7 +398,7 @@ export const storageModule = defineToolModule({
|
|
|
396
398
|
ctx.logger.info("Powering off drive", { drive: parsed.drive });
|
|
397
399
|
const result = await ctx.client.driveOff(parsed.drive);
|
|
398
400
|
if (!result.success) {
|
|
399
|
-
throw new ToolExecutionError("C64
|
|
401
|
+
throw new ToolExecutionError("C64 backend reported failure while powering off drive", {
|
|
400
402
|
details: normaliseFailure(result.details),
|
|
401
403
|
});
|
|
402
404
|
}
|
|
@@ -439,7 +441,7 @@ export const storageModule = defineToolModule({
|
|
|
439
441
|
ctx.logger.info("Loading drive ROM", { drive: parsed.drive, path: parsed.path });
|
|
440
442
|
const result = await ctx.client.driveLoadRom(parsed.drive, parsed.path);
|
|
441
443
|
if (!result.success) {
|
|
442
|
-
throw new ToolExecutionError("C64
|
|
444
|
+
throw new ToolExecutionError("C64 backend reported failure while loading drive ROM", {
|
|
443
445
|
details: normaliseFailure(result.details),
|
|
444
446
|
});
|
|
445
447
|
}
|
|
@@ -482,7 +484,7 @@ export const storageModule = defineToolModule({
|
|
|
482
484
|
ctx.logger.info("Setting drive mode", { drive: parsed.drive, mode: parsed.mode });
|
|
483
485
|
const result = await ctx.client.driveSetMode(parsed.drive, parsed.mode);
|
|
484
486
|
if (!result.success) {
|
|
485
|
-
throw new ToolExecutionError("C64
|
|
487
|
+
throw new ToolExecutionError("C64 backend reported failure while setting drive mode", {
|
|
486
488
|
details: normaliseFailure(result.details),
|
|
487
489
|
});
|
|
488
490
|
}
|
|
@@ -541,7 +543,7 @@ export const storageModule = defineToolModule({
|
|
|
541
543
|
{
|
|
542
544
|
name: "create_d64",
|
|
543
545
|
description: "Create a blank D64 disk image on the Ultimate filesystem.",
|
|
544
|
-
summary: "Allocates a 35
|
|
546
|
+
summary: "Allocates a 35 to 41 track D64 image with an optional disk name.",
|
|
545
547
|
inputSchema: createD64ArgsSchema.jsonSchema,
|
|
546
548
|
relatedResources: ["c64://guide/bootstrap"],
|
|
547
549
|
tags: ["disk", "create"],
|
|
@@ -569,14 +571,16 @@ export const storageModule = defineToolModule({
|
|
|
569
571
|
diskname: parsed.diskname,
|
|
570
572
|
});
|
|
571
573
|
if (!result.success) {
|
|
572
|
-
throw new ToolExecutionError("C64
|
|
574
|
+
throw new ToolExecutionError("C64 backend reported failure while creating D64 image", {
|
|
573
575
|
details: normaliseFailure(result.details),
|
|
574
576
|
});
|
|
575
577
|
}
|
|
578
|
+
const firmwareTracks = toRecord(result.details)?.tracks;
|
|
576
579
|
return textResult(`Created D64 image at ${parsed.path}.`, {
|
|
577
580
|
success: true,
|
|
578
581
|
path: parsed.path,
|
|
579
|
-
|
|
582
|
+
// The firmware reports the track count it used, including its default of 35.
|
|
583
|
+
tracks: typeof firmwareTracks === "number" ? firmwareTracks : parsed.tracks ?? 35,
|
|
580
584
|
diskname: parsed.diskname ?? null,
|
|
581
585
|
details: toRecord(result.details) ?? {},
|
|
582
586
|
});
|
|
@@ -618,7 +622,7 @@ export const storageModule = defineToolModule({
|
|
|
618
622
|
diskname: parsed.diskname,
|
|
619
623
|
});
|
|
620
624
|
if (!result.success) {
|
|
621
|
-
throw new ToolExecutionError("C64
|
|
625
|
+
throw new ToolExecutionError("C64 backend reported failure while creating D71 image", {
|
|
622
626
|
details: normaliseFailure(result.details),
|
|
623
627
|
});
|
|
624
628
|
}
|
|
@@ -666,7 +670,7 @@ export const storageModule = defineToolModule({
|
|
|
666
670
|
diskname: parsed.diskname,
|
|
667
671
|
});
|
|
668
672
|
if (!result.success) {
|
|
669
|
-
throw new ToolExecutionError("C64
|
|
673
|
+
throw new ToolExecutionError("C64 backend reported failure while creating D81 image", {
|
|
670
674
|
details: normaliseFailure(result.details),
|
|
671
675
|
});
|
|
672
676
|
}
|
|
@@ -717,7 +721,7 @@ export const storageModule = defineToolModule({
|
|
|
717
721
|
diskname: parsed.diskname,
|
|
718
722
|
});
|
|
719
723
|
if (!result.success) {
|
|
720
|
-
throw new ToolExecutionError("C64
|
|
724
|
+
throw new ToolExecutionError("C64 backend reported failure while creating DNP image", {
|
|
721
725
|
details: normaliseFailure(result.details),
|
|
722
726
|
});
|
|
723
727
|
}
|
package/dist/tools/streaming.js
CHANGED
|
@@ -80,7 +80,7 @@ export const streamingModule = defineToolModule({
|
|
|
80
80
|
});
|
|
81
81
|
const result = await ctx.client.streamStart(parsed.stream, parsed.target);
|
|
82
82
|
if (!result.success) {
|
|
83
|
-
throw new ToolExecutionError(withFailureReason("C64
|
|
83
|
+
throw new ToolExecutionError(withFailureReason("C64 backend reported failure while starting stream", result.details), {
|
|
84
84
|
details: toRecord(result.details),
|
|
85
85
|
});
|
|
86
86
|
}
|
|
@@ -120,7 +120,7 @@ export const streamingModule = defineToolModule({
|
|
|
120
120
|
});
|
|
121
121
|
const result = await ctx.client.streamStop(parsed.stream);
|
|
122
122
|
if (!result.success) {
|
|
123
|
-
throw new ToolExecutionError(withFailureReason("C64
|
|
123
|
+
throw new ToolExecutionError(withFailureReason("C64 backend reported failure while stopping stream", result.details), {
|
|
124
124
|
details: toRecord(result.details),
|
|
125
125
|
});
|
|
126
126
|
}
|
|
@@ -126,7 +126,13 @@ export function assemblyToPrgDetailed(source, options) {
|
|
|
126
126
|
const assembler = new Assembler(statements, loadAddress);
|
|
127
127
|
assembler.runPass(0);
|
|
128
128
|
assembler.runPass(1);
|
|
129
|
-
|
|
129
|
+
const prg = assembler.buildPrg();
|
|
130
|
+
const loadAddressOfPrg = prg.length >= 2 ? prg.readUInt16LE(0) : loadAddress;
|
|
131
|
+
const stubTarget = prg.length >= 2 ? prgEntryAddress(prg) : loadAddressOfPrg;
|
|
132
|
+
const entryAddress = stubTarget !== loadAddressOfPrg
|
|
133
|
+
? stubTarget
|
|
134
|
+
: assembler.getFirstInstructionAddress() ?? loadAddressOfPrg;
|
|
135
|
+
return { prg, symbols: assembler.getSymbols(), entryAddress };
|
|
130
136
|
}
|
|
131
137
|
function normalizeSource(input) {
|
|
132
138
|
return input.replace(/\r\n?/g, "\n");
|
|
@@ -510,8 +516,14 @@ function parseUnaryExpression(stream, terminators) {
|
|
|
510
516
|
}
|
|
511
517
|
// In operand position, "<" and ">" are byte selectors and "*" is the current
|
|
512
518
|
// PC; parseBinaryExpression consumes them as binary operators elsewhere.
|
|
519
|
+
// As in ACME, 64tass, DASM and Kick Assembler, "<" and ">" apply to the
|
|
520
|
+
// whole rest of the expression: "#>msg+1" is the high byte of msg+1.
|
|
521
|
+
if (token.kind === "punct" && (token.value === "<" || token.value === ">")) {
|
|
522
|
+
const operand = parseBinaryExpression(stream, terminators, 0);
|
|
523
|
+
return { type: "unary", op: token.value, operand };
|
|
524
|
+
}
|
|
513
525
|
if (token.kind === "punct" &&
|
|
514
|
-
(token.value === "+" || token.value === "-" || token.value === "!" || token.value === "~"
|
|
526
|
+
(token.value === "+" || token.value === "-" || token.value === "!" || token.value === "~")) {
|
|
515
527
|
const operand = parseUnaryExpression(stream, terminators);
|
|
516
528
|
return { type: "unary", op: token.value, operand };
|
|
517
529
|
}
|
|
@@ -708,6 +720,8 @@ class Assembler {
|
|
|
708
720
|
this.pc = 0;
|
|
709
721
|
this.lowestWritten = MAX_ADDRESS;
|
|
710
722
|
this.highestWritten = 0;
|
|
723
|
+
/** Reserved (.res) ranges from the last pass, as [start, end] inclusive. */
|
|
724
|
+
this.reserved = [];
|
|
711
725
|
this.symbolTable.reset();
|
|
712
726
|
}
|
|
713
727
|
runPass(pass) {
|
|
@@ -719,6 +733,7 @@ class Assembler {
|
|
|
719
733
|
this.highestWritten = 0;
|
|
720
734
|
this.firstInstructionAddress = undefined;
|
|
721
735
|
}
|
|
736
|
+
this.reserved = [];
|
|
722
737
|
// Loading a PRG at $0801 makes BASIC relink the bytes there as program
|
|
723
738
|
// lines, which corrupts raw machine code. Code that starts at $0801
|
|
724
739
|
// without its own org therefore gets a "10 SYS" stub that jumps to the
|
|
@@ -766,13 +781,33 @@ class Assembler {
|
|
|
766
781
|
if (this.lowestWritten > this.highestWritten) {
|
|
767
782
|
return Buffer.alloc(0);
|
|
768
783
|
}
|
|
769
|
-
|
|
770
|
-
|
|
784
|
+
let start = this.lowestWritten;
|
|
785
|
+
let end = this.highestWritten;
|
|
786
|
+
// Reserved space that adjoins the emitted bytes (a trailing "counter: .res 1")
|
|
787
|
+
// is part of the image and zero-filled, as in ca65 and DASM; detached blocks
|
|
788
|
+
// such as zero-page variables are not, so they do not stretch the PRG.
|
|
789
|
+
for (let changed = true; changed;) {
|
|
790
|
+
changed = false;
|
|
791
|
+
for (const [resStart, resEnd] of this.reserved) {
|
|
792
|
+
// Adjoining or overlapping blocks extend the image.
|
|
793
|
+
if (resStart <= end + 1 && resEnd > end) {
|
|
794
|
+
end = resEnd;
|
|
795
|
+
changed = true;
|
|
796
|
+
}
|
|
797
|
+
if (resEnd >= start - 1 && resStart < start) {
|
|
798
|
+
start = resStart;
|
|
799
|
+
changed = true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
771
803
|
const body = this.memory.subarray(start, end + 1);
|
|
772
804
|
const header = Buffer.alloc(2);
|
|
773
805
|
header.writeUInt16LE(start, 0);
|
|
774
806
|
return Buffer.concat([header, Buffer.from(body)]);
|
|
775
807
|
}
|
|
808
|
+
getFirstInstructionAddress() {
|
|
809
|
+
return this.firstInstructionAddress;
|
|
810
|
+
}
|
|
776
811
|
getSymbols() {
|
|
777
812
|
return this.symbolTable.allGlobals();
|
|
778
813
|
}
|
|
@@ -898,14 +933,11 @@ class Assembler {
|
|
|
898
933
|
if (amount < 0) {
|
|
899
934
|
throw new AssemblyError("reserve directive requires a non-negative size", location);
|
|
900
935
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
else {
|
|
907
|
-
this.pc += amount;
|
|
908
|
-
}
|
|
936
|
+
// Reserved space advances the PC without emitting; buildPrg includes it
|
|
937
|
+
// (zero-filled) only where it adjoins emitted bytes.
|
|
938
|
+
if (amount > 0)
|
|
939
|
+
this.reserved.push([this.pc, Math.min(0xffff, this.pc + amount - 1)]);
|
|
940
|
+
this.pc = (this.pc + amount) & 0xffff;
|
|
909
941
|
break;
|
|
910
942
|
}
|
|
911
943
|
case "include": {
|
|
@@ -116,7 +116,11 @@ function tokenize(content) {
|
|
|
116
116
|
}
|
|
117
117
|
continue;
|
|
118
118
|
}
|
|
119
|
-
|
|
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;
|
|
120
124
|
bytes.push(asciiToPetscii(plainChar));
|
|
121
125
|
if (inData && !inString && plainChar === ":") {
|
|
122
126
|
inData = false;
|
|
@@ -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/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/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
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
|
|
@@ -434,11 +481,51 @@ export class ViceMockServer {
|
|
|
434
481
|
for (const meta of this.registerMetadata) {
|
|
435
482
|
this.registerValues.set(meta.id, 0);
|
|
436
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() {
|
|
437
489
|
this.resources = new Map([
|
|
438
|
-
["C64Model", 2],
|
|
439
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"],
|
|
440
504
|
]);
|
|
441
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
|
+
}
|
|
442
529
|
renderHello() {
|
|
443
530
|
if (this.helloReady)
|
|
444
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.
|
|
@@ -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
|
},
|