@henols/c64-re-tools 2.1.0-rc.1 → 2.1.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 (103) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +24 -9
  3. package/skills/c64-assembler/scripts/assembler.ts +95 -0
  4. package/skills/c64-basic/SKILL.md +17 -9
  5. package/skills/c64-basic/scripts/basic.ts +26 -36
  6. package/skills/c64-disk/SKILL.md +17 -7
  7. package/skills/c64-disk/scripts/disk.ts +130 -104
  8. package/skills/c64-disk/scripts/lib/containers.ts +10 -12
  9. package/skills/c64-docs/SKILL.md +47 -28
  10. package/skills/c64-docs/references/style.md +6 -6
  11. package/skills/c64-docs/scripts/docs.ts +323 -0
  12. package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
  13. package/skills/c64-emulator/SKILL.md +11 -7
  14. package/skills/c64-knowledge/SKILL.md +34 -21
  15. package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
  16. package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
  17. package/skills/c64-memory-map/SKILL.md +18 -10
  18. package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
  19. package/skills/c64-provenance/SKILL.md +33 -24
  20. package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
  21. package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
  22. package/skills/c64-provenance/scripts/lib/library.ts +15 -25
  23. package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
  24. package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
  25. package/skills/c64-provenance/scripts/provenance.ts +177 -279
  26. package/skills/c64-reverse-engineering/SKILL.md +13 -12
  27. package/skills/c64-source/SKILL.md +28 -16
  28. package/skills/c64-source/scripts/lib/loader.ts +52 -77
  29. package/skills/c64-source/scripts/lib/source.ts +33 -42
  30. package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
  31. package/skills/c64-static-analysis/SKILL.md +24 -13
  32. package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
  33. package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
  34. package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
  35. package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
  36. package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
  37. package/skills/c64-testing/SKILL.md +48 -32
  38. package/skills/c64-testing/references/recording-format.md +48 -19
  39. package/skills/c64-testing/references/scenario-format.md +24 -19
  40. package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
  41. package/skills/c64-testing/scripts/lib/diff.ts +5 -3
  42. package/skills/c64-testing/scripts/lib/play.ts +244 -54
  43. package/skills/c64-testing/scripts/lib/recording.ts +153 -120
  44. package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
  45. package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
  46. package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
  47. package/skills/c64-testing/scripts/testing.ts +189 -0
  48. package/skills/c64-unpacker/SKILL.md +42 -25
  49. package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
  50. package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
  51. package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
  52. package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
  53. package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
  54. package/src/c64.ts +35 -20
  55. package/src/cli/main.ts +2 -2
  56. package/src/errors.ts +38 -0
  57. package/src/graphics.ts +3 -1
  58. package/src/host/server.ts +3 -2
  59. package/src/host/vice/session.ts +4 -4
  60. package/src/host-client/entry-state.ts +106 -0
  61. package/src/host-client/image.ts +21 -10
  62. package/src/host-client/steps.ts +285 -108
  63. package/src/host-client/transfer.ts +21 -15
  64. package/src/host-client/vice-session.ts +1 -1
  65. package/src/knowledge/database.ts +3 -23
  66. package/src/knowledge/expression.ts +8 -11
  67. package/src/knowledge/history.ts +2 -2
  68. package/src/knowledge/import.ts +13 -14
  69. package/src/knowledge/names.ts +50 -0
  70. package/src/knowledge/program.ts +11 -10
  71. package/src/knowledge/read.ts +16 -21
  72. package/src/knowledge/schema.ts +9 -3
  73. package/src/knowledge/text.ts +5 -5
  74. package/src/knowledge/write.ts +93 -137
  75. package/src/mcp/inputs.ts +105 -0
  76. package/src/mcp/output.ts +2 -13
  77. package/src/mcp/server.ts +12 -60
  78. package/src/mcp/tools/debug.ts +17 -12
  79. package/src/mcp/tools/execution.ts +5 -32
  80. package/src/mcp/tools/input.ts +7 -32
  81. package/src/mcp/tools/media.ts +6 -5
  82. package/src/mcp/tools/memory.ts +11 -38
  83. package/src/mcp/tools/video.ts +6 -5
  84. package/src/native/c64run.ts +1 -1
  85. package/src/native/dxa.ts +7 -6
  86. package/src/native/ghidra/analyze.ts +2 -1
  87. package/src/native/ghidra/index.ts +2 -1
  88. package/src/native/processes.ts +4 -3
  89. package/src/native/status.ts +2 -2
  90. package/src/native/types.ts +3 -7
  91. package/src/opcodes.ts +13 -13
  92. package/src/platform.ts +9 -9
  93. package/src/project.ts +16 -26
  94. package/src/protocol/messages.ts +3 -21
  95. package/src/protocol/vice.ts +7 -7
  96. package/src/script.ts +491 -49
  97. package/skills/c64-assembler/scripts/assemble.ts +0 -138
  98. package/skills/c64-docs/scripts/generate.ts +0 -109
  99. package/skills/c64-docs/scripts/link.ts +0 -239
  100. package/skills/c64-memory-map/scripts/memmap.ts +0 -92
  101. package/skills/c64-testing/scripts/checklist.ts +0 -51
  102. package/skills/c64-testing/scripts/record.ts +0 -96
  103. package/skills/c64-testing/scripts/test.ts +0 -65
@@ -1,7 +1,10 @@
1
1
  // A person plays the program in the replay core: a page on localhost shows
2
- // the screen and sends the keyboard and gamepad. The core runs one frame each
3
- // 20 ms. The reads of the program keep the input, so the recording needs
4
- // nothing from the page. Stop on the page ends the session.
2
+ // the screen and sends the keyboard and gamepad. The machine is frozen at the
3
+ // start screen until Start on the page; then the core runs one frame each
4
+ // 20 ms. End freezes it again and the recording is saved; Cancel ends the
5
+ // session without one. The reads of the program keep the input, so the
6
+ // recording needs nothing from the page. RESTORE is no read: it is an event
7
+ // of the recording (see record in recording.ts).
5
8
 
6
9
  import { createServer, type ServerResponse } from "node:http";
7
10
 
@@ -9,17 +12,64 @@ import { PAL_FRAME_CYCLES, type C64RunSession } from "#src/native/c64run.ts";
9
12
 
10
13
  const FRAME_MS = 20;
11
14
 
15
+ /** The reason that play gives when the user clicks Cancel. */
16
+ export const CANCELLED = "the user cancelled the session on the page";
17
+
12
18
  export interface PlayOptions {
13
19
  /** The TCP port of the page; 0 takes a free one. */
14
20
  httpPort: number;
15
- /** The control port that the arrow keys, space and the gamepad move. */
21
+ /** The control port that the joystick keys and the gamepad move first; F9 on the page changes it. */
16
22
  joystick: 1 | 2;
17
23
  /** Called with the address of the page when it listens. */
18
24
  ready: (url: string) => void;
25
+ /** Presses RESTORE as an event of the recording. A reason stops the session. */
26
+ restore: () => Promise<string | undefined>;
19
27
  }
20
28
 
