c64bridge 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +2 -2
  2. package/data/embeddings_basic.json +1 -1
  3. package/data/embeddings_hardware.json +1 -1
  4. package/data/embeddings_mixed.json +1 -1
  5. package/data/embeddings_other.json +1 -1
  6. package/data/graphics/character-set.csv +64 -64
  7. package/data/sound/sidwave.md +113 -36
  8. package/data/sound/sidwave.schema.json +138 -28
  9. package/dist/.tsbuildinfo +1 -1
  10. package/dist/c64Client.js +144 -150
  11. package/dist/config.js +93 -39
  12. package/dist/device.js +252 -130
  13. package/dist/diagnostics.js +10 -2
  14. package/dist/knowledge.js +2 -1
  15. package/dist/mcp-server.js +12 -4
  16. package/dist/petscii.js +18 -15
  17. package/dist/petsciiArt.js +40 -14
  18. package/dist/rag/indexer.js +2 -1
  19. package/dist/sidwave.js +453 -61
  20. package/dist/sidwaveCompiler.js +1424 -275
  21. package/dist/tools/audio.js +97 -18
  22. package/dist/tools/debug.js +9 -9
  23. package/dist/tools/developer.js +7 -7
  24. package/dist/tools/errors.js +19 -1
  25. package/dist/tools/graphics.js +4 -4
  26. package/dist/tools/hex.js +9 -0
  27. package/dist/tools/input.js +21 -18
  28. package/dist/tools/machineControl.js +6 -6
  29. package/dist/tools/memory.js +11 -10
  30. package/dist/tools/meta/audio.js +8 -2
  31. package/dist/tools/meta/background.js +6 -1
  32. package/dist/tools/meta/filesystem.js +37 -10
  33. package/dist/tools/meta/graphics.js +11 -16
  34. package/dist/tools/meta/memory.js +16 -22
  35. package/dist/tools/meta/program.js +28 -9
  36. package/dist/tools/pollValidator.js +30 -2
  37. package/dist/tools/printer.js +4 -4
  38. package/dist/tools/programRunners.js +10 -9
  39. package/dist/tools/registry/disk.js +7 -5
  40. package/dist/tools/storage.js +21 -17
  41. package/dist/tools/streaming.js +2 -2
  42. package/dist/tools/translation/assembler.js +192 -57
  43. package/dist/tools/translation/basicTokenizer.js +44 -10
  44. package/dist/tools/translation/opcodeInventory.js +1 -1
  45. package/dist/tools/types.js +11 -4
  46. package/dist/tools/vice.js +1 -1
  47. package/dist/vicBitmap.js +16 -0
  48. package/dist/vice/mockServer.js +99 -8
  49. package/dist/vice/readiness.js +4 -9
  50. package/dist/vice/viceClient.js +34 -6
  51. package/mcp/schemas/c64_disk.schema.json +2 -2
  52. package/mcp/schemas/c64_input.schema.json +3 -3
  53. package/mcp/schemas/c64_program.schema.json +2 -2
  54. package/mcp/schemas/c64_system.schema.json +6 -4
  55. package/mcp/schemas/c64_vice.schema.json +3 -3
  56. package/mcp/server.json +1 -1
  57. package/mcp/tools.json +20 -14
  58. package/mcp.json +1 -1
  59. package/package.json +1 -1
  60. package/scripts/mockC64Server.mjs +113 -20
  61. package/scripts/run-coverage.mjs +31 -1
  62. package/scripts/update-readme.ts +3 -2
  63. package/server.json +2 -2
package/dist/petscii.js CHANGED
@@ -61,8 +61,6 @@ function ensureScreenCodeMap() {
61
61
  }
62
62
  SCREEN_CODE_TO_ASCII.set(code, deriveGlyphChar(glyph));
63
63
  }
64
- // Common control characters / gaps default to space to keep output tidy.
65
- SCREEN_CODE_TO_ASCII.set(0x00, " ");
66
64
  screenCodeMapInitialised = true;
67
65
  }
