jsbeeb 1.22.1 → 1.22.3
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 +28 -31
- package/package.json +1 -1
- package/src/app/electron.js +4 -2
- package/src/basic-loader.js +5 -0
- package/src/main.js +64 -81
- package/src/web/accessibility-switches.js +26 -0
- package/src/web/analogue-inputs.js +20 -6
- package/src/web/archive-list.js +1 -6
- package/src/web/audio-handler.js +2 -2
- package/src/web/autoboot.js +2 -2
- package/src/web/config.js +1 -1
- package/src/web/debug.js +1 -1
- package/src/web/display.js +0 -3
- package/src/web/drives.js +4 -5
- package/src/web/front-panel.js +0 -2
- package/src/web/google-drive-picker.js +11 -7
- package/src/web/hfe-picker.js +1 -0
- package/src/web/keyboard-setup.js +11 -46
- package/src/web/media-loader.js +18 -21
- package/src/web/modals.js +24 -21
- package/src/web/rewind-ui.js +15 -12
- package/src/web/settings.js +9 -10
- package/src/web/snapshot-ui.js +9 -11
- package/src/web/sth-picker.js +18 -4
- package/src/web/url-state.js +12 -1
- package/tests/test-machine.js +2 -2
- /package/{COPYING → LICENSE} +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
[](https://github.com/mattgodbolt/jsbeeb/actions/workflows/test-and-deploy.yml)
|
|
2
|
+
[](https://bbc.xania.org/coverage/index.html)
|
|
2
3
|
|
|
3
4
|
# jsbeeb - JavaScript BBC Micro Emulator
|
|
4
5
|
|
|
@@ -15,6 +16,7 @@ different peripherals.
|
|
|
15
16
|
- [Emulator Shortcuts](#emulator-shortcuts)
|
|
16
17
|
- [Printer Output](#printer-output)
|
|
17
18
|
- [Save State and Rewind](#save-state-and-rewind)
|
|
19
|
+
- [Joystick Support](#joystick-support)
|
|
18
20
|
- [Getting Set Up to Run Locally](#getting-set-up-to-run-locally)
|
|
19
21
|
- [Running as a Desktop Application](#running-as-a-desktop-application)
|
|
20
22
|
- [URL Parameters](#url-parameters)
|
|
@@ -119,10 +121,10 @@ Save and load full emulator state snapshots from the **State** menu (or `Ctrl+S`
|
|
|
119
121
|
|
|
120
122
|
The emulator continuously captures snapshots into a 30-slot rewind buffer (~1 per second). Open the rewind scrubber from **State > Rewind** or press **Alt+PageDown** to browse recent states as a visual filmstrip:
|
|
121
123
|
|
|
122
|
-
- **Left/Right arrows
|
|
124
|
+
- **Left/Right arrows**: navigate between snapshots (the main screen updates live)
|
|
123
125
|
- **Click** a thumbnail to jump to that point
|
|
124
|
-
- **Enter
|
|
125
|
-
- **Escape
|
|
126
|
+
- **Enter**: commit selection and close the panel
|
|
127
|
+
- **Escape**: cancel and restore the original state
|
|
126
128
|
|
|
127
129
|
### Joystick Support
|
|
128
130
|
|
|
@@ -145,13 +147,13 @@ presses `SPACE`. To play Superior's Space Invaders on a pad, where `COPY` fires:
|
|
|
145
147
|
|
|
146
148
|
The gamepad control names are:
|
|
147
149
|
|
|
148
|
-
- `FIRE
|
|
149
|
-
- `UP` `DOWN` `LEFT` `RIGHT
|
|
150
|
+
- `FIRE`: every button at once, which is usually what you want for a one-button game
|
|
151
|
+
- `UP` `DOWN` `LEFT` `RIGHT`: both analogue sticks at once, plus one face button each (`UP` is also `A`, `DOWN` is
|
|
150
152
|
`X`, `LEFT` is `Y`, `RIGHT` is `B`)
|
|
151
|
-
- `UP1` `DOWN1` `LEFT1` `RIGHT1
|
|
152
|
-
face buttons only
|
|
153
|
-
- `A` `B` `X` `Y` `START` `BACK` `LB` `RB` `LT` `RT
|
|
154
|
-
- `FIRE1` `FIRE2
|
|
153
|
+
- `UP1` `DOWN1` `LEFT1` `RIGHT1`: the left stick only; `UP2` `DOWN2` and so on: the right stick only; `UP3` `DOWN3`
|
|
154
|
+
and so on: the face buttons only
|
|
155
|
+
- `A` `B` `X` `Y` `START` `BACK` `LB` `RB` `LT` `RT`: individual buttons, by their Xbox 360 names
|
|
156
|
+
- `FIRE1` `FIRE2`: clicking the left and right sticks
|
|
155
157
|
|
|
156
158
|
The BBC key names are the same as for the keyboard, and digits may be written either way round: `GP.A=1` and `GP.A=K1`
|
|
157
159
|
both press `1`. Unlike `KEY.`, gamepad mappings are BBC-only: there's no Atom equivalent. The D-pad's default mapping
|
|
@@ -190,7 +192,9 @@ site to make it smaller and faster to load when it's deployed to [https://bbc.xa
|
|
|
190
192
|
|
|
191
193
|
## Running as a Desktop Application
|
|
192
194
|
|
|
193
|
-
jsbeeb can also run as a standalone desktop application using Electron
|
|
195
|
+
jsbeeb can also run as a standalone desktop application using Electron. Prebuilt packages (Debian/Ubuntu `.deb`,
|
|
196
|
+
Fedora/RHEL `.rpm` and a Windows installer) are attached to each
|
|
197
|
+
[GitHub release](https://github.com/mattgodbolt/jsbeeb/releases), or you can build your own:
|
|
194
198
|
|
|
195
199
|
### Running in Development
|
|
196
200
|
|
|
@@ -234,17 +238,15 @@ USE_SYSTEM_FPM=true npm run electron:build
|
|
|
234
238
|
**Debian/Ubuntu:**
|
|
235
239
|
|
|
236
240
|
```sh
|
|
237
|
-
sudo apt install ./out/dist/
|
|
241
|
+
sudo apt install ./out/dist/jsbeeb_<version>_amd64.deb
|
|
238
242
|
```
|
|
239
243
|
|
|
240
244
|
**Fedora/RHEL/CentOS:**
|
|
241
245
|
|
|
242
246
|
```sh
|
|
243
|
-
sudo rpm -i out/dist/jsbeeb
|
|
247
|
+
sudo rpm -i out/dist/jsbeeb-<version>.x86_64.rpm
|
|
244
248
|
```
|
|
245
249
|
|
|
246
|
-
**Note:** Electron support was re-enabled in November 2024 after being disabled during the ESM migration in 2021. It now works with Electron 28+ which added full ES Modules support.
|
|
247
|
-
|
|
248
250
|
## URL Parameters
|
|
249
251
|
|
|
250
252
|
- `autoboot` - fakes a shift break
|
|
@@ -334,19 +336,11 @@ Note that every update you make means you need to make a new raw link.
|
|
|
334
336
|
|
|
335
337
|
If you're looking to help:
|
|
336
338
|
|
|
337
|
-
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
dreadfully choppy on Chrome at least).
|
|
343
|
-
- Save disc support
|
|
344
|
-
- Local discs need to be made more workable and need an "export" feature
|
|
345
|
-
- Multiple discs need a UI
|
|
346
|
-
- `git grep -i todo`
|
|
347
|
-
- Optimisation
|
|
348
|
-
- While every attempt to make things fast has been made, I'm sure there's some more clever things that can be done
|
|
349
|
-
without compromising emulation accuracy
|
|
339
|
+
- Play lots of games and report anything that doesn't behave like the real machine, either on
|
|
340
|
+
[GitHub](https://github.com/mattgodbolt/jsbeeb/issues) or by email (matt@godbolt.org).
|
|
341
|
+
- Pick something from the [issue tracker](https://github.com/mattgodbolt/jsbeeb/issues): there's a mix of emulation
|
|
342
|
+
accuracy work, missing hardware and peripherals, user interface improvements and performance ideas in there, at all
|
|
343
|
+
levels of difficulty.
|
|
350
344
|
|
|
351
345
|
## Tests
|
|
352
346
|
|
|
@@ -369,12 +363,14 @@ For timing correctness, we have:
|
|
|
369
363
|
correctness-sensitive when it comes to the timers and interrupts of the BBC.
|
|
370
364
|
- Some 65C12-specific read-modify-write tests written by Ed Spittles.
|
|
371
365
|
|
|
372
|
-
Tests
|
|
373
|
-
|
|
366
|
+
Tests are run via npm scripts: `npm test` runs the whole suite. The vitest suites are `npm run test:unit`,
|
|
367
|
+
`npm run test:integration` and `npm run test:shader`; CPU tests are `npm run test:cpu`, and the smoke test is
|
|
368
|
+
`npm run test:smoke` (the shader and smoke tests need Chrome installed). Please note it can take a while to run the
|
|
369
|
+
whole test suite.
|
|
374
370
|
|
|
375
371
|
## Thanks
|
|
376
372
|
|
|
377
|
-
jsbeeb was heavily based on Sarah Walker's C [B-Em emulator](https://github.com/stardot/b-em)
|
|
373
|
+
jsbeeb was heavily based on Sarah Walker's C [B-Em emulator](https://github.com/stardot/b-em); thanks to her for her
|
|
378
374
|
hard work and for open sourcing her code. B-em is now being maintained by a group of enthusiasts - thanks to them too!
|
|
379
375
|
|
|
380
376
|
Huge thanks to Richard Talbot-Watkins for his advice and help along the way in fathoming out the instruction timings,
|
|
@@ -409,7 +405,8 @@ and posted the [video up on YouTube](https://www.youtube.com/watch?v=37jyHQT7fXQ
|
|
|
409
405
|
|
|
410
406
|
## License
|
|
411
407
|
|
|
412
|
-
This project is licensed under the
|
|
408
|
+
This project is licensed under the GNU General Public License v3.0 (or later); see the [LICENSE](LICENSE) file for
|
|
409
|
+
details.
|
|
413
410
|
|
|
414
411
|
## Contact
|
|
415
412
|
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.22.
|
|
10
|
+
"version": "1.22.3",
|
|
11
11
|
"//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=24.15.0"
|
package/src/app/electron.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Handles IPC communication for loading disc/tape images and showing modals from Electron's main process.
|
|
5
5
|
|
|
6
6
|
function init(args) {
|
|
7
|
-
const { loadDiscImage, loadTapeImage, loadStateFile, processor, modals, actions, config } = args;
|
|
7
|
+
const { loadDiscImage, loadTapeImage, loadStateFile, processor, modals, actions, config, media } = args;
|
|
8
8
|
const api = window.electronAPI;
|
|
9
9
|
|
|
10
10
|
api.onLoadDisc(async (message) => {
|
|
@@ -58,7 +58,9 @@ function init(args) {
|
|
|
58
58
|
config.addEventListener("settings-changed", (e) => {
|
|
59
59
|
api.saveSettings(e.detail);
|
|
60
60
|
});
|
|
61
|
-
|
|
61
|
+
}
|
|
62
|
+
if (media) {
|
|
63
|
+
media.addEventListener("media-changed", (e) => {
|
|
62
64
|
api.saveSettings(e.detail);
|
|
63
65
|
});
|
|
64
66
|
}
|
package/src/basic-loader.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
/** Where the OS sits waiting for a keypress, per machine: the place to interrupt with a program. */
|
|
4
|
+
export function basicIdleAddr(model) {
|
|
5
|
+
return model.isMaster ? 0xe7e6 : 0xe581;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* Pokes a tokenised BASIC program into memory at PAGE, and sets TOP and
|
|
5
10
|
* VARTOP after it, exactly as if it had just been typed.
|
package/src/main.js
CHANGED
|
@@ -22,6 +22,7 @@ import { Display } from "./web/display.js";
|
|
|
22
22
|
import { Layout } from "./web/layout.js";
|
|
23
23
|
import { EmulationLoop, RewindCaptureInterval } from "./web/emulation-loop.js";
|
|
24
24
|
import { KeyboardSetup } from "./web/keyboard-setup.js";
|
|
25
|
+
import { AccessibilitySwitches } from "./web/accessibility-switches.js";
|
|
25
26
|
import { AnalogueInputs } from "./web/analogue-inputs.js";
|
|
26
27
|
import { FrontPanel } from "./web/front-panel.js";
|
|
27
28
|
import { Machine } from "./web/machine.js";
|
|
@@ -63,10 +64,7 @@ const noSeek = !!parsedQuery.noseek;
|
|
|
63
64
|
const stationId = parsedQuery.stationId !== undefined ? parsedQuery.stationId : 101;
|
|
64
65
|
|
|
65
66
|
const tryGl = parsedQuery.glEnabled ?? true;
|
|
66
|
-
|
|
67
|
-
if (parsedQuery.lowLatency !== undefined) {
|
|
68
|
-
lowLatency = parsedQuery.lowLatency === "true";
|
|
69
|
-
}
|
|
67
|
+
const lowLatency = parsedQuery.lowLatency ?? true;
|
|
70
68
|
|
|
71
69
|
if (parsedQuery.embed) {
|
|
72
70
|
for (const el of document.querySelectorAll(".embed-hide")) el.style.display = "none";
|
|
@@ -95,21 +93,7 @@ const printer = new Printer({
|
|
|
95
93
|
}),
|
|
96
94
|
});
|
|
97
95
|
|
|
98
|
-
const
|
|
99
|
-
enterDebugger: () => loop.stop(true),
|
|
100
|
-
reload: () => window.location.reload(),
|
|
101
|
-
toggleFast: () => loop.toggleFastAsPossible(),
|
|
102
|
-
openRewind: () => {
|
|
103
|
-
if (rewindUI) rewindUI.open();
|
|
104
|
-
},
|
|
105
|
-
openPrinter: () => frontPanel.checkPrinterWindow(),
|
|
106
|
-
pause: () => loop.stop(false),
|
|
107
|
-
resume: () => loop.go(),
|
|
108
|
-
onAnyKeyDown: () => {
|
|
109
|
-
audioHandler.tryResume();
|
|
110
|
-
inputs.ensureMicrophoneRunning();
|
|
111
|
-
},
|
|
112
|
-
});
|
|
96
|
+
const accessibilitySwitches = new AccessibilitySwitches();
|
|
113
97
|
|
|
114
98
|
const settings = new Settings({ urlState });
|
|
115
99
|
const { config, speechOutput, keyLayout, displayMode, audioOutput, speakerAmount } = settings;
|
|
@@ -138,12 +122,6 @@ if (cpuMultiplier !== 1) console.log(`CPU multiplier set to ${cpuMultiplier}`);
|
|
|
138
122
|
const cpuSpeed = model.cyclesPerSecond;
|
|
139
123
|
const clocksPerSecond = (cpuMultiplier * cpuSpeed) | 0;
|
|
140
124
|
|
|
141
|
-
const modals = new Modals({
|
|
142
|
-
isRunning: () => loop.isRunning(),
|
|
143
|
-
stop: (debug) => loop.stop(debug),
|
|
144
|
-
go: () => loop.go(),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
125
|
const screenCanvas = document.getElementById("screen");
|
|
148
126
|
const display = new Display({
|
|
149
127
|
screenCanvas,
|
|
@@ -187,7 +165,7 @@ const quickSettings = new QuickSettings(
|
|
|
187
165
|
);
|
|
188
166
|
|
|
189
167
|
// ------------------------------------------------------------------------
|
|
190
|
-
// The machine, and everything that feeds it.
|
|
168
|
+
// The machine, the loop that runs it, and everything that feeds it.
|
|
191
169
|
// ------------------------------------------------------------------------
|
|
192
170
|
|
|
193
171
|
// Depends on the settings having applied the URL parameters.
|
|
@@ -199,7 +177,7 @@ const machine = new Machine({
|
|
|
199
177
|
cpuMultiplier,
|
|
200
178
|
extraRoms,
|
|
201
179
|
stationId,
|
|
202
|
-
userPort:
|
|
180
|
+
userPort: accessibilitySwitches.userPort,
|
|
203
181
|
printer,
|
|
204
182
|
speechOutput,
|
|
205
183
|
video,
|
|
@@ -208,23 +186,63 @@ const machine = new Machine({
|
|
|
208
186
|
});
|
|
209
187
|
const processor = machine.processor;
|
|
210
188
|
|
|
211
|
-
const
|
|
189
|
+
const keys = new KeyboardSetup({
|
|
190
|
+
actions: {
|
|
191
|
+
enterDebugger: () => loop.stop(true),
|
|
192
|
+
reload: () => window.location.reload(),
|
|
193
|
+
toggleFast: () => loop.toggleFastAsPossible(),
|
|
194
|
+
openRewind: () => rewindUI.open(),
|
|
195
|
+
openPrinter: () => frontPanel.checkPrinterWindow(),
|
|
196
|
+
pause: () => loop.stop(false),
|
|
197
|
+
resume: () => loop.go(),
|
|
198
|
+
onAnyKeyDown: () => {
|
|
199
|
+
audioHandler.tryResume();
|
|
200
|
+
inputs.ensureMicrophoneRunning();
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
accessibilitySwitches,
|
|
204
|
+
processor,
|
|
205
|
+
dbgr,
|
|
206
|
+
keyLayout,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const rewindBuffer = new RewindBuffer(30);
|
|
210
|
+
const loop = new EmulationLoop({
|
|
211
|
+
processor,
|
|
212
|
+
display,
|
|
213
|
+
audioHandler,
|
|
214
|
+
dbgr,
|
|
215
|
+
gamepad,
|
|
216
|
+
keyboard: keys,
|
|
217
|
+
syncLights: () => frontPanel.syncLights(),
|
|
218
|
+
rewindBuffer,
|
|
219
|
+
onRewindCaptured: () => rewindUI.updateButtonState(),
|
|
220
|
+
clocksPerSecond,
|
|
221
|
+
cpuSpeed,
|
|
222
|
+
fastTape: !!parsedQuery.fasttape,
|
|
223
|
+
audioStatsNode,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const debugPause = document.getElementById("debug-pause");
|
|
227
|
+
const debugPlay = document.getElementById("debug-play");
|
|
228
|
+
loop.addEventListener("running", () => {
|
|
229
|
+
const running = loop.isRunning();
|
|
230
|
+
keys.setRunning(running);
|
|
231
|
+
debugPlay.disabled = running;
|
|
232
|
+
debugPause.disabled = !running;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const modals = new Modals({ loop });
|
|
236
|
+
|
|
237
|
+
const drives = new Drives({ fdc: processor.fdc, driveTracks, confirm: modals.confirm.bind(modals) });
|
|
212
238
|
const media = new MediaLoader({
|
|
213
239
|
processor,
|
|
214
240
|
model,
|
|
215
241
|
drives,
|
|
216
242
|
urlState,
|
|
217
|
-
config,
|
|
218
243
|
modals,
|
|
219
244
|
isSnapshotFile,
|
|
220
245
|
loadSnapshot: (file, buffer) => snapshots.loadStateFromFile(file, buffer),
|
|
221
|
-
// The archives are created further down; each source resolves when used.
|
|
222
|
-
sources: {
|
|
223
|
-
sth: (name) => sthPicker.discs.fetch(name),
|
|
224
|
-
tapeSth: (name) => sthPicker.tapes.fetch(name),
|
|
225
|
-
hfe: (path) => hfePicker.archive.fetch(path),
|
|
226
|
-
drive: (cat, layout) => drivePicker.load(cat, layout),
|
|
227
|
-
},
|
|
228
246
|
});
|
|
229
247
|
const autoBoot = new Autoboot({
|
|
230
248
|
model,
|
|
@@ -240,7 +258,7 @@ const sthPicker = new SthPicker({
|
|
|
240
258
|
processor,
|
|
241
259
|
autoboot: (image) => autoBoot.boot(image),
|
|
242
260
|
});
|
|
243
|
-
|
|
261
|
+
new HfePicker({
|
|
244
262
|
media,
|
|
245
263
|
drives,
|
|
246
264
|
modals,
|
|
@@ -248,7 +266,7 @@ const hfePicker = new HfePicker({
|
|
|
248
266
|
processor,
|
|
249
267
|
autoboot: (image) => autoBoot.boot(image),
|
|
250
268
|
});
|
|
251
|
-
|
|
269
|
+
new GoogleDrivePicker({ media, drives, modals, processor });
|
|
252
270
|
const snapshots = new SnapshotUI({
|
|
253
271
|
processor,
|
|
254
272
|
model,
|
|
@@ -257,9 +275,7 @@ const snapshots = new SnapshotUI({
|
|
|
257
275
|
drives,
|
|
258
276
|
urlState,
|
|
259
277
|
modals,
|
|
260
|
-
|
|
261
|
-
stop: (debug) => loop.stop(debug),
|
|
262
|
-
go: () => loop.go(),
|
|
278
|
+
loop,
|
|
263
279
|
});
|
|
264
280
|
|
|
265
281
|
const inputs = new AnalogueInputs({
|
|
@@ -280,55 +296,25 @@ if (parsedQuery.microphoneChannel !== undefined) {
|
|
|
280
296
|
// Apply ADC source settings from URL parameters
|
|
281
297
|
inputs.updateAdcSources(parsedQuery.mouseJoystickEnabled, parsedQuery.microphoneChannel);
|
|
282
298
|
|
|
283
|
-
keys.attach({ processor, dbgr, keyLayout });
|
|
284
|
-
|
|
285
299
|
const frontPanel = new FrontPanel({ processor, model, printer });
|
|
286
300
|
|
|
287
301
|
// ------------------------------------------------------------------------
|
|
288
|
-
//
|
|
302
|
+
// Rewind, the visualiser and the layout.
|
|
289
303
|
// ------------------------------------------------------------------------
|
|
290
304
|
|
|
291
|
-
const rewindBuffer = new RewindBuffer(30);
|
|
292
|
-
const loop = new EmulationLoop({
|
|
293
|
-
processor,
|
|
294
|
-
display,
|
|
295
|
-
audioHandler,
|
|
296
|
-
dbgr,
|
|
297
|
-
gamepad,
|
|
298
|
-
keyboard: keys,
|
|
299
|
-
syncLights: () => frontPanel.syncLights(),
|
|
300
|
-
rewindBuffer,
|
|
301
|
-
onRewindCaptured: () => rewindUI.updateButtonState(),
|
|
302
|
-
clocksPerSecond,
|
|
303
|
-
cpuSpeed,
|
|
304
|
-
fastTape: !!parsedQuery.fasttape,
|
|
305
|
-
audioStatsNode,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const debugPause = document.getElementById("debug-pause");
|
|
309
|
-
const debugPlay = document.getElementById("debug-play");
|
|
310
|
-
loop.addEventListener("running", () => {
|
|
311
|
-
const running = loop.isRunning();
|
|
312
|
-
keys.setRunning(running);
|
|
313
|
-
debugPlay.disabled = running;
|
|
314
|
-
debugPause.disabled = !running;
|
|
315
|
-
});
|
|
316
|
-
|
|
317
305
|
const rewindUI = new RewindUI({
|
|
318
306
|
rewindBuffer,
|
|
319
307
|
processor,
|
|
320
308
|
video,
|
|
321
309
|
captureInterval: RewindCaptureInterval,
|
|
322
|
-
|
|
323
|
-
go: () => loop.go(),
|
|
324
|
-
isRunning: () => loop.isRunning(),
|
|
310
|
+
loop,
|
|
325
311
|
});
|
|
326
312
|
rewindUI.updateButtonState();
|
|
327
313
|
|
|
328
314
|
if (processor.fdc) new DiscVisualiser({ fdc: processor.fdc });
|
|
329
315
|
else document.getElementById("disc-visualiser-open").classList.add("disabled");
|
|
330
316
|
|
|
331
|
-
new Layout({
|
|
317
|
+
const layout = new Layout({
|
|
332
318
|
screenCanvas,
|
|
333
319
|
display,
|
|
334
320
|
embed: parsedQuery.embed !== undefined,
|
|
@@ -336,7 +322,7 @@ new Layout({
|
|
|
336
322
|
});
|
|
337
323
|
|
|
338
324
|
// Everything a setting can reach now exists.
|
|
339
|
-
settings.wire({ audioHandler, display, quickSettings, machine, keys, inputs, modals });
|
|
325
|
+
settings.wire({ audioHandler, display, layout, quickSettings, machine, keys, inputs, modals });
|
|
340
326
|
|
|
341
327
|
// ------------------------------------------------------------------------
|
|
342
328
|
// The page's own handlers.
|
|
@@ -391,11 +377,7 @@ window.addEventListener("beforeunload", function (event) {
|
|
|
391
377
|
});
|
|
392
378
|
|
|
393
379
|
function hardReset() {
|
|
394
|
-
|
|
395
|
-
rewindUI.close();
|
|
396
|
-
rewindBuffer.clear();
|
|
397
|
-
rewindUI.updateButtonState();
|
|
398
|
-
}
|
|
380
|
+
rewindUI.reset();
|
|
399
381
|
processor.reset(true);
|
|
400
382
|
}
|
|
401
383
|
|
|
@@ -517,6 +499,7 @@ electron({
|
|
|
517
499
|
loadTapeImage: media.loadTapeImage.bind(media),
|
|
518
500
|
processor,
|
|
519
501
|
config,
|
|
502
|
+
media,
|
|
520
503
|
modals: {
|
|
521
504
|
show: (modalId, sthType) => {
|
|
522
505
|
if (modalId === "sth" && sthType) {
|
|
@@ -530,7 +513,7 @@ electron({
|
|
|
530
513
|
actions: {
|
|
531
514
|
"soft-reset": () => processor.reset(false),
|
|
532
515
|
"hard-reset": hardReset,
|
|
533
|
-
"save-state": () =>
|
|
516
|
+
"save-state": () => snapshots.saveState(),
|
|
534
517
|
rewind: () => rewindUI.open(),
|
|
535
518
|
pause: pauseIntoDebugger,
|
|
536
519
|
resume: resumeFromDebugger,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The accessibility switches on the user port. Bits 0-7 correspond to switches
|
|
3
|
+
* 1-8, active low: 0xff = no switches pressed; a clear bit = that switch held.
|
|
4
|
+
*
|
|
5
|
+
* On real hardware, the Brilliant Computing switch interface box and special-ed
|
|
6
|
+
* joystick connect to the User Port only; they do not touch the analogue port
|
|
7
|
+
* or the System VIA fire buttons (PB4/PB5), which belong to the standard
|
|
8
|
+
* analogue joystick connector.
|
|
9
|
+
*/
|
|
10
|
+
export class AccessibilitySwitches {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.switchState = 0xff;
|
|
13
|
+
const switches = this;
|
|
14
|
+
this.userPort = {
|
|
15
|
+
write() {},
|
|
16
|
+
read() {
|
|
17
|
+
return switches.switchState;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setSwitch(index, held) {
|
|
23
|
+
if (held) this.switchState &= ~(1 << index);
|
|
24
|
+
else this.switchState |= 1 << index;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -4,6 +4,8 @@ import { MouseJoystickSource } from "../mouse-joystick-source.js";
|
|
|
4
4
|
import { calculateMouseCoordinates } from "../mouse-coordinates.js";
|
|
5
5
|
import { toast } from "./toast.js";
|
|
6
6
|
|
|
7
|
+
const AdcChannelCount = 4;
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* What feeds the analogue port and the touchscreen: the gamepad, the mouse
|
|
9
11
|
* acting as a joystick, and the microphone, with the mouse on the monitor
|
|
@@ -59,7 +61,7 @@ export class AnalogueInputs {
|
|
|
59
61
|
updateAdcSources(mouseJoystickEnabled, microphoneChannel) {
|
|
60
62
|
const { processor } = this;
|
|
61
63
|
// Default all channels to the gamepad source.
|
|
62
|
-
for (let ch = 0; ch <
|
|
64
|
+
for (let ch = 0; ch < AdcChannelCount; ch++) {
|
|
63
65
|
processor.adconverter.setChannelSource(ch, this.gamepadSource);
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -73,11 +75,24 @@ export class AnalogueInputs {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
// Apply microphone if configured (can override any channel)
|
|
76
|
-
if (microphoneChannel
|
|
78
|
+
if (microphoneChannel === undefined) return;
|
|
79
|
+
if (Number.isInteger(microphoneChannel) && microphoneChannel >= 0 && microphoneChannel < AdcChannelCount) {
|
|
77
80
|
processor.adconverter.setChannelSource(microphoneChannel, this.microphoneInput);
|
|
81
|
+
} else {
|
|
82
|
+
toast(
|
|
83
|
+
`There is no analogue channel ${microphoneChannel}; channels are 0 to 3. ` +
|
|
84
|
+
`The microphone channel has been turned off.`,
|
|
85
|
+
{ title: "Microphone" },
|
|
86
|
+
);
|
|
87
|
+
this.clearMicrophoneChannel();
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
|
|
91
|
+
clearMicrophoneChannel() {
|
|
92
|
+
this.config.setMicrophoneChannel(undefined);
|
|
93
|
+
this.urlState.set({ microphoneChannel: undefined });
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
async ensureMicrophoneRunning() {
|
|
82
97
|
const { microphoneInput } = this;
|
|
83
98
|
if (microphoneInput.audioContext && microphoneInput.audioContext.state !== "running") {
|
|
@@ -93,6 +108,8 @@ export class AnalogueInputs {
|
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
async setupMicrophone() {
|
|
111
|
+
// The channel can have been turned off between the request and now.
|
|
112
|
+
if (this.urlState.params.microphoneChannel === undefined) return;
|
|
96
113
|
const micPermissionStatus = document.getElementById("micPermissionStatus");
|
|
97
114
|
micPermissionStatus.textContent = "Requesting microphone access...";
|
|
98
115
|
|
|
@@ -110,10 +127,7 @@ export class AnalogueInputs {
|
|
|
110
127
|
document.addEventListener("click", tryAgain);
|
|
111
128
|
} else {
|
|
112
129
|
micPermissionStatus.textContent = `Error: ${this.microphoneInput.getErrorMessage() || "Unknown error"}`;
|
|
113
|
-
this.
|
|
114
|
-
// Update URL to remove the parameter
|
|
115
|
-
delete this.urlState.params.microphoneChannel;
|
|
116
|
-
this.urlState.updateUrl();
|
|
130
|
+
this.clearMicrophoneChannel();
|
|
117
131
|
}
|
|
118
132
|
}
|
|
119
133
|
}
|
package/src/web/archive-list.js
CHANGED
|
@@ -28,12 +28,7 @@ export class AutobootTicks {
|
|
|
28
28
|
for (const check of this.checks) {
|
|
29
29
|
check.addEventListener("click", () => {
|
|
30
30
|
this.show(check.checked);
|
|
31
|
-
|
|
32
|
-
urlState.params.autoboot = "";
|
|
33
|
-
} else {
|
|
34
|
-
delete urlState.params.autoboot;
|
|
35
|
-
}
|
|
36
|
-
urlState.updateUrl();
|
|
31
|
+
urlState.set({ autoboot: check.checked ? true : undefined });
|
|
37
32
|
});
|
|
38
33
|
}
|
|
39
34
|
}
|
package/src/web/audio-handler.js
CHANGED
|
@@ -14,8 +14,8 @@ const StallSpikeHeight = 20;
|
|
|
14
14
|
|
|
15
15
|
// Nobody is watching an unfocused window, so its sound can run far behind
|
|
16
16
|
// the picture, deep enough to ride out the browser starving the tab.
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const UnfocusedLatencyMs = 200;
|
|
18
|
+
const DefaultLatencyMs = 20;
|
|
19
19
|
|
|
20
20
|
export class AudioHandler {
|
|
21
21
|
constructor({
|
package/src/web/autoboot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as utils from "../utils.js";
|
|
2
2
|
import * as utils_atom from "../utils_atom.js";
|
|
3
3
|
import * as tokeniser from "../basic-tokenise.js";
|
|
4
|
-
import { installBasic } from "../basic-loader.js";
|
|
4
|
+
import { basicIdleAddr, installBasic } from "../basic-loader.js";
|
|
5
5
|
|
|
6
6
|
/** Booting and typing for the machine at startup: shift-break, *TAPE incantations and BASIC programs. */
|
|
7
7
|
export class Autoboot {
|
|
@@ -69,7 +69,7 @@ export class Autoboot {
|
|
|
69
69
|
const tokenised = await t.tokenise(prog);
|
|
70
70
|
|
|
71
71
|
const { processor } = this;
|
|
72
|
-
const idleAddr = processor.model
|
|
72
|
+
const idleAddr = basicIdleAddr(processor.model);
|
|
73
73
|
const hook = processor.debugInstruction.add((addr) => {
|
|
74
74
|
if (addr !== idleAddr) return;
|
|
75
75
|
installBasic(tokenised, {
|
package/src/web/config.js
CHANGED
|
@@ -25,7 +25,7 @@ export function fittedRoms({ model, hasEconet, hasMusic5000, hasTeletextAdaptor
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** The settings the dialog presents as checkboxes. `enables` names a control only usable while ticked. */
|
|
28
|
-
|
|
28
|
+
const CheckboxSettings = [
|
|
29
29
|
{ id: "65c02", field: "coProcessor", restartRequired: true, enables: "tubeCpuMultiplier" },
|
|
30
30
|
{ id: "hasTeletextAdaptor", field: "hasTeletextAdaptor", restartRequired: true },
|
|
31
31
|
{ id: "hasEconet", field: "hasEconet", restartRequired: true },
|
package/src/web/debug.js
CHANGED
package/src/web/display.js
CHANGED
|
@@ -111,9 +111,6 @@ export class Display {
|
|
|
111
111
|
this.sizeCanvasFor(this.filterClass);
|
|
112
112
|
this.video.paint();
|
|
113
113
|
this.setCrtPic();
|
|
114
|
-
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 1);
|
|
115
|
-
// Relayout now as well: the monitor picture may have changed shape.
|
|
116
|
-
window.dispatchEvent(new Event("resize"));
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
sizeCanvasFor(filterClass) {
|
package/src/web/drives.js
CHANGED
|
@@ -11,7 +11,7 @@ const tracksPerStepFor = (tracks) => (tracks === "40" ? 2 : 1);
|
|
|
11
11
|
* switches on the Discs menu, and downloading what is in drive 0.
|
|
12
12
|
*/
|
|
13
13
|
export class Drives {
|
|
14
|
-
constructor({ fdc, driveTracks,
|
|
14
|
+
constructor({ fdc, driveTracks, confirm }) {
|
|
15
15
|
this.fdc = fdc;
|
|
16
16
|
this.driveTracks = driveTracks;
|
|
17
17
|
this.saidWritesAreNotKept = false;
|
|
@@ -33,7 +33,7 @@ export class Drives {
|
|
|
33
33
|
if (drive) this.showDriveTracks(driveIndex);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
document.getElementById("download-drive-link").addEventListener("click", () => {
|
|
36
|
+
document.getElementById("download-drive-link").addEventListener("click", async () => {
|
|
37
37
|
const disc = this.discToDownload();
|
|
38
38
|
if (!disc) return;
|
|
39
39
|
const save = (options) =>
|
|
@@ -41,9 +41,8 @@ export class Drives {
|
|
|
41
41
|
try {
|
|
42
42
|
save();
|
|
43
43
|
} catch (e) {
|
|
44
|
-
|
|
45
|
-
save({ force: true })
|
|
46
|
-
);
|
|
44
|
+
if (await confirm(`${e.message} Save anyway, losing what will not fit?`, "Save anyway", "Cancel"))
|
|
45
|
+
save({ force: true });
|
|
47
46
|
}
|
|
48
47
|
});
|
|
49
48
|
|
package/src/web/front-panel.js
CHANGED
|
@@ -16,6 +16,7 @@ export class GoogleDrivePicker {
|
|
|
16
16
|
this.modals = modals;
|
|
17
17
|
this.processor = processor;
|
|
18
18
|
this.googleDrive = loader;
|
|
19
|
+
media.addSource("drive", (cat, layout) => this.load(cat, layout));
|
|
19
20
|
|
|
20
21
|
this.authEl = document.getElementById("google-drive-auth");
|
|
21
22
|
this.el = document.getElementById("google-drive");
|
|
@@ -33,18 +34,18 @@ export class GoogleDrivePicker {
|
|
|
33
34
|
|
|
34
35
|
// Loading the Google client holds the main thread for ~100ms, so it waits for
|
|
35
36
|
// someone to ask for Drive.
|
|
36
|
-
document.getElementById("open-drive-link").addEventListener("click", async () => {
|
|
37
|
+
document.getElementById("open-drive-link").addEventListener("click", async (e) => {
|
|
38
|
+
e.preventDefault();
|
|
37
39
|
try {
|
|
38
40
|
await this.googleDrive.initialise();
|
|
39
41
|
} catch (error) {
|
|
40
42
|
toast(`Google Drive is unavailable: ${errorText(error)}`, { title: "Google Drive" });
|
|
41
|
-
return
|
|
43
|
+
return;
|
|
42
44
|
}
|
|
43
45
|
const authed = await this.auth(false);
|
|
44
46
|
if (authed) {
|
|
45
47
|
this.modal.show();
|
|
46
48
|
}
|
|
47
|
-
return false;
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
this.el.addEventListener("show.bs.modal", () => this.showList());
|
|
@@ -55,9 +56,10 @@ export class GoogleDrivePicker {
|
|
|
55
56
|
try {
|
|
56
57
|
return await this.googleDrive.authorize(imm);
|
|
57
58
|
} catch (err) {
|
|
58
|
-
console.log("Error handling google auth: " + err);
|
|
59
|
+
console.log("Error handling google auth: " + errorText(err));
|
|
59
60
|
this.el.querySelector(".loading").textContent =
|
|
60
|
-
|
|
61
|
+
`There was an error accessing your Google Drive account: ${errorText(err)}`;
|
|
62
|
+
return false;
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -131,7 +133,6 @@ export class GoogleDrivePicker {
|
|
|
131
133
|
let name = document.querySelector("#google-drive .disc-name").value;
|
|
132
134
|
if (!name) return;
|
|
133
135
|
|
|
134
|
-
this.modals.popupLoading("Connecting to Google Drive");
|
|
135
136
|
this.modal.hide();
|
|
136
137
|
this.modals.popupLoading("Creating '" + name + "' on Google Drive");
|
|
137
138
|
|
|
@@ -150,7 +151,10 @@ export class GoogleDrivePicker {
|
|
|
150
151
|
// TODO support HFE, I guess?
|
|
151
152
|
const discType = disc.guessDiscTypeFromName(name);
|
|
152
153
|
if (!discType.byteSize) {
|
|
153
|
-
|
|
154
|
+
this.modals.loadingFinished(
|
|
155
|
+
`Unable to create ${name} on Google Drive: blank ${discType.extension} discs have no known size`,
|
|
156
|
+
);
|
|
157
|
+
return;
|
|
154
158
|
}
|
|
155
159
|
data = new Uint8Array(discType.byteSize);
|
|
156
160
|
if (discType.supportsCatalogue) {
|
package/src/web/hfe-picker.js
CHANGED
|
@@ -41,6 +41,7 @@ export class HfePicker {
|
|
|
41
41
|
showArchiveMessage("hfe", "hfe-list", "There was an error accessing the HFE archive");
|
|
42
42
|
},
|
|
43
43
|
);
|
|
44
|
+
media.addSource("hfe", (path) => this.archive.fetch(path));
|
|
44
45
|
|
|
45
46
|
this.modal = new bootstrap.Modal(document.getElementById("hfe"));
|
|
46
47
|
document.getElementById("hfe").addEventListener("shown.bs.modal", () => {
|
|
@@ -2,36 +2,16 @@ import * as utils from "../utils.js";
|
|
|
2
2
|
import { Keyboard } from "../keyboard.js";
|
|
3
3
|
import { showNotice } from "./reporting.js";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* The page's keyboard: the emulated one, the browser shortcuts around it, and
|
|
7
|
-
* the accessibility switches on the user port. Built in two steps because the
|
|
8
|
-
* user port has to exist before the processor does, and the keyboard after.
|
|
9
|
-
*/
|
|
5
|
+
/** The page's keyboard: the emulated one and the browser shortcuts around it. */
|
|
10
6
|
export class KeyboardSetup {
|
|
11
7
|
/**
|
|
12
|
-
* @param {object}
|
|
8
|
+
* @param {object} opts
|
|
9
|
+
* @param {object} opts.actions what each shortcut does, supplied late-bound:
|
|
13
10
|
* enterDebugger, reload, toggleFast, openRewind, openPrinter,
|
|
14
11
|
* pause, resume, onAnyKeyDown
|
|
12
|
+
* @param {import("./accessibility-switches.js").AccessibilitySwitches} opts.accessibilitySwitches
|
|
15
13
|
*/
|
|
16
|
-
constructor(actions) {
|
|
17
|
-
this.actions = actions;
|
|
18
|
-
this.keyboard = null;
|
|
19
|
-
|
|
20
|
-
// Accessibility switch state: bits 0-7 correspond to switches 1-8.
|
|
21
|
-
// Active low: 0xff = no switches pressed; clearing a bit = that switch is pressed.
|
|
22
|
-
this.switchState = 0xff;
|
|
23
|
-
const setup = this;
|
|
24
|
-
this.userPort = {
|
|
25
|
-
write() {},
|
|
26
|
-
read() {
|
|
27
|
-
return setup.switchState;
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Initialise keyboard now that processor exists */
|
|
33
|
-
attach({ processor, dbgr, keyLayout }) {
|
|
34
|
-
const { actions } = this;
|
|
14
|
+
constructor({ actions, accessibilitySwitches, processor, dbgr, keyLayout }) {
|
|
35
15
|
const keyboard = (this.keyboard = new Keyboard({
|
|
36
16
|
processor,
|
|
37
17
|
inputEnabledFunction: () => document.activeElement && document.activeElement.id === "paste-text",
|
|
@@ -66,22 +46,11 @@ export class KeyboardSetup {
|
|
|
66
46
|
keyboard.registerKeyHandler(utils.keyCodes.PAGEDOWN, onDown("pagedown", actions.openRewind), alt);
|
|
67
47
|
keyboard.registerKeyHandler(utils.keyCodes.B, onDown(null, actions.openPrinter), ctrl);
|
|
68
48
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
// joystick connect to the User Port only; they do not touch the analogue port
|
|
75
|
-
// or the System VIA fire buttons (PB4/PB5), which belong to the standard
|
|
76
|
-
// analogue joystick connector. So we only update switchState here.
|
|
77
|
-
const handleSwitch = (bit) => (down) => {
|
|
78
|
-
if (down) this.switchState &= ~(1 << bit);
|
|
79
|
-
else this.switchState |= 1 << bit;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Alt+1-8 and Alt+F1-F8 trigger the switches. Using Alt means the underlying
|
|
83
|
-
// key is never forwarded to the BBC Micro (keyboard.js bails out early when a
|
|
84
|
-
// handler fires), so typing numbers or using function keys works normally.
|
|
49
|
+
// Alt+1-8 and Alt+F1-F8 trigger the accessibility switches. Using Alt means
|
|
50
|
+
// the underlying key is never forwarded to the BBC Micro (keyboard.js bails
|
|
51
|
+
// out early when a handler fires), so typing numbers or using function keys
|
|
52
|
+
// works normally.
|
|
53
|
+
const handleSwitch = (index) => (down) => accessibilitySwitches.setSwitch(index, down);
|
|
85
54
|
for (let i = 0; i < 8; i++) {
|
|
86
55
|
keyboard.registerKeyHandler(utils.keyCodes.K1 + i, handleSwitch(i), alt);
|
|
87
56
|
keyboard.registerKeyHandler(utils.keyCodes.F1 + i, handleSwitch(i), alt);
|
|
@@ -96,11 +65,7 @@ export class KeyboardSetup {
|
|
|
96
65
|
}
|
|
97
66
|
|
|
98
67
|
sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
|
|
99
|
-
|
|
100
|
-
this.keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
|
|
101
|
-
} else {
|
|
102
|
-
console.warn("Tried to send keys before keyboard was initialised");
|
|
103
|
-
}
|
|
68
|
+
this.keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
|
|
104
69
|
}
|
|
105
70
|
|
|
106
71
|
clearKeys() {
|
package/src/web/media-loader.js
CHANGED
|
@@ -50,22 +50,20 @@ function readFileAsBinaryString(file) {
|
|
|
50
50
|
* URL schema can name, the local file inputs, the drop zone and the built-in
|
|
51
51
|
* list. Choosing what goes in a drive funnels through drives.putDiscIn.
|
|
52
52
|
*/
|
|
53
|
-
export class MediaLoader {
|
|
53
|
+
export class MediaLoader extends EventTarget {
|
|
54
54
|
/**
|
|
55
55
|
* @param {object} deps
|
|
56
|
-
* @param {object} deps.sources fetchers keyed by schema: sth, tapeSth and hfe
|
|
57
|
-
* resolve an archive name to image data; drive loads a Google Drive file
|
|
58
56
|
* @param {Function} deps.isSnapshotFile says whether a dropped file is a save state
|
|
59
57
|
* @param {Function} deps.loadSnapshot restores a dropped save state
|
|
60
58
|
*/
|
|
61
|
-
constructor({ processor, model, drives, urlState,
|
|
59
|
+
constructor({ processor, model, drives, urlState, modals, isSnapshotFile, loadSnapshot }) {
|
|
60
|
+
super();
|
|
62
61
|
this.processor = processor;
|
|
63
62
|
this.model = model;
|
|
64
63
|
this.drives = drives;
|
|
65
64
|
this.urlState = urlState;
|
|
66
|
-
this.config = config;
|
|
67
65
|
this.modals = modals;
|
|
68
|
-
this.sources =
|
|
66
|
+
this.sources = {};
|
|
69
67
|
|
|
70
68
|
document.getElementById("disc_load").addEventListener("change", async (evt) => {
|
|
71
69
|
if (evt.target.files.length === 0) return;
|
|
@@ -105,8 +103,7 @@ export class MediaLoader {
|
|
|
105
103
|
tapeName = unzipped.name;
|
|
106
104
|
}
|
|
107
105
|
this.setProcessorTape(await loadTapeFromData(tapeName, tapeData, model));
|
|
108
|
-
|
|
109
|
-
urlState.updateUrl();
|
|
106
|
+
urlState.set({ tape: undefined });
|
|
110
107
|
modals.hide("tapes");
|
|
111
108
|
} catch (error) {
|
|
112
109
|
reportLoadFailure(file.name, error);
|
|
@@ -167,6 +164,11 @@ export class MediaLoader {
|
|
|
167
164
|
return this.urlState.params;
|
|
168
165
|
}
|
|
169
166
|
|
|
167
|
+
/** Register the fetcher behind an image schema; each picker calls this as it is constructed. */
|
|
168
|
+
addSource(schema, fetcher) {
|
|
169
|
+
this.sources[schema] = fetcher;
|
|
170
|
+
}
|
|
171
|
+
|
|
170
172
|
/** Route tape to the correct interface (ACIA for BBC, PPIA for Atom) */
|
|
171
173
|
setProcessorTape(tape) {
|
|
172
174
|
if (this.model.isAtom) {
|
|
@@ -177,22 +179,18 @@ export class MediaLoader {
|
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
setDisc1Image(name) {
|
|
180
|
-
|
|
181
|
-
this.
|
|
182
|
-
this.urlState.updateUrl();
|
|
183
|
-
this.config.dispatchEvent(new CustomEvent("media-changed", { detail: { disc1: name } }));
|
|
182
|
+
this.urlState.set({ disc: undefined, disc1: name });
|
|
183
|
+
this.dispatchEvent(new CustomEvent("media-changed", { detail: { disc1: name } }));
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
setDisc2Image(name) {
|
|
187
|
-
this.
|
|
188
|
-
this.
|
|
189
|
-
this.config.dispatchEvent(new CustomEvent("media-changed", { detail: { disc2: name } }));
|
|
187
|
+
this.urlState.set({ disc2: name });
|
|
188
|
+
this.dispatchEvent(new CustomEvent("media-changed", { detail: { disc2: name } }));
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
setTapeImage(name) {
|
|
193
|
-
this.
|
|
194
|
-
this.
|
|
195
|
-
this.config.dispatchEvent(new CustomEvent("media-changed", { detail: { tape: name } }));
|
|
192
|
+
this.urlState.set({ tape: name });
|
|
193
|
+
this.dispatchEvent(new CustomEvent("media-changed", { detail: { tape: name } }));
|
|
196
194
|
}
|
|
197
195
|
|
|
198
196
|
async loadHTMLFile(file) {
|
|
@@ -207,9 +205,7 @@ export class MediaLoader {
|
|
|
207
205
|
// Local file: retain the image bytes for embedding in save-to-file snapshots.
|
|
208
206
|
loadedDisc.setOriginalImage(imageData);
|
|
209
207
|
this.drives.putDiscIn(0, loadedDisc);
|
|
210
|
-
|
|
211
|
-
delete this.params.disc1;
|
|
212
|
-
this.urlState.updateUrl();
|
|
208
|
+
this.urlState.set({ disc: undefined, disc1: undefined });
|
|
213
209
|
this.modals.hide("discs");
|
|
214
210
|
}
|
|
215
211
|
|
|
@@ -309,6 +305,7 @@ export class MediaLoader {
|
|
|
309
305
|
}
|
|
310
306
|
|
|
311
307
|
async loadTapeImage(tapeImage) {
|
|
308
|
+
if (!tapeImage) return null;
|
|
312
309
|
const split = splitImage(tapeImage);
|
|
313
310
|
tapeImage = split.image;
|
|
314
311
|
const schema = split.schema;
|
package/src/web/modals.js
CHANGED
|
@@ -7,7 +7,7 @@ import { toast } from "./toast.js";
|
|
|
7
7
|
* if it was running before.
|
|
8
8
|
*/
|
|
9
9
|
export class Modals {
|
|
10
|
-
constructor({
|
|
10
|
+
constructor({ loop }) {
|
|
11
11
|
this.errorDialog = document.getElementById("error-dialog");
|
|
12
12
|
this.errorModal = new bootstrap.Modal(this.errorDialog);
|
|
13
13
|
this.loadingDialog = document.getElementById("loading-dialog");
|
|
@@ -18,11 +18,11 @@ export class Modals {
|
|
|
18
18
|
|
|
19
19
|
let savedRunning = false;
|
|
20
20
|
document.addEventListener("show.bs.modal", () => {
|
|
21
|
-
if (!this.anyVisible()) savedRunning = isRunning();
|
|
22
|
-
if (isRunning()) stop(false);
|
|
21
|
+
if (!this.anyVisible()) savedRunning = loop.isRunning();
|
|
22
|
+
if (loop.isRunning()) loop.stop(false);
|
|
23
23
|
});
|
|
24
24
|
document.addEventListener("hidden.bs.modal", () => {
|
|
25
|
-
if (!this.anyVisible() && savedRunning) go();
|
|
25
|
+
if (!this.anyVisible() && savedRunning) loop.go();
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -58,26 +58,29 @@ export class Modals {
|
|
|
58
58
|
if (message) toast(message);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/** @returns {Promise<boolean>} true for the yes button; false for any other way out of the dialog */
|
|
62
|
+
confirm(message, yesText, noText) {
|
|
62
63
|
const yesButton = this.aysEl.querySelector(".ays-yes");
|
|
63
64
|
this.aysEl.querySelector(".context").textContent = message;
|
|
64
65
|
this.aysEl.querySelector(".ays-no").textContent = noText;
|
|
65
66
|
yesButton.textContent = yesText;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
let confirmed = false;
|
|
69
|
+
const onYes = () => {
|
|
70
|
+
confirmed = true;
|
|
71
|
+
this.aysModal.hide();
|
|
72
|
+
};
|
|
73
|
+
yesButton.addEventListener("click", onYes, { once: true });
|
|
74
|
+
// The "no" button, Escape and a click outside raise no event of their own: they only hide the modal.
|
|
75
|
+
this.aysEl.addEventListener(
|
|
76
|
+
"hidden.bs.modal",
|
|
77
|
+
() => {
|
|
78
|
+
yesButton.removeEventListener("click", onYes);
|
|
79
|
+
resolve(confirmed);
|
|
80
|
+
},
|
|
81
|
+
{ once: true },
|
|
82
|
+
);
|
|
83
|
+
this.aysModal.show();
|
|
84
|
+
});
|
|
82
85
|
}
|
|
83
86
|
}
|
package/src/web/rewind-ui.js
CHANGED
|
@@ -13,18 +13,14 @@ export class RewindUI {
|
|
|
13
13
|
* @param {object} options.processor - Cpu6502 instance
|
|
14
14
|
* @param {object} options.video - Video instance
|
|
15
15
|
* @param {number} options.captureInterval - rewind capture interval in frames
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {function} options.go - function to resume the emulator
|
|
18
|
-
* @param {function} options.isRunning - function returning current running state
|
|
16
|
+
* @param {object} options.loop - the emulation loop, for pausing and resuming
|
|
19
17
|
*/
|
|
20
|
-
constructor({ rewindBuffer, processor, video, captureInterval,
|
|
18
|
+
constructor({ rewindBuffer, processor, video, captureInterval, loop }) {
|
|
21
19
|
this.rewindBuffer = rewindBuffer;
|
|
22
20
|
this.processor = processor;
|
|
23
21
|
this.video = video;
|
|
24
22
|
this.captureInterval = captureInterval;
|
|
25
|
-
this.
|
|
26
|
-
this.go = go;
|
|
27
|
-
this.isRunning = isRunning;
|
|
23
|
+
this.loop = loop;
|
|
28
24
|
|
|
29
25
|
this.panel = document.getElementById("rewind-panel");
|
|
30
26
|
this.filmstrip = document.getElementById("rewind-filmstrip");
|
|
@@ -45,6 +41,13 @@ export class RewindUI {
|
|
|
45
41
|
});
|
|
46
42
|
}
|
|
47
43
|
|
|
44
|
+
/** Forget everything captured, as on a hard reset. */
|
|
45
|
+
reset() {
|
|
46
|
+
this.close();
|
|
47
|
+
this.rewindBuffer.clear();
|
|
48
|
+
this.updateButtonState();
|
|
49
|
+
}
|
|
50
|
+
|
|
48
51
|
/** Open the rewind scrubber panel. */
|
|
49
52
|
open() {
|
|
50
53
|
if (this.isOpen) return;
|
|
@@ -52,8 +55,8 @@ export class RewindUI {
|
|
|
52
55
|
this.snapshots = this.rewindBuffer.getAll();
|
|
53
56
|
if (this.snapshots.length === 0) return;
|
|
54
57
|
|
|
55
|
-
this.wasRunning = this.isRunning();
|
|
56
|
-
if (this.wasRunning) this.stop(false);
|
|
58
|
+
this.wasRunning = this.loop.isRunning();
|
|
59
|
+
if (this.wasRunning) this.loop.stop(false);
|
|
57
60
|
|
|
58
61
|
this.isOpen = true;
|
|
59
62
|
this.savedState = this.processor.snapshotState();
|
|
@@ -70,7 +73,7 @@ export class RewindUI {
|
|
|
70
73
|
} catch (e) {
|
|
71
74
|
this.processor.restoreState(this.savedState);
|
|
72
75
|
this._closePanel();
|
|
73
|
-
if (this.wasRunning) this.go();
|
|
76
|
+
if (this.wasRunning) this.loop.go();
|
|
74
77
|
throw e;
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -97,7 +100,7 @@ export class RewindUI {
|
|
|
97
100
|
this.processor.restoreState(this.snapshots[this.selectedIndex]);
|
|
98
101
|
}
|
|
99
102
|
this._closePanel();
|
|
100
|
-
if (this.wasRunning) this.go();
|
|
103
|
+
if (this.wasRunning) this.loop.go();
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
/**
|
|
@@ -107,7 +110,7 @@ export class RewindUI {
|
|
|
107
110
|
if (!this.isOpen) return;
|
|
108
111
|
this._renderState(this.savedState);
|
|
109
112
|
this._closePanel();
|
|
110
|
-
if (this.wasRunning) this.go();
|
|
113
|
+
if (this.wasRunning) this.loop.go();
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
/** Alias for cancel — closing the panel without explicit commit cancels. */
|
package/src/web/settings.js
CHANGED
|
@@ -39,13 +39,13 @@ export class Settings {
|
|
|
39
39
|
if (changed.displayMode) this.applyDisplayMode(changed.displayMode);
|
|
40
40
|
},
|
|
41
41
|
(changed) => this.onDialogClosed(changed),
|
|
42
|
-
() => {
|
|
43
|
-
this.targets.modals.
|
|
42
|
+
async () => {
|
|
43
|
+
const restart = await this.targets.modals.confirm(
|
|
44
44
|
"Your change is saved, but only takes effect when the emulator restarts. Restart now?",
|
|
45
45
|
"Restart now",
|
|
46
46
|
"Later",
|
|
47
|
-
() => window.location.reload(),
|
|
48
47
|
);
|
|
48
|
+
if (restart) window.location.reload();
|
|
49
49
|
},
|
|
50
50
|
);
|
|
51
51
|
|
|
@@ -106,8 +106,7 @@ export class Settings {
|
|
|
106
106
|
this.config.setAudioOutput(output);
|
|
107
107
|
this.targets.quickSettings?.showAudioOutput(output);
|
|
108
108
|
window.localStorage.audioOutput = output;
|
|
109
|
-
this.urlState.
|
|
110
|
-
this.urlState.updateUrl();
|
|
109
|
+
this.urlState.set({ audioOutput: output });
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
applySpeakerAmount(amount) {
|
|
@@ -123,24 +122,25 @@ export class Settings {
|
|
|
123
122
|
applyDisplayMode(mode) {
|
|
124
123
|
this.displayMode = mode;
|
|
125
124
|
this.targets.display.setMode(mode);
|
|
125
|
+
// The monitor picture may have changed shape.
|
|
126
|
+
this.targets.layout.resize();
|
|
126
127
|
this.config.setDisplayMode(mode);
|
|
127
128
|
this.targets.quickSettings?.showDisplayMode(mode);
|
|
128
129
|
window.localStorage.displayMode = mode;
|
|
129
|
-
this.urlState.
|
|
130
|
-
this.urlState.updateUrl();
|
|
130
|
+
this.urlState.set({ displayMode: mode });
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
onDialogClosed(changed) {
|
|
134
134
|
const { urlState } = this;
|
|
135
135
|
const { machine, keys, inputs } = this.targets;
|
|
136
136
|
const parsedQuery = urlState.params;
|
|
137
|
-
|
|
137
|
+
urlState.set(changed);
|
|
138
138
|
if (changed.keyLayout) {
|
|
139
139
|
window.localStorage.keyLayout = changed.keyLayout;
|
|
140
140
|
machine.emulationConfig.keyLayout = changed.keyLayout;
|
|
141
141
|
keys.setKeyLayout(changed.keyLayout);
|
|
142
142
|
}
|
|
143
|
-
if (changed.mouseJoystickEnabled !== undefined || changed
|
|
143
|
+
if (changed.mouseJoystickEnabled !== undefined || Object.hasOwn(changed, "microphoneChannel")) {
|
|
144
144
|
inputs.updateAdcSources(parsedQuery.mouseJoystickEnabled, parsedQuery.microphoneChannel);
|
|
145
145
|
|
|
146
146
|
if (changed.microphoneChannel !== undefined) {
|
|
@@ -155,6 +155,5 @@ export class Settings {
|
|
|
155
155
|
machine.processor.tube.cpuMultiplier = changed.tubeCpuMultiplier;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
urlState.updateUrl();
|
|
159
158
|
}
|
|
160
159
|
}
|
package/src/web/snapshot-ui.js
CHANGED
|
@@ -38,7 +38,7 @@ export function isSnapshotFile(filename, arrayBuffer) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** The saved snapshot in whichever of the formats we read the buffer holds. */
|
|
41
|
-
|
|
41
|
+
async function readSnapshot(arrayBuffer) {
|
|
42
42
|
if (isBemSnapshot(arrayBuffer)) return await parseBemSnapshot(arrayBuffer);
|
|
43
43
|
if (isUefSnapshot(arrayBuffer)) return parseUefSnapshot(arrayBuffer);
|
|
44
44
|
// Detect gzip (magic bytes 0x1f 0x8b) or plain JSON
|
|
@@ -84,7 +84,7 @@ export function snapshotMedia(fdcDrives, params) {
|
|
|
84
84
|
|
|
85
85
|
/** Saving and restoring states: the menu item, the file input and the reload across a model change. */
|
|
86
86
|
export class SnapshotUI {
|
|
87
|
-
constructor({ processor, model, video, media, drives, urlState, modals,
|
|
87
|
+
constructor({ processor, model, video, media, drives, urlState, modals, loop }) {
|
|
88
88
|
this.processor = processor;
|
|
89
89
|
this.model = model;
|
|
90
90
|
this.video = video;
|
|
@@ -92,9 +92,7 @@ export class SnapshotUI {
|
|
|
92
92
|
this.drives = drives;
|
|
93
93
|
this.urlState = urlState;
|
|
94
94
|
this.modals = modals;
|
|
95
|
-
this.
|
|
96
|
-
this.stop = stop;
|
|
97
|
-
this.go = go;
|
|
95
|
+
this.loop = loop;
|
|
98
96
|
|
|
99
97
|
document.getElementById("save-state").addEventListener("click", async (event) => {
|
|
100
98
|
event.preventDefault();
|
|
@@ -110,8 +108,8 @@ export class SnapshotUI {
|
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
async saveState() {
|
|
113
|
-
const wasRunning = this.isRunning();
|
|
114
|
-
if (wasRunning) this.stop(false);
|
|
111
|
+
const wasRunning = this.loop.isRunning();
|
|
112
|
+
if (wasRunning) this.loop.stop(false);
|
|
115
113
|
try {
|
|
116
114
|
const manifest = snapshotMedia(this.processor.fdc.drives, this.urlState.params);
|
|
117
115
|
const snapshot = createSnapshot(this.processor, this.model, manifest);
|
|
@@ -122,12 +120,12 @@ export class SnapshotUI {
|
|
|
122
120
|
} catch (e) {
|
|
123
121
|
this.modals.showError("saving state", e);
|
|
124
122
|
}
|
|
125
|
-
if (wasRunning) this.go();
|
|
123
|
+
if (wasRunning) this.loop.go();
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
async loadStateFromFile(file, preReadBuffer) {
|
|
129
|
-
const wasRunning = this.isRunning();
|
|
130
|
-
if (wasRunning) this.stop(false);
|
|
127
|
+
const wasRunning = this.loop.isRunning();
|
|
128
|
+
if (wasRunning) this.loop.stop(false);
|
|
131
129
|
try {
|
|
132
130
|
const arrayBuffer = preReadBuffer || (await file.arrayBuffer());
|
|
133
131
|
const snapshot = await readSnapshot(arrayBuffer);
|
|
@@ -146,7 +144,7 @@ export class SnapshotUI {
|
|
|
146
144
|
} catch (e) {
|
|
147
145
|
this.modals.showError("loading state", e);
|
|
148
146
|
}
|
|
149
|
-
if (wasRunning) this.go();
|
|
147
|
+
if (wasRunning) this.loop.go();
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
/** Picks up the state a cross-model reload stashed, once the matching machine is up. */
|
package/src/web/sth-picker.js
CHANGED
|
@@ -22,8 +22,18 @@ export class SthPicker {
|
|
|
22
22
|
document.getElementById("sth-filter").focus();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Anything that clears the list takes a new ticket; a chain whose ticket is
|
|
26
|
+
// stale gives up (the same scheme as hfe-picker.js).
|
|
27
|
+
this.renderTicket = 0;
|
|
28
|
+
|
|
29
|
+
const startLoad = () => {
|
|
30
|
+
this.renderTicket++;
|
|
31
|
+
showArchiveMessage("sth", "sth-list", "Loading catalog from STH archive");
|
|
32
|
+
};
|
|
33
|
+
const onError = () => {
|
|
34
|
+
this.renderTicket++;
|
|
35
|
+
showArchiveMessage("sth", "sth-list", "There was an error accessing the STH archive");
|
|
36
|
+
};
|
|
27
37
|
this.discs = new StairwayToHell(
|
|
28
38
|
startLoad,
|
|
29
39
|
(cat) => this.renderCatalogue(cat, (item) => this.pickDisc(item)),
|
|
@@ -36,6 +46,8 @@ export class SthPicker {
|
|
|
36
46
|
onError,
|
|
37
47
|
true,
|
|
38
48
|
);
|
|
49
|
+
media.addSource("sth", (name) => this.discs.fetch(name));
|
|
50
|
+
media.addSource("tapeSth", (name) => this.tapes.fetch(name));
|
|
39
51
|
|
|
40
52
|
document.addEventListener("click", (e) => {
|
|
41
53
|
const target = e.target.closest("a.sth");
|
|
@@ -95,17 +107,19 @@ export class SthPicker {
|
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
renderCatalogue(cat, onClick) {
|
|
110
|
+
const ticket = ++this.renderTicket;
|
|
98
111
|
clearArchiveList("sth-list");
|
|
99
112
|
const sthList = document.getElementById("sth-list");
|
|
100
113
|
document.querySelector("#sth .loading").style.display = "none";
|
|
101
114
|
const template = sthList.querySelector(".template");
|
|
102
115
|
|
|
103
116
|
const doSome = (all) => {
|
|
117
|
+
if (ticket !== this.renderTicket) return;
|
|
104
118
|
const MaxAtATime = 100;
|
|
105
119
|
const Delay = 30;
|
|
106
120
|
const batch = all.slice(0, MaxAtATime);
|
|
107
121
|
const remaining = all.slice(MaxAtATime);
|
|
108
|
-
const filter = document.getElementById("sth-filter").value;
|
|
122
|
+
const filter = document.getElementById("sth-filter").value.toLowerCase();
|
|
109
123
|
for (const name of batch) {
|
|
110
124
|
const row = template.cloneNode(true);
|
|
111
125
|
row.classList.remove("template");
|
|
@@ -117,7 +131,7 @@ export class SthPicker {
|
|
|
117
131
|
});
|
|
118
132
|
row.style.display = name.toLowerCase().indexOf(filter) >= 0 ? "" : "none";
|
|
119
133
|
}
|
|
120
|
-
if (
|
|
134
|
+
if (remaining.length) setTimeout(() => doSome(remaining), Delay);
|
|
121
135
|
};
|
|
122
136
|
|
|
123
137
|
doSome(cat);
|
package/src/web/url-state.js
CHANGED
|
@@ -18,6 +18,7 @@ export const UrlParamTypes = {
|
|
|
18
18
|
hasTeletextAdaptor: ParamTypes.BOOL,
|
|
19
19
|
hasEconet: ParamTypes.BOOL,
|
|
20
20
|
glEnabled: ParamTypes.BOOL,
|
|
21
|
+
lowLatency: ParamTypes.BOOL,
|
|
21
22
|
fakeVideo: ParamTypes.BOOL,
|
|
22
23
|
logFdcCommands: ParamTypes.BOOL,
|
|
23
24
|
logFdcStateChanges: ParamTypes.BOOL,
|
|
@@ -55,7 +56,8 @@ export const UrlParamTypes = {
|
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* The page's settings as carried in its URL. `params` is the one parsed
|
|
58
|
-
* object: whoever changes a setting
|
|
59
|
+
* object: whoever changes a setting hands the change to set(), which edits
|
|
60
|
+
* it in place and writes the URL in one step.
|
|
59
61
|
*/
|
|
60
62
|
export class UrlState {
|
|
61
63
|
constructor(location, history, paramTypes = UrlParamTypes) {
|
|
@@ -77,6 +79,15 @@ export class UrlState {
|
|
|
77
79
|
return buildUrlFromParams(this.baseUrl, { ...this.params, ...overrides }, this.paramTypes);
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
/** Apply `changes` to the parameters (undefined deletes a key) and push the new URL. */
|
|
83
|
+
set(changes) {
|
|
84
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
85
|
+
if (value === undefined) delete this.params[key];
|
|
86
|
+
else this.params[key] = value;
|
|
87
|
+
}
|
|
88
|
+
this.updateUrl();
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
updateUrl() {
|
|
81
92
|
this.history.pushState(null, null, this.url());
|
|
82
93
|
}
|
package/tests/test-machine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { installBasic } from "../src/basic-loader.js";
|
|
1
|
+
import { basicIdleAddr, installBasic } from "../src/basic-loader.js";
|
|
2
2
|
import * as fdc from "../src/fdc.js";
|
|
3
3
|
import { fake6502 } from "../src/fake6502.js";
|
|
4
4
|
import { findModel } from "../src/models.js";
|
|
@@ -182,7 +182,7 @@ export class TestMachine {
|
|
|
182
182
|
assert(hit, "Atom did not reach keyboard input in time");
|
|
183
183
|
return this.runFor(10 * 1000);
|
|
184
184
|
}
|
|
185
|
-
const idleAddr = this.processor.model
|
|
185
|
+
const idleAddr = basicIdleAddr(this.processor.model);
|
|
186
186
|
let hit = false;
|
|
187
187
|
const hook = this.processor.debugInstruction.add((addr) => {
|
|
188
188
|
if (addr === idleAddr) {
|
/package/{COPYING → LICENSE}
RENAMED
|
File without changes
|