21
- /** The C64 keys of the page: KeyboardEvent.key → the key code of the replay core. */
22
- const KEYS: Record<string, number> = { Enter: 0x0d, Backspace: 0x01, F1: 0xf1, F2: 0xf2, F3: 0xf3, F4: 0xf4, F5: 0xf5, F6: 0xf6, F7: 0xf7, F8: 0xf8, Escape: 0x03 };
29
+ /** The key codes of the replay core: chips c64.h, and 11, 12 and 5e of c64run. */
30
+ const C64 = {
31
+ del: 0x01, clr: 0x02, stop: 0x03, leftArrow: 0x04, run: 0x07, csrLeft: 0x08, csrRight: 0x09, csrDown: 0x0a, csrUp: 0x0b,
32
+ home: 0x0c, return: 0x0d, ctrl: 0x0e, cbm: 0x0f, inst: 0x10, leftShift: 0x11, rightShift: 0x12, space: 0x20, upArrow: 0x5e, pound: 0x7e,
33
+ } as const;
34
+ const F_KEYS = [0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8];
35
+ const LETTERS = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"];
36
+ const DIGITS = [..."0123456789"];
37
+ const code = (character: string) => character.charCodeAt(0);
38
+
39
+ /**
40
+ * The key maps of the page. Each one maps a KeyboardEvent property to a key code.
41
+ * The symbolic map (event.key) gives the character on the PC key; the positional
42
+ * map (event.code) gives the C64 key at the place of the PC key, with each SHIFT alone.
43
+ */
44
+ export const KEY_MAPS = {
45
+ /** event.code, in both maps. */
46
+ both: { Tab: C64.ctrl, AltLeft: C64.cbm },
47
+ /** event.code, when the arrow keys and space are keyboard keys. */
48
+ arrows: { ArrowUp: C64.csrUp, ArrowDown: C64.csrDown, ArrowLeft: C64.csrLeft, ArrowRight: C64.csrRight, Space: C64.space },
49
+ /** event.code */
50
+ positional: {
51
+ ...Object.fromEntries(LETTERS.map((letter) => ["Key" + letter, code(letter)])),
52
+ ...Object.fromEntries(DIGITS.map((digit) => ["Digit" + digit, code(digit)])),
53
+ ...Object.fromEntries(F_KEYS.map((key, index) => ["F" + (index + 1), key])),
54
+ Backquote: C64.leftArrow, Minus: code("+"), Equal: code("-"), Insert: C64.pound, Home: C64.home, Backspace: C64.del,
55
+ BracketLeft: code("@"), BracketRight: code("*"), Delete: C64.upArrow,
56
+ Escape: C64.stop, Semicolon: code(":"), Quote: code(";"), Backslash: code("="), Enter: C64.return,
57
+ ShiftLeft: C64.leftShift, ShiftRight: C64.rightShift, Comma: code(","), Period: code("."), Slash: code("/"),
58
+ } as Record<string, number>,
59
+ /** event.key; a-z are the C64 letters, A-Z the letters with SHIFT. */
60
+ symbolic: {
61
+ ...Object.fromEntries(LETTERS.flatMap((letter) => [[letter.toLowerCase(), code(letter)], [letter, code(letter.toLowerCase())]])),
62
+ ...Object.fromEntries([...DIGITS, ..."+-.:@,*;=/!\"#$%&'()[]<>?"].map((character) => [character, code(character)])),
63
+ ...Object.fromEntries(F_KEYS.map((key, index) => ["F" + (index + 1), key])),
64
+ "£": C64.pound, "^": C64.upArrow, "`": C64.leftArrow,
65
+ Enter: C64.return, Backspace: C64.del, Delete: C64.del, Insert: C64.inst, Home: C64.home, Escape: C64.stop,
66
+ } as Record<string, number>,
67
+ /** event.key with SHIFT held, in the symbolic map. */
68
+ symbolicShift: { Home: C64.clr, Backspace: C64.inst, Escape: C64.run, F1: 0xf2, F3: 0xf4, F5: 0xf6, F7: 0xf8 } as Record<string, number>,
69
+ };
70
+
71
+ /** The key codes that the page can send: every code of the key maps. */
72
+ const PAGE_KEYS = new Set(Object.values(KEY_MAPS).flatMap((map) => Object.values(map)));
23
73
 
24
74
  const PAGE = String.raw`<!doctype html>
25
75
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
@@ -27,32 +77,73 @@ const PAGE = String.raw`<!doctype html>
27
77
  <style>
