jsbeeb 1.22.3 → 1.22.4
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/package.json +1 -1
- package/src/6502.js +6 -1
- package/src/cmos.js +3 -3
- package/src/main.js +3 -0
- package/src/web/debug.js +43 -36
- package/src/web/google-drive-picker.js +30 -10
- package/src/web/machine.js +8 -6
- package/src/web/media-loader.js +2 -1
- package/src/web/snapshot-ui.js +47 -14
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.4",
|
|
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/6502.js
CHANGED
|
@@ -1195,6 +1195,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1195
1195
|
while (this.model.swram[romIndex]) {
|
|
1196
1196
|
romIndex--;
|
|
1197
1197
|
}
|
|
1198
|
+
if (romIndex < 0) throw new Error("Too many extra ROMs (no free sideways ROM banks)");
|
|
1198
1199
|
|
|
1199
1200
|
awaiting.push(this.loadRom(extraRoms[i_2], this.romOffset + romIndex * 16384));
|
|
1200
1201
|
}
|
|
@@ -1823,7 +1824,11 @@ export class AtomCpu6502 extends Cpu6502 {
|
|
|
1823
1824
|
|
|
1824
1825
|
// Override loadRom to accept 4KB ROMs
|
|
1825
1826
|
async loadRom(name, offset) {
|
|
1826
|
-
|
|
1827
|
+
if (name.indexOf("http") !== 0) name = "roms/" + name;
|
|
1828
|
+
let data = await utils.loadData(name);
|
|
1829
|
+
if (/\.zip/i.test(name)) {
|
|
1830
|
+
data = (await utils.unzipRomImage(data)).data;
|
|
1831
|
+
}
|
|
1827
1832
|
const len = data.length;
|
|
1828
1833
|
if (len !== 16384 && len !== 8192 && len !== 4096) {
|
|
1829
1834
|
throw new Error(`Broken ROM file ${name} (length=${len})`);
|
package/src/cmos.js
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
// STNICC-beeb to hang on the Master 128 because the WD1770 seek overran the
|
|
10
10
|
// vsync window. Confirmed by @tom-seddon (b2 emulator):
|
|
11
11
|
// https://github.com/mattgodbolt/jsbeeb/issues/576
|
|
12
|
-
const defaultCmos = [
|
|
12
|
+
const defaultCmos = Object.freeze([
|
|
13
13
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0xeb, 0x00,
|
|
14
14
|
0xc9, 0xff, 0xff, 0x12, 0x00, 0x17, 0xc8, 0x1e, 0x05, 0x00, 0x35, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
15
15
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
16
|
-
];
|
|
16
|
+
]);
|
|
17
17
|
|
|
18
18
|
let timeOffset = 0;
|
|
19
19
|
|
|
@@ -82,7 +82,7 @@ export class Cmos {
|
|
|
82
82
|
this.cmosAddr = 0;
|
|
83
83
|
|
|
84
84
|
if (!this.store) {
|
|
85
|
-
this.store = defaultCmos;
|
|
85
|
+
this.store = [...defaultCmos];
|
|
86
86
|
}
|
|
87
87
|
if (cmosOverride) {
|
|
88
88
|
this.store = cmosOverride(this.store);
|
package/src/main.js
CHANGED
|
@@ -50,6 +50,7 @@ let secondDiscImage = null;
|
|
|
50
50
|
const { discImage: queryDiscImage, secondDiscImage: querySecondDisc, mmcImage } = parseMediaParams(parsedQuery);
|
|
51
51
|
if (queryDiscImage) discImage = queryDiscImage;
|
|
52
52
|
if (querySecondDisc) secondDiscImage = querySecondDisc;
|
|
53
|
+
const defaultBootDisc = queryDiscImage ? undefined : discImage;
|
|
53
54
|
const { settings: driveTracks, warnings: driveTrackWarnings } = processDriveTrackParams(parsedQuery);
|
|
54
55
|
|
|
55
56
|
const extraRoms = [];
|
|
@@ -276,6 +277,7 @@ const snapshots = new SnapshotUI({
|
|
|
276
277
|
urlState,
|
|
277
278
|
modals,
|
|
278
279
|
loop,
|
|
280
|
+
defaultBootDisc,
|
|
279
281
|
});
|
|
280
282
|
|
|
281
283
|
const inputs = new AnalogueInputs({
|
|
@@ -392,6 +394,7 @@ document.getElementById("soft-reset").addEventListener("click", function (event)
|
|
|
392
394
|
});
|
|
393
395
|
|
|
394
396
|
document.getElementById("download-filestore-link").addEventListener("click", function () {
|
|
397
|
+
if (!processor.filestore) return;
|
|
395
398
|
downloadDriveData(processor.filestore.scsi, "scsi", ".dat");
|
|
396
399
|
});
|
|
397
400
|
|
package/src/web/debug.js
CHANGED
|
@@ -89,6 +89,48 @@ class MemoryView {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Guesses the address of the instruction before the given one by scoring each
|
|
94
|
+
* possible run of instructions leading up to it: a point per "common"
|
|
95
|
+
* instruction (loads, stores, branches, compares, arithmetic and
|
|
96
|
+
* carry-set/clear that avoid unusual indexing modes) and a large boost for a
|
|
97
|
+
* run that passes through the current program counter. The highest-scoring
|
|
98
|
+
* run wins and its last instruction is the answer.
|
|
99
|
+
* Good test cases:
|
|
100
|
+
* Repton 2 @ 2cbb
|
|
101
|
+
* MOS @ cfc8
|
|
102
|
+
* also, just starting from the back of ROM and going up...
|
|
103
|
+
*/
|
|
104
|
+
export function prevInstruction(disassemble, pc, address) {
|
|
105
|
+
const commonInstructions =
|
|
106
|
+
/(RTS|B..|JMP|JSR|LD[AXY]|ST[AXY]|TA[XY]|T[XY]A|AD[DC]|SUB|SBC|CLC|SEC|CMP|EOR|ORR|AND|INC|DEC).*/;
|
|
107
|
+
const uncommonInstrucions = /.*,\s*([XY]|X\))$/;
|
|
108
|
+
|
|
109
|
+
address &= 0xffff;
|
|
110
|
+
let bestAddr = address - 1;
|
|
111
|
+
let bestScore = 0;
|
|
112
|
+
for (let startingPoint = address - 20; startingPoint !== address; startingPoint++) {
|
|
113
|
+
let score = 0;
|
|
114
|
+
let addr = startingPoint & 0xffff;
|
|
115
|
+
while (addr < address) {
|
|
116
|
+
const result = disassemble(addr);
|
|
117
|
+
if (addr === pc) score += 10;
|
|
118
|
+
if (result[0].match(commonInstructions) && !result[0].match(uncommonInstrucions)) {
|
|
119
|
+
score++;
|
|
120
|
+
}
|
|
121
|
+
if (result[1] === address) {
|
|
122
|
+
if (score > bestScore) {
|
|
123
|
+
bestScore = score;
|
|
124
|
+
bestAddr = addr;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
addr = result[1];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return bestAddr;
|
|
132
|
+
}
|
|
133
|
+
|
|
92
134
|
export class Debugger {
|
|
93
135
|
constructor() {
|
|
94
136
|
this.patchInstructions = new Map();
|
|
@@ -382,42 +424,7 @@ export class Debugger {
|
|
|
382
424
|
}
|
|
383
425
|
|
|
384
426
|
prevInstruction(address) {
|
|
385
|
-
|
|
386
|
-
// up to the target by counting all "common" instructions as a point. The highest-scoring run of
|
|
387
|
-
// instructions is picked as the most likely, and the previous from that is used. Common instructions
|
|
388
|
-
// here mean loads, stores, branches, compares, arithmetic and carry-set/clear that don't use "unusual"
|
|
389
|
-
// indexing modes like abs,X, abs,Y and (zp,X).
|
|
390
|
-
// Good test cases:
|
|
391
|
-
// Repton 2 @ 2cbb
|
|
392
|
-
// MOS @ cfc8
|
|
393
|
-
// also, just starting from the back of ROM and going up...
|
|
394
|
-
const commonInstructions =
|
|
395
|
-
/(RTS|B..|JMP|JSR|LD[AXY]|ST[AXY]|TA[XY]|T[XY]A|AD[DC]|SUB|SBC|CLC|SEC|CMP|EOR|ORR|AND|INC|DEC).*/;
|
|
396
|
-
const uncommonInstrucions = /.*,\s*([XY]|X\))$/;
|
|
397
|
-
|
|
398
|
-
address &= 0xffff;
|
|
399
|
-
let bestAddr = address - 1;
|
|
400
|
-
let bestScore = 0;
|
|
401
|
-
for (let startingPoint = address - 20; startingPoint !== address; startingPoint++) {
|
|
402
|
-
let score = 0;
|
|
403
|
-
let addr = startingPoint & 0xffff;
|
|
404
|
-
while (addr < address) {
|
|
405
|
-
const result = this.disassemble(addr);
|
|
406
|
-
if (result[0] === this.cpu.pc) score += 10; // huge boost if this instruction was executed
|
|
407
|
-
if (result[0].match(commonInstructions) && !result[0].match(uncommonInstrucions)) {
|
|
408
|
-
score++;
|
|
409
|
-
}
|
|
410
|
-
if (result[1] === address) {
|
|
411
|
-
if (score > bestScore) {
|
|
412
|
-
bestScore = score;
|
|
413
|
-
bestAddr = addr;
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
addr = result[1];
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return bestAddr;
|
|
427
|
+
return prevInstruction((addr) => this.disassemble(addr), this.cpu.pc, address);
|
|
421
428
|
}
|
|
422
429
|
|
|
423
430
|
updatePrevMem() {
|
|
@@ -28,10 +28,13 @@ export class GoogleDrivePicker {
|
|
|
28
28
|
this.authEl.style.display = "none";
|
|
29
29
|
e.preventDefault();
|
|
30
30
|
const authed = await this.auth(false);
|
|
31
|
-
if (authed) this.authResolve();
|
|
32
|
-
else this.authReject(new Error("Unable to authorize Google Drive"));
|
|
31
|
+
if (authed) this.authResolve?.(true);
|
|
32
|
+
else this.authReject?.(new Error("Unable to authorize Google Drive"));
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
// Closing the loading dialog while the auth panel waits is a cancellation, not an error.
|
|
36
|
+
document.getElementById("loading-dialog").addEventListener("hidden.bs.modal", () => this.authResolve?.(false));
|
|
37
|
+
|
|
35
38
|
// Loading the Google client holds the main thread for ~100ms, so it waits for
|
|
36
39
|
// someone to ask for Drive.
|
|
37
40
|
document.getElementById("open-drive-link").addEventListener("click", async (e) => {
|
|
@@ -74,11 +77,21 @@ export class GoogleDrivePicker {
|
|
|
74
77
|
console.log("Google Drive authed=", authed);
|
|
75
78
|
|
|
76
79
|
if (!authed) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
let granted;
|
|
81
|
+
try {
|
|
82
|
+
granted = await new Promise((resolve, reject) => {
|
|
83
|
+
this.authResolve = resolve;
|
|
84
|
+
this.authReject = reject;
|
|
85
|
+
this.authEl.style.display = "";
|
|
86
|
+
});
|
|
87
|
+
} finally {
|
|
88
|
+
this.authResolve = this.authReject = null;
|
|
89
|
+
}
|
|
90
|
+
if (!granted) {
|
|
91
|
+
console.log("Google Drive authorization dismissed");
|
|
92
|
+
this.modals.loadingFinished();
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
const ssd = await this.googleDrive.load(this.processor.fdc, cat.id, layout);
|
|
@@ -93,7 +106,8 @@ export class GoogleDrivePicker {
|
|
|
93
106
|
return ssd;
|
|
94
107
|
} catch (error) {
|
|
95
108
|
console.error("Google Drive loading error:", error);
|
|
96
|
-
this.modals.loadingFinished(
|
|
109
|
+
this.modals.loadingFinished();
|
|
110
|
+
throw error;
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -122,8 +136,14 @@ export class GoogleDrivePicker {
|
|
|
122
136
|
utils.noteEvent("google-drive", "click", item.name);
|
|
123
137
|
this.media.setDisc1Image(`gd:${item.id}/${item.name}`);
|
|
124
138
|
this.modal.hide();
|
|
125
|
-
|
|
126
|
-
|
|
139
|
+
try {
|
|
140
|
+
const ssd = await this.load(item, this.drives.layoutForDrive(0));
|
|
141
|
+
if (ssd) this.drives.putDiscIn(0, ssd);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
toast(`Unable to load ${item.name} from Google Drive: ${errorText(error)}`, {
|
|
144
|
+
title: "Google Drive",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
127
147
|
});
|
|
128
148
|
}
|
|
129
149
|
}
|
package/src/web/machine.js
CHANGED
|
@@ -162,15 +162,17 @@ export class Machine {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
if (discImage) {
|
|
165
|
-
startImageLoad(`disc ${discImage}`, async () =>
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
startImageLoad(`disc ${discImage}`, async () => {
|
|
166
|
+
const loadedDisc = await media.loadDiscImage(discImage, drives.layoutForDrive(0));
|
|
167
|
+
if (loadedDisc) drives.putDiscIn(0, loadedDisc);
|
|
168
|
+
});
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
if (secondDiscImage) {
|
|
171
|
-
startImageLoad(`disc ${secondDiscImage}`, async () =>
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
startImageLoad(`disc ${secondDiscImage}`, async () => {
|
|
173
|
+
const loadedDisc = await media.loadDiscImage(secondDiscImage, drives.layoutForDrive(1));
|
|
174
|
+
if (loadedDisc) drives.putDiscIn(1, loadedDisc);
|
|
175
|
+
});
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
if (tape) {
|
package/src/web/media-loader.js
CHANGED
|
@@ -210,8 +210,9 @@ export class MediaLoader extends EventTarget {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
async loadSCSIFile(file) {
|
|
213
|
-
const binaryData = await readFileAsBinaryString(file);
|
|
214
213
|
const { processor } = this;
|
|
214
|
+
if (!processor.filestore) return;
|
|
215
|
+
const binaryData = await readFileAsBinaryString(file);
|
|
215
216
|
processor.filestore.scsi = utils.stringToUint8Array(binaryData);
|
|
216
217
|
|
|
217
218
|
processor.filestore.PC = 0x400;
|
package/src/web/snapshot-ui.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as disc from "../fdc.js";
|
|
2
2
|
import { DiscLayout } from "../disc.js";
|
|
3
3
|
import { downloadBlob } from "../dom-utils.js";
|
|
4
|
-
import { toast } from "./toast.js";
|
|
5
4
|
import {
|
|
6
5
|
createSnapshot,
|
|
7
6
|
restoreSnapshot,
|
|
@@ -58,9 +57,14 @@ async function readSnapshot(arrayBuffer) {
|
|
|
58
57
|
* where they have one, the image bytes where they do not, and CRCs for
|
|
59
58
|
* saying when a source has changed underneath a state.
|
|
60
59
|
*/
|
|
61
|
-
export function snapshotMedia(fdcDrives, params) {
|
|
60
|
+
export function snapshotMedia(fdcDrives, params, defaultBootDisc) {
|
|
62
61
|
const manifest = {};
|
|
62
|
+
const drive0Disc = fdcDrives[0].disc;
|
|
63
63
|
if (params.disc1 || params.disc) manifest.disc1 = params.disc1 || params.disc;
|
|
64
|
+
// A default boot loads the built-in disc without naming it in the URL; record it by
|
|
65
|
+
// name so the state can reload it, but only while that disc is still in the drive.
|
|
66
|
+
else if (defaultBootDisc && drive0Disc && drive0Disc.name === defaultBootDisc && !drive0Disc.originalImageData)
|
|
67
|
+
manifest.disc1 = defaultBootDisc;
|
|
64
68
|
if (params.disc2) manifest.disc2 = params.disc2;
|
|
65
69
|
|
|
66
70
|
// For each drive with a disc loaded, include CRC32 for verification
|
|
@@ -84,7 +88,7 @@ export function snapshotMedia(fdcDrives, params) {
|
|
|
84
88
|
|
|
85
89
|
/** Saving and restoring states: the menu item, the file input and the reload across a model change. */
|
|
86
90
|
export class SnapshotUI {
|
|
87
|
-
constructor({ processor, model, video, media, drives, urlState, modals, loop }) {
|
|
91
|
+
constructor({ processor, model, video, media, drives, urlState, modals, loop, defaultBootDisc }) {
|
|
88
92
|
this.processor = processor;
|
|
89
93
|
this.model = model;
|
|
90
94
|
this.video = video;
|
|
@@ -93,6 +97,7 @@ export class SnapshotUI {
|
|
|
93
97
|
this.urlState = urlState;
|
|
94
98
|
this.modals = modals;
|
|
95
99
|
this.loop = loop;
|
|
100
|
+
this.defaultBootDisc = defaultBootDisc;
|
|
96
101
|
|
|
97
102
|
document.getElementById("save-state").addEventListener("click", async (event) => {
|
|
98
103
|
event.preventDefault();
|
|
@@ -110,22 +115,27 @@ export class SnapshotUI {
|
|
|
110
115
|
async saveState() {
|
|
111
116
|
const wasRunning = this.loop.isRunning();
|
|
112
117
|
if (wasRunning) this.loop.stop(false);
|
|
118
|
+
let failure = null;
|
|
113
119
|
try {
|
|
114
|
-
const manifest = snapshotMedia(this.processor.fdc.drives, this.urlState.params);
|
|
120
|
+
const manifest = snapshotMedia(this.processor.fdc.drives, this.urlState.params, this.defaultBootDisc);
|
|
115
121
|
const snapshot = createSnapshot(this.processor, this.model, manifest);
|
|
116
122
|
const json = snapshotToJSON(snapshot);
|
|
117
123
|
const blob = await compressBlob(new Blob([json]));
|
|
118
124
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
119
125
|
downloadBlob(blob, `jsbeeb-${this.model.name}-${timestamp}.json.gz`);
|
|
120
126
|
} catch (e) {
|
|
121
|
-
|
|
127
|
+
failure = e;
|
|
122
128
|
}
|
|
129
|
+
// Resume before reporting failure: the error dialog pauses a running
|
|
130
|
+
// loop itself, and resumes it on close.
|
|
123
131
|
if (wasRunning) this.loop.go();
|
|
132
|
+
if (failure) this.modals.showError("saving state", failure);
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
async loadStateFromFile(file, preReadBuffer) {
|
|
127
136
|
const wasRunning = this.loop.isRunning();
|
|
128
137
|
if (wasRunning) this.loop.stop(false);
|
|
138
|
+
let failure = null;
|
|
129
139
|
try {
|
|
130
140
|
const arrayBuffer = preReadBuffer || (await file.arrayBuffer());
|
|
131
141
|
const snapshot = await readSnapshot(arrayBuffer);
|
|
@@ -142,9 +152,10 @@ export class SnapshotUI {
|
|
|
142
152
|
// Force a repaint so the display updates even while paused
|
|
143
153
|
this.video.paint();
|
|
144
154
|
} catch (e) {
|
|
145
|
-
|
|
155
|
+
failure = e;
|
|
146
156
|
}
|
|
147
157
|
if (wasRunning) this.loop.go();
|
|
158
|
+
if (failure) this.modals.showError("loading state", failure);
|
|
148
159
|
}
|
|
149
160
|
|
|
150
161
|
/** Picks up the state a cross-model reload stashed, once the matching machine is up. */
|
|
@@ -192,16 +203,38 @@ export class SnapshotUI {
|
|
|
192
203
|
// Retain the image bytes so subsequent saves can re-embed them.
|
|
193
204
|
loadedDisc.setOriginalImage(imageData);
|
|
194
205
|
}
|
|
195
|
-
if (!loadedDisc)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
if (!loadedDisc) {
|
|
207
|
+
if (savedMedia[crcKey] != null) {
|
|
208
|
+
// A state may name no source (older default-boot saves); the disc
|
|
209
|
+
// already in the drive can still satisfy the CRC, but only laid out
|
|
210
|
+
// the way the state's dirty tracks expect.
|
|
211
|
+
const currentDisc = this.processor.fdc.drives[driveIndex].disc;
|
|
212
|
+
const currentLayout = currentDisc?.is40Track ? DiscLayout.expanded40 : DiscLayout.contiguous;
|
|
213
|
+
if (
|
|
214
|
+
currentDisc &&
|
|
215
|
+
currentDisc.originalImageCrc32 === savedMedia[crcKey] &&
|
|
216
|
+
currentLayout === layout
|
|
217
|
+
)
|
|
218
|
+
continue;
|
|
219
|
+
const problem = savedMedia[discKey]
|
|
220
|
+
? `The disc for drive ${driveIndex} (${savedMedia[discKey]}) could not be reloaded`
|
|
221
|
+
: `This state does not record where the disc in drive ${driveIndex} came from`;
|
|
222
|
+
throw new Error(
|
|
223
|
+
`${problem}, and the drive does not hold a matching disc. ` +
|
|
224
|
+
`Load the right disc, then load the state again.`,
|
|
203
225
|
);
|
|
204
226
|
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
savedMedia[crcKey] != null &&
|
|
232
|
+
loadedDisc.originalImageCrc32 != null &&
|
|
233
|
+
loadedDisc.originalImageCrc32 !== savedMedia[crcKey]
|
|
234
|
+
) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`${loadedDisc.name} has changed since this state was saved, so the state cannot be restored over it.`,
|
|
237
|
+
);
|
|
205
238
|
}
|
|
206
239
|
|
|
207
240
|
this.drives.putDiscIn(driveIndex, loadedDisc);
|