c64bridge 0.5.0

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 (138) hide show
  1. package/.github/prompts/README.md +6 -0
  2. package/.github/prompts/assembly-routine-session.prompt.md +13 -0
  3. package/.github/prompts/basic-program-session.prompt.md +13 -0
  4. package/.github/prompts/drive-management-session.prompt.md +13 -0
  5. package/.github/prompts/petscii-art-session.prompt.md +13 -0
  6. package/.github/prompts/play-classic-game.prompt.md +13 -0
  7. package/.github/prompts/sid-music-session.prompt.md +13 -0
  8. package/AGENTS.md +173 -0
  9. package/LICENSE +339 -0
  10. package/README.md +582 -0
  11. package/data/README.md +16 -0
  12. package/data/api/basic-api-spec.md +154 -0
  13. package/data/api/kernal-api-spec.md +68 -0
  14. package/data/assembly/assembly-spec.md +196 -0
  15. package/data/assembly/examples/effects/sinewave.asm +5 -0
  16. package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
  17. package/data/assembly/examples/sid-file-structure.md +8 -0
  18. package/data/assembly/examples/sid-overview.md +11 -0
  19. package/data/audio/examples/Biosphere.sid +0 -0
  20. package/data/audio/sid-file-structure.md +91 -0
  21. package/data/audio/sid-programming-best-practices.md +198 -0
  22. package/data/audio/sid-spec.md +166 -0
  23. package/data/audio/sidwave.md +131 -0
  24. package/data/audio/sidwave.schema.json +64 -0
  25. package/data/basic/basic-spec.md +230 -0
  26. package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
  27. package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
  28. package/data/basic/examples/io/joystick.bas +5 -0
  29. package/data/basic/examples/video/bounce.bas +8 -0
  30. package/data/basic/examples/video/entchen-petscii.bas +16 -0
  31. package/data/basic/examples/video/hello-world.bas +4 -0
  32. package/data/basic/examples/video/wave.bas +7 -0
  33. package/data/context/bootstrap.md +148 -0
  34. package/data/context/chat.md +36 -0
  35. package/data/drive/drive-spec.md +215 -0
  36. package/data/embeddings_asm.json +1 -0
  37. package/data/embeddings_basic.json +1 -0
  38. package/data/embeddings_hardware.json +1 -0
  39. package/data/embeddings_mixed.json +1 -0
  40. package/data/embeddings_other.json +1 -0
  41. package/data/io/cia-spec.md +115 -0
  42. package/data/io/io-spec.md +124 -0
  43. package/data/memory/kernal-memory-map.md +227 -0
  44. package/data/memory/low-memory-map.md +133 -0
  45. package/data/memory/memory-map.md +186 -0
  46. package/data/printer/printer-commodore-bitmap.md +31 -0
  47. package/data/printer/printer-commodore.md +28 -0
  48. package/data/printer/printer-epson-bitmap.md +29 -0
  49. package/data/printer/printer-epson.md +30 -0
  50. package/data/printer/printer-prompts.md +29 -0
  51. package/data/printer/printer-spec.md +180 -0
  52. package/data/video/character-set.csv +257 -0
  53. package/data/video/vic-spec.md +276 -0
  54. package/dist/assemblyConverter.js +1299 -0
  55. package/dist/audio/record_and_analyze_audio.js +292 -0
  56. package/dist/basicConverter.js +193 -0
  57. package/dist/bootstrap/stdio-logger.js +32 -0
  58. package/dist/c64Client.js +1052 -0
  59. package/dist/chargen.js +197 -0
  60. package/dist/config.js +142 -0
  61. package/dist/context.js +157 -0
  62. package/dist/device.js +413 -0
  63. package/dist/index.js +7 -0
  64. package/dist/knowledge.js +454 -0
  65. package/dist/logger.js +182 -0
  66. package/dist/loggingHttpClient.js +88 -0
  67. package/dist/mcp-server.js +426 -0
  68. package/dist/petscii.js +135 -0
  69. package/dist/petsciiArt.js +356 -0
  70. package/dist/platform.js +82 -0
  71. package/dist/prompts/registry.js +560 -0
  72. package/dist/rag/discover.config.json +41 -0
  73. package/dist/rag/discover.js +401 -0
  74. package/dist/rag/embeddings.js +60 -0
  75. package/dist/rag/externalFetcher.js +934 -0
  76. package/dist/rag/indexer.js +770 -0
  77. package/dist/rag/init.js +139 -0
  78. package/dist/rag/knowledgeIndex.js +361 -0
  79. package/dist/rag/loggingRetriever.js +46 -0
  80. package/dist/rag/rateLimiter.js +127 -0
  81. package/dist/rag/retriever.js +74 -0
  82. package/dist/rag/sources.csv +31 -0
  83. package/dist/rag/types.js +5 -0
  84. package/dist/rag/urlUtils.js +38 -0
  85. package/dist/sidplayRunner.js +240 -0
  86. package/dist/sidwave.js +101 -0
  87. package/dist/sidwaveCompiler.js +329 -0
  88. package/dist/tools/audio.js +944 -0
  89. package/dist/tools/developer.js +515 -0
  90. package/dist/tools/errors.js +59 -0
  91. package/dist/tools/graphics.js +469 -0
  92. package/dist/tools/machineControl.js +267 -0
  93. package/dist/tools/memory.js +224 -0
  94. package/dist/tools/printer.js +482 -0
  95. package/dist/tools/programRunners.js +315 -0
  96. package/dist/tools/rag.js +153 -0
  97. package/dist/tools/registry.js +58 -0
  98. package/dist/tools/responses.js +36 -0
  99. package/dist/tools/schema.js +304 -0
  100. package/dist/tools/storage.js +740 -0
  101. package/dist/tools/streaming.js +132 -0
  102. package/dist/tools/types.js +99 -0
  103. package/dist/viceRunner.js +239 -0
  104. package/doc/MCP_SETUP.md +153 -0
  105. package/doc/README.md +7 -0
  106. package/doc/developer.md +312 -0
  107. package/doc/img/logo.png +0 -0
  108. package/doc/img/prompts/duck_petscii.png +0 -0
  109. package/doc/img/prompts/duck_song.png +0 -0
  110. package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
  111. package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
  112. package/doc/prompts/README.md +96 -0
  113. package/doc/prompts/user-prompt-ideas.md +147 -0
  114. package/doc/rest/c64-openapi.yaml +979 -0
  115. package/doc/rest/c64-rest-api.md +106 -0
  116. package/doc/troubleshooting-mcp.md +177 -0
  117. package/generated/c64/index.js +857 -0
  118. package/mcp.json +106 -0
  119. package/package.json +79 -0
  120. package/scripts/c64-cli.mjs +142 -0
  121. package/scripts/c64-tool.sh +83 -0
  122. package/scripts/check-package.mjs +89 -0
  123. package/scripts/cli.js +61 -0
  124. package/scripts/fetch-external-sources.mjs +67 -0
  125. package/scripts/generate-changelog.mjs +131 -0
  126. package/scripts/postbuild.mjs +101 -0
  127. package/scripts/prepare-release.mjs +58 -0
  128. package/scripts/rag-rebuild.mjs +53 -0
  129. package/scripts/register-ts-node.mjs +6 -0
  130. package/scripts/run-mcp-check.sh +104 -0
  131. package/scripts/run-tests.mjs +195 -0
  132. package/scripts/sid2wav.sh +94 -0
  133. package/scripts/smoke-run.sh +65 -0
  134. package/scripts/start-mock-c64.mjs +51 -0
  135. package/scripts/start-mock.mjs +31 -0
  136. package/scripts/start.mjs +72 -0
  137. package/scripts/update-readme.ts +164 -0
  138. package/scripts/verify-package.mjs +147 -0
@@ -0,0 +1,1052 @@
1
+ /*
2
+ C64 Bridge - An MCP Server for the Commodore 64 Ultimate
3
+ Copyright (C) 2025 Christian Gleissner
4
+
5
+ Licensed under the GNU General Public License v2.0 or later.
6
+ See <https://www.gnu.org/licenses/> for details.
7
+ */
8
+ import { Buffer } from "node:buffer";
9
+ import axios from "axios";
10
+ import { basicToPrg } from "./basicConverter.js";
11
+ import { assemblyToPrg } from "./assemblyConverter.js";
12
+ import { petsciiToAscii } from "./petscii.js";
13
+ import { resolveAddressSymbol } from "./knowledge.js";
14
+ import { createFacade } from "./device.js";
15
+ import { Api } from "../generated/c64/index.js";
16
+ import { createLoggingHttpClient } from "./loggingHttpClient.js";
17
+ export class C64Client {
18
+ constructor(baseUrl) {
19
+ this.http = createLoggingHttpClient({ baseURL: baseUrl, timeout: 10000 });
20
+ this.api = new Api(this.http);
21
+ // Select backend once lazily; keep REST for hardware-specific fallbacks
22
+ this.facadePromise = createFacade(undefined, { preferredC64uBaseUrl: baseUrl }).then((sel) => sel.facade);
23
+ }
24
+ /**
25
+ * Generate a BASIC program that opens the printer (device 4), prints the provided text,
26
+ * and closes the channel. Assumes Commodore MPS (PETSCII) by default.
27
+ */
28
+ async printTextOnPrinterAndRun(options) {
29
+ const program = buildPrinterBasicProgram(options);
30
+ return this.uploadAndRunBasic(program);
31
+ }
32
+ /**
33
+ * Generate and run a Commodore MPS Bit Image Mode (BIM) program for one bitmap row,
34
+ * optionally repeated.
35
+ */
36
+ async printBitmapOnCommodoreAndRun(options) {
37
+ const program = buildCommodoreBitmapBasicProgram(options);
38
+ return this.uploadAndRunBasic(program);
39
+ }
40
+ /**
41
+ * Generate and run an Epson FX ESC/P bitmap program for one row (repeated lines).
42
+ */
43
+ async printBitmapOnEpsonAndRun(options) {
44
+ const program = buildEpsonBitmapBasicProgram(options);
45
+ return this.uploadAndRunBasic(program);
46
+ }
47
+ /**
48
+ * Generate and run a Commodore MPS DLL (custom characters) program. On emulator this
49
+ * is ignored but we still verify generation and transmission.
50
+ */
51
+ async defineCustomCharsOnCommodoreAndRun(options) {
52
+ const program = buildCommodoreDllBasicProgram(options);
53
+ return this.uploadAndRunBasic(program);
54
+ }
55
+ async uploadAndRunBasic(program) {
56
+ const prg = basicToPrg(program);
57
+ return this.runPrg(prg);
58
+ }
59
+ /**
60
+ * Build a simple sprite PRG from raw 63-byte sprite data and position/color attributes.
61
+ * Returns the REST result after uploading and running the generated PRG.
62
+ */
63
+ async generateAndRunSpritePrg(options) {
64
+ const prg = buildSingleSpriteProgram(options);
65
+ return this.runPrg(prg);
66
+ }
67
+ /**
68
+ * Build a BASIC program that draws a PETSCII screen (optionally set border/bg colours),
69
+ * then upload and run it.
70
+ */
71
+ async renderPetsciiScreenAndRun(options) {
72
+ const program = buildPetsciiScreenBasic(options);
73
+ return this.uploadAndRunBasic(program);
74
+ }
75
+ async uploadAndRunAsm(program) {
76
+ const prg = assemblyToPrg(program);
77
+ return this.runPrg(prg);
78
+ }
79
+ async runPrg(prg) {
80
+ try {
81
+ if (process.env.C64_TEST_TARGET === "mock") {
82
+ const payload = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
83
+ const response = await this.api.v1.runnersRunPrgCreate(":run_prg", payload, {
84
+ headers: { "Content-Type": "application/octet-stream" },
85
+ });
86
+ return { success: true, details: response.data };
87
+ }
88
+ const facade = await this.facadePromise;
89
+ const res = await facade.runPrg(prg);
90
+ return { success: res.success, details: res.details };
91
+ }
92
+ catch (error) {
93
+ return { success: false, details: this.normaliseError(error) };
94
+ }
95
+ }
96
+ /** Upload a SID binary and instruct firmware to play it (attachment mode). */
97
+ async sidplayAttachment(sid, options) {
98
+ try {
99
+ const facade = await this.facadePromise;
100
+ const res = await facade.sidplayAttachment(sid, options);
101
+ return { success: res.success, details: res.details };
102
+ }
103
+ catch (error) {
104
+ return { success: false, details: this.normaliseError(error) };
105
+ }
106
+ }
107
+ async loadPrgFile(path) {
108
+ try {
109
+ const facade = await this.facadePromise;
110
+ const res = await facade.loadPrgFile(path);
111
+ return { success: res.success, details: res.details };
112
+ }
113
+ catch (error) {
114
+ return { success: false, details: this.normaliseError(error) };
115
+ }
116
+ }
117
+ async runPrgFile(path) {
118
+ try {
119
+ const facade = await this.facadePromise;
120
+ const res = await facade.runPrgFile(path);
121
+ return { success: res.success, details: res.details };
122
+ }
123
+ catch (error) {
124
+ return { success: false, details: this.normaliseError(error) };
125
+ }
126
+ }
127
+ async runCrtFile(path) {
128
+ try {
129
+ const facade = await this.facadePromise;
130
+ const res = await facade.runCrtFile(path);
131
+ return { success: res.success, details: res.details };
132
+ }
133
+ catch (error) {
134
+ return { success: false, details: this.normaliseError(error) };
135
+ }
136
+ }
137
+ async sidplayFile(path, songnr) {
138
+ try {
139
+ const facade = await this.facadePromise;
140
+ const res = await facade.sidplayFile(path, songnr);
141
+ return { success: res.success, details: res.details };
142
+ }
143
+ catch (error) {
144
+ return { success: false, details: this.normaliseError(error) };
145
+ }
146
+ }
147
+ async modplayFile(path) {
148
+ try {
149
+ const facade = await this.facadePromise;
150
+ if (!facade.modplayFile)
151
+ throw new Error("modplay not supported by selected backend");
152
+ const res = await facade.modplayFile(path);
153
+ return { success: res.success, details: res.details };
154
+ }
155
+ catch (error) {
156
+ return { success: false, details: this.normaliseError(error) };
157
+ }
158
+ }
159
+ async readScreen() {
160
+ // Exposed as MCP tool: read_screen
161
+ const bytes = await this.readMemoryRaw(0x0400, 0x1000);
162
+ return petsciiToAscii(bytes);
163
+ }
164
+ async reset() {
165
+ try {
166
+ if (process.env.C64_TEST_TARGET === "mock") {
167
+ const response = await this.api.v1.machineResetUpdate(":reset");
168
+ return { success: true, details: response.data };
169
+ }
170
+ const facade = await this.facadePromise;
171
+ const res = await facade.reset();
172
+ return { success: res.success, details: res.details };
173
+ }
174
+ catch (error) {
175
+ return { success: false, details: this.normaliseError(error) };
176
+ }
177
+ }
178
+ async reboot() {
179
+ try {
180
+ if (process.env.C64_TEST_TARGET === "mock") {
181
+ const response = await this.api.v1.machineRebootUpdate(":reboot");
182
+ return { success: true, details: response.data };
183
+ }
184
+ const facade = await this.facadePromise;
185
+ const res = await facade.reboot();
186
+ return { success: res.success, details: res.details };
187
+ }
188
+ catch (error) {
189
+ return { success: false, details: this.normaliseError(error) };
190
+ }
191
+ }
192
+ async readMemory(addressInput, lengthInput) {
193
+ try {
194
+ const resolved = resolveAddressSymbol(addressInput);
195
+ const address = resolved ?? this.parseNumeric(addressInput);
196
+ const length = this.parseNumeric(lengthInput);
197
+ if (length <= 0) {
198
+ throw new Error("Length must be greater than zero");
199
+ }
200
+ const rawBytes = await this.readMemoryRaw(address, length);
201
+ const bytes = rawBytes.slice(0, length);
202
+ return {
203
+ success: true,
204
+ data: this.bytesToHex(bytes),
205
+ details: {
206
+ address: this.formatAddress(address),
207
+ length,
208
+ },
209
+ };
210
+ }
211
+ catch (error) {
212
+ return {
213
+ success: false,
214
+ details: this.normaliseError(error),
215
+ };
216
+ }
217
+ }
218
+ async writeMemory(addressInput, bytesInput) {
219
+ try {
220
+ const resolved = resolveAddressSymbol(addressInput);
221
+ const address = resolved ?? this.parseNumeric(addressInput);
222
+ const dataBuffer = this.hexStringToBuffer(bytesInput);
223
+ if (dataBuffer.length === 0) {
224
+ throw new Error("No bytes provided");
225
+ }
226
+ // Prefer PUT with hex data for up to 128 bytes; fall back to POST binary for larger writes
227
+ const addrStr = this.formatAddress(address);
228
+ try {
229
+ const facade = await this.facadePromise;
230
+ await facade.writeMemory(address, dataBuffer);
231
+ return {
232
+ success: true,
233
+ details: {
234
+ address: addrStr,
235
+ bytes: this.bytesToHex(dataBuffer),
236
+ },
237
+ };
238
+ }
239
+ catch (facadeError) {
240
+ if (!(facadeError instanceof Error) || facadeError.code !== "UNSUPPORTED") {
241
+ throw facadeError;
242
+ }
243
+ }
244
+ let response;
245
+ if (dataBuffer.length <= 128) {
246
+ const put = await this.api.v1.machineWritememUpdate(":writemem", {
247
+ address: addrStr,
248
+ data: this.bytesToHex(dataBuffer, false),
249
+ });
250
+ response = put.data;
251
+ }
252
+ else {
253
+ const post = await this.api.v1.machineWritememCreate(":writemem", { address: addrStr }, Buffer.from(dataBuffer), { headers: { "Content-Type": "application/octet-stream" } });
254
+ response = post.data;
255
+ }
256
+ return {
257
+ success: true,
258
+ details: {
259
+ address: addrStr,
260
+ bytes: this.bytesToHex(dataBuffer),
261
+ response,
262
+ },
263
+ };
264
+ }
265
+ catch (error) {
266
+ return {
267
+ success: false,
268
+ details: this.normaliseError(error),
269
+ };
270
+ }
271
+ }
272
+ // --- SID/Music helpers ---
273
+ async sidSetVolume(volume) {
274
+ const clamped = Math.max(0, Math.min(15, Math.floor(volume)));
275
+ const byte = Buffer.from([clamped]);
276
+ return this.writeMemory("$D418", this.bytesToHex(byte));
277
+ }
278
+ async sidReset(hard = false) {
279
+ try {
280
+ const facade = await this.facadePromise;
281
+ if (hard) {
282
+ const span = 0x19;
283
+ const ff = Buffer.alloc(span, 0xff);
284
+ const zz = Buffer.alloc(span, 0x00);
285
+ await facade.writeMemory(0xd400, ff);
286
+ await facade.writeMemory(0xd400, zz);
287
+ return { success: true };
288
+ }
289
+ await facade.writeMemory(0xd404, Buffer.from([0x00]));
290
+ await facade.writeMemory(0xd40b, Buffer.from([0x00]));
291
+ await facade.writeMemory(0xd412, Buffer.from([0x00]));
292
+ await facade.writeMemory(0xd418, Buffer.from([0x00]));
293
+ return { success: true };
294
+ }
295
+ catch (error) {
296
+ return { success: false, details: this.normaliseError(error) };
297
+ }
298
+ }
299
+ async sidNoteOn(options) {
300
+ const voice = options.voice ?? 1;
301
+ if (voice < 1 || voice > 3) {
302
+ return { success: false, details: { message: "Voice must be 1..3" } };
303
+ }
304
+ const system = options.system ?? "PAL";
305
+ const hz = options.frequencyHz ?? (options.note ? this.noteNameToHz(options.note) : 440);
306
+ const freq16 = this.hzToSidFrequency(hz, system);
307
+ const freqLo = freq16 & 0xff;
308
+ const freqHi = (freq16 >> 8) & 0xff;
309
+ const pulseWidth = Math.max(0, Math.min(0x0fff, Math.floor(options.pulseWidth ?? 0x0800)));
310
+ const pwLo = pulseWidth & 0xff;
311
+ const pwHi = (pulseWidth >> 8) & 0x0f; // upper 4 bits used
312
+ const waveform = options.waveform ?? "pulse";
313
+ let ctrl = 0x00;
314
+ if (waveform === "tri")
315
+ ctrl |= 1 << 4;
316
+ else if (waveform === "saw")
317
+ ctrl |= 1 << 5;
318
+ else if (waveform === "pulse")
319
+ ctrl |= 1 << 6;
320
+ else if (waveform === "noise")
321
+ ctrl |= 1 << 7;
322
+ ctrl |= 1 << 0; // GATE on
323
+ const attack = Math.max(0, Math.min(15, Math.floor(options.attack ?? 0x1)));
324
+ const decay = Math.max(0, Math.min(15, Math.floor(options.decay ?? 0x1)));
325
+ const sustain = Math.max(0, Math.min(15, Math.floor(options.sustain ?? 0xf)));
326
+ const release = Math.max(0, Math.min(15, Math.floor(options.release ?? 0x3)));
327
+ const ad = (attack << 4) | decay;
328
+ const sr = (sustain << 4) | release;
329
+ const base = 0xd400 + (voice - 1) * 7;
330
+ const bytes = Buffer.from([freqLo, freqHi, pwLo, pwHi, ctrl, ad, sr]);
331
+ try {
332
+ const facade = await this.facadePromise;
333
+ await facade.writeMemory(base, bytes);
334
+ return { success: true };
335
+ }
336
+ catch (error) {
337
+ return { success: false, details: this.normaliseError(error) };
338
+ }
339
+ }
340
+ async sidNoteOff(voice) {
341
+ if (voice < 1 || voice > 3) {
342
+ return { success: false, details: { message: "Voice must be 1..3" } };
343
+ }
344
+ const ctrlAddr = 0xd400 + (voice - 1) * 7 + 4;
345
+ try {
346
+ const facade = await this.facadePromise;
347
+ await facade.writeMemory(ctrlAddr, Buffer.from([0x00]));
348
+ return { success: true };
349
+ }
350
+ catch (error) {
351
+ return { success: false, details: this.normaliseError(error) };
352
+ }
353
+ }
354
+ async sidSilenceAll() {
355
+ return this.sidReset(false);
356
+ }
357
+ // --- Additional API wrappers to cover full REST surface ---
358
+ async version() {
359
+ const res = await this.api.v1.versionList();
360
+ return res.data;
361
+ }
362
+ async info() {
363
+ const res = await this.api.v1.infoList();
364
+ return res.data;
365
+ }
366
+ async pause() {
367
+ try {
368
+ if (process.env.C64_TEST_TARGET === "mock") {
369
+ const res = await this.api.v1.machinePauseUpdate(":pause");
370
+ return { success: true, details: res.data };
371
+ }
372
+ const facade = await this.facadePromise;
373
+ return await facade.pause();
374
+ }
375
+ catch (error) {
376
+ return { success: false, details: this.normaliseError(error) };
377
+ }
378
+ }
379
+ async resume() {
380
+ try {
381
+ if (process.env.C64_TEST_TARGET === "mock") {
382
+ const res = await this.api.v1.machineResumeUpdate(":resume");
383
+ return { success: true, details: res.data };
384
+ }
385
+ const facade = await this.facadePromise;
386
+ return await facade.resume();
387
+ }
388
+ catch (error) {
389
+ return { success: false, details: this.normaliseError(error) };
390
+ }
391
+ }
392
+ async poweroff() {
393
+ try {
394
+ const facade = await this.facadePromise;
395
+ return await facade.poweroff();
396
+ }
397
+ catch (error) {
398
+ return { success: false, details: this.normaliseError(error) };
399
+ }
400
+ }
401
+ async menuButton() {
402
+ try {
403
+ if (process.env.C64_TEST_TARGET === "mock") {
404
+ const res = await this.api.v1.machineMenuButtonUpdate(":menu_button");
405
+ return { success: true, details: res.data };
406
+ }
407
+ const facade = await this.facadePromise;
408
+ return await facade.menuButton();
409
+ }
410
+ catch (error) {
411
+ return { success: false, details: this.normaliseError(error) };
412
+ }
413
+ }
414
+ async debugregRead() {
415
+ try {
416
+ if (process.env.C64_TEST_TARGET === "mock") {
417
+ const res = await this.api.v1.machineDebugregList(":debugreg");
418
+ return { success: true, value: res.data.value, details: res.data };
419
+ }
420
+ const facade = await this.facadePromise;
421
+ return await facade.debugregRead();
422
+ }
423
+ catch (error) {
424
+ return { success: false, details: this.normaliseError(error) };
425
+ }
426
+ }
427
+ async debugregWrite(value) {
428
+ try {
429
+ if (process.env.C64_TEST_TARGET === "mock") {
430
+ const res = await this.api.v1.machineDebugregUpdate(":debugreg", { value });
431
+ return { success: true, value: res.data.value, details: res.data };
432
+ }
433
+ const facade = await this.facadePromise;
434
+ return await facade.debugregWrite(value);
435
+ }
436
+ catch (error) {
437
+ return { success: false, details: this.normaliseError(error) };
438
+ }
439
+ }
440
+ async drivesList() {
441
+ const facade = await this.facadePromise;
442
+ return facade.drivesList();
443
+ }
444
+ async driveMount(drive, imagePath, options) {
445
+ try {
446
+ const facade = await this.facadePromise;
447
+ return await facade.driveMount(drive, imagePath, options);
448
+ }
449
+ catch (error) {
450
+ return { success: false, details: this.normaliseError(error) };
451
+ }
452
+ }
453
+ async driveRemove(drive) {
454
+ try {
455
+ const facade = await this.facadePromise;
456
+ return await facade.driveRemove(drive);
457
+ }
458
+ catch (error) {
459
+ return { success: false, details: this.normaliseError(error) };
460
+ }
461
+ }
462
+ async driveReset(drive) {
463
+ try {
464
+ const facade = await this.facadePromise;
465
+ return await facade.driveReset(drive);
466
+ }
467
+ catch (error) {
468
+ return { success: false, details: this.normaliseError(error) };
469
+ }
470
+ }
471
+ async driveOn(drive) {
472
+ try {
473
+ const facade = await this.facadePromise;
474
+ return await facade.driveOn(drive);
475
+ }
476
+ catch (error) {
477
+ return { success: false, details: this.normaliseError(error) };
478
+ }
479
+ }
480
+ async driveOff(drive) {
481
+ try {
482
+ const facade = await this.facadePromise;
483
+ return await facade.driveOff(drive);
484
+ }
485
+ catch (error) {
486
+ return { success: false, details: this.normaliseError(error) };
487
+ }
488
+ }
489
+ async driveLoadRom(drive, path) {
490
+ try {
491
+ if (!drive || !path)
492
+ throw new Error("Drive and path are required");
493
+ if (process.env.C64_TEST_TARGET === "mock") {
494
+ const res = await this.api.v1.drivesLoadRomUpdate(drive, ":load_rom", { file: path });
495
+ return { success: true, details: res.data };
496
+ }
497
+ const facade = await this.facadePromise;
498
+ const result = await facade.driveLoadRom(drive, path);
499
+ return { success: result.success, details: result.details };
500
+ }
501
+ catch (error) {
502
+ return { success: false, details: this.normaliseError(error) };
503
+ }
504
+ }
505
+ async driveSetMode(drive, mode) {
506
+ try {
507
+ const facade = await this.facadePromise;
508
+ return await facade.driveSetMode(drive, mode);
509
+ }
510
+ catch (error) {
511
+ return { success: false, details: this.normaliseError(error) };
512
+ }
513
+ }
514
+ async streamStart(stream, ip) {
515
+ try {
516
+ const facade = await this.facadePromise;
517
+ return await facade.streamStart(stream, ip);
518
+ }
519
+ catch (error) {
520
+ return { success: false, details: this.normaliseError(error) };
521
+ }
522
+ }
523
+ async streamStop(stream) {
524
+ try {
525
+ const facade = await this.facadePromise;
526
+ return await facade.streamStop(stream);
527
+ }
528
+ catch (error) {
529
+ return { success: false, details: this.normaliseError(error) };
530
+ }
531
+ }
532
+ async configsList() {
533
+ const facade = await this.facadePromise;
534
+ return facade.configsList();
535
+ }
536
+ async configGet(category, item) {
537
+ const facade = await this.facadePromise;
538
+ return facade.configGet(category, item);
539
+ }
540
+ async configSet(category, item, value) {
541
+ try {
542
+ const facade = await this.facadePromise;
543
+ return await facade.configSet(category, item, value);
544
+ }
545
+ catch (error) {
546
+ return { success: false, details: this.normaliseError(error) };
547
+ }
548
+ }
549
+ async configBatchUpdate(payload) {
550
+ try {
551
+ const facade = await this.facadePromise;
552
+ return await facade.configBatchUpdate(payload);
553
+ }
554
+ catch (error) {
555
+ return { success: false, details: this.normaliseError(error) };
556
+ }
557
+ }
558
+ async configLoadFromFlash() {
559
+ try {
560
+ const facade = await this.facadePromise;
561
+ return await facade.configLoadFromFlash();
562
+ }
563
+ catch (error) {
564
+ return { success: false, details: this.normaliseError(error) };
565
+ }
566
+ }
567
+ async configSaveToFlash() {
568
+ try {
569
+ const facade = await this.facadePromise;
570
+ return await facade.configSaveToFlash();
571
+ }
572
+ catch (error) {
573
+ return { success: false, details: this.normaliseError(error) };
574
+ }
575
+ }
576
+ async configResetToDefault() {
577
+ try {
578
+ const facade = await this.facadePromise;
579
+ return await facade.configResetToDefault();
580
+ }
581
+ catch (error) {
582
+ return { success: false, details: this.normaliseError(error) };
583
+ }
584
+ }
585
+ async filesInfo(path) {
586
+ const facade = await this.facadePromise;
587
+ return facade.filesInfo(path);
588
+ }
589
+ async filesCreateD64(path, options) {
590
+ try {
591
+ const facade = await this.facadePromise;
592
+ return await facade.filesCreateD64(path, options);
593
+ }
594
+ catch (error) {
595
+ return { success: false, details: this.normaliseError(error) };
596
+ }
597
+ }
598
+ async filesCreateD71(path, options) {
599
+ try {
600
+ const facade = await this.facadePromise;
601
+ return await facade.filesCreateD71(path, options);
602
+ }
603
+ catch (error) {
604
+ return { success: false, details: this.normaliseError(error) };
605
+ }
606
+ }
607
+ async filesCreateD81(path, options) {
608
+ try {
609
+ const facade = await this.facadePromise;
610
+ return await facade.filesCreateD81(path, options);
611
+ }
612
+ catch (error) {
613
+ return { success: false, details: this.normaliseError(error) };
614
+ }
615
+ }
616
+ async filesCreateDnp(path, tracks, options) {
617
+ try {
618
+ const facade = await this.facadePromise;
619
+ return await facade.filesCreateDnp(path, tracks, options);
620
+ }
621
+ catch (error) {
622
+ return { success: false, details: this.normaliseError(error) };
623
+ }
624
+ }
625
+ extractBytes(data) {
626
+ if (!data) {
627
+ return new Uint8Array();
628
+ }
629
+ // Raw binary (ArrayBuffer) response
630
+ if (data instanceof ArrayBuffer) {
631
+ return new Uint8Array(data);
632
+ }
633
+ // Node.js Buffer
634
+ if (Buffer.isBuffer(data)) {
635
+ return new Uint8Array(data);
636
+ }
637
+ // Already a Uint8Array
638
+ if (data instanceof Uint8Array) {
639
+ return data;
640
+ }
641
+ if (typeof data === "string") {
642
+ try {
643
+ return Uint8Array.from(Buffer.from(data, "base64"));
644
+ }
645
+ catch {
646
+ return Uint8Array.from(Buffer.from(data, "hex"));
647
+ }
648
+ }
649
+ if (Array.isArray(data.data)) {
650
+ return Uint8Array.from((data.data) ?? []);
651
+ }
652
+ if (Array.isArray(data)) {
653
+ return Uint8Array.from(data);
654
+ }
655
+ if (typeof data === "object" && data !== null) {
656
+ const maybe = data.data;
657
+ if (typeof maybe === "string") {
658
+ return Uint8Array.from(Buffer.from(maybe, "base64"));
659
+ }
660
+ if (Array.isArray(maybe)) {
661
+ return Uint8Array.from(maybe);
662
+ }
663
+ }
664
+ return new Uint8Array();
665
+ }
666
+ /**
667
+ * Low-level memory read that transparently handles devices returning either
668
+ * raw binary bytes or JSON with a base64 payload.
669
+ */
670
+ async readMemoryRaw(address, length) {
671
+ try {
672
+ const facade = await this.facadePromise;
673
+ return await facade.readMemory(address, length);
674
+ }
675
+ catch (facadeError) {
676
+ if (!(facadeError instanceof Error) || facadeError.code !== "UNSUPPORTED") {
677
+ throw facadeError;
678
+ }
679
+ }
680
+ const addrStr = this.formatAddress(address);
681
+ const response = await this.api.v1.machineReadmemList(":readmem", { address: addrStr, length }, { format: "arraybuffer", headers: { Accept: "application/octet-stream, application/json" } });
682
+ const contentType = (response.headers?.["content-type"] ?? "").toString().toLowerCase();
683
+ const body = response.data;
684
+ if (contentType.includes("application/json")) {
685
+ const text = Buffer.from(body).toString("utf8");
686
+ try {
687
+ const parsed = JSON.parse(text);
688
+ return this.extractBytes(parsed?.data ?? parsed);
689
+ }
690
+ catch {
691
+ return this.extractBytes(text);
692
+ }
693
+ }
694
+ return body instanceof ArrayBuffer ? new Uint8Array(body) : this.extractBytes(body);
695
+ }
696
+ normaliseError(error) {
697
+ if (axios.isAxiosError(error)) {
698
+ return {
699
+ message: error.message,
700
+ status: error.response?.status,
701
+ data: error.response?.data,
702
+ };
703
+ }
704
+ if (error instanceof Error) {
705
+ return { message: error.message };
706
+ }
707
+ return error;
708
+ }
709
+ parseNumeric(value) {
710
+ if (typeof value !== "string") {
711
+ throw new Error("Expected string input");
712
+ }
713
+ const trimmed = value.trim();
714
+ if (trimmed.length === 0) {
715
+ throw new Error("Empty numeric value");
716
+ }
717
+ const lower = trimmed.toLowerCase();
718
+ let radix = 10;
719
+ let literal = lower;
720
+ if (lower.startsWith("$")) {
721
+ radix = 16;
722
+ literal = lower.slice(1);
723
+ }
724
+ else if (lower.startsWith("0x")) {
725
+ radix = 16;
726
+ literal = lower.slice(2);
727
+ }
728
+ else if (lower.startsWith("%")) {
729
+ radix = 2;
730
+ literal = lower.slice(1);
731
+ }
732
+ const parsed = Number.parseInt(literal, radix);
733
+ if (!Number.isFinite(parsed) || Number.isNaN(parsed) || parsed < 0) {
734
+ throw new Error(`Unable to parse numeric value "${value}"`);
735
+ }
736
+ return parsed;
737
+ }
738
+ formatAddress(address) {
739
+ if (!Number.isInteger(address) || address < 0 || address > 0xffff) {
740
+ throw new Error("Address must be within 0x0000 - 0xFFFF");
741
+ }
742
+ return address.toString(16).toUpperCase().padStart(4, "0");
743
+ }
744
+ bytesToHex(bytes, withPrefix = true) {
745
+ const hex = Buffer.from(bytes).toString("hex").toUpperCase();
746
+ return withPrefix ? `$${hex}` : hex;
747
+ }
748
+ hexStringToBuffer(input) {
749
+ if (typeof input !== "string") {
750
+ throw new Error("Expected byte string");
751
+ }
752
+ const trimmed = input.trim();
753
+ if (trimmed.length === 0) {
754
+ throw new Error("Expected non-empty byte string");
755
+ }
756
+ const withoutPrefix = trimmed.startsWith("$") ? trimmed.slice(1) : trimmed;
757
+ const cleaned = withoutPrefix.replace(/[\s_]/g, "").toLowerCase();
758
+ if (cleaned.length === 0) {
759
+ throw new Error("No hexadecimal data provided");
760
+ }
761
+ if (cleaned.length % 2 !== 0) {
762
+ throw new Error("Hex string must contain an even number of characters");
763
+ }
764
+ return Buffer.from(cleaned, "hex");
765
+ }
766
+ hzToSidFrequency(hz, system = "PAL") {
767
+ const phi2 = system === "PAL" ? 985248 : 1022727;
768
+ const value = Math.round((hz * 65536) / phi2);
769
+ // Clamp to 16-bit
770
+ return Math.max(0, Math.min(0xffff, value));
771
+ }
772
+ noteNameToHz(note) {
773
+ // Parse note like C#4, Db3, A4
774
+ const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec(note.trim());
775
+ if (!m)
776
+ return 440; // default A4
777
+ const letter = m[1].toUpperCase();
778
+ const accidental = m[2];
779
+ const octave = Number(m[3]);
780
+ const semitoneMap = {
781
+ C: 0,
782
+ D: 2,
783
+ E: 4,
784
+ F: 5,
785
+ G: 7,
786
+ A: 9,
787
+ B: 11,
788
+ };
789
+ let semitone = semitoneMap[letter] ?? 9;
790
+ if (accidental === "#")
791
+ semitone += 1;
792
+ if (accidental === "b")
793
+ semitone -= 1;
794
+ const midi = (octave + 1) * 12 + semitone; // MIDI note number (C-1 => 0)
795
+ const hz = 440 * Math.pow(2, (midi - 69) / 12);
796
+ return hz;
797
+ }
798
+ }
799
+ // --- Helpers to synthesize tiny programs for sprites and PETSCII screens ---
800
+ function toByte(value, fallback = 0) {
801
+ const v = value ?? fallback;
802
+ return Math.max(0, Math.min(255, v)) & 0xff;
803
+ }
804
+ function buildSingleSpriteProgram(opts) {
805
+ const index = Math.max(0, Math.min(7, opts.spriteIndex ?? 0));
806
+ const mx = Math.max(0, Math.min(511, opts.x ?? 100));
807
+ const xLo = mx & 0xff;
808
+ const xMsbBit = (mx & 0x100) ? (1 << index) : 0;
809
+ const y = toByte(opts.y, 100);
810
+ const color = toByte(opts.color, 1);
811
+ const multicolour = !!opts.multicolour;
812
+ const spriteData = Buffer.from(opts.spriteBytes);
813
+ if (spriteData.length !== 63) {
814
+ throw new Error("spriteBytes must be exactly 63 bytes");
815
+ }
816
+ // We'll assemble a tiny machine-code program that:
817
+ // - Copies 63 bytes to a safe sprite data page ($2000 by default)
818
+ // - Sets screen memory base to $0400, sprite pointer to point into $2000
819
+ // - Positions and enables the sprite
820
+ // - Loops forever
821
+ // This avoids relying on KERNAL calls and works from a cold start.
822
+ const SPRITE_BASE = 0x2000; // must be 64-byte aligned
823
+ const POINTER_PAGE = 0x07f8 + index; // sprite pointer table location
824
+ const pointerValue = (SPRITE_BASE >> 6) & 0xff;
825
+ // Place code starting at $0801 so it runs as a program via SYS.
826
+ // We'll create an assembler source and reuse assemblyToPrg.
827
+ const lines = [];
828
+ lines.push("* = $0801");
829
+ // Tiny BASIC loader header not needed; we will use pure ML and jump via RESET runner which executes by SYS 2061
830
+ // Build code at $0810 to avoid conflicting with potential KERNAL vectors
831
+ lines.push("* = $0810");
832
+ lines.push("\nstart:");
833
+ // Copy 63 bytes from inlined table to SPRITE_BASE
834
+ lines.push(" LDY #$00");
835
+ lines.push("copy_loop:");
836
+ lines.push(" LDA sprite_data,Y");
837
+ lines.push(` STA $${hex16(SPRITE_BASE)},Y`);
838
+ lines.push(" INY");
839
+ lines.push(" CPY #$3F");
840
+ lines.push(" BNE copy_loop");
841
+ // Set sprite pointer, color, coordinates, enable
842
+ lines.push(` LDA #$${pointerValue.toString(16).toUpperCase().padStart(2, "0")}`);
843
+ lines.push(` STA $${(POINTER_PAGE).toString(16).toUpperCase()}`);
844
+ lines.push(` LDA #$${color.toString(16).toUpperCase().padStart(2, "0")}`);
845
+ lines.push(` STA $${hex16(0xD027 + index)}`);
846
+ lines.push(` LDA #$${xLo.toString(16).toUpperCase().padStart(2, "0")}`);
847
+ lines.push(` STA $${(0xD000 + index * 2).toString(16).toUpperCase()}`);
848
+ lines.push(` LDA #$${y.toString(16).toUpperCase().padStart(2, "0")}`);
849
+ lines.push(` STA $${(0xD001 + index * 2).toString(16).toUpperCase()}`);
850
+ // MSB X if needed
851
+ if (xMsbBit) {
852
+ const bit = xMsbBit;
853
+ lines.push(` LDA $D010`);
854
+ lines.push(` ORA #$${bit.toString(16).toUpperCase().padStart(2, "0")}`);
855
+ lines.push(` STA $D010`);
856
+ }
857
+ // Multicolour toggle per-sprite
858
+ if (multicolour) {
859
+ const bit = 1 << index;
860
+ lines.push(` LDA $D01C`);
861
+ lines.push(` ORA #$${bit.toString(16).toUpperCase().padStart(2, "0")}`);
862
+ lines.push(` STA $D01C`);
863
+ }
864
+ // Enable sprite
865
+ {
866
+ const bit = 1 << index;
867
+ lines.push(` LDA $D015`);
868
+ lines.push(` ORA #$${bit.toString(16).toUpperCase().padStart(2, "0")}`);
869
+ lines.push(` STA $D015`);
870
+ }
871
+ // Idle loop
872
+ lines.push("forever: JMP forever");
873
+ // Sprite data table
874
+ lines.push("\nsprite_data:");
875
+ for (let i = 0; i < 63; i += 3) {
876
+ const a = spriteData[i] ?? 0;
877
+ const b = spriteData[i + 1] ?? 0;
878
+ const c = spriteData[i + 2] ?? 0;
879
+ lines.push(` .byte $${hex2(a)},$${hex2(b)},$${hex2(c)}`);
880
+ }
881
+ const source = lines.join("\n");
882
+ return assemblyToPrg(source, { fileName: "sprite_gen.asm", loadAddress: 0x0801 });
883
+ }
884
+ function buildPetsciiScreenBasic(opts) {
885
+ const border = toByte(opts.borderColor ?? 6); // default blue-ish
886
+ const bg = toByte(opts.backgroundColor ?? 0); // default black
887
+ // Clear screen, set colours, print text starting at 1,1
888
+ // Note: CHR$(147) clears the screen.
889
+ const sanitized = opts.text.replace(/\r\n?|\n/g, "\\n");
890
+ const program = [
891
+ `10 POKE 53280,${border}:POKE 53281,${bg}:PRINT CHR$(147)`,
892
+ `20 PRINT "${sanitized}"`,
893
+ `30 GETA$:IFA$<>""THENEND:REM wait for key then end`,
894
+ ].join("\n");
895
+ return program;
896
+ }
897
+ export function buildPrinterBasicProgram(opts) {
898
+ const target = opts.target ?? "commodore";
899
+ const saddr = typeof opts.secondaryAddress === "number" ? opts.secondaryAddress : undefined;
900
+ const lines = [];
901
+ // OPEN printer device (#1 to device 4 with optional secondary address)
902
+ if (saddr === 0 || saddr === 7) {
903
+ lines.push(`10 OPEN1,4,${saddr}`);
904
+ }
905
+ else {
906
+ lines.push("10 OPEN1,4");
907
+ }
908
+ // Prepare text: split by CR/LF and emit PRINT# statements.
909
+ // We chunk long logical lines to avoid very long BASIC lines; join chunks with ';' to avoid extra CRs.
910
+ const raw = opts.text ?? "";
911
+ const logicalLines = raw.split(/\r\n|\r|\n/);
912
+ let ln = 20;
913
+ for (const logical of logicalLines) {
914
+ if (logical.length === 0) {
915
+ lines.push(`${ln} PRINT#1`);
916
+ ln += 10;
917
+ continue;
918
+ }
919
+ const chunks = chunkString(logical, 60);
920
+ for (let i = 0; i < chunks.length; i += 1) {
921
+ const chunk = escapeBasicQuotes(chunks[i]);
922
+ const tail = i < chunks.length - 1 ? ";" : ""; // avoid CR between chunks within the same logical line
923
+ lines.push(`${ln} PRINT#1,"${chunk}"${tail}`);
924
+ ln += 10;
925
+ }
926
+ }
927
+ if (opts.formFeed) {
928
+ lines.push(`${ln} PRINT#1,CHR$(12)`);
929
+ ln += 10;
930
+ }
931
+ // Minimal target-specific toggles could be inserted here in future
932
+ // (e.g., ESC/P mode selections for Epson). Default is raw text output.
933
+ lines.push(`${ln} CLOSE1`);
934
+ return lines.join("\n");
935
+ }
936
+ function chunkString(input, maxLen) {
937
+ if (input.length <= maxLen)
938
+ return [input];
939
+ const parts = [];
940
+ let i = 0;
941
+ while (i < input.length) {
942
+ parts.push(input.slice(i, i + maxLen));
943
+ i += maxLen;
944
+ }
945
+ return parts;
946
+ }
947
+ function escapeBasicQuotes(input) {
948
+ // In Commodore BASIC, embed a double quote by doubling it
949
+ return input.replace(/"/g, '""');
950
+ }
951
+ export function buildCommodoreBitmapBasicProgram(opts) {
952
+ const repeats = Math.max(1, Math.floor(opts.repeats ?? 1));
953
+ const saddr = typeof opts.secondaryAddress === "number" ? opts.secondaryAddress : 7;
954
+ const ensureMsb = opts.ensureMsb !== false;
955
+ const cols = (opts.columns ?? []).map((v) => {
956
+ const n = Math.max(0, Math.min(255, Math.floor(v)));
957
+ return ensureMsb ? (n | 0x80) : n;
958
+ });
959
+ const lines = [];
960
+ lines.push(`10 OPEN1,4,${saddr}`);
961
+ lines.push(`20 A$=""`);
962
+ lines.push(`30 FOR I=1 TO ${cols.length}`);
963
+ lines.push(`40 READ A:A$=A$+CHR$(A)`);
964
+ lines.push(`50 NEXT I`);
965
+ lines.push(`60 FOR J=1 TO ${repeats}`);
966
+ if (typeof opts.useSubRepeat === "number") {
967
+ const r = Math.max(0, Math.min(255, Math.floor(opts.useSubRepeat)));
968
+ lines.push(`70 PRINT#1,CHR$(8);CHR$(26);CHR$(${r});A$`);
969
+ }
970
+ else {
971
+ lines.push(`70 PRINT#1,CHR$(8);A$`);
972
+ }
973
+ lines.push(`80 NEXT J`);
974
+ lines.push(`90 CLOSE1`);
975
+ lines.push(`100 END`);
976
+ let ln = 110;
977
+ for (let i = 0; i < cols.length; i += 8) {
978
+ const group = cols.slice(i, i + 8);
979
+ lines.push(`${ln} DATA ${group.join(",")}`);
980
+ ln += 10;
981
+ }
982
+ return lines.join("\n");
983
+ }
984
+ export function buildEpsonBitmapBasicProgram(opts) {
985
+ const cols = (opts.columns ?? []).map((v) => Math.max(0, Math.min(255, Math.floor(v))));
986
+ const len = cols.length;
987
+ const n = len & 0xff;
988
+ const m = (len >> 8) & 0xff;
989
+ const mode = (opts.mode ?? "K").toUpperCase();
990
+ const repeats = Math.max(1, Math.floor(opts.repeats ?? 1));
991
+ const timesPerLine = Math.max(1, Math.floor(opts.timesPerLine ?? 4));
992
+ function modeCode(mo) {
993
+ const map = { K: 75, L: 76, Y: 89, Z: 90 };
994
+ return map[mo] ?? null;
995
+ }
996
+ const lines = [];
997
+ lines.push(`10 OPEN1,4`);
998
+ if (mode === "*") {
999
+ const density = Math.max(0, Math.min(6, Math.floor(opts.density ?? 0)));
1000
+ lines.push(`20 A$=CHR$(27)+"*"+CHR$(${density})+CHR$(${n})+CHR$(${m})`);
1001
+ }
1002
+ else {
1003
+ const mc = modeCode(mode);
1004
+ lines.push(`20 A$=CHR$(27)+CHR$(${mc})+CHR$(${n})+CHR$(${m})`);
1005
+ }
1006
+ lines.push(`30 FOR I=1 TO ${cols.length}`);
1007
+ lines.push(`40 READ A:A$=A$+CHR$(A)`);
1008
+ lines.push(`50 NEXT I`);
1009
+ lines.push(`60 PRINT#1,CHR$(27);CHR$(65);CHR$(8);CHR$(10);CHR$(13)`);
1010
+ lines.push(`70 FOR J=1 TO ${repeats}`);
1011
+ const seg = Array.from({ length: timesPerLine }).map(() => "A$").join(";");
1012
+ lines.push(`80 PRINT#1,${seg};CHR$(10);CHR$(13)`);
1013
+ lines.push(`90 NEXT J`);
1014
+ lines.push(`100 CLOSE1`);
1015
+ lines.push(`110 END`);
1016
+ let ln = 120;
1017
+ for (let i = 0; i < cols.length; i += 8) {
1018
+ const group = cols.slice(i, i + 8);
1019
+ lines.push(`${ln} DATA ${group.join(",")}`);
1020
+ ln += 10;
1021
+ }
1022
+ return lines.join("\n");
1023
+ }
1024
+ export function buildCommodoreDllBasicProgram(opts) {
1025
+ const firstChar = Math.max(33, Math.min(126, Math.floor(opts.firstChar)));
1026
+ const numChars = Math.max(1, Math.floor(opts.chars?.length ?? 0));
1027
+ const saddr = typeof opts.secondaryAddress === "number" ? opts.secondaryAddress : 0;
1028
+ const t = numChars * 13 + 2;
1029
+ const n = Math.floor(t / 256);
1030
+ const m = t - n * 256;
1031
+ const s = 32;
1032
+ const a = Math.max(0, Math.min(1, Math.floor(opts.chars?.[0]?.a ?? 0)));
1033
+ const lines = [];
1034
+ lines.push(`10 OPEN1,4${saddr === 0 ? "" : "," + saddr}`);
1035
+ lines.push(`20 PRINT#1,CHR$(27);"=";CHR$(${m});CHR$(${n});CHR$(${firstChar});CHR$(${s});CHR$(${a})`);
1036
+ let ln = 30;
1037
+ for (let idx = 0; idx < numChars; idx += 1) {
1038
+ const cols = (opts.chars[idx]?.columns ?? []).slice(0, 11).map((v) => Math.max(0, Math.min(255, Math.floor(v))));
1039
+ while (cols.length < 11)
1040
+ cols.push(0);
1041
+ lines.push(`${ln} PRINT#1${cols.map((v) => `,CHR$(${v})`).join("")}`);
1042
+ ln += 10;
1043
+ }
1044
+ lines.push(`${ln} CLOSE1`);
1045
+ return lines.join("\n");
1046
+ }
1047
+ function hex2(n) {
1048
+ return (n & 0xff).toString(16).toUpperCase().padStart(2, "0");
1049
+ }
1050
+ function hex16(n) {
1051
+ return (n & 0xffff).toString(16).toUpperCase().padStart(4, "0");
1052
+ }