28
78
  body{margin:0;background:#111;color:#ddd;font:14px system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px}
29
79
  canvas{image-rendering:pixelated;width:min(784px,100%);aspect-ratio:392/272;background:#000}
30
- button{font:inherit;padding:6px 16px}
80
+ button{font:inherit;font-size:12px;padding:3px 8px}
81
+ .row{display:flex;flex-wrap:wrap;gap:6px;align-items:center;justify-content:center}
82
+ .gap{width:12px}
83
+ table{border-collapse:collapse;font-size:13px}td{padding:2px 10px;border-top:1px solid #333}
31
84
  </style></head><body>
85
+ <div class="row"><button id="start" disabled>Start (Enter)</button><button id="end" disabled>End and save (F12)</button><button id="cancel" disabled>Cancel (F11)</button><span class="gap"></span><button id="port">Port 2 (F9)</button><button id="arrows">Arrows: joystick (F10)</button><button id="map">Keys: symbolic</button></div>
32
86
  <canvas id="screen" width="392" height="272"></canvas>
33
- <div>Joystick: arrow keys and space, or a gamepad. Other keys go to the C64 keyboard. <span id="state">connecting</span></div>
34
- <button id="stop">Stop and save the recording</button>
87
+ <div id="state">connecting</div>
88
+ <table>
89
+ <tr><td>Start, End, Cancel</td><td>Enter (on the start screen), F12, F11</td></tr>
90
+ <tr><td>Joystick</td><td>arrow keys and space (when F10 sets joystick), or a gamepad</td></tr>
91
+ <tr><td>RUN/STOP, RESTORE</td><td>Esc, Page Up</td></tr>
92
+ <tr><td>CTRL, C=</td><td>Tab, left Alt</td></tr>
93
+ <tr><td>CLR/HOME, INST/DEL</td><td>Home, Backspace</td></tr>
94
+ <tr><td>Symbolic map</td><td>the characters on the PC keys; £, ^ (up arrow) and the backquote (left arrow)</td></tr>
95
+ <tr><td>Positional map</td><td>the C64 keys at the places of the PC keys; each SHIFT alone; Insert £, Delete up arrow, the backquote left arrow</td></tr>
96
+ </table>
35
97
  <script>
36
- const canvas = document.getElementById("screen"), context = canvas.getContext("2d"), state = document.getElementById("state");
37
- let palette = [], image = context.createImageData(392, 272), frames = 0;
98
+ const KEYS = __KEYS__;
99
+ const BITS = { ArrowUp: 1, ArrowDown: 2, ArrowLeft: 4, ArrowRight: 8, Space: 16 };
100
+ const $ = (id) => document.getElementById(id);
101
+ const canvas = $("screen"), context = canvas.getContext("2d"), state = $("state");
102
+ let palette = [], image = context.createImageData(392, 272), frames = 0, phase = "connecting", port = 2, arrows = "joystick", keyMap = "symbolic", sent = -1;
103
+ // The C64 key of each PC key that is down (by event.code), and the joystick keys that are down.
104
+ const pressed = new Map(), stick = new Set();
105
+ // One request at a time, so that the core gets each key down before its key up.
106
+ let queue = Promise.resolve();
107
+ function send(path) { queue = queue.then(() => fetch(path, { method: "POST" })).catch(() => {}); }
108
+ function show() {
109
+ $("start").disabled = phase !== "waiting";
110
+ $("end").disabled = phase !== "playing";
111
+ $("cancel").disabled = phase !== "waiting" && phase !== "playing";
112
+ $("port").textContent = "Port " + port + " (F9)";
113
+ $("arrows").textContent = "Arrows: " + arrows + " (F10)";
114
+ $("map").textContent = "Keys: " + keyMap;
115
+ if (phase === "waiting") state.textContent = "Frozen at the start: read the screen, then press Start.";
116
+ else if (phase === "playing") state.textContent = "Frame " + frames;
117
+ else if (phase === "ended") state.textContent = "Frozen at frame " + frames + ". The script saves the recording.";
118
+ else if (phase === "cancelled") state.textContent = "Cancelled. No recording is saved.";
119
+ else if (phase === "failed") state.textContent = "The session stopped. The script output gives the reason. No recording is saved.";
120
+ else state.textContent = phase;
121
+ }
122
+ function releaseAll() {
123
+ for (const code of pressed.values()) send("key?code=" + code.toString(16) + "&down=0");
124
+ pressed.clear();
125
+ stick.clear();
126
+ }
38
127
  const source = new EventSource("frames");
39
128
  source.addEventListener("palette", (event) => { palette = event.data.split(" ").map((hex) => [0, 2, 4].map((at) => parseInt(hex.slice(at, at + 2), 16))); });
129
+ source.addEventListener("state", (event) => { phase = event.data; if (phase === "playing") sent = -1; show(); });
130
+ source.addEventListener("port", (event) => { port = +event.data; show(); });
40
131
  source.onmessage = (event) => {
41
132
  const [width, height, data] = event.data.split(" ");
42
133
  if (image.width != width) { canvas.width = width; canvas.height = height; image = context.createImageData(+width, +height); }
43
134
  const bytes = atob(data);
44
135
  for (let at = 0; at < bytes.length; at++) { const rgb = palette[bytes.charCodeAt(at)] || [0, 0, 0]; image.data.set(rgb, at * 4); image.data[at * 4 + 3] = 255; }
45
136
  context.putImageData(image, 0, 0);
46
- state.textContent = "frame " + ++frames;
137
+ if (phase === "playing") { frames++; show(); }
138
+ };
139
+ // The canvas keeps the last frame when the stream closes.
140
+ source.onerror = () => {
141
+ source.close();
142
+ if (phase !== "ended" && phase !== "cancelled" && phase !== "failed") { phase = "the session ended"; show(); }
47
143
  };
48
- source.onerror = () => { state.textContent = "the session ended"; };
49
- const held = new Set();
50
- const bits = { ArrowUp: 1, ArrowDown: 2, ArrowLeft: 4, ArrowRight: 8, " ": 16 };
51
- let sent = -1;
52
- function send(path) { fetch(path, { method: "POST" }); }
53
144
  function joystick() {
54
145
  let mask = 0;
55
- for (const key of held) mask |= bits[key] || 0;
146
+ for (const key of stick) mask |= BITS[key];
56
147
  for (const pad of navigator.getGamepads ? navigator.getGamepads() : []) {
57
148
  if (!pad) continue;
58
149
  const [x, y] = pad.axes;
@@ -62,77 +153,175 @@ function joystick() {
62
153
  if (x > 0.5 || pad.buttons[15]?.pressed) mask |= 8;
63
154
  if (pad.buttons[0]?.pressed || pad.buttons[1]?.pressed) mask |= 16;
64
155
  }
65
- if (mask !== sent) { sent = mask; send("joystick?mask=" + mask); }
156
+ if (phase === "playing" && mask !== sent) { sent = mask; send("joystick?mask=" + mask); }
66
157
  requestAnimationFrame(joystick);
67
158
  }
68
159
  requestAnimationFrame(joystick);
160
+ function c64Key(event) {
161
+ if (event.code in KEYS.both) return KEYS.both[event.code];
162
+ if (arrows === "keyboard" && event.code in KEYS.arrows) return KEYS.arrows[event.code];
163
+ if (keyMap === "positional") return KEYS.positional[event.code];
164
+ if (event.shiftKey && event.key in KEYS.symbolicShift) return KEYS.symbolicShift[event.key];
165
+ return KEYS.symbolic[event.key];
166
+ }
167
+ function toggleArrows() { releaseAll(); arrows = arrows === "joystick" ? "keyboard" : "joystick"; show(); }
168
+ function toggleMap() { releaseAll(); keyMap = keyMap === "symbolic" ? "positional" : "symbolic"; show(); }
69
169
  function key(event, down) {
70
- if (event.key in bits) { event.preventDefault(); down ? held.add(event.key) : held.delete(event.key); return; }
71
- const name = event.key.length === 1 ? event.key.toUpperCase() : event.key;
72
- if (name.length === 1 || /^(Enter|Backspace|Escape|F[1-8])$/.test(name)) { event.preventDefault(); if (!event.repeat) send("key?name=" + encodeURIComponent(name) + "&down=" + (down ? 1 : 0)); }
170
+ // Ctrl and Cmd keep their browser use: they are not C64 keys.
171
+ if (event.ctrlKey || event.metaKey) return;
172
+ // The keys of the page: they are not C64 keys. Enter is Start only on the start screen.
173
+ const command = { F9: "port", F10: "arrows", F11: "cancel", F12: "end", PageUp: "restore" }[event.code] ?? (phase === "waiting" && event.code === "Enter" ? "start" : undefined);
174
+ if (command !== undefined) {
175
+ event.preventDefault();
176
+ if (!down || event.repeat) return;
177
+ if (command === "arrows") toggleArrows();
178
+ else if (command !== "restore" || phase === "playing") send(command);
179
+ return;
180
+ }
181
+ if (arrows === "joystick" && event.code in BITS) { event.preventDefault(); down ? stick.add(event.code) : stick.delete(event.code); return; }
182
+ if (!down) {
183
+ const code = pressed.get(event.code);
184
+ if (code === undefined) return;
185
+ event.preventDefault();
186
+ pressed.delete(event.code);
187
+ send("key?code=" + code.toString(16) + "&down=0");
188
+ return;
189
+ }
190
+ const code = c64Key(event);
191
+ if (code === undefined) return;
192
+ event.preventDefault();
193
+ if (event.repeat || phase !== "playing" || pressed.has(event.code)) return;
194
+ pressed.set(event.code, code);
195
+ send("key?code=" + code.toString(16) + "&down=1");
73
196
  }
74
197
  addEventListener("keydown", (event) => key(event, true));
75
198
  addEventListener("keyup", (event) => key(event, false));
76
- document.getElementById("stop").onclick = () => { send("stop"); state.textContent = "saving"; };
77
- </script></body></html>`;
199
+ addEventListener("blur", releaseAll);
200
+ $("start").onclick = () => send("start");
201
+ $("end").onclick = () => send("end");
202
+ $("cancel").onclick = () => send("cancel");
203
+ $("port").onclick = () => send("port");
204
+ $("arrows").onclick = toggleArrows;
205
+ $("map").onclick = toggleMap;
206
+ show();
207
+ </script></body></html>`.replace("__KEYS__", JSON.stringify(KEY_MAPS));
78
208
 
79
- /** The key code of a key name from the page, or undefined for a key that the C64 does not have. */
80
- function keyCode(name: string): number | undefined {
81
- if (name in KEYS) return KEYS[name];
82
- return name.length === 1 && /[A-Z0-9 +\-.:@,*;=/!"#$%&'()[\]<>?]/.test(name) ? name.charCodeAt(0) : undefined;
83
- }
209
+ /** The phases of a session: the start screen, the play, then End, Cancel or a failed RESTORE. */
210
+ type Phase = "waiting" | "playing" | "ended" | "cancelled" | "failed";
211
+
212
+ /** A RESTORE press in the queue of the page input. */
213
+ const RESTORE = Symbol("restore");
84
214
 
85
- /** Runs the core in real time with the page as its joystick and keyboard, until Stop. */
86
- export async function play(core: C64RunSession, options: PlayOptions): Promise<void> {
215
+ /**
216
+ * Shows the start screen frozen until Start, then runs the core in real time
217
+ * with the page as its joystick and keyboard. End freezes the machine and gives
218
+ * undefined: the recording is complete. Cancel gives CANCELLED, and a RESTORE
219
+ * that fails gives its reason. No frame runs after End or Cancel.
220
+ */
221
+ export async function play(core: Pick<C64RunSession, "command" | "status">, options: PlayOptions): Promise<string | undefined> {
87
222
  const palette = (await core.command("palette")).split(" ").slice(1).join(" ");
88
223
  const viewers = new Set<ServerResponse>();
89
- const pending: string[] = [];
224
+ const pending: Array<string | typeof RESTORE> = [];
90
225
  let mask = 0;
91
- let stop: () => void = () => {};
92
- const stopped = new Promise<void>((resolve) => (stop = resolve));
226
+ let port = options.joystick;
227
+ let reason: string | undefined;
228
+ // Not narrowed: the request handlers change it.
229
+ let phase = "waiting" as Phase;
230
+ let leaveWaiting: () => void = () => {};
231
+ const leftWaiting = new Promise<void>((resolve) => (leaveWaiting = resolve));
232
+ const tell = (viewer: ServerResponse) => viewer.write("event: state\ndata: " + phase + "\n\nevent: port\ndata: " + port + "\n\n");
233
+ // The phase changes in the request handler, so the loop sees it before its next frame.
234
+ const enter = (next: Phase) => {
235
+ phase = next;
236
+ for (const viewer of viewers) tell(viewer);
237
+ leaveWaiting();
238
+ };
93
239
  const server = createServer((request, response) => {
94
240
  const url = new URL(request.url ?? "/", "http://localhost");
241
+ const answer = (allowed: boolean, next?: Phase) => {
242
+ if (!allowed) return void response.writeHead(409).end();
243
+ if (next !== undefined) enter(next);
244
+ response.writeHead(204).end();
245
+ };
95
246
  if (request.method === "GET" && url.pathname === "/") {
96
247
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(PAGE);
97
248
  } else if (request.method === "GET" && url.pathname === "/frames") {
98
249
  response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store" });
99
- response.write(`event: palette\ndata: ${palette}\n\n`);
250
+ response.write("event: palette\ndata: " + palette + "\n\n");
251
+ tell(response);
100
252
  viewers.add(response);
101
253
  request.on("close", () => viewers.delete(response));
254
+ // The frozen start screen; in play, the loop sends each frame.
255
+ if (phase === "waiting") {
256
+ core.command("frame").then(
257
+ (frame) => void response.write("data: " + frame.slice(3) + "\n\n"),
258
+ () => {},
259
+ );
260
+ }
102
261
  } else if (request.method === "POST" && url.pathname === "/joystick") {
103
- mask = Number(url.searchParams.get("mask")) & 0x1f;
104
- response.writeHead(204).end();
262
+ if (phase === "playing") mask = Number(url.searchParams.get("mask")) & 0x1f;
263
+ answer(phase === "playing");
105
264
  } else if (request.method === "POST" && url.pathname === "/key") {
106
- const code = keyCode(url.searchParams.get("name") ?? "");
107
- if (code !== undefined) pending.push(`key ${code.toString(16)} ${url.searchParams.get("down") === "1" ? "down" : "up"}`);
108
- response.writeHead(204).end();
109
- } else if (request.method === "POST" && url.pathname === "/stop") {
110
- response.writeHead(204).end();
111
- stop();
265
+ const key = Number.parseInt(url.searchParams.get("code") ?? "", 16);
266
+ if (!PAGE_KEYS.has(key)) return void response.writeHead(400).end();
267
+ if (phase === "playing") pending.push("key " + key.toString(16) + " " + (url.searchParams.get("down") === "1" ? "down" : "up"));
268
+ answer(phase === "playing");
269
+ } else if (request.method === "POST" && url.pathname === "/restore") {
270
+ if (phase === "playing") pending.push(RESTORE);
271
+ answer(phase === "playing");
272
+ } else if (request.method === "POST" && url.pathname === "/port") {
273
+ // The other port; the joystick mask moves with it, the old port is released.
274
+ if (phase === "waiting" || phase === "playing") {
275
+ port = port === 1 ? 2 : 1;
276
+ for (const viewer of viewers) tell(viewer);
277
+ }
278
+ answer(phase === "waiting" || phase === "playing");
279
+ } else if (request.method === "POST" && url.pathname === "/start") {
280
+ answer(phase === "waiting", "playing");
281
+ } else if (request.method === "POST" && url.pathname === "/end") {
282
+ answer(phase === "playing", "ended");
283
+ } else if (request.method === "POST" && url.pathname === "/cancel") {
284
+ if (phase === "waiting" || phase === "playing") reason = CANCELLED;
285
+ answer(phase === "waiting" || phase === "playing", "cancelled");
112
286
  } else {
113
287
  response.writeHead(404).end();
114
288
  }
115
289
  });
116
290
  await new Promise<void>((resolve) => server.listen(options.httpPort, "127.0.0.1", resolve));
117
291
  const address = server.address();
118
- options.ready(`http://localhost:${typeof address === "object" && address !== null ? address.port : options.httpPort}/`);
119
- let running = true;
120
- void stopped.then(() => (running = false));
292
+ options.ready("http://localhost:" + (typeof address === "object" && address !== null ? address.port : options.httpPort) + "/");
121
293
  let tick = (await core.status("status")).tick;
122
- let sent = -1;
294
+ let sent = "";
123
295
  try {
124
- while (running) {
296
+ await leftWaiting;
297
+ while (phase === "playing") {
125
298
  const started = Date.now();
126
- if (mask !== sent) {
127
- await core.command(options.joystick === 1 ? `joy ${mask.toString(16)} 0` : `joy 0 ${mask.toString(16)}`);
128
- sent = mask;
299
+ const joy = port === 1 ? "joy " + mask.toString(16) + " 0" : "joy 0 " + mask.toString(16);
300
+ if (joy !== sent) {
301
+ await core.command(joy);
302
+ sent = joy;
303
+ }
304
+ while (pending.length > 0 && phase === "playing") {
305
+ const next = pending.shift()!;
306
+ if (next !== RESTORE) {
307
+ await core.command(next);
308
+ continue;
309
+ }
310
+ const failed = await options.restore();
311
+ if (failed !== undefined) {
312
+ reason = failed;
313
+ enter("failed");
314
+ }
315
+ // The event ran the machine to its point and held RESTORE: the frames go on from there.
316
+ tick = (await core.status("status")).tick;
129
317
  }
130
- while (pending.length > 0) await core.command(pending.shift()!);
318
+ // End or Cancel while the input went in: the machine stays at the frame on the page.
319
+ if (phase !== "playing") break;
131
320
  tick += PAL_FRAME_CYCLES;
132
- await core.status(`run ${tick}`);
321
+ await core.status("run " + tick);
133
322
  if (viewers.size > 0) {
134
323
  const frame = (await core.command("frame")).slice(3);
135
- for (const viewer of viewers) viewer.write(`data: ${frame}\n\n`);
324
+ for (const viewer of viewers) viewer.write("data: " + frame + "\n\n");
136
325
  }
137
326
  await new Promise((resolve) => setTimeout(resolve, Math.max(0, FRAME_MS - (Date.now() - started))));
138
327
  }
@@ -141,4 +330,5 @@ export async function play(core: C64RunSession, options: PlayOptions): Promise<v
141
330
  for (const viewer of viewers) viewer.end();
142
331
  await new Promise((resolve) => server.close(resolve));
143
332
  }
333
+ return reason;
144
334
  }