68
66
  export function screenCodesToAscii(bytes, options) {
@@ -132,7 +130,7 @@ const NAMED_CHARS = Object.freeze({
132
130
  "cursor_left": 157,
133
131
  // Graphic glyphs (commonly referenced by name)
134
132
  // Note: Exact byte shown on screen depends on mode; these are PETSCII/CHR$ values
135
- "heart": 81, // widely used PETSCII code for heart glyph
133
+ "heart": 115, // $73 (shifted S); $51 would print the letter Q
136
134
  "up_arrow": 94,
137
135
  "uparrow": 94,
138
136
  "left_arrow": 95,
@@ -146,7 +144,8 @@ export function listNamedPetscii() {
146
144
  }
147
145
  export function lookupNamedPetscii(name) {
148
146
  const key = name.trim().toLowerCase().replace(/\s+/g, "_");
149
- return NAMED_CHARS[key];
147
+ // Own keys only, so names like "constructor" don't resolve to Object.prototype members.
148
+ return Object.prototype.hasOwnProperty.call(NAMED_CHARS, key) ? NAMED_CHARS[key] : undefined;
150
149
  }
151
150
  /**
152
151
  * Try to parse a named character token starting at the given index.
@@ -160,15 +159,16 @@ export function parseNamedCharToken(source, startIndex) {
160
159
  return null;
161
160
  }
162
161
  const close = open === "{" ? "}" : open === ":" ? ":" : ">";
163
- let idx = startIndex + 1;
164
- let name = "";
165
- while (idx < source.length && source[idx] !== close) {
166
- name += source[idx]
167
- .replace(/[^A-Za-z0-9_\s-]/g, "") // restrict to sane name characters
168
- .trim();
169
- idx += 1;
170
- }
171
- if (idx >= source.length || source[idx] !== close) {
162
+ const idx = source.indexOf(close, startIndex + 1);
163
+ if (idx < 0) {
164
+ return null;
165
+ }
166
+ // Only sane name characters; anything else is ordinary text that happens to use the delimiters.
167
+ // Multi-word names such as {cursor up} need braces: ":" and "<" also appear in
168
+ // ordinary text ("Mode: reverse on: yes"), so they only delimit single words.
169
+ const name = source.slice(startIndex + 1, idx).trim();
170
+ const namePattern = open === "{" ? /^[A-Za-z0-9_\s-]+$/ : /^[A-Za-z0-9_-]+$/;
171
+ if (!namePattern.test(name)) {
172
172
  return null;
173
173
  }
174
174
  const value = lookupNamedPetscii(name.replace(/[-\s]+/g, "_"));
@@ -181,7 +181,7 @@ export function parseNamedCharToken(source, startIndex) {
181
181
  * Encode a text containing optional named tokens into PETSCII bytes.
182
182
  * Named tokens may appear as {heart}, :heart:, or <heart>.
183
183
  */
184
- export function encodeStringWithNames(input) {
184
+ export function encodeStringWithNames(input, options = {}) {
185
185
  const bytes = [];
186
186
  let i = 0;
187
187
  while (i < input.length) {
@@ -191,7 +191,10 @@ export function encodeStringWithNames(input) {
191
191
  i = maybe.endIndex;
192
192
  continue;
193
193
  }
194
- bytes.push(asciiToPetscii(input[i]));
194
+ // ASCII a-z are PETSCII graphics characters in the default character set,
195
+ // so callers can fold them to the letters $41-$5A. Named codes are not folded.
196
+ const char = input[i];
197
+ bytes.push(asciiToPetscii(options.foldLowercase && char >= "a" && char <= "z" ? char.toUpperCase() : char));
195
198
  i += 1;
196
199
  }
197
200
  return Uint8Array.from(bytes);
@@ -19,6 +19,11 @@ const POPCOUNT = (() => {
19
19
  }
20
20
  return table;
21
21
  })();
22
+ const RVS_ON = 18;
23
+ const RVS_OFF = 146;
24
+ // Marks the end of a character row in the generated DATA statements.
25
+ const ROW_END = -1;
26
+ const SCREEN_COLUMNS = 40;
22
27
  const DEFAULT_MAX_WIDTH = 320;
23
28
  const DEFAULT_MAX_HEIGHT = 200;
24
29
  const SHAPES = [
@@ -252,10 +257,14 @@ function distanceBetween(block, glyph) {
252
257
  }
253
258
  return score;
254
259
  }
260
+ /**
261
+ * Match each 8x8 block to a glyph. `glyphs` has one entry per cell; `codes` is the
262
+ * PETSCII stream that prints them, with each run of reverse-video glyphs (screen
263
+ * codes $80-$FF) wrapped in RVS ON/RVS OFF. `rowCodes` holds that stream per row.
264
+ */
255
265
  export function bitmapToPetsciiCodes(bitmap) {
256
266
  const { blocks, columns, rows } = splitIntoBlocks(bitmap);
257
267
  const glyphs = getChargenGlyphs();
258
- const codes = [];
259
268
  const matchedGlyphs = [];
260
269
  for (const block of blocks) {
261
270
  let bestMatch;
@@ -270,11 +279,26 @@ export function bitmapToPetsciiCodes(bitmap) {
270
279
  }
271
280
  }
272
281
  }
273
- const selected = bestMatch ?? glyphs[0];
274
- codes.push(selected.petsciiCode);
275
- matchedGlyphs.push(selected);
282
+ matchedGlyphs.push(bestMatch ?? glyphs[0]);
276
283
  }
277
- return { codes, glyphs: matchedGlyphs, columns, rows };
284
+ const rowCodes = [];
285
+ for (let row = 0; row < rows; row += 1) {
286
+ const stream = [];
287
+ let reverse = false;
288
+ for (const glyph of matchedGlyphs.slice(row * columns, (row + 1) * columns)) {
289
+ const glyphReverse = (glyph.screenCode & 0x80) !== 0;
290
+ if (glyphReverse !== reverse) {
291
+ stream.push(glyphReverse ? RVS_ON : RVS_OFF);
292
+ reverse = glyphReverse;
293
+ }
294
+ stream.push(glyph.petsciiCode);
295
+ }
296
+ if (reverse) {
297
+ stream.push(RVS_OFF);
298
+ }
299
+ rowCodes.push(stream);
300
+ }
301
+ return { codes: rowCodes.flat(), rowCodes, glyphs: matchedGlyphs, columns, rows };
278
302
  }
279
303
  function buildBasicProgram(params) {
280
304
  const lines = [];
@@ -288,19 +312,21 @@ function buildBasicProgram(params) {
288
312
  lines.push(`${lineNumber} POKE 646,${fg}`);
289
313
  lineNumber += 10;
290
314
  }
315
+ // Rows vary in length because of the RVS ON/OFF codes, so each row's DATA ends with ROW_END.
291
316
  lines.push(`${lineNumber} FORY=0TO${Math.max(0, params.rows - 1)}`);
292
317
  lineNumber += 10;
293
- lines.push(`${lineNumber} FORX=0TO${Math.max(0, params.columns - 1)}`);
294
- lineNumber += 10;
295
- lines.push(`${lineNumber} READC:PRINT CHR$(C);`);
296
- lineNumber += 10;
297
- lines.push(`${lineNumber} NEXTX:PRINT`);
318
+ const readLine = lineNumber;
319
+ // POKE212,0 clears quote mode: after a printed '"' glyph the editor would
320
+ // otherwise draw the following RVS ON/OFF codes as characters.
321
+ lines.push(`${readLine} READC:IFC>=0THENPRINT CHR$(C);:POKE212,0:GOTO${readLine}`);
298
322
  lineNumber += 10;
299
- lines.push(`${lineNumber} NEXTY`);
323
+ // A full 40-column row already wraps the cursor; an extra PRINT would leave a blank line.
324
+ lines.push(params.columns >= SCREEN_COLUMNS ? `${lineNumber} NEXTY` : `${lineNumber} PRINT:NEXTY`);
300
325
  lineNumber += 10;
326
+ const data = params.rowCodes.flatMap((row) => [...row, ROW_END]);
301
327
  const chunkSize = 16;
302
- for (let i = 0; i < params.codes.length; i += chunkSize) {
303
- const slice = params.codes.slice(i, i + chunkSize);
328
+ for (let i = 0; i < data.length; i += chunkSize) {
329
+ const slice = data.slice(i, i + chunkSize);
304
330
  const dataLine = slice.join(",");
305
331
  lines.push(`${lineNumber} DATA ${dataLine}`);
306
332
  lineNumber += 10;
@@ -334,7 +360,7 @@ export function createPetsciiArt(options) {
334
360
  const converted = bitmapToPetsciiCodes(bitmap);
335
361
  const codes = converted.codes;
336
362
  const program = buildBasicProgram({
337
- codes,
363
+ rowCodes: converted.rowCodes,
338
364
  columns: converted.columns,
339
365
  rows: converted.rows,
340
366
  borderColor: options.borderColor,
@@ -12,7 +12,8 @@ const BASIC_DIR = path.join(ASSET_ROOT, "data/basic/examples");
12
12
  const ASM_DIR = path.join(ASSET_ROOT, "data/assembly/examples");
13
13
  const EXTERNAL_DIR = path.join(ASSET_ROOT, "external");
14
14
  const DOC_ROOT = path.join(ASSET_ROOT, "doc");
15
- const DOC_EXCLUDE_DIRS = new Set(["plans"]);
15
+ // Plans and reviews are internal engineering notes, not C64 reference material.
16
+ const DOC_EXCLUDE_DIRS = new Set(["plans", "reviews"]);
16
17
  const CONTEXT_DIR = path.join(ASSET_ROOT, "data/context");
17
18
  const BOOTSTRAP_PATH = path.join(CONTEXT_DIR, "bootstrap.md");
18
19
  const AGENTS_PATH = path.join(ASSET_ROOT, "AGENTS.md");