Refactor disk parsing into webworker (#83)
* Refactor disk handling to allow disk processing to happen in a worker * Type cleanup * Convert format handlers to TypeScript * Convert CFFA to TypeScript
This commit is contained in:
parent
3abd168627
commit
ce3631f3a2
|
@ -110,6 +110,14 @@
|
|||
"env": {
|
||||
"commonjs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"workers/*"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "workers/tsconfig.json"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["coverage/**/*"]
|
||||
|
|
418
js/cards/cffa.js
418
js/cards/cffa.js
|
@ -1,418 +0,0 @@
|
|||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { debug, toHex } from '../util';
|
||||
import { rom } from '../roms/cards/cffa';
|
||||
import _2MG from '../formats/2mg';
|
||||
import { ProDOSVolume } from '../formats/prodos';
|
||||
import BlockVolume from '../formats/block';
|
||||
import { dump } from '../formats/prodos/utils';
|
||||
|
||||
export default function CFFA() {
|
||||
var COMMANDS = {
|
||||
ATACRead: 0x20,
|
||||
ATACWrite: 0x30,
|
||||
ATAIdentify: 0xEC
|
||||
};
|
||||
|
||||
// CFFA Card Settings
|
||||
|
||||
var SETTINGS = {
|
||||
Max32MBPartitionsDev0: 0x800,
|
||||
Max32MBPartitionsDev1: 0x801,
|
||||
DefaultBootDevice: 0x802,
|
||||
DefaultBootPartition: 0x803,
|
||||
Reserved: 0x804, // 4 bytes
|
||||
WriteProtectBits: 0x808,
|
||||
MenuSnagMask: 0x809,
|
||||
MenuSnagKey: 0x80A,
|
||||
BootTimeDelayTenths: 0x80B,
|
||||
BusResetSeconds: 0x80C,
|
||||
CheckDeviceTenths: 0x80D,
|
||||
ConfigOptionBits: 0x80E,
|
||||
BlockOffsetDev0: 0x80F, // 3 bytes
|
||||
BlockOffsetDev1: 0x812, // 3 bytes
|
||||
Unused: 0x815
|
||||
};
|
||||
|
||||
// CFFA ATA Register Locations
|
||||
|
||||
var LOC = {
|
||||
ATADataHigh: 0x80,
|
||||
SetCSMask: 0x81,
|
||||
ClearCSMask: 0x82,
|
||||
WriteEEPROM: 0x83,
|
||||
NoWriteEEPROM: 0x84,
|
||||
ATADevCtrl: 0x86,
|
||||
ATAAltStatus: 0x86,
|
||||
ATADataLow: 0x88,
|
||||
AError: 0x89,
|
||||
ASectorCnt: 0x8a,
|
||||
ASector: 0x8b,
|
||||
ATACylinder: 0x8c,
|
||||
ATACylinderH: 0x8d,
|
||||
ATAHead: 0x8e,
|
||||
ATACommand: 0x8f,
|
||||
ATAStatus: 0x8f
|
||||
};
|
||||
|
||||
// ATA Status Bits
|
||||
|
||||
var STATUS = {
|
||||
BSY: 0x80, // Busy
|
||||
DRDY: 0x40, // Drive ready. 1 when ready
|
||||
DWF: 0x20, // Drive write fault. 1 when fault
|
||||
DSC: 0x10, // Disk seek complete. 1 when not seeking
|
||||
DRQ: 0x08, // Data request. 1 when ready to write
|
||||
CORR: 0x04, // Correct data. 1 on correctable error
|
||||
IDX: 0x02, // 1 once per revolution
|
||||
ERR: 0x01 // Error. 1 on error
|
||||
};
|
||||
|
||||
// ATA Identity Block Locations
|
||||
|
||||
var IDENTITY = {
|
||||
SectorCountLow: 58,
|
||||
SectorCountHigh: 57
|
||||
};
|
||||
|
||||
// CFFA internal Flags
|
||||
|
||||
var _disableSignalling = false;
|
||||
var _writeEEPROM = true;
|
||||
|
||||
var _lba = true;
|
||||
|
||||
// LBA/CHS registers
|
||||
|
||||
var _sectorCnt = 1;
|
||||
var _sector = 0;
|
||||
var _cylinder = 0;
|
||||
var _cylinderH = 0;
|
||||
var _head = 0;
|
||||
var _drive = 0;
|
||||
|
||||
// CFFA Data High register
|
||||
|
||||
var _dataHigh = 0;
|
||||
|
||||
// Current Sector
|
||||
|
||||
var _curSector = [];
|
||||
var _curWord = 0;
|
||||
|
||||
// ATA Status registers
|
||||
|
||||
var _interruptsEnabled = false;
|
||||
var _altStatus = 0;
|
||||
var _error = 0;
|
||||
|
||||
var _identity = [[], []];
|
||||
|
||||
// Disk data
|
||||
|
||||
var _partitions = [
|
||||
// Drive 1
|
||||
[],
|
||||
// Drive 2
|
||||
[]
|
||||
];
|
||||
|
||||
var _sectors = [
|
||||
// Drive 1
|
||||
[],
|
||||
// Drive 2
|
||||
[]
|
||||
];
|
||||
|
||||
function _init() {
|
||||
debug('CFFA');
|
||||
|
||||
for (var idx = 0; idx < 0x100; idx++) {
|
||||
_identity[0][idx] = 0;
|
||||
_identity[1][idx] = 0;
|
||||
}
|
||||
|
||||
rom[SETTINGS.Max32MBPartitionsDev0] = 0x1;
|
||||
rom[SETTINGS.Max32MBPartitionsDev1] = 0x0;
|
||||
rom[SETTINGS.BootTimeDelayTenths] = 0x5; // 0.5 seconds
|
||||
rom[SETTINGS.CheckDeviceTenths] = 0x5; // 0.5 seconds
|
||||
}
|
||||
|
||||
// Verbose debug method
|
||||
|
||||
function _debug() {
|
||||
// debug.apply(this, arguments);
|
||||
}
|
||||
|
||||
function _reset() {
|
||||
_debug('reset');
|
||||
|
||||
_sectorCnt = 1;
|
||||
_sector = 0;
|
||||
_cylinder = 0;
|
||||
_cylinderH = 0;
|
||||
_head = 0;
|
||||
_drive = 0;
|
||||
|
||||
_dataHigh = 0;
|
||||
}
|
||||
|
||||
// Convert status register into readable string
|
||||
|
||||
function _statusString(status) {
|
||||
var statusArray = [];
|
||||
for (var flag in STATUS) {
|
||||
if(status & STATUS[flag]) {
|
||||
statusArray.push(flag);
|
||||
}
|
||||
}
|
||||
return statusArray.join('|');
|
||||
}
|
||||
|
||||
// Dump sector as hex and ascii
|
||||
|
||||
function _dumpSector(sector) {
|
||||
if (sector >= _sectors[_drive].length) {
|
||||
_debug('dump sector out of range', sector);
|
||||
return;
|
||||
}
|
||||
for (var idx = 0; idx < 16; idx++) {
|
||||
var row = [];
|
||||
var charRow = [];
|
||||
for (var jdx = 0; jdx < 16; jdx++) {
|
||||
var val = _sectors[_drive][sector][idx * 16 + jdx];
|
||||
row.push(toHex(val, 4));
|
||||
var low = val & 0x7f;
|
||||
var hi = val >> 8 & 0x7f;
|
||||
charRow.push(low > 0x1f ? String.fromCharCode(low) : '.');
|
||||
charRow.push(hi > 0x1f ? String.fromCharCode(hi) : '.');
|
||||
}
|
||||
_debug(row.join(' '), ' ', charRow.join(''));
|
||||
}
|
||||
}
|
||||
|
||||
// Card I/O access
|
||||
|
||||
function _access(off, val) {
|
||||
var readMode = val === undefined;
|
||||
var retVal = readMode ? 0 : undefined;
|
||||
var sector;
|
||||
|
||||
if (readMode) {
|
||||
switch (off & 0x8f) {
|
||||
case LOC.ATADataHigh: // 0x00
|
||||
retVal = _dataHigh;
|
||||
break;
|
||||
case LOC.SetCSMask: // 0x01
|
||||
_disableSignalling = true;
|
||||
break;
|
||||
case LOC.ClearCSMask: // 0x02
|
||||
_disableSignalling = false;
|
||||
break;
|
||||
case LOC.WriteEEPROM: // 0x03
|
||||
_writeEEPROM = true;
|
||||
break;
|
||||
case LOC.NoWriteEEPROM: // 0x04
|
||||
_writeEEPROM = false;
|
||||
break;
|
||||
case LOC.ATAAltStatus: // 0x06
|
||||
retVal = _altStatus;
|
||||
break;
|
||||
case LOC.ATADataLow: // 0x08
|
||||
_dataHigh = _curSector[_curWord] >> 8;
|
||||
retVal = _curSector[_curWord] & 0xff;
|
||||
if (!_disableSignalling) {
|
||||
_curWord++;
|
||||
}
|
||||
break;
|
||||
case LOC.AError: // 0x09
|
||||
retVal = _error;
|
||||
break;
|
||||
case LOC.ASectorCnt: // 0x0A
|
||||
retVal = _sectorCnt;
|
||||
break;
|
||||
case LOC.ASector: // 0x0B
|
||||
retVal = _sector;
|
||||
break;
|
||||
case LOC.ATACylinder: // 0x0C
|
||||
retVal = _cylinder;
|
||||
break;
|
||||
case LOC.ATACylinderH: // 0x0D
|
||||
retVal = _cylinderH;
|
||||
break;
|
||||
case LOC.ATAHead: // 0x0E
|
||||
retVal = _head | (_lba ? 0x40 : 0) | (_drive ? 0x10 : 0) | 0xA0;
|
||||
break;
|
||||
case LOC.ATAStatus: // 0x0F
|
||||
retVal = _sectors[_drive].length > 0 ? STATUS.DRDY | STATUS.DSC : 0;
|
||||
_debug('returning status', _statusString(retVal));
|
||||
break;
|
||||
default:
|
||||
debug('read unknown soft switch', toHex(off));
|
||||
}
|
||||
|
||||
if (off & 0x7) { // Anything but data high/low
|
||||
_debug('read soft switch', toHex(off), toHex(retVal));
|
||||
}
|
||||
} else {
|
||||
if (off & 0x7) { // Anything but data high/low
|
||||
_debug('write soft switch', toHex(off), toHex(val));
|
||||
}
|
||||
|
||||
switch (off & 0x8f) {
|
||||
case LOC.ATADataHigh: // 0x00
|
||||
_dataHigh = val;
|
||||
break;
|
||||
case LOC.SetCSMask: // 0x01
|
||||
_disableSignalling = true;
|
||||
break;
|
||||
case LOC.ClearCSMask: // 0x02
|
||||
_disableSignalling = false;
|
||||
break;
|
||||
case LOC.WriteEEPROM: // 0x03
|
||||
_writeEEPROM = true;
|
||||
break;
|
||||
case LOC.NoWriteEEPROM: // 0x04
|
||||
_writeEEPROM = false;
|
||||
break;
|
||||
case LOC.ATADevCtrl: // 0x06
|
||||
_debug('devCtrl:', toHex(val));
|
||||
_interruptsEnabled = (val & 0x04) ? true : false;
|
||||
_debug('Interrupts', _interruptsEnabled ? 'enabled' : 'disabled');
|
||||
if (val & 0x02) {
|
||||
_reset();
|
||||
}
|
||||
break;
|
||||
case LOC.ATADataLow: // 0x08
|
||||
_curSector[_curWord] = _dataHigh << 8 | val;
|
||||
_curWord++;
|
||||
break;
|
||||
case LOC.ASectorCnt: // 0x0a
|
||||
_debug('setting sector count', val);
|
||||
_sectorCnt = val;
|
||||
break;
|
||||
case LOC.ASector: // 0x0b
|
||||
_debug('setting sector', toHex(val));
|
||||
_sector = val;
|
||||
break;
|
||||
case LOC.ATACylinder: // 0x0c
|
||||
_debug('setting cylinder', toHex(val));
|
||||
_cylinder = val;
|
||||
break;
|
||||
case LOC.ATACylinderH: // 0x0d
|
||||
_debug('setting cylinder high', toHex(val));
|
||||
_cylinderH = val;
|
||||
break;
|
||||
case LOC.ATAHead:
|
||||
_head = val & 0xf;
|
||||
_lba = val & 0x40 ? true : false;
|
||||
_drive = val & 0x10 ? 1 : 0;
|
||||
_debug('setting head', toHex(val & 0xf), 'drive', _drive);
|
||||
if (!_lba) {
|
||||
console.error('CHS mode not supported');
|
||||
}
|
||||
break;
|
||||
case LOC.ATACommand: // 0x0f
|
||||
_debug('command:', toHex(val));
|
||||
sector = _head << 24 | _cylinderH << 16 | _cylinder << 8 | _sector;
|
||||
_dumpSector(sector);
|
||||
|
||||
switch (val) {
|
||||
case COMMANDS.ATAIdentify:
|
||||
_debug('ATA identify');
|
||||
_curSector = _identity[_drive];
|
||||
_curWord = 0;
|
||||
break;
|
||||
case COMMANDS.ATACRead:
|
||||
_debug('ATA read sector', toHex(_cylinderH), toHex(_cylinder), toHex(_sector), sector);
|
||||
_curSector = _sectors[_drive][sector];
|
||||
_curWord = 0;
|
||||
break;
|
||||
case COMMANDS.ATACWrite:
|
||||
_debug('ATA write sector', toHex(_cylinderH), toHex(_cylinder), toHex(_sector), sector);
|
||||
_curSector = _sectors[_drive][sector];
|
||||
_curWord = 0;
|
||||
break;
|
||||
default:
|
||||
debug('unknown command', toHex(val));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
debug('write unknown soft switch', toHex(off), toHex(val));
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
_init();
|
||||
|
||||
return {
|
||||
ioSwitch: function (off, val) {
|
||||
return _access(off, val);
|
||||
},
|
||||
|
||||
read: function(page, off) {
|
||||
return rom[(page - 0xc0) << 8 | off];
|
||||
},
|
||||
|
||||
write: function(page, off, val) {
|
||||
if (_writeEEPROM) {
|
||||
_debug('writing', toHex(page << 8 | off), toHex(val));
|
||||
rom[(page - 0xc0) << 8 | off] - val;
|
||||
}
|
||||
},
|
||||
|
||||
getState() {
|
||||
// TODO CFFA State
|
||||
return {};
|
||||
},
|
||||
|
||||
setState(_) {},
|
||||
|
||||
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
|
||||
|
||||
setBinary: function(drive, name, ext, rawData) {
|
||||
drive = drive - 1;
|
||||
var disk;
|
||||
var options = {
|
||||
rawData,
|
||||
name
|
||||
};
|
||||
|
||||
if (ext === '2mg') {
|
||||
disk = new _2MG(options);
|
||||
} else {
|
||||
disk = new BlockVolume(options);
|
||||
}
|
||||
|
||||
// Convert 512 byte blocks into 256 word sectors
|
||||
_sectors[drive] = disk.blocks.map(function(block) {
|
||||
return new Uint16Array(block.buffer);
|
||||
});
|
||||
|
||||
_identity[drive][IDENTITY.SectorCountHigh] = _sectors[0].length & 0xffff;
|
||||
_identity[drive][IDENTITY.SectorCountLow] = _sectors[0].length >> 16;
|
||||
|
||||
var prodos = new ProDOSVolume(disk);
|
||||
dump(prodos);
|
||||
|
||||
_partitions[drive] = prodos;
|
||||
|
||||
if (drive) {
|
||||
rom[SETTINGS.Max32MBPartitionsDev1] = 0x1;
|
||||
} else {
|
||||
rom[SETTINGS.Max32MBPartitionsDev0] = 0x1;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,482 @@
|
|||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import type { byte, Card, Restorable } from '../types';
|
||||
import { debug, toHex } from '../util';
|
||||
import { rom as readOnlyRom } from '../roms/cards/cffa';
|
||||
import { read2MGHeader } from '../formats/2mg';
|
||||
import { ProDOSVolume } from '../formats/prodos';
|
||||
import createBlockDisk from '../formats/block';
|
||||
import { dump } from '../formats/prodos/utils';
|
||||
import {
|
||||
BlockDisk,
|
||||
BlockFormat,
|
||||
ENCODING_BLOCK,
|
||||
MassStorage,
|
||||
} from 'js/formats/types';
|
||||
|
||||
const rom = new Uint8Array(readOnlyRom);
|
||||
|
||||
const COMMANDS = {
|
||||
ATACRead: 0x20,
|
||||
ATACWrite: 0x30,
|
||||
ATAIdentify: 0xEC
|
||||
};
|
||||
|
||||
// CFFA Card Settings
|
||||
|
||||
const SETTINGS = {
|
||||
Max32MBPartitionsDev0: 0x800,
|
||||
Max32MBPartitionsDev1: 0x801,
|
||||
DefaultBootDevice: 0x802,
|
||||
DefaultBootPartition: 0x803,
|
||||
Reserved: 0x804, // 4 bytes
|
||||
WriteProtectBits: 0x808,
|
||||
MenuSnagMask: 0x809,
|
||||
MenuSnagKey: 0x80A,
|
||||
BootTimeDelayTenths: 0x80B,
|
||||
BusResetSeconds: 0x80C,
|
||||
CheckDeviceTenths: 0x80D,
|
||||
ConfigOptionBits: 0x80E,
|
||||
BlockOffsetDev0: 0x80F, // 3 bytes
|
||||
BlockOffsetDev1: 0x812, // 3 bytes
|
||||
Unused: 0x815
|
||||
};
|
||||
|
||||
// CFFA ATA Register Locations
|
||||
|
||||
const LOC = {
|
||||
ATADataHigh: 0x80,
|
||||
SetCSMask: 0x81,
|
||||
ClearCSMask: 0x82,
|
||||
WriteEEPROM: 0x83,
|
||||
NoWriteEEPROM: 0x84,
|
||||
ATADevCtrl: 0x86,
|
||||
ATAAltStatus: 0x86,
|
||||
ATADataLow: 0x88,
|
||||
AError: 0x89,
|
||||
ASectorCnt: 0x8a,
|
||||
ASector: 0x8b,
|
||||
ATACylinder: 0x8c,
|
||||
ATACylinderH: 0x8d,
|
||||
ATAHead: 0x8e,
|
||||
ATACommand: 0x8f,
|
||||
ATAStatus: 0x8f
|
||||
};
|
||||
|
||||
// ATA Status Bits
|
||||
|
||||
const STATUS = {
|
||||
BSY: 0x80, // Busy
|
||||
DRDY: 0x40, // Drive ready. 1 when ready
|
||||
DWF: 0x20, // Drive write fault. 1 when fault
|
||||
DSC: 0x10, // Disk seek complete. 1 when not seeking
|
||||
DRQ: 0x08, // Data request. 1 when ready to write
|
||||
CORR: 0x04, // Correct data. 1 on correctable error
|
||||
IDX: 0x02, // 1 once per revolution
|
||||
ERR: 0x01 // Error. 1 on error
|
||||
};
|
||||
|
||||
// ATA Identity Block Locations
|
||||
|
||||
const IDENTITY = {
|
||||
SectorCountLow: 58,
|
||||
SectorCountHigh: 57
|
||||
};
|
||||
|
||||
export interface CFFAState {
|
||||
disks: Array<BlockDisk | null>
|
||||
}
|
||||
|
||||
type Partition = ReturnType<typeof ProDOSVolume>
|
||||
export default class CFFA implements Card, MassStorage, Restorable<CFFAState> {
|
||||
|
||||
// CFFA internal Flags
|
||||
|
||||
private _disableSignalling = false;
|
||||
private _writeEEPROM = true;
|
||||
|
||||
private _lba = true;
|
||||
|
||||
// LBA/CHS registers
|
||||
|
||||
private _sectorCnt = 1;
|
||||
private _sector = 0;
|
||||
private _cylinder = 0;
|
||||
private _cylinderH = 0;
|
||||
private _head = 0;
|
||||
private _drive = 0;
|
||||
|
||||
// CFFA Data High register
|
||||
|
||||
private _dataHigh = 0;
|
||||
|
||||
// Current Sector
|
||||
|
||||
private _curSector: Uint16Array | number[];
|
||||
private _curWord = 0;
|
||||
|
||||
// ATA Status registers
|
||||
|
||||
private _interruptsEnabled = false;
|
||||
private _altStatus = 0;
|
||||
private _error = 0;
|
||||
|
||||
private _identity: number[][] = [[], []];
|
||||
|
||||
// Disk data
|
||||
|
||||
private _partitions: Array<Partition|null> = [
|
||||
// Drive 1
|
||||
null,
|
||||
// Drive 2
|
||||
null
|
||||
];
|
||||
|
||||
private _sectors: Uint16Array[][] = [
|
||||
// Drive 1
|
||||
[],
|
||||
// Drive 2
|
||||
[]
|
||||
];
|
||||
|
||||
constructor() {
|
||||
debug('CFFA');
|
||||
|
||||
for (let idx = 0; idx < 0x100; idx++) {
|
||||
this._identity[0][idx] = 0;
|
||||
this._identity[1][idx] = 0;
|
||||
}
|
||||
|
||||
rom[SETTINGS.Max32MBPartitionsDev0] = 0x1;
|
||||
rom[SETTINGS.Max32MBPartitionsDev1] = 0x0;
|
||||
rom[SETTINGS.BootTimeDelayTenths] = 0x5; // 0.5 seconds
|
||||
rom[SETTINGS.CheckDeviceTenths] = 0x5; // 0.5 seconds
|
||||
}
|
||||
|
||||
// Verbose debug method
|
||||
|
||||
private _debug(..._args: any[]) {
|
||||
// debug.apply(this, arguments);
|
||||
}
|
||||
|
||||
private _reset() {
|
||||
this._debug('reset');
|
||||
|
||||
this._sectorCnt = 1;
|
||||
this._sector = 0;
|
||||
this._cylinder = 0;
|
||||
this._cylinderH = 0;
|
||||
this._head = 0;
|
||||
this._drive = 0;
|
||||
|
||||
this._dataHigh = 0;
|
||||
}
|
||||
|
||||
// Convert status register into readable string
|
||||
|
||||
private _statusString(status: byte) {
|
||||
const statusArray = [];
|
||||
let flag: keyof typeof STATUS;
|
||||
for (flag in STATUS) {
|
||||
if(status & STATUS[flag]) {
|
||||
statusArray.push(flag);
|
||||
}
|
||||
}
|
||||
return statusArray.join('|');
|
||||
}
|
||||
|
||||
// Dump sector as hex and ascii
|
||||
|
||||
private _dumpSector(sector: number) {
|
||||
if (sector >= this._sectors[this._drive].length) {
|
||||
this._debug('dump sector out of range', sector);
|
||||
return;
|
||||
}
|
||||
for (let idx = 0; idx < 16; idx++) {
|
||||
const row = [];
|
||||
const charRow = [];
|
||||
for (let jdx = 0; jdx < 16; jdx++) {
|
||||
const val = this._sectors[this._drive][sector][idx * 16 + jdx];
|
||||
row.push(toHex(val, 4));
|
||||
const low = val & 0x7f;
|
||||
const hi = val >> 8 & 0x7f;
|
||||
charRow.push(low > 0x1f ? String.fromCharCode(low) : '.');
|
||||
charRow.push(hi > 0x1f ? String.fromCharCode(hi) : '.');
|
||||
}
|
||||
this._debug(row.join(' '), ' ', charRow.join(''));
|
||||
}
|
||||
}
|
||||
|
||||
// Card I/O access
|
||||
|
||||
private _access(off: byte, val: byte) {
|
||||
const readMode = val === undefined;
|
||||
let retVal;
|
||||
let sector;
|
||||
|
||||
if (readMode) {
|
||||
retVal = 0;
|
||||
switch (off & 0x8f) {
|
||||
case LOC.ATADataHigh: // 0x00
|
||||
retVal = this._dataHigh;
|
||||
break;
|
||||
case LOC.SetCSMask: // 0x01
|
||||
this._disableSignalling = true;
|
||||
break;
|
||||
case LOC.ClearCSMask: // 0x02
|
||||
this._disableSignalling = false;
|
||||
break;
|
||||
case LOC.WriteEEPROM: // 0x03
|
||||
this._writeEEPROM = true;
|
||||
break;
|
||||
case LOC.NoWriteEEPROM: // 0x04
|
||||
this._writeEEPROM = false;
|
||||
break;
|
||||
case LOC.ATAAltStatus: // 0x06
|
||||
retVal = this._altStatus;
|
||||
break;
|
||||
case LOC.ATADataLow: // 0x08
|
||||
this._dataHigh = this._curSector[this._curWord] >> 8;
|
||||
retVal = this._curSector[this._curWord] & 0xff;
|
||||
if (!this._disableSignalling) {
|
||||
this._curWord++;
|
||||
}
|
||||
break;
|
||||
case LOC.AError: // 0x09
|
||||
retVal = this._error;
|
||||
break;
|
||||
case LOC.ASectorCnt: // 0x0A
|
||||
retVal = this._sectorCnt;
|
||||
break;
|
||||
case LOC.ASector: // 0x0B
|
||||
retVal = this._sector;
|
||||
break;
|
||||
case LOC.ATACylinder: // 0x0C
|
||||
retVal = this._cylinder;
|
||||
break;
|
||||
case LOC.ATACylinderH: // 0x0D
|
||||
retVal = this._cylinderH;
|
||||
break;
|
||||
case LOC.ATAHead: // 0x0E
|
||||
retVal = this._head | (this._lba ? 0x40 : 0) | (this._drive ? 0x10 : 0) | 0xA0;
|
||||
break;
|
||||
case LOC.ATAStatus: // 0x0F
|
||||
retVal = this._sectors[this._drive].length > 0 ? STATUS.DRDY | STATUS.DSC : 0;
|
||||
this._debug('returning status', this._statusString(retVal));
|
||||
break;
|
||||
default:
|
||||
debug('read unknown soft switch', toHex(off));
|
||||
}
|
||||
|
||||
if (off & 0x7) { // Anything but data high/low
|
||||
this._debug('read soft switch', toHex(off), toHex(retVal));
|
||||
}
|
||||
} else {
|
||||
if (off & 0x7) { // Anything but data high/low
|
||||
this._debug('write soft switch', toHex(off), toHex(val));
|
||||
}
|
||||
|
||||
switch (off & 0x8f) {
|
||||
case LOC.ATADataHigh: // 0x00
|
||||
this._dataHigh = val;
|
||||
break;
|
||||
case LOC.SetCSMask: // 0x01
|
||||
this._disableSignalling = true;
|
||||
break;
|
||||
case LOC.ClearCSMask: // 0x02
|
||||
this._disableSignalling = false;
|
||||
break;
|
||||
case LOC.WriteEEPROM: // 0x03
|
||||
this._writeEEPROM = true;
|
||||
break;
|
||||
case LOC.NoWriteEEPROM: // 0x04
|
||||
this._writeEEPROM = false;
|
||||
break;
|
||||
case LOC.ATADevCtrl: // 0x06
|
||||
this._debug('devCtrl:', toHex(val));
|
||||
this._interruptsEnabled = (val & 0x04) ? true : false;
|
||||
this._debug('Interrupts', this._interruptsEnabled ? 'enabled' : 'disabled');
|
||||
if (val & 0x02) {
|
||||
this._reset();
|
||||
}
|
||||
break;
|
||||
case LOC.ATADataLow: // 0x08
|
||||
this._curSector[this._curWord] = this._dataHigh << 8 | val;
|
||||
this._curWord++;
|
||||
break;
|
||||
case LOC.ASectorCnt: // 0x0a
|
||||
this._debug('setting sector count', val);
|
||||
this._sectorCnt = val;
|
||||
break;
|
||||
case LOC.ASector: // 0x0b
|
||||
this._debug('setting sector', toHex(val));
|
||||
this._sector = val;
|
||||
break;
|
||||
case LOC.ATACylinder: // 0x0c
|
||||
this._debug('setting cylinder', toHex(val));
|
||||
this._cylinder = val;
|
||||
break;
|
||||
case LOC.ATACylinderH: // 0x0d
|
||||
this._debug('setting cylinder high', toHex(val));
|
||||
this._cylinderH = val;
|
||||
break;
|
||||
case LOC.ATAHead:
|
||||
this._head = val & 0xf;
|
||||
this._lba = val & 0x40 ? true : false;
|
||||
this._drive = val & 0x10 ? 1 : 0;
|
||||
this._debug('setting head', toHex(val & 0xf), 'drive', this._drive);
|
||||
if (!this._lba) {
|
||||
console.error('CHS mode not supported');
|
||||
}
|
||||
break;
|
||||
case LOC.ATACommand: // 0x0f
|
||||
this._debug('command:', toHex(val));
|
||||
sector = this._head << 24 | this._cylinderH << 16 | this._cylinder << 8 | this._sector;
|
||||
this._dumpSector(sector);
|
||||
|
||||
switch (val) {
|
||||
case COMMANDS.ATAIdentify:
|
||||
this._debug('ATA identify');
|
||||
this._curSector = this._identity[this._drive];
|
||||
this._curWord = 0;
|
||||
break;
|
||||
case COMMANDS.ATACRead:
|
||||
this._debug('ATA read sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
|
||||
this._curSector = this._sectors[this._drive][sector];
|
||||
this._curWord = 0;
|
||||
break;
|
||||
case COMMANDS.ATACWrite:
|
||||
this._debug('ATA write sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
|
||||
this._curSector = this._sectors[this._drive][sector];
|
||||
this._curWord = 0;
|
||||
break;
|
||||
default:
|
||||
debug('unknown command', toHex(val));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
debug('write unknown soft switch', toHex(off), toHex(val));
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
ioSwitch(off: byte, val: byte) {
|
||||
return this._access(off, val);
|
||||
}
|
||||
|
||||
read(page: byte, off: byte) {
|
||||
return rom[(page - 0xc0) << 8 | off];
|
||||
}
|
||||
|
||||
write(page: byte, off: byte, val: byte) {
|
||||
if (this._writeEEPROM) {
|
||||
this._debug('writing', toHex(page << 8 | off), toHex(val));
|
||||
rom[(page - 0xc0) << 8 | off] - val;
|
||||
}
|
||||
}
|
||||
|
||||
getState() {
|
||||
return {
|
||||
disks: this._partitions.map(
|
||||
(partition) => {
|
||||
let result: BlockDisk | null = null;
|
||||
if (partition) {
|
||||
const disk: BlockDisk = partition.disk();
|
||||
result = {
|
||||
blocks: disk.blocks.map(
|
||||
(block) => new Uint8Array(block)
|
||||
),
|
||||
encoding: ENCODING_BLOCK,
|
||||
readOnly: disk.readOnly,
|
||||
name: disk.name,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
setState(state: CFFAState) {
|
||||
state.disks.forEach(
|
||||
(disk, idx) => {
|
||||
if (disk) {
|
||||
this.setBlockVolume(idx + 1, disk);
|
||||
} else {
|
||||
this.resetBlockVolume(idx + 1);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resetBlockVolume(drive: number) {
|
||||
drive = drive - 1;
|
||||
|
||||
this._sectors[drive] = [];
|
||||
|
||||
this._identity[drive][IDENTITY.SectorCountHigh] = 0;
|
||||
this._identity[drive][IDENTITY.SectorCountLow] = 0;
|
||||
|
||||
if (drive) {
|
||||
rom[SETTINGS.Max32MBPartitionsDev1] = 0x0;
|
||||
} else {
|
||||
rom[SETTINGS.Max32MBPartitionsDev0] = 0x0;
|
||||
}
|
||||
}
|
||||
|
||||
setBlockVolume(drive: number, disk: BlockDisk) {
|
||||
drive = drive - 1;
|
||||
|
||||
// Convert 512 byte blocks into 256 word sectors
|
||||
this._sectors[drive] = disk.blocks.map(function(block) {
|
||||
return new Uint16Array(block.buffer);
|
||||
});
|
||||
|
||||
this._identity[drive][IDENTITY.SectorCountHigh] = this._sectors[0].length & 0xffff;
|
||||
this._identity[drive][IDENTITY.SectorCountLow] = this._sectors[0].length >> 16;
|
||||
|
||||
const prodos = ProDOSVolume(disk);
|
||||
dump(prodos);
|
||||
|
||||
this._partitions[drive] = prodos;
|
||||
|
||||
if (drive) {
|
||||
rom[SETTINGS.Max32MBPartitionsDev1] = 0x1;
|
||||
} else {
|
||||
rom[SETTINGS.Max32MBPartitionsDev0] = 0x1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
|
||||
|
||||
setBinary(drive: number, name: string, ext: BlockFormat, rawData: ArrayBuffer) {
|
||||
const volume = 254;
|
||||
const readOnly = false;
|
||||
|
||||
if (ext === '2mg') {
|
||||
const { bytes, offset } = read2MGHeader(rawData);
|
||||
rawData = rawData.slice(offset, offset + bytes);
|
||||
}
|
||||
const options = {
|
||||
rawData,
|
||||
name,
|
||||
volume,
|
||||
readOnly
|
||||
};
|
||||
const disk = createBlockDisk(options);
|
||||
|
||||
return this.setBlockVolume(drive, disk);
|
||||
}
|
||||
}
|
|
@ -9,22 +9,42 @@
|
|||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { base64_decode, base64_encode} from '../base64';
|
||||
import { bit, byte, Card, DiskFormat, MemberOf, memory, nibble, rom } from '../types';
|
||||
import { base64_encode} from '../base64';
|
||||
import type {
|
||||
bit,
|
||||
byte,
|
||||
Card,
|
||||
memory,
|
||||
nibble,
|
||||
rom,
|
||||
} from '../types';
|
||||
|
||||
import {
|
||||
FormatWorkerMessage,
|
||||
FormatWorkerResponse,
|
||||
NibbleFormat,
|
||||
DISK_PROCESSED,
|
||||
DRIVE_NUMBERS,
|
||||
DriveNumber,
|
||||
JSONDisk,
|
||||
ENCODING_NIBBLE,
|
||||
PROCESS_BINARY,
|
||||
PROCESS_JSON_DISK,
|
||||
PROCESS_JSON,
|
||||
ENCODING_BITSTREAM,
|
||||
} from '../formats/types';
|
||||
|
||||
import {
|
||||
createDisk,
|
||||
createDiskFromJsonDisk
|
||||
} from '../formats/create_disk';
|
||||
|
||||
import { debug, toHex } from '../util';
|
||||
import { Disk, JSONDisk, jsonDecode, jsonEncode, readSector } from '../formats/format_utils';
|
||||
import { jsonDecode, jsonEncode, readSector } from '../formats/format_utils';
|
||||
|
||||
import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2';
|
||||
|
||||
import _2MG from '../formats/2mg';
|
||||
import D13 from '../formats/d13';
|
||||
import DOS from '../formats/do';
|
||||
import ProDOS from '../formats/po';
|
||||
import Woz from '../formats/woz';
|
||||
import Nibble from '../formats/nib';
|
||||
import Apple2IO from '../apple2io';
|
||||
|
||||
|
||||
/** Softswitch locations */
|
||||
const LOC = {
|
||||
// Disk II Controller Commands
|
||||
|
@ -144,10 +164,6 @@ const PHASE_DELTA = [
|
|||
[-2, -1, 0, 1],
|
||||
[1, -2, -1, 0]
|
||||
] as const;
|
||||
|
||||
export const DRIVE_NUMBERS = [1, 2] as const;
|
||||
export type DriveNumber = MemberOf<typeof DRIVE_NUMBERS>;
|
||||
|
||||
export interface Callbacks {
|
||||
driveLight: (drive: DriveNumber, on: boolean) => void;
|
||||
dirty: (drive: DriveNumber, dirty: boolean) => void;
|
||||
|
@ -155,9 +171,10 @@ export interface Callbacks {
|
|||
}
|
||||
|
||||
/** Common information for Nibble and WOZ disks. */
|
||||
|
||||
interface BaseDrive {
|
||||
/** Current disk format. */
|
||||
format: DiskFormat,
|
||||
format: NibbleFormat,
|
||||
/** Current disk volume number. */
|
||||
volume: byte,
|
||||
/** Displayed disk name */
|
||||
|
@ -176,6 +193,8 @@ interface BaseDrive {
|
|||
|
||||
/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */
|
||||
interface WozDrive extends BaseDrive {
|
||||
/** Woz encoding */
|
||||
encoding: typeof ENCODING_BITSTREAM
|
||||
/** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */
|
||||
trackMap: byte[];
|
||||
/** Unique track bitstreams. The index is arbitrary; it is NOT the track number. */
|
||||
|
@ -184,19 +203,25 @@ interface WozDrive extends BaseDrive {
|
|||
|
||||
/** Nibble format track data. */
|
||||
interface NibbleDrive extends BaseDrive {
|
||||
/** Nibble encoding */
|
||||
encoding: typeof ENCODING_NIBBLE
|
||||
/** Nibble data. The index is the track number. */
|
||||
tracks: memory[];
|
||||
}
|
||||
|
||||
type Drive = WozDrive | NibbleDrive;
|
||||
|
||||
function isNibbleDrive(drive: Drive): drive is NibbleDrive {
|
||||
return 'tracks' in drive;
|
||||
function isNibbleDrive(drive: Drive): drive is NibbleDrive {
|
||||
return drive.encoding === ENCODING_NIBBLE;
|
||||
}
|
||||
|
||||
function isWozDrive(drive: Drive): drive is WozDrive {
|
||||
return drive.encoding === ENCODING_BITSTREAM;
|
||||
}
|
||||
|
||||
// Does not support WOZ disks
|
||||
interface DriveState {
|
||||
format: DiskFormat,
|
||||
format: NibbleFormat,
|
||||
encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE
|
||||
volume: byte,
|
||||
name: string,
|
||||
tracks: memory[],
|
||||
|
@ -205,6 +230,8 @@ interface DriveState {
|
|||
phase: Phase,
|
||||
readOnly: boolean,
|
||||
dirty: boolean,
|
||||
trackMap: number[],
|
||||
rawTracks: bit[][],
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -216,10 +243,10 @@ interface State {
|
|||
drive: DriveNumber;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
function getDriveState(drive: Drive): DriveState {
|
||||
const result: DriveState = {
|
||||
format: drive.format,
|
||||
encoding: drive.encoding,
|
||||
volume: drive.volume,
|
||||
name: drive.name,
|
||||
tracks: [],
|
||||
|
@ -227,34 +254,61 @@ function getDriveState(drive: Drive): DriveState {
|
|||
head: drive.head,
|
||||
phase: drive.phase,
|
||||
readOnly: drive.readOnly,
|
||||
dirty: drive.dirty
|
||||
dirty: drive.dirty,
|
||||
trackMap: [],
|
||||
rawTracks: [],
|
||||
};
|
||||
if (!isNibbleDrive(drive)) {
|
||||
throw Error('No tracks.');
|
||||
|
||||
if (isNibbleDrive(drive)) {
|
||||
for (let idx = 0; idx < drive.tracks.length; idx++) {
|
||||
result.tracks.push(new Uint8Array(drive.tracks[idx]));
|
||||
}
|
||||
}
|
||||
for (let idx = 0; idx < drive.tracks.length; idx++) {
|
||||
result.tracks.push(new Uint8Array(drive.tracks[idx]));
|
||||
if (isWozDrive(drive)) {
|
||||
result.trackMap = [...drive.trackMap];
|
||||
for (let idx = 0; idx < drive.rawTracks.length; idx++) {
|
||||
result.rawTracks.push([...drive.rawTracks[idx]]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
function setDriveState(state: DriveState) {
|
||||
const result: Drive = {
|
||||
format: state.format,
|
||||
volume: state.volume,
|
||||
name: state.name,
|
||||
tracks: [] as memory[],
|
||||
track: state.track,
|
||||
head: state.head,
|
||||
phase: state.phase,
|
||||
readOnly: state.readOnly,
|
||||
dirty: state.dirty
|
||||
};
|
||||
for (let idx = 0; idx < state.tracks.length; idx++) {
|
||||
result.tracks!.push(new Uint8Array(state.tracks[idx]));
|
||||
let result: Drive;
|
||||
if (state.encoding === ENCODING_NIBBLE) {
|
||||
result = {
|
||||
format: state.format,
|
||||
encoding: ENCODING_NIBBLE,
|
||||
volume: state.volume,
|
||||
name: state.name,
|
||||
tracks: [],
|
||||
track: state.track,
|
||||
head: state.head,
|
||||
phase: state.phase,
|
||||
readOnly: state.readOnly,
|
||||
dirty: state.dirty,
|
||||
};
|
||||
for (let idx = 0; idx < state.tracks.length; idx++) {
|
||||
result.tracks.push(new Uint8Array(state.tracks[idx]));
|
||||
}
|
||||
} else {
|
||||
result = {
|
||||
format: state.format,
|
||||
encoding: ENCODING_BITSTREAM,
|
||||
volume: state.volume,
|
||||
name: state.name,
|
||||
track: state.track,
|
||||
head: state.head,
|
||||
phase: state.phase,
|
||||
readOnly: state.readOnly,
|
||||
dirty: state.dirty,
|
||||
trackMap: [...state.trackMap],
|
||||
rawTracks: [],
|
||||
};
|
||||
for (let idx = 0; idx < state.rawTracks.length; idx++) {
|
||||
result.rawTracks.push([...state.rawTracks[idx]]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -266,6 +320,7 @@ export default class DiskII implements Card {
|
|||
private drives: Drive[] = [
|
||||
{ // Drive 1
|
||||
format: 'dsk',
|
||||
encoding: ENCODING_NIBBLE,
|
||||
volume: 254,
|
||||
name: 'Disk 1',
|
||||
tracks: [],
|
||||
|
@ -273,10 +328,11 @@ export default class DiskII implements Card {
|
|||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false
|
||||
dirty: false,
|
||||
},
|
||||
{ // Drive 2
|
||||
format: 'dsk',
|
||||
encoding: ENCODING_NIBBLE,
|
||||
volume: 254,
|
||||
name: 'Disk 2',
|
||||
tracks: [],
|
||||
|
@ -284,7 +340,7 @@ export default class DiskII implements Card {
|
|||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false
|
||||
dirty: false,
|
||||
}];
|
||||
|
||||
private skip = 0;
|
||||
|
@ -307,6 +363,9 @@ export default class DiskII implements Card {
|
|||
/** Current drive object. */
|
||||
private cur = this.drives[this.drive - 1];
|
||||
|
||||
/** Nibbles read this on cycle */
|
||||
private nibbleCount = 0;
|
||||
|
||||
/** Q0-Q3: Coil states. */
|
||||
private q = [false, false, false, false];
|
||||
|
||||
|
@ -328,37 +387,37 @@ export default class DiskII implements Card {
|
|||
/** Contents of the P6 ROM. */
|
||||
private sequencerRom: typeof SEQUENCER_ROM_16 | typeof SEQUENCER_ROM_13;
|
||||
|
||||
private worker: Worker;
|
||||
|
||||
/** Builds a new Disk ][ card. */
|
||||
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) {
|
||||
this.debug('Disk ][');
|
||||
|
||||
this.lastCycles = this.io.cycles();
|
||||
this.bootstrapRom = this.sectors == 16 ? BOOTSTRAP_ROM_16 : BOOTSTRAP_ROM_13;
|
||||
this.sequencerRom = this.sectors == 16 ? SEQUENCER_ROM_16 : SEQUENCER_ROM_13;
|
||||
|
||||
this.init();
|
||||
this.initWorker();
|
||||
}
|
||||
|
||||
private debug(..._args: any[]) {
|
||||
// debug.apply(this, arguments);
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.debug('Disk ][');
|
||||
}
|
||||
|
||||
// Only used for WOZ disks
|
||||
private moveHead() {
|
||||
if (isNibbleDrive(this.cur)) {
|
||||
return;
|
||||
}
|
||||
const track: bit[] =
|
||||
this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0];
|
||||
|
||||
const cycles = this.io.cycles();
|
||||
|
||||
// Spin the disk the number of elapsed cycles since last call
|
||||
let workCycles = (cycles - this.lastCycles) * 2;
|
||||
this.lastCycles = cycles;
|
||||
|
||||
if (!isWozDrive(this.cur)) {
|
||||
return;
|
||||
}
|
||||
const track: bit[] =
|
||||
this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0];
|
||||
|
||||
while (workCycles-- > 0) {
|
||||
let pulse: bit = 0;
|
||||
if (this.clock == 4) {
|
||||
|
@ -531,6 +590,7 @@ export default class DiskII implements Card {
|
|||
this.debug('Drive Off');
|
||||
this.on = false;
|
||||
this.callbacks.driveLight(this.drive, false);
|
||||
this.debug('nibbles read', this.nibbleCount);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
@ -543,6 +603,7 @@ export default class DiskII implements Card {
|
|||
}
|
||||
if (!this.on) {
|
||||
this.debug('Drive On');
|
||||
this.nibbleCount = 0;
|
||||
this.on = true;
|
||||
this.lastCycles = this.io.cycles();
|
||||
this.callbacks.driveLight(this.drive, true);
|
||||
|
@ -619,6 +680,9 @@ export default class DiskII implements Card {
|
|||
// also cause conflicts with the disk controller commands.
|
||||
if ((off & 0x01) === 0) {
|
||||
result = this.latch;
|
||||
if (result & 0x80) {
|
||||
this.nibbleCount++;
|
||||
}
|
||||
} else {
|
||||
result = 0;
|
||||
}
|
||||
|
@ -665,7 +729,6 @@ export default class DiskII implements Card {
|
|||
this.moveHead();
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
getState() {
|
||||
const result = {
|
||||
drives: [] as DriveState[],
|
||||
|
@ -682,7 +745,6 @@ export default class DiskII implements Card {
|
|||
return result;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
setState(state: State) {
|
||||
this.skip = state.skip;
|
||||
this.latch = state.latch;
|
||||
|
@ -699,22 +761,17 @@ export default class DiskII implements Card {
|
|||
this.cur = this.drives[this.drive - 1];
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work for WOZ disks
|
||||
getMetadata(driveNo: DriveNumber) {
|
||||
const drive = this.drives[driveNo - 1];
|
||||
if (isNibbleDrive(drive)) {
|
||||
return {
|
||||
format: drive.format,
|
||||
volume: drive.volume,
|
||||
track: drive.track,
|
||||
head: drive.head,
|
||||
phase: drive.phase,
|
||||
readOnly: drive.readOnly,
|
||||
dirty: drive.dirty
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
format: drive.format,
|
||||
volume: drive.volume,
|
||||
track: drive.track,
|
||||
head: drive.head,
|
||||
phase: drive.phase,
|
||||
readOnly: drive.readOnly,
|
||||
dirty: drive.dirty
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work on WOZ disks
|
||||
|
@ -728,63 +785,28 @@ export default class DiskII implements Card {
|
|||
|
||||
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
|
||||
// TODO(flan): This implementation is not very safe.
|
||||
setDisk(drive: DriveNumber, disk: JSONDisk) {
|
||||
const fmt = disk.type;
|
||||
const readOnly = disk.readOnly;
|
||||
const name = disk.name;
|
||||
|
||||
let data: memory[] | memory[][];
|
||||
if (disk.encoding == 'base64') {
|
||||
data = [];
|
||||
for (let t = 0; t < disk.data.length; t++) {
|
||||
if (fmt == 'nib') {
|
||||
data[t] = base64_decode(disk.data[t] as string);
|
||||
} else {
|
||||
data[t] = [];
|
||||
for (let s = 0; s < disk.data[t].length; s++) {
|
||||
data[t][s] = base64_decode(disk.data[t][s] as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
setDisk(drive: DriveNumber, jsonDisk: JSONDisk) {
|
||||
if (this.worker) {
|
||||
const message: FormatWorkerMessage = {
|
||||
type: PROCESS_JSON_DISK,
|
||||
payload: {
|
||||
drive,
|
||||
jsonDisk
|
||||
},
|
||||
};
|
||||
this.worker.postMessage(message);
|
||||
return true;
|
||||
} else {
|
||||
data = disk.data;
|
||||
const disk = createDiskFromJsonDisk(jsonDisk);
|
||||
if (disk) {
|
||||
const cur = this.drives[drive - 1];
|
||||
Object.assign(cur, disk);
|
||||
this.updateDirty(drive, false);
|
||||
this.callbacks.label(drive, disk.name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const cur = this.drives[drive - 1];
|
||||
|
||||
// var v = (fmt === 'dsk' ? data[0x11][0x00][0x06] : 0xfe);
|
||||
// if (v == 0x00) {
|
||||
const volume = disk.volume || 0xfe;
|
||||
// }
|
||||
|
||||
const options = {
|
||||
volume,
|
||||
readOnly,
|
||||
name,
|
||||
data
|
||||
};
|
||||
|
||||
let newDisk: Disk;
|
||||
switch (fmt) {
|
||||
case 'd13':
|
||||
newDisk = D13(options);
|
||||
break;
|
||||
case 'do':
|
||||
case 'dsk':
|
||||
newDisk = DOS(options);
|
||||
break;
|
||||
case 'nib':
|
||||
newDisk = Nibble(options);
|
||||
break;
|
||||
case 'po':
|
||||
newDisk = ProDOS(options);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(cur, newDisk);
|
||||
this.updateDirty(drive, false);
|
||||
this.callbacks.label(drive, name);
|
||||
return false;
|
||||
}
|
||||
|
||||
getJSON(drive: DriveNumber, pretty: boolean = false) {
|
||||
|
@ -795,52 +817,78 @@ export default class DiskII implements Card {
|
|||
return jsonEncode(cur, pretty);
|
||||
}
|
||||
|
||||
setJSON(drive: DriveNumber, data: string) {
|
||||
const cur = this.drives[drive - 1];
|
||||
Object.assign(cur, jsonDecode(data));
|
||||
setJSON(drive: DriveNumber, json: string) {
|
||||
if (this.worker) {
|
||||
const message: FormatWorkerMessage = {
|
||||
type: PROCESS_JSON,
|
||||
payload: {
|
||||
drive,
|
||||
json
|
||||
},
|
||||
};
|
||||
this.worker.postMessage(message);
|
||||
} else {
|
||||
const cur = this.drives[drive - 1];
|
||||
Object.assign(cur, jsonDecode(json));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: ArrayBuffer) {
|
||||
let disk;
|
||||
const cur = this.drives[drive - 1];
|
||||
setBinary(drive: DriveNumber, name: string, fmt: NibbleFormat, rawData: ArrayBuffer) {
|
||||
const readOnly = false;
|
||||
const volume = 254;
|
||||
const options = {
|
||||
name,
|
||||
rawData,
|
||||
readOnly,
|
||||
volume
|
||||
volume,
|
||||
};
|
||||
|
||||
switch (fmt) {
|
||||
case '2mg':
|
||||
disk = _2MG(options);
|
||||
break;
|
||||
case 'd13':
|
||||
disk = D13(options);
|
||||
break;
|
||||
case 'do':
|
||||
case 'dsk':
|
||||
disk = DOS(options);
|
||||
break;
|
||||
case 'nib':
|
||||
disk = Nibble(options);
|
||||
break;
|
||||
case 'po':
|
||||
disk = ProDOS(options);
|
||||
break;
|
||||
case 'woz':
|
||||
disk = Woz(options);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if (this.worker) {
|
||||
const message: FormatWorkerMessage = {
|
||||
type: PROCESS_BINARY,
|
||||
payload: {
|
||||
drive,
|
||||
fmt,
|
||||
options,
|
||||
}
|
||||
};
|
||||
this.worker.postMessage(message);
|
||||
|
||||
Object.assign(cur, disk);
|
||||
this.updateDirty(drive, true);
|
||||
this.callbacks.label(this.drive, name);
|
||||
return true;
|
||||
return true;
|
||||
} else {
|
||||
const disk = createDisk(fmt, options);
|
||||
if (disk) {
|
||||
const cur = this.drives[drive - 1];
|
||||
Object.assign(cur, disk);
|
||||
this.updateDirty(drive, true);
|
||||
this.callbacks.label(this.drive, name);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
initWorker() {
|
||||
this.worker = new Worker('dist/format_worker.bundle.js');
|
||||
|
||||
this.worker.addEventListener('message', (message: MessageEvent<FormatWorkerResponse>) => {
|
||||
const { data } = message;
|
||||
switch (data.type) {
|
||||
case DISK_PROCESSED:
|
||||
{
|
||||
const { drive, disk } = data.payload;
|
||||
if (disk) {
|
||||
const cur = this.drives[drive - 1];
|
||||
Object.assign(cur, disk);
|
||||
this.updateDirty(drive, true);
|
||||
this.callbacks.label(drive, disk.name);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work with WOZ disks
|
||||
|
|
|
@ -12,16 +12,13 @@
|
|||
import { debug, toHex } from '../util';
|
||||
import { rom as smartPortRom } from '../roms/cards/smartport';
|
||||
import { Card, Restorable, byte, word, rom } from '../types';
|
||||
import { MassStorage, BlockDisk, ENCODING_BLOCK } from '../formats/types';
|
||||
import CPU6502, { CpuState, flags } from '../cpu6502';
|
||||
|
||||
type SmartDisk = Uint8Array[];
|
||||
|
||||
interface BlockDevice {
|
||||
blocks: Uint8Array
|
||||
}
|
||||
import { read2MGHeader } from '../formats/2mg';
|
||||
import createBlockDisk from '../formats/block';
|
||||
|
||||
export interface SmartPortState {
|
||||
disks: SmartDisk[]
|
||||
disks: BlockDisk[]
|
||||
}
|
||||
|
||||
export interface SmartPortOptions {
|
||||
|
@ -100,10 +97,10 @@ const ADDRESS_LO = 0x44;
|
|||
const BLOCK_LO = 0x46;
|
||||
// const BLOCK_HI = 0x47;
|
||||
|
||||
export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||
export default class SmartPort implements Card, MassStorage, Restorable<SmartPortState> {
|
||||
|
||||
private rom: rom;
|
||||
private disks: SmartDisk[] = [];
|
||||
private disks: BlockDisk[] = [];
|
||||
|
||||
constructor(private cpu: CPU6502, options: SmartPortOptions) {
|
||||
if (options?.block) {
|
||||
|
@ -117,13 +114,6 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
}
|
||||
}
|
||||
|
||||
private decodeDisk(unit: number, disk: BlockDevice) {
|
||||
this.disks[unit] = [];
|
||||
for (let idx = 0; idx < disk.blocks.length; idx++) {
|
||||
this.disks[unit][idx] = new Uint8Array(disk.blocks[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
private debug(..._args: any[]) {
|
||||
// debug.apply(this, arguments);
|
||||
}
|
||||
|
@ -131,16 +121,16 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
/*
|
||||
* dumpBlock
|
||||
*/
|
||||
/*
|
||||
dumpBlock(drive, block) {
|
||||
const result = '';
|
||||
const b;
|
||||
const jdx;
|
||||
|
||||
for (const idx = 0; idx < 32; idx++) {
|
||||
dumpBlock(drive: number, block: number) {
|
||||
let result = '';
|
||||
let b;
|
||||
let jdx;
|
||||
|
||||
for (let idx = 0; idx < 32; idx++) {
|
||||
result += toHex(idx << 4, 4) + ': ';
|
||||
for (jdx = 0; jdx < 16; jdx++) {
|
||||
b = disks[drive][block][idx * 16 + jdx];
|
||||
b = this.disks[drive].blocks[block][idx * 16 + jdx];
|
||||
if (jdx == 8) {
|
||||
result += ' ';
|
||||
}
|
||||
|
@ -148,7 +138,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
}
|
||||
result += ' ';
|
||||
for (jdx = 0; jdx < 16; jdx++) {
|
||||
b = disks[drive][block][idx * 16 + jdx] & 0x7f;
|
||||
b = this.disks[drive].blocks[block][idx * 16 + jdx] & 0x7f;
|
||||
if (jdx == 8) {
|
||||
result += ' ';
|
||||
}
|
||||
|
@ -162,14 +152,14 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
* getDeviceInfo
|
||||
*/
|
||||
|
||||
getDeviceInfo(state: CpuState, drive: number) {
|
||||
if (this.disks[drive]) {
|
||||
const blocks = this.disks[drive].length;
|
||||
const blocks = this.disks[drive].blocks.length;
|
||||
state.x = blocks & 0xff;
|
||||
state.y = blocks >> 8;
|
||||
|
||||
|
@ -190,7 +180,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
this.debug('read buffer=' + buffer);
|
||||
this.debug('read block=$' + toHex(block));
|
||||
|
||||
if (!this.disks[drive] || !this.disks[drive].length) {
|
||||
if (!this.disks[drive]?.blocks.length) {
|
||||
debug('Drive', drive, 'is empty');
|
||||
return;
|
||||
}
|
||||
|
@ -198,7 +188,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
// debug('read', '\n' + dumpBlock(drive, block));
|
||||
|
||||
for (let idx = 0; idx < 512; idx++) {
|
||||
buffer.writeByte(this.disks[drive][block][idx]);
|
||||
buffer.writeByte(this.disks[drive].blocks[block][idx]);
|
||||
buffer = buffer.inc(1);
|
||||
}
|
||||
|
||||
|
@ -215,7 +205,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
this.debug('write buffer=' + buffer);
|
||||
this.debug('write block=$' + toHex(block));
|
||||
|
||||
if (!this.disks[drive] || !this.disks[drive].length) {
|
||||
if (!this.disks[drive]?.blocks.length) {
|
||||
debug('Drive', drive, 'is empty');
|
||||
return;
|
||||
}
|
||||
|
@ -223,7 +213,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
// debug('write', '\n' + dumpBlock(drive, block));
|
||||
|
||||
for (let idx = 0; idx < 512; idx++) {
|
||||
this.disks[drive][block][idx] = buffer.readByte();
|
||||
this.disks[drive].blocks[block][idx] = buffer.readByte();
|
||||
buffer = buffer.inc(1);
|
||||
}
|
||||
state.a = 0;
|
||||
|
@ -235,10 +225,10 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
*/
|
||||
|
||||
formatDevice(state: CpuState, drive: number) {
|
||||
for (let idx = 0; idx < this.disks[drive].length; idx++) {
|
||||
this.disks[drive][idx] = new Uint8Array();
|
||||
for (let idx = 0; idx < this.disks[drive].blocks.length; idx++) {
|
||||
this.disks[drive].blocks[idx] = new Uint8Array();
|
||||
for (let jdx = 0; jdx < 512; jdx++) {
|
||||
this.disks[drive][idx][jdx] = 0;
|
||||
this.disks[drive].blocks[idx][jdx] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -371,7 +361,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
default: // Unit 1
|
||||
switch (status) {
|
||||
case 0:
|
||||
blocks = this.disks[unit].length;
|
||||
blocks = this.disks[unit].blocks.length;
|
||||
buffer.writeByte(0xf0); // W/R Block device in drive
|
||||
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
|
||||
buffer.inc(2).writeByte((blocks & 0xff00) >> 8);
|
||||
|
@ -433,33 +423,53 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||
getState() {
|
||||
return {
|
||||
disks: this.disks.map(
|
||||
(disk) => disk.map(
|
||||
(block) => new Uint8Array(block)
|
||||
)
|
||||
(disk) => {
|
||||
const result: BlockDisk = {
|
||||
blocks: disk.blocks.map(
|
||||
(block) => new Uint8Array(block)
|
||||
),
|
||||
encoding: ENCODING_BLOCK,
|
||||
readOnly: disk.readOnly,
|
||||
name: disk.name,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
setState(state: SmartPortState) {
|
||||
this.disks = state.disks.map(
|
||||
(disk) => disk.map(
|
||||
(block) => new Uint8Array(block)
|
||||
)
|
||||
(disk) => {
|
||||
const result: BlockDisk = {
|
||||
blocks: disk.blocks.map(
|
||||
(block) => new Uint8Array(block)
|
||||
),
|
||||
encoding: ENCODING_BLOCK,
|
||||
readOnly: disk.readOnly,
|
||||
name: disk.name,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setBinary(drive: number, _name: string, fmt: string, data: ArrayBuffer) {
|
||||
this.disks[drive] = [];
|
||||
setBinary(drive: number, name: string, fmt: string, rawData: ArrayBuffer) {
|
||||
const volume = 254;
|
||||
const readOnly = false;
|
||||
if (fmt == '2mg') {
|
||||
data = data.slice(64);
|
||||
}
|
||||
for (let idx = 0; idx < data.byteLength; idx += 512) {
|
||||
this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512)));
|
||||
const { bytes, offset } = read2MGHeader(rawData);
|
||||
rawData = rawData.slice(offset, offset + bytes);
|
||||
}
|
||||
const options = {
|
||||
rawData,
|
||||
name,
|
||||
readOnly,
|
||||
volume,
|
||||
};
|
||||
|
||||
this.disks[drive] = createBlockDisk(options);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setDisk(drive: number, json: BlockDevice) {
|
||||
this.decodeDisk(drive, json);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import BlockVolume from './block';
|
||||
import DOS from './do';
|
||||
import Nibble from './nib';
|
||||
import ProDOS from './po';
|
||||
|
||||
import { numToString, debug } from '../util';
|
||||
|
||||
/**
|
||||
* Returns a `Disk` object from a 2mg image.
|
||||
* @param {*} options the disk image and options
|
||||
* @returns {import('./format_utils').Disk}
|
||||
*/
|
||||
export default function _2MG(options) {
|
||||
var OFFSETS = {
|
||||
CREATOR: 0x04,
|
||||
FLAGS: 0x0A,
|
||||
FORMAT: 0x0C,
|
||||
BLOCKS: 0x14,
|
||||
DATA_OFFSET: 0x18,
|
||||
BYTES: 0x1C,
|
||||
};
|
||||
|
||||
var FLAGS = {
|
||||
READ_ONLY: 0x80000000,
|
||||
VOLUME_VALID: 0x00000100,
|
||||
VOLUME_MASK: 0x000000FF
|
||||
};
|
||||
|
||||
var { rawData, arrayConstructor } = options;
|
||||
var disk;
|
||||
var volume = 254;
|
||||
|
||||
// Standard header size is 64 bytes. Make assumptions.
|
||||
var prefix = new DataView(rawData);
|
||||
var signature = numToString(prefix.getInt32(0x0, true));
|
||||
if (signature !== '2IMG') {
|
||||
throw new Error('Unrecognized 2mg signature: ' + signature);
|
||||
}
|
||||
var creator = numToString(prefix.getInt32(OFFSETS.CREATOR, true));
|
||||
var format = prefix.getInt32(OFFSETS.FORMAT, true);
|
||||
var bytes = prefix.getInt32(OFFSETS.BYTES, true);
|
||||
var offset = prefix.getInt32(OFFSETS.DATA_OFFSET, true);
|
||||
var flags = prefix.getInt32(OFFSETS.FLAGS, true);
|
||||
var readOnly = (flags & FLAGS.READ_ONLY) !== 0;
|
||||
if (flags & FLAGS.VOLUME_VALID) {
|
||||
volume = flags & FLAGS.VOLUME_MASK;
|
||||
}
|
||||
|
||||
debug('created by', creator);
|
||||
rawData = rawData.slice(offset, offset + bytes);
|
||||
|
||||
var blockVolume = options.blockVolume || rawData.byteLength >= (800 * 1024);
|
||||
|
||||
options = { rawData, readOnly, volume, arrayConstructor };
|
||||
|
||||
if (blockVolume) {
|
||||
disk = new BlockVolume(options);
|
||||
} else {
|
||||
// Check image format.
|
||||
// Sure, it's really 64 bits. But only 2 are actually used.
|
||||
switch (format) {
|
||||
case 1: // PO
|
||||
disk = new ProDOS(options);
|
||||
break;
|
||||
case 2: // NIB
|
||||
disk = new Nibble(options);
|
||||
break;
|
||||
case 0: // dsk
|
||||
default: // Something hinky, assume 'dsk'
|
||||
disk = new DOS(options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return disk;
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import DOS from './do';
|
||||
import Nibble from './nib';
|
||||
import ProDOS from './po';
|
||||
import { DiskOptions } from './types';
|
||||
|
||||
import { numToString, debug } from '../util';
|
||||
|
||||
const OFFSETS = {
|
||||
CREATOR: 0x04,
|
||||
FLAGS: 0x0A,
|
||||
FORMAT: 0x0C,
|
||||
BLOCKS: 0x14,
|
||||
DATA_OFFSET: 0x18,
|
||||
BYTES: 0x1C,
|
||||
};
|
||||
|
||||
const FLAGS = {
|
||||
READ_ONLY: 0x80000000,
|
||||
VOLUME_VALID: 0x00000100,
|
||||
VOLUME_MASK: 0x000000FF
|
||||
};
|
||||
|
||||
export function read2MGHeader(rawData: ArrayBuffer) {
|
||||
const prefix = new DataView(rawData);
|
||||
const signature = numToString(prefix.getInt32(0x0, true));
|
||||
if (signature !== '2IMG') {
|
||||
throw new Error('Unrecognized 2mg signature: ' + signature);
|
||||
}
|
||||
const creator = numToString(prefix.getInt32(OFFSETS.CREATOR, true));
|
||||
const format = prefix.getInt32(OFFSETS.FORMAT, true);
|
||||
const bytes = prefix.getInt32(OFFSETS.BYTES, true);
|
||||
const offset = prefix.getInt32(OFFSETS.DATA_OFFSET, true);
|
||||
const flags = prefix.getInt32(OFFSETS.FLAGS, true);
|
||||
const readOnly = (flags & FLAGS.READ_ONLY) !== 0;
|
||||
let volume = 254;
|
||||
if (flags & FLAGS.VOLUME_VALID) {
|
||||
volume = flags & FLAGS.VOLUME_MASK;
|
||||
}
|
||||
|
||||
debug('created by', creator);
|
||||
|
||||
return {
|
||||
bytes,
|
||||
creator,
|
||||
format,
|
||||
offset,
|
||||
readOnly,
|
||||
volume,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a `Disk` object from a 2mg image.
|
||||
* @param options the disk image and options
|
||||
*/
|
||||
export default function createDiskFrom2MG(options: DiskOptions) {
|
||||
let { rawData } = options;
|
||||
let disk;
|
||||
|
||||
if (!rawData) {
|
||||
throw new Error('Requires rawData');
|
||||
}
|
||||
|
||||
const { bytes, format, offset, readOnly, volume } = read2MGHeader(rawData);
|
||||
rawData = rawData.slice(offset, offset + bytes);
|
||||
options = { ...options, rawData, readOnly, volume };
|
||||
|
||||
// Check image format.
|
||||
// Sure, it's really 64 bits. But only 2 are actually used.
|
||||
switch (format) {
|
||||
case 1: // PO
|
||||
disk = ProDOS(options);
|
||||
break;
|
||||
case 2: // NIB
|
||||
disk = Nibble(options);
|
||||
break;
|
||||
case 0: // dsk
|
||||
default: // Something hinky, assume 'dsk'
|
||||
disk = DOS(options);
|
||||
break;
|
||||
}
|
||||
|
||||
return disk;
|
||||
}
|
|
@ -9,24 +9,28 @@
|
|||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { DiskOptions, BlockDisk, ENCODING_BLOCK } from './types';
|
||||
|
||||
/**
|
||||
* Returns a `Disk` object for a block volume with block-ordered data.
|
||||
* @param {*} options the disk image and options
|
||||
* @returns {import('./format_utils').Disk}
|
||||
* @param options the disk image and options
|
||||
*/
|
||||
export default function BlockVolume(options) {
|
||||
var { rawData, readOnly, name } = options;
|
||||
var disk;
|
||||
export default function createBlockDisk(options: DiskOptions): BlockDisk {
|
||||
const { rawData, readOnly, name } = options;
|
||||
|
||||
var blocks = [];
|
||||
blocks = [];
|
||||
var offset = 0;
|
||||
if (!rawData) {
|
||||
throw new Error('Requires rawData');
|
||||
}
|
||||
|
||||
const blocks = [];
|
||||
let offset = 0;
|
||||
while (offset < rawData.byteLength) {
|
||||
blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200)));
|
||||
offset += 0x200;
|
||||
}
|
||||
|
||||
disk = {
|
||||
const disk: BlockDisk = {
|
||||
encoding: ENCODING_BLOCK,
|
||||
blocks,
|
||||
name,
|
||||
readOnly,
|
|
@ -0,0 +1,82 @@
|
|||
import { includes, memory } from '../types';
|
||||
import { base64_decode } from '../base64';
|
||||
import { Disk, NibbleFormat, DiskOptions, JSONDisk, NIBBLE_FORMATS, NibbleDisk } from './types';
|
||||
import createDiskFrom2MG from './2mg';
|
||||
import createDiskFromD13 from './d13';
|
||||
import createDiskFromDOS from './do';
|
||||
import createDiskFromProDOS from './po';
|
||||
import createDiskFromWoz from './woz';
|
||||
import createDiskFromNibble from './nib';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fmt Type of
|
||||
* @param options
|
||||
* @returns A nibblized disk
|
||||
*/
|
||||
export function createDisk(fmt: NibbleFormat, options: DiskOptions): NibbleDisk | null {
|
||||
let disk: NibbleDisk | null = null;
|
||||
|
||||
switch (fmt) {
|
||||
case '2mg':
|
||||
disk = createDiskFrom2MG(options);
|
||||
break;
|
||||
case 'd13':
|
||||
disk = createDiskFromD13(options);
|
||||
break;
|
||||
case 'do':
|
||||
case 'dsk':
|
||||
disk = createDiskFromDOS(options);
|
||||
break;
|
||||
case 'nib':
|
||||
disk = createDiskFromNibble(options);
|
||||
break;
|
||||
case 'po':
|
||||
disk = createDiskFromProDOS(options);
|
||||
break;
|
||||
case 'woz':
|
||||
disk = createDiskFromWoz(options);
|
||||
break;
|
||||
}
|
||||
|
||||
return disk;
|
||||
}
|
||||
|
||||
export function createDiskFromJsonDisk(disk: JSONDisk): Disk | null {
|
||||
const fmt = disk.type;
|
||||
const readOnly = disk.readOnly;
|
||||
const name = disk.name;
|
||||
|
||||
if (includes(NIBBLE_FORMATS, fmt)) {
|
||||
let trackData: memory[][];
|
||||
if (disk.encoding == 'base64') {
|
||||
trackData = [];
|
||||
for (let t = 0; t < disk.data.length; t++) {
|
||||
trackData[t] = [];
|
||||
if (fmt == 'nib') {
|
||||
trackData[t][0] = base64_decode(disk.data[t] as string);
|
||||
} else {
|
||||
for (let s = 0; s < disk.data[t].length; s++) {
|
||||
trackData[t][s] = base64_decode(disk.data[t][s] as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trackData = disk.data;
|
||||
}
|
||||
|
||||
const volume = disk.volume || 0xfe;
|
||||
|
||||
const options = {
|
||||
volume,
|
||||
readOnly,
|
||||
name,
|
||||
data: trackData
|
||||
} as DiskOptions;
|
||||
|
||||
return createDisk(fmt, options);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,24 +10,28 @@
|
|||
*/
|
||||
|
||||
import { explodeSector13, D13O } from './format_utils';
|
||||
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
|
||||
|
||||
/**
|
||||
* Returns a `Disk` object from DOS 3.2-ordered image data.
|
||||
* @param {*} options the disk image and options
|
||||
* @returns {import('./format_utils').Disk}
|
||||
* @param options the disk image and options
|
||||
* @returns A nibblized disk
|
||||
*/
|
||||
export default function DOS13(options) {
|
||||
var { data, name, rawData, volume, readOnly } = options;
|
||||
var disk = {
|
||||
export default function createDiskFromDOS13(options: DiskOptions) {
|
||||
const { data, name, rawData, volume, readOnly } = options;
|
||||
const disk: NibbleDisk = {
|
||||
format: 'd13',
|
||||
encoding: ENCODING_NIBBLE,
|
||||
name,
|
||||
volume,
|
||||
readOnly,
|
||||
tracks: [],
|
||||
trackMap: null,
|
||||
rawTracks: null
|
||||
tracks: []
|
||||
};
|
||||
|
||||
if (!data && !rawData) {
|
||||
throw new Error('data or rawData required');
|
||||
}
|
||||
|
||||
/*
|
||||
* DOS 13-sector disks have the physical sectors skewed on the track. The skew
|
||||
* between physical sectors is 10 (A), resulting in the following physical order:
|
||||
|
@ -38,22 +42,24 @@ export default function DOS13(options) {
|
|||
* differently from the DOS and ProDOS nibblizers.
|
||||
*/
|
||||
|
||||
for (var t = 0; t < 35; t++) {
|
||||
var track = [];
|
||||
for (var disk_sector = 0; disk_sector < 13; disk_sector++) {
|
||||
var physical_sector = D13O[disk_sector];
|
||||
var sector;
|
||||
for (let t = 0; t < 35; t++) {
|
||||
let track: number[] = [];
|
||||
for (let disk_sector = 0; disk_sector < 13; disk_sector++) {
|
||||
const physical_sector = D13O[disk_sector];
|
||||
let sector: Uint8Array;
|
||||
if (rawData) {
|
||||
var off = (13 * t + physical_sector) * 256;
|
||||
const off = (13 * t + physical_sector) * 256;
|
||||
sector = new Uint8Array(rawData.slice(off, off + 256));
|
||||
} else {
|
||||
} else if (data) {
|
||||
sector = data[t][physical_sector];
|
||||
} else {
|
||||
throw new Error('Requires data or rawData');
|
||||
}
|
||||
track = track.concat(
|
||||
explodeSector13(volume, t, physical_sector, sector)
|
||||
);
|
||||
}
|
||||
disk.tracks.push(track);
|
||||
disk.tracks.push(new Uint8Array(track));
|
||||
}
|
||||
|
||||
return disk;
|
|
@ -11,34 +11,37 @@
|
|||
|
||||
import { explodeSector16, DO } from './format_utils';
|
||||
import { bytify } from '../util';
|
||||
import { byte } from '../types';
|
||||
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
|
||||
|
||||
/**
|
||||
* Returns a `Disk` object from DOS-ordered image data.
|
||||
* @param {*} options the disk image and options
|
||||
* @returns {import('./format_utils').Disk}
|
||||
* @param options the disk image and options
|
||||
* @returns A nibblized disk
|
||||
*/
|
||||
export default function DOS(options) {
|
||||
var { data, name, rawData, volume, readOnly } = options;
|
||||
var disk = {
|
||||
export default function createDiskFromDOS(options: DiskOptions): NibbleDisk {
|
||||
const { data, name, rawData, volume, readOnly } = options;
|
||||
const disk: NibbleDisk = {
|
||||
format: 'dsk',
|
||||
encoding: ENCODING_NIBBLE,
|
||||
name,
|
||||
volume,
|
||||
readOnly,
|
||||
tracks: [],
|
||||
trackMap: null,
|
||||
rawTracks: null
|
||||
};
|
||||
|
||||
for (var t = 0; t < 35; t++) {
|
||||
var track = [];
|
||||
for (var physical_sector = 0; physical_sector < 16; physical_sector++) {
|
||||
for (let t = 0; t < 35; t++) {
|
||||
let track: byte[] = [];
|
||||
for (let physical_sector = 0; physical_sector < 16; physical_sector++) {
|
||||
const dos_sector = DO[physical_sector];
|
||||
var sector;
|
||||
let sector: Uint8Array;
|
||||
if (rawData) {
|
||||
const off = (16 * t + dos_sector) * 256;
|
||||
sector = new Uint8Array(rawData.slice(off, off + 256));
|
||||
} else if (data) {
|
||||
sector = new Uint8Array(data[t][dos_sector]);
|
||||
} else {
|
||||
sector = data[t][dos_sector];
|
||||
throw new Error('Requires data or rawData');
|
||||
}
|
||||
track = track.concat(
|
||||
explodeSector16(volume, t, physical_sector, sector)
|
|
@ -9,64 +9,10 @@
|
|||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { byte, DiskFormat, memory } from '../types';
|
||||
import { byte, memory } from '../types';
|
||||
import { base64_decode, base64_encode } from '../base64';
|
||||
import { bytify, debug, toHex } from '../util';
|
||||
import { GamepadConfiguration } from '../ui/gamepad';
|
||||
|
||||
export interface Disk {
|
||||
format: DiskFormat
|
||||
name: string
|
||||
volume: byte
|
||||
tracks: memory[]
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Base format for JSON defined disks
|
||||
*/
|
||||
export class JSONDiskBase {
|
||||
type: DiskFormat
|
||||
name: string
|
||||
disk?: number
|
||||
category?: string
|
||||
writeProtected?: boolean
|
||||
volume: byte
|
||||
readOnly: boolean
|
||||
gamepad?: GamepadConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Disk format with base64 encoded tracks
|
||||
*/
|
||||
|
||||
export interface Base64JSONDisk extends JSONDiskBase {
|
||||
encoding: 'base64'
|
||||
data: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Disk format with byte array tracks
|
||||
*/
|
||||
|
||||
export interface BinaryJSONDisk extends JSONDiskBase {
|
||||
encoding: 'binary'
|
||||
data: memory[][]
|
||||
}
|
||||
|
||||
/**
|
||||
* General JSON Disk format
|
||||
*/
|
||||
|
||||
export type JSONDisk = Base64JSONDisk | BinaryJSONDisk;
|
||||
|
||||
export interface Drive {
|
||||
format: DiskFormat
|
||||
volume: byte
|
||||
tracks: memory[]
|
||||
readOnly: boolean
|
||||
dirty: boolean
|
||||
}
|
||||
import { NibbleDisk, ENCODING_NIBBLE } from './types';
|
||||
|
||||
/**
|
||||
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
|
||||
|
@ -74,7 +20,7 @@ export interface Drive {
|
|||
export const DO = [
|
||||
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
|
||||
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
|
||||
];
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
|
||||
|
@ -82,7 +28,7 @@ export const DO = [
|
|||
export const _DO = [
|
||||
0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
|
||||
0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
|
||||
];
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
|
||||
|
@ -90,7 +36,7 @@ export const _DO = [
|
|||
export const PO = [
|
||||
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
|
||||
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
|
||||
];
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
|
||||
|
@ -98,7 +44,7 @@ export const PO = [
|
|||
export const _PO = [
|
||||
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
||||
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
|
||||
];
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* DOS 13-sector disk physical sector order (index is disk sector, value is
|
||||
|
@ -106,18 +52,18 @@ export const _PO = [
|
|||
*/
|
||||
export const D13O = [
|
||||
0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const _D13O = [
|
||||
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc
|
||||
];
|
||||
] as const;
|
||||
|
||||
const _trans53 = [
|
||||
0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
|
||||
0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
|
||||
0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef,
|
||||
0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
|
||||
];
|
||||
] as const;
|
||||
|
||||
const _trans62 = [
|
||||
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
|
||||
|
@ -128,7 +74,7 @@ const _trans62 = [
|
|||
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
|
||||
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
|
||||
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const detrans62 = [
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
|
@ -147,10 +93,13 @@ export const detrans62 = [
|
|||
0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
||||
0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
||||
0x00, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
|
||||
];
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* From Beneath Apple DOS
|
||||
* Converts a byte into its 4x4 encoded representation
|
||||
*
|
||||
* @param val byte to encode.
|
||||
* @returns A two byte array of representing the 4x4 encoding.
|
||||
*/
|
||||
export function fourXfour(val: byte): [xx: byte, yy: byte] {
|
||||
let xx = val & 0xaa;
|
||||
|
@ -163,11 +112,28 @@ export function fourXfour(val: byte): [xx: byte, yy: byte] {
|
|||
return [xx, yy];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts 2 4x4 encoded bytes into a byte value
|
||||
*
|
||||
* @param xx First encoded byte.
|
||||
* @param yy Second encoded byte.
|
||||
* @returns The decoded value.
|
||||
*/
|
||||
export function defourXfour(xx: byte, yy: byte): byte {
|
||||
return ((xx << 1) | 0x01) & yy;
|
||||
}
|
||||
|
||||
export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory) {
|
||||
/**
|
||||
* Converts a raw sector into a nibblized representation to be combined into a
|
||||
* nibblized 16 sector track.
|
||||
*
|
||||
* @param volume volume number
|
||||
* @param track track number
|
||||
* @param sector sector number
|
||||
* @param data sector data
|
||||
* @returns a nibblized representation of the sector data
|
||||
*/
|
||||
export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory): byte[] {
|
||||
let buf = [];
|
||||
let gap;
|
||||
|
||||
|
@ -255,7 +221,17 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m
|
|||
return buf;
|
||||
}
|
||||
|
||||
export function explodeSector13(volume: byte, track: byte, sector: byte, data: byte[]) {
|
||||
/**
|
||||
* Converts a raw sector into a nibblized representation to be combined into
|
||||
* a nibblized 13 sector track.
|
||||
*
|
||||
* @param volume volume number
|
||||
* @param track track number
|
||||
* @param sector sector number
|
||||
* @param data sector data
|
||||
* @returns a nibblized representation of the sector data
|
||||
*/
|
||||
export function explodeSector13(volume: byte, track: byte, sector: byte, data: memory): byte[] {
|
||||
let buf = [];
|
||||
let gap;
|
||||
|
||||
|
@ -354,13 +330,22 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: b
|
|||
return buf;
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work on WOZ disks
|
||||
export function readSector(drive: Drive, track: byte, sector: byte) {
|
||||
const _sector = drive.format == 'po' ? _PO[sector] : _DO[sector];
|
||||
/**
|
||||
* Reads a sector of data from a nibblized disk
|
||||
*
|
||||
* TODO(flan): Does not work on WOZ disks
|
||||
*
|
||||
* @param disk Nibble disk
|
||||
* @param track track number to read
|
||||
* @param sector sector number to read
|
||||
* @returns An array of sector data bytes.
|
||||
*/
|
||||
export function readSector(disk: NibbleDisk, track: byte, sector: byte): memory {
|
||||
const _sector = disk.format == 'po' ? _PO[sector] : _DO[sector];
|
||||
let val, state = 0;
|
||||
let idx = 0;
|
||||
let retry = 0;
|
||||
const cur = drive.tracks[track];
|
||||
const cur = disk.tracks[track];
|
||||
|
||||
function _readNext() {
|
||||
const result = cur[idx++];
|
||||
|
@ -450,32 +435,46 @@ export function readSector(drive: Drive, track: byte, sector: byte) {
|
|||
return new Uint8Array();
|
||||
}
|
||||
|
||||
export function jsonEncode(cur: Drive, pretty: boolean) {
|
||||
/**
|
||||
* Convert a nibblized disk into a JSON string for storage.
|
||||
*
|
||||
* @param disk Nibblized disk
|
||||
* @param pretty Whether to format the output string
|
||||
* @returns A JSON string representing the disk
|
||||
*/
|
||||
export function jsonEncode(disk: NibbleDisk, pretty: boolean): string {
|
||||
// For 'nib', tracks are encoded as strings. For all other formats,
|
||||
// tracks are arrays of sectors which are encoded as strings.
|
||||
const data: string[] | string[][] = [];
|
||||
let format = 'dsk';
|
||||
for (let t = 0; t < cur.tracks.length; t++) {
|
||||
for (let t = 0; t < disk.tracks.length; t++) {
|
||||
data[t] = [];
|
||||
if (cur.format === 'nib') {
|
||||
if (disk.format === 'nib') {
|
||||
format = 'nib';
|
||||
data[t] = base64_encode(cur.tracks[t]);
|
||||
data[t] = base64_encode(disk.tracks[t]);
|
||||
} else {
|
||||
for (let s = 0; s < 0x10; s++) {
|
||||
(data[t] as string[])[s] = base64_encode(readSector(cur, t, s));
|
||||
(data[t] as string[])[s] = base64_encode(readSector(disk, t, s));
|
||||
}
|
||||
}
|
||||
}
|
||||
return JSON.stringify({
|
||||
'type': format,
|
||||
'encoding': 'base64',
|
||||
'volume': cur.volume,
|
||||
'volume': disk.volume,
|
||||
'data': data,
|
||||
'readOnly': cur.readOnly,
|
||||
'readOnly': disk.readOnly,
|
||||
}, undefined, pretty ? ' ' : undefined);
|
||||
}
|
||||
|
||||
export function jsonDecode(data: string) {
|
||||
/**
|
||||
* Convert a JSON string into a nibblized disk.
|
||||
*
|
||||
* @param data JSON string representing a disk image, created by [jsonEncode].
|
||||
* @returns A nibblized disk
|
||||
*/
|
||||
|
||||
export function jsonDecode(data: string): NibbleDisk {
|
||||
const tracks: memory[] = [];
|
||||
const json = JSON.parse(data);
|
||||
const v = json.volume;
|
||||
|
@ -483,20 +482,78 @@ export function jsonDecode(data: string) {
|
|||
for (let t = 0; t < json.data.length; t++) {
|
||||
let track: byte[] = [];
|
||||
for (let s = 0; s < json.data[t].length; s++) {
|
||||
const _s = 15 - s;
|
||||
const _s = json.type == 'po' ? PO[s] : DO[s];
|
||||
const sector: string = json.data[t][_s];
|
||||
const d = base64_decode(sector);
|
||||
track = track.concat(explodeSector16(v, t, s, d));
|
||||
}
|
||||
tracks[t] = bytify(track);
|
||||
}
|
||||
const cur: Drive = {
|
||||
const disk: NibbleDisk = {
|
||||
volume: v,
|
||||
format: json.type,
|
||||
encoding: ENCODING_NIBBLE,
|
||||
name: json.name,
|
||||
tracks,
|
||||
readOnly,
|
||||
dirty: false,
|
||||
};
|
||||
|
||||
return cur;
|
||||
return disk;
|
||||
}
|
||||
|
||||
export function analyseDisk(disk: NibbleDisk) {
|
||||
for (let track = 0; track < 35; track++) {
|
||||
let outStr = `${toHex(track)}: `;
|
||||
let val, state = 0;
|
||||
let idx = 0;
|
||||
const cur = disk.tracks[track];
|
||||
|
||||
const _readNext = () => {
|
||||
const result = cur[idx++];
|
||||
return result;
|
||||
};
|
||||
|
||||
const _skipBytes = (count: number) => {
|
||||
idx += count;
|
||||
};
|
||||
|
||||
let t = 0, s = 0, v = 0, checkSum;
|
||||
while (idx < cur.length) {
|
||||
switch (state) {
|
||||
case 0:
|
||||
val = _readNext();
|
||||
state = (val === 0xd5) ? 1 : 0;
|
||||
break;
|
||||
case 1:
|
||||
val = _readNext();
|
||||
state = (val === 0xaa) ? 2 : 0;
|
||||
break;
|
||||
case 2:
|
||||
val = _readNext();
|
||||
state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0);
|
||||
break;
|
||||
case 3: // Address
|
||||
v = defourXfour(_readNext(), _readNext()); // Volume
|
||||
t = defourXfour(_readNext(), _readNext());
|
||||
s = defourXfour(_readNext(), _readNext());
|
||||
checkSum = defourXfour(_readNext(), _readNext());
|
||||
if (checkSum != (v ^ t ^ s)) {
|
||||
debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
|
||||
} else {
|
||||
outStr += toHex(s, 1);
|
||||
}
|
||||
_skipBytes(3); // Skip footer
|
||||
state = 0;
|
||||
break;
|
||||
case 4: // Valid header
|
||||
_skipBytes(0x159); // Skip data, checksum and footer
|
||||
state = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
debug(outStr);
|
||||
}
|
||||
return new Uint8Array();
|
||||
}
|
||||
|
|
|
@ -9,30 +9,34 @@
|
|||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
|
||||
import { memory } from '../types';
|
||||
|
||||
/**
|
||||
* Returns a `Disk` object from raw nibble image data.
|
||||
* @param {*} options the disk image and options
|
||||
* @returns {import('./format_utils').Disk}
|
||||
* @param options the disk image and options
|
||||
* @returns A nibblized disk
|
||||
*/
|
||||
export default function Nibble(options) {
|
||||
var { data, name, rawData, volume, readOnly } = options;
|
||||
var disk = {
|
||||
export default function createDiskFromNibble(options: DiskOptions): NibbleDisk {
|
||||
const { data, name, rawData, volume, readOnly } = options;
|
||||
const disk: NibbleDisk = {
|
||||
format: 'nib',
|
||||
encoding: ENCODING_NIBBLE,
|
||||
name,
|
||||
volume: volume || 254,
|
||||
readOnly: readOnly || false,
|
||||
tracks: [],
|
||||
trackMap: null,
|
||||
rawTracks: null
|
||||
tracks: []
|
||||
};
|
||||
|
||||
for (var t = 0; t < 35; t++) {
|
||||
var track;
|
||||
for (let t = 0; t < 35; t++) {
|
||||
let track: memory;
|
||||
if (rawData) {
|
||||
var off = t * 0x1a00;
|
||||
track = new Uint8Array(data.slice(off, off + 0x1a00));
|
||||
const off = t * 0x1a00;
|
||||
track = new Uint8Array(rawData.slice(off, off + 0x1a00));
|
||||
} else if (data) {
|
||||
track = data[t][0];
|
||||
} else {
|
||||
track = data[t];
|
||||
throw new Error('Requires data or rawData');
|
||||
}
|
||||
disk.tracks[t] = track;
|
||||
}
|
|
@ -11,34 +11,37 @@
|
|||
|
||||
import { explodeSector16, PO } from './format_utils';
|
||||
import { bytify } from '../util';
|
||||
import type { byte } from '../types';
|
||||
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
|
||||
|
||||
/**
|
||||
* Returns a `Disk` object from ProDOS-ordered image data.
|
||||
* @param {*} options the disk image and options
|
||||
* @returns {import('./format_utils').Disk}
|
||||
* @param options the disk image and options
|
||||
* @returns A nibblized disk
|
||||
*/
|
||||
export default function ProDOS(options) {
|
||||
var { data, name, rawData, volume, readOnly } = options;
|
||||
var disk = {
|
||||
export default function createDiskFromProDOS(options: DiskOptions) {
|
||||
const { data, name, rawData, volume, readOnly } = options;
|
||||
const disk: NibbleDisk = {
|
||||
format: 'nib',
|
||||
encoding: ENCODING_NIBBLE,
|
||||
name,
|
||||
volume: volume || 254,
|
||||
tracks: [],
|
||||
readOnly: readOnly || false,
|
||||
trackMap: null,
|
||||
rawTracks: null
|
||||
};
|
||||
|
||||
for (var physical_track = 0; physical_track < 35; physical_track++) {
|
||||
var track = [];
|
||||
for (var physical_sector = 0; physical_sector < 16; physical_sector++) {
|
||||
for (let physical_track = 0; physical_track < 35; physical_track++) {
|
||||
let track: byte[] = [];
|
||||
for (let physical_sector = 0; physical_sector < 16; physical_sector++) {
|
||||
const prodos_sector = PO[physical_sector];
|
||||
var sector;
|
||||
let sector;
|
||||
if (rawData) {
|
||||
var off = (16 * physical_track + prodos_sector) * 256;
|
||||
const off = (16 * physical_track + prodos_sector) * 256;
|
||||
sector = new Uint8Array(rawData.slice(off, off + 256));
|
||||
} else {
|
||||
} else if (data) {
|
||||
sector = data[physical_track][prodos_sector];
|
||||
} else {
|
||||
throw new Error('Requires data or rawData');
|
||||
}
|
||||
track = track.concat(
|
||||
explodeSector16(volume, physical_track, physical_sector, sector)
|
|
@ -18,6 +18,10 @@ export function ProDOSVolume(disk) {
|
|||
var _bitMap;
|
||||
|
||||
return {
|
||||
disk() {
|
||||
return _disk;
|
||||
},
|
||||
|
||||
blocks() {
|
||||
return _disk.blocks;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
/* Copyright 2021 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import type { bit, byte, memory, MemberOf } from '../types';
|
||||
import type { GamepadConfiguration } from '../ui/types';
|
||||
|
||||
export const DRIVE_NUMBERS = [1, 2] as const;
|
||||
export type DriveNumber = MemberOf<typeof DRIVE_NUMBERS>;
|
||||
|
||||
/**
|
||||
* Arguments for the disk format processors.
|
||||
*/
|
||||
|
||||
export interface DiskOptions {
|
||||
name: string
|
||||
volume: byte
|
||||
readOnly: boolean
|
||||
data?: memory[][]
|
||||
rawData?: ArrayBuffer
|
||||
blockVolume?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Return value from disk format processors. Describes raw disk
|
||||
* data which the DiskII card can process.
|
||||
*/
|
||||
|
||||
export interface Disk {
|
||||
name: string
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
export const ENCODING_NIBBLE = 'nibble';
|
||||
export const ENCODING_BITSTREAM = 'bitstream';
|
||||
export const ENCODING_BLOCK = 'block';
|
||||
|
||||
export interface NibbleDisk extends Disk {
|
||||
encoding: typeof ENCODING_NIBBLE
|
||||
format: DiskFormat
|
||||
volume: byte
|
||||
tracks: memory[]
|
||||
}
|
||||
|
||||
export interface WozDisk extends Disk {
|
||||
encoding: typeof ENCODING_BITSTREAM
|
||||
trackMap: number[]
|
||||
rawTracks: bit[][]
|
||||
tracks: memory[]
|
||||
}
|
||||
|
||||
export interface BlockDisk extends Disk {
|
||||
encoding: typeof ENCODING_BLOCK
|
||||
blocks: Uint8Array[]
|
||||
}
|
||||
|
||||
/**
|
||||
* File types supported by the disk format processors and
|
||||
* block devices.
|
||||
*/
|
||||
|
||||
export const NIBBLE_FORMATS = [
|
||||
'2mg',
|
||||
'd13',
|
||||
'do',
|
||||
'dsk',
|
||||
'po',
|
||||
'nib',
|
||||
'woz'
|
||||
] as const;
|
||||
|
||||
export const BLOCK_FORMATS = [
|
||||
'2mg',
|
||||
'hdv',
|
||||
'po',
|
||||
] as const;
|
||||
|
||||
export const DISK_FORMATS = [...NIBBLE_FORMATS, ...BLOCK_FORMATS ] as const;
|
||||
|
||||
export type NibbleFormat = MemberOf<typeof NIBBLE_FORMATS>;
|
||||
export type BlockFormat = MemberOf<typeof BLOCK_FORMATS>;
|
||||
export type DiskFormat = MemberOf<typeof DISK_FORMATS>;
|
||||
|
||||
/**
|
||||
* Base format for JSON defined disks
|
||||
*/
|
||||
|
||||
export class JSONDiskBase {
|
||||
type: DiskFormat
|
||||
name: string
|
||||
disk?: number
|
||||
category?: string
|
||||
writeProtected?: boolean
|
||||
volume: byte
|
||||
readOnly: boolean
|
||||
gamepad?: GamepadConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Disk format with base64 encoded tracks
|
||||
*/
|
||||
|
||||
export interface Base64JSONDisk extends JSONDiskBase {
|
||||
encoding: 'base64'
|
||||
data: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Disk format with byte array tracks
|
||||
*/
|
||||
|
||||
export interface BinaryJSONDisk extends JSONDiskBase {
|
||||
encoding: 'binary'
|
||||
data: memory[][]
|
||||
}
|
||||
|
||||
/**
|
||||
* General JSON Disk format
|
||||
*/
|
||||
|
||||
export type JSONDisk = Base64JSONDisk | BinaryJSONDisk;
|
||||
|
||||
/**
|
||||
* Process Disk message payloads for worker
|
||||
*/
|
||||
|
||||
export const PROCESS_BINARY = 'PROCESS_BINARY';
|
||||
export const PROCESS_JSON_DISK = 'PROCESS_JSON_DISK';
|
||||
export const PROCESS_JSON = 'PROCESS_JSON';
|
||||
|
||||
/** Binary disk file message */
|
||||
export interface ProcessBinaryMessage {
|
||||
type: typeof PROCESS_BINARY
|
||||
payload: {
|
||||
drive: DriveNumber
|
||||
fmt: NibbleFormat
|
||||
options: DiskOptions
|
||||
}
|
||||
}
|
||||
|
||||
/** Processed JSON file message (used for localStorage) */
|
||||
export interface ProcessJsonDiskMessage {
|
||||
type: typeof PROCESS_JSON_DISK
|
||||
payload: {
|
||||
drive: DriveNumber
|
||||
jsonDisk: JSONDisk
|
||||
}
|
||||
}
|
||||
|
||||
/** Raw JSON file message */
|
||||
export interface ProcessJsonMessage {
|
||||
type: typeof PROCESS_JSON
|
||||
payload: {
|
||||
drive: DriveNumber
|
||||
json: string
|
||||
}
|
||||
}
|
||||
|
||||
export type FormatWorkerMessage =
|
||||
ProcessBinaryMessage |
|
||||
ProcessJsonDiskMessage |
|
||||
ProcessJsonMessage;
|
||||
|
||||
/**
|
||||
* Format work result message type
|
||||
*/
|
||||
|
||||
export const DISK_PROCESSED = 'DISK_PROCESSED';
|
||||
|
||||
export interface DiskProcessedResponse {
|
||||
type: typeof DISK_PROCESSED
|
||||
payload: {
|
||||
drive: DriveNumber
|
||||
disk: Disk | null
|
||||
}
|
||||
}
|
||||
|
||||
export type FormatWorkerResponse =
|
||||
DiskProcessedResponse
|
||||
|
||||
/**
|
||||
* Block device common interface
|
||||
*/
|
||||
export interface MassStorage {
|
||||
setBinary(drive: number, name: string, ext: BlockFormat, data: ArrayBuffer): boolean
|
||||
}
|
|
@ -10,13 +10,14 @@
|
|||
*/
|
||||
|
||||
import { debug, toHex } from '../util';
|
||||
import { ENCODING_BITSTREAM } from './types';
|
||||
|
||||
var WOZ_HEADER_START = 0;
|
||||
var WOZ_HEADER_SIZE = 12;
|
||||
const WOZ_HEADER_START = 0;
|
||||
const WOZ_HEADER_SIZE = 12;
|
||||
|
||||
var WOZ1_SIGNATURE = 0x315A4F57;
|
||||
var WOZ2_SIGNATURE = 0x325A4F57;
|
||||
var WOZ_INTEGRITY_CHECK = 0x0a0d0aff;
|
||||
const WOZ1_SIGNATURE = 0x315A4F57;
|
||||
const WOZ2_SIGNATURE = 0x325A4F57;
|
||||
const WOZ_INTEGRITY_CHECK = 0x0a0d0aff;
|
||||
|
||||
function stringFromBytes(data, start, end) {
|
||||
return String.fromCharCode.apply(
|
||||
|
@ -26,11 +27,11 @@ function stringFromBytes(data, start, end) {
|
|||
}
|
||||
|
||||
function grabNibble(bits, offset) {
|
||||
var nibble = 0;
|
||||
var waitForOne = true;
|
||||
let nibble = 0;
|
||||
let waitForOne = true;
|
||||
|
||||
while (offset < bits.length) {
|
||||
var bit = bits[offset];
|
||||
const bit = bits[offset];
|
||||
if (bit) {
|
||||
nibble = (nibble << 1) | 0x01;
|
||||
waitForOne = false;
|
||||
|
@ -79,7 +80,7 @@ function InfoChunk(data) {
|
|||
function TMapChunk(data) {
|
||||
this.trackMap = [];
|
||||
|
||||
for (var idx = 0; idx < 160; idx++) {
|
||||
for (let idx = 0; idx < 160; idx++) {
|
||||
this.trackMap.push(data.getUint8(idx));
|
||||
}
|
||||
|
||||
|
@ -87,29 +88,28 @@ function TMapChunk(data) {
|
|||
}
|
||||
|
||||
function TrksChunk(data) {
|
||||
var WOZ_TRACK_SIZE = 6656;
|
||||
var WOZ_TRACK_INFO_BITS = 6648;
|
||||
const WOZ_TRACK_SIZE = 6656;
|
||||
const WOZ_TRACK_INFO_BITS = 6648;
|
||||
|
||||
this.rawTracks = [];
|
||||
this.tracks = [];
|
||||
for (var trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) {
|
||||
var jdx;
|
||||
var track = [];
|
||||
var rawTrack = [];
|
||||
var slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE);
|
||||
var trackData = new Uint8Array(slice);
|
||||
var trackInfo = new DataView(slice);
|
||||
var trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true);
|
||||
for (jdx = 0; jdx < trackBitCount; jdx++) {
|
||||
var byteIndex = jdx >> 3;
|
||||
var bitIndex = 7 - (jdx & 0x07);
|
||||
for (let trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) {
|
||||
let track = [];
|
||||
const rawTrack = [];
|
||||
const slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE);
|
||||
const trackData = new Uint8Array(slice);
|
||||
const trackInfo = new DataView(slice);
|
||||
const trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true);
|
||||
for (let jdx = 0; jdx < trackBitCount; jdx++) {
|
||||
const byteIndex = jdx >> 3;
|
||||
const bitIndex = 7 - (jdx & 0x07);
|
||||
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1;
|
||||
}
|
||||
|
||||
track = [];
|
||||
var offset = 0;
|
||||
let offset = 0;
|
||||
while (offset < rawTrack.length) {
|
||||
var result = grabNibble(rawTrack, offset);
|
||||
const result = grabNibble(rawTrack, offset);
|
||||
if (!result.nibble) { break; }
|
||||
track.push(result.nibble);
|
||||
offset = result.offset + 1;
|
||||
|
@ -123,12 +123,12 @@ function TrksChunk(data) {
|
|||
}
|
||||
|
||||
function TrksChunk2(data) {
|
||||
var trackNo;
|
||||
let trackNo;
|
||||
this.trks = [];
|
||||
for (trackNo = 0; trackNo < 160; trackNo++) {
|
||||
var startBlock = data.getUint16(trackNo * 8, true);
|
||||
var blockCount = data.getUint16(trackNo * 8 + 2, true);
|
||||
var bitCount = data.getUint32(trackNo * 8 + 4, true);
|
||||
const startBlock = data.getUint16(trackNo * 8, true);
|
||||
const blockCount = data.getUint16(trackNo * 8 + 2, true);
|
||||
const bitCount = data.getUint32(trackNo * 8 + 4, true);
|
||||
if (bitCount === 0) { break; }
|
||||
this.trks.push({
|
||||
startBlock: startBlock,
|
||||
|
@ -139,42 +139,42 @@ function TrksChunk2(data) {
|
|||
this.tracks = [];
|
||||
this.rawTracks = [];
|
||||
|
||||
var bits = data.buffer;
|
||||
const bits = data.buffer;
|
||||
for (trackNo = 0; trackNo < this.trks.length; trackNo++) {
|
||||
var trk = this.trks[trackNo];
|
||||
var track = [];
|
||||
var rawTrack = [];
|
||||
var start = trk.startBlock * 512;
|
||||
var end = start + trk.blockCount * 512;
|
||||
var slice = bits.slice(start, end);
|
||||
var trackData = new Uint8Array(slice);
|
||||
for (var jdx = 0; jdx < trk.bitCount; jdx++) {
|
||||
var byteIndex = jdx >> 3;
|
||||
var bitIndex = 7 - (jdx & 0x07);
|
||||
const trk = this.trks[trackNo];
|
||||
let track = [];
|
||||
const rawTrack = [];
|
||||
const start = trk.startBlock * 512;
|
||||
const end = start + trk.blockCount * 512;
|
||||
const slice = bits.slice(start, end);
|
||||
const trackData = new Uint8Array(slice);
|
||||
for (let jdx = 0; jdx < trk.bitCount; jdx++) {
|
||||
const byteIndex = jdx >> 3;
|
||||
const bitIndex = 7 - (jdx & 0x07);
|
||||
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1;
|
||||
}
|
||||
|
||||
track = [];
|
||||
var offset = 0;
|
||||
let offset = 0;
|
||||
while (offset < rawTrack.length) {
|
||||
var result = grabNibble(rawTrack, offset);
|
||||
const result = grabNibble(rawTrack, offset);
|
||||
if (!result.nibble) { break; }
|
||||
track.push(result.nibble);
|
||||
offset = result.offset + 1;
|
||||
}
|
||||
|
||||
this.tracks[trackNo] = track;
|
||||
this.rawTracks[trackNo] = rawTrack;
|
||||
this.tracks[trackNo] = new Uint8Array(track);
|
||||
this.rawTracks[trackNo] = new Uint8Array(rawTrack);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
function MetaChunk(data) {
|
||||
var infoStr = stringFromBytes(data, 0, data.byteLength);
|
||||
var parts = infoStr.split('\n');
|
||||
var info = parts.reduce(function(acc, part) {
|
||||
var subParts = part.split('\t');
|
||||
const infoStr = stringFromBytes(data, 0, data.byteLength);
|
||||
const parts = infoStr.split('\n');
|
||||
const info = parts.reduce(function(acc, part) {
|
||||
const subParts = part.split('\t');
|
||||
acc[subParts[0]] = subParts[1];
|
||||
return acc;
|
||||
}, {});
|
||||
|
@ -189,19 +189,20 @@ function MetaChunk(data) {
|
|||
* @param {*} options the disk image and options
|
||||
* @returns {import('./format_utils').Disk}
|
||||
*/
|
||||
export default function Woz(options) {
|
||||
var { rawData } = options;
|
||||
var dv = new DataView(rawData, 0);
|
||||
var dvOffset = 0;
|
||||
var disk = {
|
||||
format: 'woz'
|
||||
export default function createDiskFromWoz(options) {
|
||||
const { rawData } = options;
|
||||
const dv = new DataView(rawData, 0);
|
||||
let dvOffset = 0;
|
||||
const disk = {
|
||||
format: 'woz',
|
||||
encoding: ENCODING_BITSTREAM,
|
||||
};
|
||||
|
||||
var wozVersion;
|
||||
var chunks = {};
|
||||
let wozVersion;
|
||||
const chunks = {};
|
||||
|
||||
function readHeader() {
|
||||
var wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true);
|
||||
const wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true);
|
||||
|
||||
switch (wozSignature) {
|
||||
case WOZ1_SIGNATURE:
|
||||
|
@ -226,9 +227,9 @@ export default function Woz(options) {
|
|||
return null;
|
||||
}
|
||||
|
||||
var type = dv.getUint32(dvOffset, true);
|
||||
var size = dv.getUint32(dvOffset + 4, true);
|
||||
var data = new DataView(dv.buffer, dvOffset + 8, size);
|
||||
const type = dv.getUint32(dvOffset, true);
|
||||
const size = dv.getUint32(dvOffset + 4, true);
|
||||
const data = new DataView(dv.buffer, dvOffset + 8, size);
|
||||
dvOffset += size + 8;
|
||||
|
||||
return {
|
||||
|
@ -240,7 +241,7 @@ export default function Woz(options) {
|
|||
|
||||
if (readHeader()) {
|
||||
dvOffset = WOZ_HEADER_SIZE;
|
||||
var chunk = readChunk();
|
||||
let chunk = readChunk();
|
||||
while (chunk) {
|
||||
switch (chunk.type) {
|
||||
case 0x4F464E49: // INFO
|
||||
|
@ -275,7 +276,7 @@ export default function Woz(options) {
|
|||
disk.tracks = chunks.trks.tracks;
|
||||
disk.rawTracks = chunks.trks.rawTracks;
|
||||
disk.readOnly = true; //chunks.info.writeProtected === 1;
|
||||
disk.name = chunks.info.title;
|
||||
disk.name = chunks.meta?.title || options.name;
|
||||
|
||||
return disk;
|
||||
}
|
||||
|
|
30
js/types.ts
30
js/types.ts
|
@ -1,4 +1,3 @@
|
|||
|
||||
/**
|
||||
* Extracts the members of a constant array as a type. Used as:
|
||||
*
|
||||
|
@ -101,35 +100,6 @@ export interface Card extends Memory, Restorable {
|
|||
ioSwitch(off: byte, val?: byte): byte | undefined;
|
||||
}
|
||||
|
||||
export const DISK_FORMATS = [
|
||||
'2mg',
|
||||
'd13',
|
||||
'do',
|
||||
'dsk',
|
||||
'hdv',
|
||||
'po',
|
||||
'nib',
|
||||
'woz'
|
||||
] as const;
|
||||
|
||||
export type DiskFormat = MemberOf<typeof DISK_FORMATS>;
|
||||
|
||||
export interface Drive {
|
||||
format: DiskFormat,
|
||||
volume: number,
|
||||
tracks: Array<byte[] | Uint8Array>,
|
||||
trackMap: unknown,
|
||||
}
|
||||
|
||||
export interface DiskIIDrive extends Drive {
|
||||
rawTracks: unknown,
|
||||
track: number,
|
||||
head: number,
|
||||
phase: number,
|
||||
readOnly: boolean,
|
||||
dirty: boolean,
|
||||
}
|
||||
|
||||
export type TapeData = Array<[duration: number, high: boolean]>;
|
||||
|
||||
export interface Restorable<T = any> {
|
||||
|
|
109
js/ui/apple2.ts
109
js/ui/apple2.ts
|
@ -3,22 +3,29 @@ import MicroModal from 'micromodal';
|
|||
import { base64_json_parse, base64_json_stringify } from '../base64';
|
||||
import { Audio, SOUND_ENABLED_OPTION } from './audio';
|
||||
import DriveLights from './drive_lights';
|
||||
import { byte, DISK_FORMATS, includes, word } from '../types';
|
||||
import { initGamepad, GamepadConfiguration } from './gamepad';
|
||||
import { byte, includes, word } from '../types';
|
||||
import { BLOCK_FORMATS, MassStorage, NIBBLE_FORMATS } from '../formats/types';
|
||||
import {
|
||||
DISK_FORMATS,
|
||||
DriveNumber,
|
||||
DRIVE_NUMBERS,
|
||||
JSONDisk
|
||||
} from '../formats/types';
|
||||
import { initGamepad } from './gamepad';
|
||||
import KeyBoard from './keyboard';
|
||||
import Tape, { TAPE_TYPES } from './tape';
|
||||
import type { GamepadConfiguration } from './types';
|
||||
|
||||
import ApplesoftDump from '../applesoft/decompiler';
|
||||
import ApplesoftCompiler from '../applesoft/compiler';
|
||||
|
||||
import { debug, gup, hup } from '../util';
|
||||
import { debug } from '../util';
|
||||
import { Apple2, Stats } from '../apple2';
|
||||
import DiskII, { DriveNumber, DRIVE_NUMBERS } from '../cards/disk2';
|
||||
import SmartPort from '../cards/smartport';
|
||||
import DiskII from '../cards/disk2';
|
||||
import CPU6502 from '../cpu6502';
|
||||
import { VideoModes } from '../videomodes';
|
||||
import Apple2IO from '../apple2io';
|
||||
import { JSONDisk } from '../formats/format_utils';
|
||||
import { } from '../formats/format_utils';
|
||||
import Printer from './printer';
|
||||
|
||||
import { OptionsModal } from './options_modal';
|
||||
|
@ -71,7 +78,7 @@ let stats: Stats;
|
|||
let vm: VideoModes;
|
||||
let tape: Tape;
|
||||
let _disk2: DiskII;
|
||||
let _smartPort: SmartPort;
|
||||
let _massStorage: MassStorage;
|
||||
let _printer: Printer;
|
||||
let audio: Audio;
|
||||
let screen: Screen;
|
||||
|
@ -98,9 +105,10 @@ export function compileAppleSoftProgram(program: string) {
|
|||
}
|
||||
|
||||
export function openLoad(driveString: string, event: MouseEvent) {
|
||||
const drive = parseInt(driveString, 10);
|
||||
const drive = parseInt(driveString, 10) as DriveNumber;
|
||||
_currentDrive = drive;
|
||||
if (event.metaKey && includes(DRIVE_NUMBERS, drive)) {
|
||||
openLoadHTTP(drive);
|
||||
openLoadHTTP();
|
||||
} else {
|
||||
if (disk_cur_cat[drive]) {
|
||||
const element = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||
|
@ -380,16 +388,25 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
|
|||
files[drive - 1] = '';
|
||||
document.location.hash = files.join('|');
|
||||
|
||||
if (result.byteLength >= 800 * 1024) {
|
||||
if (_smartPort.setBinary(drive, name, ext, result)) {
|
||||
initGamepad();
|
||||
}
|
||||
} else {
|
||||
if (includes(DISK_FORMATS, ext)
|
||||
&& _disk2.setBinary(drive, name, ext, result)) {
|
||||
initGamepad();
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
if (result.byteLength >= 800 * 1024) {
|
||||
if (
|
||||
includes(BLOCK_FORMATS, ext) &&
|
||||
_massStorage.setBinary(drive, name, ext, result)
|
||||
) {
|
||||
initGamepad();
|
||||
} else {
|
||||
openAlert(`Unable to load ${name}`);
|
||||
}
|
||||
} else {
|
||||
openAlert(`Unable to load ${name}`);
|
||||
if (
|
||||
includes(NIBBLE_FORMATS, ext) &&
|
||||
_disk2.setBinary(drive, name, ext, result)
|
||||
) {
|
||||
initGamepad();
|
||||
} else {
|
||||
openAlert(`Unable to load ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
loadingStop();
|
||||
|
@ -442,18 +459,24 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
|
|||
const fileParts = file.split('.');
|
||||
const ext = fileParts.pop()!.toLowerCase();
|
||||
const name = decodeURIComponent(fileParts.join('.'));
|
||||
if (data.byteLength >= 800 * 1024) {
|
||||
if (_smartPort.setBinary(drive, name, ext, data)) {
|
||||
initGamepad();
|
||||
}
|
||||
} else {
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
if (_disk2.setBinary(drive, name, ext, data)) {
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
if (data.byteLength >= 800 * 1024) {
|
||||
if (
|
||||
includes(BLOCK_FORMATS, ext) &&
|
||||
_massStorage.setBinary(drive, name, ext, data)
|
||||
) {
|
||||
initGamepad();
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Extension ${ext} not recognized.`);
|
||||
if (
|
||||
includes(NIBBLE_FORMATS, ext) &&
|
||||
_disk2.setBinary(drive, name, ext, data)
|
||||
) {
|
||||
initGamepad();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Extension ${ext} not recognized.`);
|
||||
}
|
||||
loadingStop();
|
||||
}).catch(function (error) {
|
||||
|
@ -463,8 +486,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function openLoadHTTP(drive: DriveNumber) {
|
||||
_currentDrive = drive;
|
||||
function openLoadHTTP() {
|
||||
MicroModal.show('http-modal');
|
||||
}
|
||||
|
||||
|
@ -685,7 +707,6 @@ const categorySelect = document.querySelector<HTMLSelectElement>('#category_sele
|
|||
declare global {
|
||||
interface Window {
|
||||
disk_index: DiskDescriptor[];
|
||||
e: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -808,7 +829,29 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: Printer, e: boolean) {
|
||||
/**
|
||||
* Returns the value of a query parameter or the empty string if it does not
|
||||
* exist.
|
||||
* @param name the parameter name. Note that `name` must not have any RegExp
|
||||
* meta-characters except '[' and ']' or it will fail.
|
||||
*/
|
||||
|
||||
function gup(name: string) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(name);
|
||||
}
|
||||
|
||||
/** Returns the URL fragment. */
|
||||
function hup() {
|
||||
const regex = new RegExp('#(.*)');
|
||||
const results = regex.exec(window.location.hash);
|
||||
if (!results)
|
||||
return '';
|
||||
else
|
||||
return results[1];
|
||||
}
|
||||
|
||||
function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) {
|
||||
_apple2 = apple2;
|
||||
cpu = _apple2.getCPU();
|
||||
io = _apple2.getIO();
|
||||
|
@ -816,7 +859,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
|
|||
vm = apple2.getVideoModes();
|
||||
tape = new Tape(io);
|
||||
_disk2 = disk2;
|
||||
_smartPort = smartPort;
|
||||
_massStorage = massStorage;
|
||||
_printer = printer;
|
||||
_e = e;
|
||||
|
||||
|
@ -905,8 +948,8 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
|
|||
);
|
||||
}
|
||||
|
||||
export function initUI(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: Printer, e: boolean) {
|
||||
export function initUI(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) {
|
||||
window.addEventListener('load', () => {
|
||||
onLoaded(apple2, disk2, smartPort, printer, e);
|
||||
onLoaded(apple2, disk2, massStorage, printer, e);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Callbacks, DriveNumber } from '../cards/disk2';
|
||||
import { Callbacks } from '../cards/disk2';
|
||||
import type { DriveNumber } from '../formats/types';
|
||||
|
||||
export default class DriveLights implements Callbacks {
|
||||
public driveLight(drive: DriveNumber, on: boolean) {
|
||||
|
|
|
@ -10,50 +10,10 @@
|
|||
*/
|
||||
|
||||
import Apple2IO from '../apple2io';
|
||||
import { KnownKeys } from '../types';
|
||||
import { BUTTON, ButtonType, GamepadConfiguration } from './types';
|
||||
|
||||
export let gamepad: Gamepad | null = null;
|
||||
|
||||
const BUTTON = {
|
||||
// Buttons
|
||||
'A': 0,
|
||||
'B': 1,
|
||||
'X': 2,
|
||||
'Y': 3,
|
||||
|
||||
// Triggers
|
||||
'L1': 4,
|
||||
'R1': 5,
|
||||
|
||||
// Analog stick buttons
|
||||
'L3': 6,
|
||||
'R3': 7,
|
||||
|
||||
// Special
|
||||
'START': 8,
|
||||
'SELECT': 9,
|
||||
'LOGO': 10,
|
||||
|
||||
// D pad
|
||||
'UP': 11,
|
||||
'DOWN': 12,
|
||||
'LEFT': 13,
|
||||
'RIGHT': 14
|
||||
} as const;
|
||||
|
||||
type ButtonType = KnownKeys<typeof BUTTON>;
|
||||
|
||||
/**
|
||||
* A `GamepadConfiguration` maps buttons on the controller to Apple Paddle
|
||||
* buttons or keys on the keyboard. If the value is a number, it must be
|
||||
* 0 | 1 | 2 and will map to the corresponding paddle button. If the value
|
||||
* is a string, the _first_ character of the string is used as a key to
|
||||
* press on the keyboard.
|
||||
*/
|
||||
export type GamepadConfiguration = {
|
||||
[K in ButtonType]?: 0 | 1 | 2 | string;
|
||||
};
|
||||
|
||||
const DEFAULT_GAMEPAD: GamepadConfiguration = {
|
||||
'A': 0,
|
||||
'B': 1,
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/* Copyright 2010-2021 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import type { KnownKeys } from '../types';
|
||||
|
||||
export const BUTTON = {
|
||||
// Buttons
|
||||
'A': 0,
|
||||
'B': 1,
|
||||
'X': 2,
|
||||
'Y': 3,
|
||||
|
||||
// Triggers
|
||||
'L1': 4,
|
||||
'R1': 5,
|
||||
|
||||
// Analog stick buttons
|
||||
'L3': 6,
|
||||
'R3': 7,
|
||||
|
||||
// Special
|
||||
'START': 8,
|
||||
'SELECT': 9,
|
||||
'LOGO': 10,
|
||||
|
||||
// D pad
|
||||
'UP': 11,
|
||||
'DOWN': 12,
|
||||
'LEFT': 13,
|
||||
'RIGHT': 14
|
||||
} as const;
|
||||
|
||||
export type ButtonType = KnownKeys<typeof BUTTON>;
|
||||
|
||||
/**
|
||||
* A `GamepadConfiguration` maps buttons on the controller to Apple Paddle
|
||||
* buttons or keys on the keyboard. If the value is a number, it must be
|
||||
* 0 | 1 | 2 and will map to the corresponding paddle button. If the value
|
||||
* is a string, the _first_ character of the string is used as a key to
|
||||
* press on the keyboard.
|
||||
*/
|
||||
export type GamepadConfiguration = {
|
||||
[K in ButtonType]?: 0 | 1 | 2 | string;
|
||||
};
|
21
js/util.ts
21
js/util.ts
|
@ -91,27 +91,6 @@ export function toBinary(v: byte) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a query parameter or the empty string if it does not
|
||||
* exist.
|
||||
* @param name the parameter name. Note that `name` must not have any RegExp
|
||||
* meta-characters except '[' and ']' or it will fail.
|
||||
*/
|
||||
export function gup(name: string) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(name);
|
||||
}
|
||||
|
||||
/** Returns the URL fragment. */
|
||||
export function hup() {
|
||||
const regex = new RegExp('#(.*)');
|
||||
const results = regex.exec(window.location.hash);
|
||||
if (!results)
|
||||
return '';
|
||||
else
|
||||
return results[1];
|
||||
}
|
||||
|
||||
/** Packs a 32-bit integer into a string in little-endian order. */
|
||||
export function numToString(num: number) {
|
||||
let result = '';
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"jest-image-snapshot": "^4.5.0",
|
||||
"node-forge": "^0.10.0",
|
||||
"raw-loader": "^4.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^26.5.0",
|
||||
"ts-loader": "^8.0.15",
|
||||
"typescript": "^4.1.3",
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
"version": "0.0.1",
|
||||
"description": "Apple II Emulator in JavaScript",
|
||||
"scripts": {
|
||||
"build": "webpack --mode=production",
|
||||
"build": "rimraf dist/* && webpack --mode=production",
|
||||
"dev": "webpack serve --mode=development",
|
||||
"index": "bin/index > json/disks/index.js",
|
||||
"lint": "eslint '**/*.js' '**/*.ts'",
|
||||
"start": "webpack serve --mode=development",
|
||||
"start": "webpack serve --mode=development --progress",
|
||||
"test": "jest"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -42,6 +42,7 @@
|
|||
"jest-image-snapshot": "^4.5.0",
|
||||
"node-forge": "^0.10.0",
|
||||
"raw-loader": "^4.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^26.5.0",
|
||||
"ts-loader": "^8.0.15",
|
||||
"typescript": "^4.1.3",
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { byte } from '../../../../js/types';
|
||||
import { memory } from '../../../../js/types';
|
||||
|
||||
function generateBytesInOrder() {
|
||||
const data: byte[][][] = [];
|
||||
const data: memory[][] = [];
|
||||
for (let t = 0; t < 35; t++) {
|
||||
const track: byte[][] = [];
|
||||
const track: memory[] = [];
|
||||
for (let s = 0; s < 13; s++) {
|
||||
const sector: byte[] = [];
|
||||
const sector: memory = new Uint8Array(256);
|
||||
for (let b = 0; b < 256; b++) {
|
||||
sector[b] = b;
|
||||
}
|
||||
|
@ -16,14 +16,14 @@ function generateBytesInOrder() {
|
|||
return data;
|
||||
}
|
||||
|
||||
export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder();
|
||||
export const BYTES_IN_ORDER: memory[][] = generateBytesInOrder();
|
||||
|
||||
function generateBytesBySector() {
|
||||
const data: byte[][][] = [];
|
||||
const data: memory[][] = [];
|
||||
for (let t = 0; t < 35; t++) {
|
||||
const track: byte[][] = [];
|
||||
const track: memory[] = [];
|
||||
for (let s = 0; s < 13; s++) {
|
||||
const sector: byte[] = [];
|
||||
const sector: memory = new Uint8Array(256);
|
||||
for (let b = 0; b < 256; b++) {
|
||||
sector[b] = s;
|
||||
}
|
||||
|
@ -34,14 +34,14 @@ function generateBytesBySector() {
|
|||
return data;
|
||||
}
|
||||
|
||||
export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector();
|
||||
export const BYTES_BY_SECTOR: memory[][] = generateBytesBySector();
|
||||
|
||||
function generateBytesByTrack() {
|
||||
const data: byte[][][] = [];
|
||||
const data: memory[][] = [];
|
||||
for (let t = 0; t < 35; t++) {
|
||||
const track: byte[][] = [];
|
||||
const track: memory[] = [];
|
||||
for (let s = 0; s < 13; s++) {
|
||||
const sector: byte[] = [];
|
||||
const sector: memory = new Uint8Array(256);
|
||||
for (let b = 0; b < 256; b++) {
|
||||
sector[b] = t;
|
||||
}
|
||||
|
@ -52,4 +52,4 @@ function generateBytesByTrack() {
|
|||
return data;
|
||||
}
|
||||
|
||||
export const BYTES_BY_TRACK: byte[][][] = generateBytesByTrack();
|
||||
export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack();
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { byte } from '../../../../js/types';
|
||||
import { memory } from '../../../../js/types';
|
||||
|
||||
function generateBytesInOrder() {
|
||||
const data: byte[][][] = [];
|
||||
const data: memory[][] = [];
|
||||
for (let t = 0; t < 35; t++) {
|
||||
const track: byte[][] = [];
|
||||
const track: memory[] = [];
|
||||
for (let s = 0; s < 16; s++) {
|
||||
const sector: byte[] = [];
|
||||
const sector = new Uint8Array(256);
|
||||
for (let b = 0; b < 256; b++) {
|
||||
sector[b] = b;
|
||||
}
|
||||
|
@ -16,14 +16,14 @@ function generateBytesInOrder() {
|
|||
return data;
|
||||
}
|
||||
|
||||
export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder();
|
||||
export const BYTES_IN_ORDER: memory[][] = generateBytesInOrder();
|
||||
|
||||
function generateBytesBySector() {
|
||||
const data: byte[][][] = [];
|
||||
const data: memory[][] = [];
|
||||
for (let t = 0; t < 35; t++) {
|
||||
const track: byte[][] = [];
|
||||
const track: memory[] = [];
|
||||
for (let s = 0; s < 16; s++) {
|
||||
const sector: byte[] = [];
|
||||
const sector = new Uint8Array(256);
|
||||
for (let b = 0; b < 256; b++) {
|
||||
sector[b] = s;
|
||||
}
|
||||
|
@ -34,14 +34,14 @@ function generateBytesBySector() {
|
|||
return data;
|
||||
}
|
||||
|
||||
export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector();
|
||||
export const BYTES_BY_SECTOR: memory[][] = generateBytesBySector();
|
||||
|
||||
function generateBytesByTrack() {
|
||||
const data: byte[][][] = [];
|
||||
const data: memory[][] = [];
|
||||
for (let t = 0; t < 35; t++) {
|
||||
const track: byte[][] = [];
|
||||
const track: memory[] = [];
|
||||
for (let s = 0; s < 16; s++) {
|
||||
const sector: byte[] = [];
|
||||
const sector = new Uint8Array(256);
|
||||
for (let b = 0; b < 256; b++) {
|
||||
sector[b] = t;
|
||||
}
|
||||
|
@ -52,4 +52,4 @@ function generateBytesByTrack() {
|
|||
return data;
|
||||
}
|
||||
|
||||
export const BYTES_BY_TRACK: byte[][][] = generateBytesByTrack();
|
||||
export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack();
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es6",
|
||||
"lib": ["DOM", "ES6"],
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const path = require('path');
|
||||
const { merge } = require('webpack-merge');
|
||||
|
||||
const baseConfig = {
|
||||
devtool: 'source-map',
|
||||
|
@ -16,22 +17,24 @@ const baseConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
publicPath: 'dist/',
|
||||
path: path.resolve('dist/'),
|
||||
filename: '[name].bundle.js',
|
||||
chunkFilename: '[name].bundle.js',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
const appConfig = merge(baseConfig,
|
||||
{
|
||||
...baseConfig,
|
||||
entry: {
|
||||
main2: path.resolve('js/entry2.js'),
|
||||
main2e: path.resolve('js/entry2e.js')
|
||||
},
|
||||
output: {
|
||||
path: path.resolve('dist/'),
|
||||
filename: '[name].bundle.js',
|
||||
chunkFilename: '[name].bundle.js',
|
||||
library: {
|
||||
name: 'Apple2',
|
||||
type: 'umd',
|
||||
|
@ -47,21 +50,34 @@ module.exports = [
|
|||
directory: __dirname,
|
||||
},
|
||||
devMiddleware: {
|
||||
publicPath: '/dist/',
|
||||
publicPath: 'dist/',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const workletConfig = merge(baseConfig,
|
||||
{
|
||||
...baseConfig,
|
||||
target: false,
|
||||
entry: {
|
||||
audio_worker: path.resolve('js/ui/audio_worker.ts')
|
||||
},
|
||||
output: {
|
||||
publicPath: '/dist/',
|
||||
path: path.resolve('dist/'),
|
||||
filename: '[name].bundle.js',
|
||||
globalObject: 'globalThis',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const workerConfig = merge(baseConfig,
|
||||
{
|
||||
target: false,
|
||||
entry: {
|
||||
format_worker: path.resolve('workers/format.worker.ts')
|
||||
},
|
||||
output: {
|
||||
globalObject: 'globalThis',
|
||||
},
|
||||
},
|
||||
];
|
||||
);
|
||||
|
||||
exports.default = [appConfig, workletConfig, workerConfig];
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/* Copyright 2021 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { debug } from '../js/util';
|
||||
import { jsonDecode } from '../js/formats/format_utils';
|
||||
import {
|
||||
createDisk,
|
||||
createDiskFromJsonDisk,
|
||||
} from '../js/formats/create_disk';
|
||||
import {
|
||||
FormatWorkerMessage,
|
||||
Disk,
|
||||
DiskProcessedResponse,
|
||||
DISK_PROCESSED,
|
||||
PROCESS_BINARY,
|
||||
PROCESS_JSON_DISK,
|
||||
PROCESS_JSON,
|
||||
} from '../js/formats/types';
|
||||
|
||||
debug('Worker loaded');
|
||||
|
||||
addEventListener('message', (message: MessageEvent<FormatWorkerMessage>) => {
|
||||
debug('Worker started', message.type);
|
||||
const data = message.data;
|
||||
const { drive } = data.payload;
|
||||
let disk: Disk | null = null;
|
||||
|
||||
switch (data.type) {
|
||||
case PROCESS_BINARY: {
|
||||
const { fmt, options } = data.payload;
|
||||
disk = createDisk(fmt, options);
|
||||
}
|
||||
break;
|
||||
|
||||
case PROCESS_JSON_DISK: {
|
||||
const { jsonDisk } = data.payload;
|
||||
disk = createDiskFromJsonDisk(jsonDisk);
|
||||
}
|
||||
break;
|
||||
|
||||
case PROCESS_JSON: {
|
||||
const { json } = data.payload;
|
||||
disk = jsonDecode(json);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const response: DiskProcessedResponse = {
|
||||
type: DISK_PROCESSED,
|
||||
payload: {
|
||||
drive,
|
||||
disk
|
||||
}
|
||||
};
|
||||
|
||||
self.postMessage(response);
|
||||
|
||||
debug('Worker complete', message.type);
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES6", "WebWorker"]
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue