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": {
|
"env": {
|
||||||
"commonjs": true
|
"commonjs": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"workers/*"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "workers/tsconfig.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ignorePatterns": ["coverage/**/*"]
|
"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.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { base64_decode, base64_encode} from '../base64';
|
import { base64_encode} from '../base64';
|
||||||
import { bit, byte, Card, DiskFormat, MemberOf, memory, nibble, rom } from '../types';
|
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 { 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 { 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';
|
import Apple2IO from '../apple2io';
|
||||||
|
|
||||||
|
|
||||||
/** Softswitch locations */
|
/** Softswitch locations */
|
||||||
const LOC = {
|
const LOC = {
|
||||||
// Disk II Controller Commands
|
// Disk II Controller Commands
|
||||||
|
@ -144,10 +164,6 @@ const PHASE_DELTA = [
|
||||||
[-2, -1, 0, 1],
|
[-2, -1, 0, 1],
|
||||||
[1, -2, -1, 0]
|
[1, -2, -1, 0]
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const DRIVE_NUMBERS = [1, 2] as const;
|
|
||||||
export type DriveNumber = MemberOf<typeof DRIVE_NUMBERS>;
|
|
||||||
|
|
||||||
export interface Callbacks {
|
export interface Callbacks {
|
||||||
driveLight: (drive: DriveNumber, on: boolean) => void;
|
driveLight: (drive: DriveNumber, on: boolean) => void;
|
||||||
dirty: (drive: DriveNumber, dirty: boolean) => void;
|
dirty: (drive: DriveNumber, dirty: boolean) => void;
|
||||||
|
@ -155,9 +171,10 @@ export interface Callbacks {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Common information for Nibble and WOZ disks. */
|
/** Common information for Nibble and WOZ disks. */
|
||||||
|
|
||||||
interface BaseDrive {
|
interface BaseDrive {
|
||||||
/** Current disk format. */
|
/** Current disk format. */
|
||||||
format: DiskFormat,
|
format: NibbleFormat,
|
||||||
/** Current disk volume number. */
|
/** Current disk volume number. */
|
||||||
volume: byte,
|
volume: byte,
|
||||||
/** Displayed disk name */
|
/** Displayed disk name */
|
||||||
|
@ -176,6 +193,8 @@ interface BaseDrive {
|
||||||
|
|
||||||
/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */
|
/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */
|
||||||
interface WozDrive extends BaseDrive {
|
interface WozDrive extends BaseDrive {
|
||||||
|
/** Woz encoding */
|
||||||
|
encoding: typeof ENCODING_BITSTREAM
|
||||||
/** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */
|
/** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */
|
||||||
trackMap: byte[];
|
trackMap: byte[];
|
||||||
/** Unique track bitstreams. The index is arbitrary; it is NOT the track number. */
|
/** 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. */
|
/** Nibble format track data. */
|
||||||
interface NibbleDrive extends BaseDrive {
|
interface NibbleDrive extends BaseDrive {
|
||||||
|
/** Nibble encoding */
|
||||||
|
encoding: typeof ENCODING_NIBBLE
|
||||||
/** Nibble data. The index is the track number. */
|
/** Nibble data. The index is the track number. */
|
||||||
tracks: memory[];
|
tracks: memory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Drive = WozDrive | NibbleDrive;
|
type Drive = WozDrive | NibbleDrive;
|
||||||
|
|
||||||
function isNibbleDrive(drive: Drive): drive is NibbleDrive {
|
function isNibbleDrive(drive: Drive): drive is NibbleDrive {
|
||||||
return 'tracks' in drive;
|
return drive.encoding === ENCODING_NIBBLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWozDrive(drive: Drive): drive is WozDrive {
|
||||||
|
return drive.encoding === ENCODING_BITSTREAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does not support WOZ disks
|
|
||||||
interface DriveState {
|
interface DriveState {
|
||||||
format: DiskFormat,
|
format: NibbleFormat,
|
||||||
|
encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE
|
||||||
volume: byte,
|
volume: byte,
|
||||||
name: string,
|
name: string,
|
||||||
tracks: memory[],
|
tracks: memory[],
|
||||||
|
@ -205,6 +230,8 @@ interface DriveState {
|
||||||
phase: Phase,
|
phase: Phase,
|
||||||
readOnly: boolean,
|
readOnly: boolean,
|
||||||
dirty: boolean,
|
dirty: boolean,
|
||||||
|
trackMap: number[],
|
||||||
|
rawTracks: bit[][],
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -216,10 +243,10 @@ interface State {
|
||||||
drive: DriveNumber;
|
drive: DriveNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(flan): Does not work for WOZ disks
|
|
||||||
function getDriveState(drive: Drive): DriveState {
|
function getDriveState(drive: Drive): DriveState {
|
||||||
const result: DriveState = {
|
const result: DriveState = {
|
||||||
format: drive.format,
|
format: drive.format,
|
||||||
|
encoding: drive.encoding,
|
||||||
volume: drive.volume,
|
volume: drive.volume,
|
||||||
name: drive.name,
|
name: drive.name,
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
@ -227,34 +254,61 @@ function getDriveState(drive: Drive): DriveState {
|
||||||
head: drive.head,
|
head: drive.head,
|
||||||
phase: drive.phase,
|
phase: drive.phase,
|
||||||
readOnly: drive.readOnly,
|
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++) {
|
if (isWozDrive(drive)) {
|
||||||
result.tracks.push(new Uint8Array(drive.tracks[idx]));
|
result.trackMap = [...drive.trackMap];
|
||||||
|
for (let idx = 0; idx < drive.rawTracks.length; idx++) {
|
||||||
|
result.rawTracks.push([...drive.rawTracks[idx]]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(flan): Does not work for WOZ disks
|
|
||||||
function setDriveState(state: DriveState) {
|
function setDriveState(state: DriveState) {
|
||||||
const result: Drive = {
|
let result: Drive;
|
||||||
format: state.format,
|
if (state.encoding === ENCODING_NIBBLE) {
|
||||||
volume: state.volume,
|
result = {
|
||||||
name: state.name,
|
format: state.format,
|
||||||
tracks: [] as memory[],
|
encoding: ENCODING_NIBBLE,
|
||||||
track: state.track,
|
volume: state.volume,
|
||||||
head: state.head,
|
name: state.name,
|
||||||
phase: state.phase,
|
tracks: [],
|
||||||
readOnly: state.readOnly,
|
track: state.track,
|
||||||
dirty: state.dirty
|
head: state.head,
|
||||||
};
|
phase: state.phase,
|
||||||
for (let idx = 0; idx < state.tracks.length; idx++) {
|
readOnly: state.readOnly,
|
||||||
result.tracks!.push(new Uint8Array(state.tracks[idx]));
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,6 +320,7 @@ export default class DiskII implements Card {
|
||||||
private drives: Drive[] = [
|
private drives: Drive[] = [
|
||||||
{ // Drive 1
|
{ // Drive 1
|
||||||
format: 'dsk',
|
format: 'dsk',
|
||||||
|
encoding: ENCODING_NIBBLE,
|
||||||
volume: 254,
|
volume: 254,
|
||||||
name: 'Disk 1',
|
name: 'Disk 1',
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
@ -273,10 +328,11 @@ export default class DiskII implements Card {
|
||||||
head: 0,
|
head: 0,
|
||||||
phase: 0,
|
phase: 0,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
dirty: false
|
dirty: false,
|
||||||
},
|
},
|
||||||
{ // Drive 2
|
{ // Drive 2
|
||||||
format: 'dsk',
|
format: 'dsk',
|
||||||
|
encoding: ENCODING_NIBBLE,
|
||||||
volume: 254,
|
volume: 254,
|
||||||
name: 'Disk 2',
|
name: 'Disk 2',
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
@ -284,7 +340,7 @@ export default class DiskII implements Card {
|
||||||
head: 0,
|
head: 0,
|
||||||
phase: 0,
|
phase: 0,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
dirty: false
|
dirty: false,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
private skip = 0;
|
private skip = 0;
|
||||||
|
@ -307,6 +363,9 @@ export default class DiskII implements Card {
|
||||||
/** Current drive object. */
|
/** Current drive object. */
|
||||||
private cur = this.drives[this.drive - 1];
|
private cur = this.drives[this.drive - 1];
|
||||||
|
|
||||||
|
/** Nibbles read this on cycle */
|
||||||
|
private nibbleCount = 0;
|
||||||
|
|
||||||
/** Q0-Q3: Coil states. */
|
/** Q0-Q3: Coil states. */
|
||||||
private q = [false, false, false, false];
|
private q = [false, false, false, false];
|
||||||
|
|
||||||
|
@ -328,37 +387,37 @@ export default class DiskII implements Card {
|
||||||
/** Contents of the P6 ROM. */
|
/** Contents of the P6 ROM. */
|
||||||
private sequencerRom: typeof SEQUENCER_ROM_16 | typeof SEQUENCER_ROM_13;
|
private sequencerRom: typeof SEQUENCER_ROM_16 | typeof SEQUENCER_ROM_13;
|
||||||
|
|
||||||
|
private worker: Worker;
|
||||||
|
|
||||||
/** Builds a new Disk ][ card. */
|
/** Builds a new Disk ][ card. */
|
||||||
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) {
|
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) {
|
||||||
|
this.debug('Disk ][');
|
||||||
|
|
||||||
this.lastCycles = this.io.cycles();
|
this.lastCycles = this.io.cycles();
|
||||||
this.bootstrapRom = this.sectors == 16 ? BOOTSTRAP_ROM_16 : BOOTSTRAP_ROM_13;
|
this.bootstrapRom = this.sectors == 16 ? BOOTSTRAP_ROM_16 : BOOTSTRAP_ROM_13;
|
||||||
this.sequencerRom = this.sectors == 16 ? SEQUENCER_ROM_16 : SEQUENCER_ROM_13;
|
this.sequencerRom = this.sectors == 16 ? SEQUENCER_ROM_16 : SEQUENCER_ROM_13;
|
||||||
|
|
||||||
this.init();
|
this.initWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
private debug(..._args: any[]) {
|
private debug(..._args: any[]) {
|
||||||
// debug.apply(this, arguments);
|
// debug.apply(this, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {
|
|
||||||
this.debug('Disk ][');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only used for WOZ disks
|
// Only used for WOZ disks
|
||||||
private moveHead() {
|
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();
|
const cycles = this.io.cycles();
|
||||||
|
|
||||||
// Spin the disk the number of elapsed cycles since last call
|
// Spin the disk the number of elapsed cycles since last call
|
||||||
let workCycles = (cycles - this.lastCycles) * 2;
|
let workCycles = (cycles - this.lastCycles) * 2;
|
||||||
this.lastCycles = cycles;
|
this.lastCycles = cycles;
|
||||||
|
|
||||||
|
if (!isWozDrive(this.cur)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const track: bit[] =
|
||||||
|
this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0];
|
||||||
|
|
||||||
while (workCycles-- > 0) {
|
while (workCycles-- > 0) {
|
||||||
let pulse: bit = 0;
|
let pulse: bit = 0;
|
||||||
if (this.clock == 4) {
|
if (this.clock == 4) {
|
||||||
|
@ -531,6 +590,7 @@ export default class DiskII implements Card {
|
||||||
this.debug('Drive Off');
|
this.debug('Drive Off');
|
||||||
this.on = false;
|
this.on = false;
|
||||||
this.callbacks.driveLight(this.drive, false);
|
this.callbacks.driveLight(this.drive, false);
|
||||||
|
this.debug('nibbles read', this.nibbleCount);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -543,6 +603,7 @@ export default class DiskII implements Card {
|
||||||
}
|
}
|
||||||
if (!this.on) {
|
if (!this.on) {
|
||||||
this.debug('Drive On');
|
this.debug('Drive On');
|
||||||
|
this.nibbleCount = 0;
|
||||||
this.on = true;
|
this.on = true;
|
||||||
this.lastCycles = this.io.cycles();
|
this.lastCycles = this.io.cycles();
|
||||||
this.callbacks.driveLight(this.drive, true);
|
this.callbacks.driveLight(this.drive, true);
|
||||||
|
@ -619,6 +680,9 @@ export default class DiskII implements Card {
|
||||||
// also cause conflicts with the disk controller commands.
|
// also cause conflicts with the disk controller commands.
|
||||||
if ((off & 0x01) === 0) {
|
if ((off & 0x01) === 0) {
|
||||||
result = this.latch;
|
result = this.latch;
|
||||||
|
if (result & 0x80) {
|
||||||
|
this.nibbleCount++;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result = 0;
|
result = 0;
|
||||||
}
|
}
|
||||||
|
@ -665,7 +729,6 @@ export default class DiskII implements Card {
|
||||||
this.moveHead();
|
this.moveHead();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(flan): Does not work for WOZ disks
|
|
||||||
getState() {
|
getState() {
|
||||||
const result = {
|
const result = {
|
||||||
drives: [] as DriveState[],
|
drives: [] as DriveState[],
|
||||||
|
@ -682,7 +745,6 @@ export default class DiskII implements Card {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(flan): Does not work for WOZ disks
|
|
||||||
setState(state: State) {
|
setState(state: State) {
|
||||||
this.skip = state.skip;
|
this.skip = state.skip;
|
||||||
this.latch = state.latch;
|
this.latch = state.latch;
|
||||||
|
@ -699,22 +761,17 @@ export default class DiskII implements Card {
|
||||||
this.cur = this.drives[this.drive - 1];
|
this.cur = this.drives[this.drive - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(flan): Does not work for WOZ disks
|
|
||||||
getMetadata(driveNo: DriveNumber) {
|
getMetadata(driveNo: DriveNumber) {
|
||||||
const drive = this.drives[driveNo - 1];
|
const drive = this.drives[driveNo - 1];
|
||||||
if (isNibbleDrive(drive)) {
|
return {
|
||||||
return {
|
format: drive.format,
|
||||||
format: drive.format,
|
volume: drive.volume,
|
||||||
volume: drive.volume,
|
track: drive.track,
|
||||||
track: drive.track,
|
head: drive.head,
|
||||||
head: drive.head,
|
phase: drive.phase,
|
||||||
phase: drive.phase,
|
readOnly: drive.readOnly,
|
||||||
readOnly: drive.readOnly,
|
dirty: drive.dirty
|
||||||
dirty: drive.dirty
|
};
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(flan): Does not work on WOZ disks
|
// 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. */
|
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
|
||||||
// TODO(flan): This implementation is not very safe.
|
// TODO(flan): This implementation is not very safe.
|
||||||
setDisk(drive: DriveNumber, disk: JSONDisk) {
|
setDisk(drive: DriveNumber, jsonDisk: JSONDisk) {
|
||||||
const fmt = disk.type;
|
if (this.worker) {
|
||||||
const readOnly = disk.readOnly;
|
const message: FormatWorkerMessage = {
|
||||||
const name = disk.name;
|
type: PROCESS_JSON_DISK,
|
||||||
|
payload: {
|
||||||
let data: memory[] | memory[][];
|
drive,
|
||||||
if (disk.encoding == 'base64') {
|
jsonDisk
|
||||||
data = [];
|
},
|
||||||
for (let t = 0; t < disk.data.length; t++) {
|
};
|
||||||
if (fmt == 'nib') {
|
this.worker.postMessage(message);
|
||||||
data[t] = base64_decode(disk.data[t] as string);
|
return true;
|
||||||
} else {
|
|
||||||
data[t] = [];
|
|
||||||
for (let s = 0; s < disk.data[t].length; s++) {
|
|
||||||
data[t][s] = base64_decode(disk.data[t][s] as string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} 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];
|
return false;
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getJSON(drive: DriveNumber, pretty: boolean = false) {
|
getJSON(drive: DriveNumber, pretty: boolean = false) {
|
||||||
|
@ -795,52 +817,78 @@ export default class DiskII implements Card {
|
||||||
return jsonEncode(cur, pretty);
|
return jsonEncode(cur, pretty);
|
||||||
}
|
}
|
||||||
|
|
||||||
setJSON(drive: DriveNumber, data: string) {
|
setJSON(drive: DriveNumber, json: string) {
|
||||||
const cur = this.drives[drive - 1];
|
if (this.worker) {
|
||||||
Object.assign(cur, jsonDecode(data));
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: ArrayBuffer) {
|
setBinary(drive: DriveNumber, name: string, fmt: NibbleFormat, rawData: ArrayBuffer) {
|
||||||
let disk;
|
|
||||||
const cur = this.drives[drive - 1];
|
|
||||||
const readOnly = false;
|
const readOnly = false;
|
||||||
const volume = 254;
|
const volume = 254;
|
||||||
const options = {
|
const options = {
|
||||||
name,
|
name,
|
||||||
rawData,
|
rawData,
|
||||||
readOnly,
|
readOnly,
|
||||||
volume
|
volume,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (fmt) {
|
if (this.worker) {
|
||||||
case '2mg':
|
const message: FormatWorkerMessage = {
|
||||||
disk = _2MG(options);
|
type: PROCESS_BINARY,
|
||||||
break;
|
payload: {
|
||||||
case 'd13':
|
drive,
|
||||||
disk = D13(options);
|
fmt,
|
||||||
break;
|
options,
|
||||||
case 'do':
|
}
|
||||||
case 'dsk':
|
};
|
||||||
disk = DOS(options);
|
this.worker.postMessage(message);
|
||||||
break;
|
|
||||||
case 'nib':
|
|
||||||
disk = Nibble(options);
|
|
||||||
break;
|
|
||||||
case 'po':
|
|
||||||
disk = ProDOS(options);
|
|
||||||
break;
|
|
||||||
case 'woz':
|
|
||||||
disk = Woz(options);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(cur, disk);
|
return true;
|
||||||
this.updateDirty(drive, true);
|
} else {
|
||||||
this.callbacks.label(this.drive, name);
|
const disk = createDisk(fmt, options);
|
||||||
return true;
|
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
|
// TODO(flan): Does not work with WOZ disks
|
||||||
|
|
|
@ -12,16 +12,13 @@
|
||||||
import { debug, toHex } from '../util';
|
import { debug, toHex } from '../util';
|
||||||
import { rom as smartPortRom } from '../roms/cards/smartport';
|
import { rom as smartPortRom } from '../roms/cards/smartport';
|
||||||
import { Card, Restorable, byte, word, rom } from '../types';
|
import { Card, Restorable, byte, word, rom } from '../types';
|
||||||
|
import { MassStorage, BlockDisk, ENCODING_BLOCK } from '../formats/types';
|
||||||
import CPU6502, { CpuState, flags } from '../cpu6502';
|
import CPU6502, { CpuState, flags } from '../cpu6502';
|
||||||
|
import { read2MGHeader } from '../formats/2mg';
|
||||||
type SmartDisk = Uint8Array[];
|
import createBlockDisk from '../formats/block';
|
||||||
|
|
||||||
interface BlockDevice {
|
|
||||||
blocks: Uint8Array
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SmartPortState {
|
export interface SmartPortState {
|
||||||
disks: SmartDisk[]
|
disks: BlockDisk[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SmartPortOptions {
|
export interface SmartPortOptions {
|
||||||
|
@ -100,10 +97,10 @@ const ADDRESS_LO = 0x44;
|
||||||
const BLOCK_LO = 0x46;
|
const BLOCK_LO = 0x46;
|
||||||
// const BLOCK_HI = 0x47;
|
// 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 rom: rom;
|
||||||
private disks: SmartDisk[] = [];
|
private disks: BlockDisk[] = [];
|
||||||
|
|
||||||
constructor(private cpu: CPU6502, options: SmartPortOptions) {
|
constructor(private cpu: CPU6502, options: SmartPortOptions) {
|
||||||
if (options?.block) {
|
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[]) {
|
private debug(..._args: any[]) {
|
||||||
// debug.apply(this, arguments);
|
// debug.apply(this, arguments);
|
||||||
}
|
}
|
||||||
|
@ -131,16 +121,16 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
/*
|
/*
|
||||||
* dumpBlock
|
* 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) + ': ';
|
result += toHex(idx << 4, 4) + ': ';
|
||||||
for (jdx = 0; jdx < 16; jdx++) {
|
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) {
|
if (jdx == 8) {
|
||||||
result += ' ';
|
result += ' ';
|
||||||
}
|
}
|
||||||
|
@ -148,7 +138,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
}
|
}
|
||||||
result += ' ';
|
result += ' ';
|
||||||
for (jdx = 0; jdx < 16; jdx++) {
|
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) {
|
if (jdx == 8) {
|
||||||
result += ' ';
|
result += ' ';
|
||||||
}
|
}
|
||||||
|
@ -162,14 +152,14 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
/*
|
/*
|
||||||
* getDeviceInfo
|
* getDeviceInfo
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getDeviceInfo(state: CpuState, drive: number) {
|
getDeviceInfo(state: CpuState, drive: number) {
|
||||||
if (this.disks[drive]) {
|
if (this.disks[drive]) {
|
||||||
const blocks = this.disks[drive].length;
|
const blocks = this.disks[drive].blocks.length;
|
||||||
state.x = blocks & 0xff;
|
state.x = blocks & 0xff;
|
||||||
state.y = blocks >> 8;
|
state.y = blocks >> 8;
|
||||||
|
|
||||||
|
@ -190,7 +180,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
this.debug('read buffer=' + buffer);
|
this.debug('read buffer=' + buffer);
|
||||||
this.debug('read block=$' + toHex(block));
|
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');
|
debug('Drive', drive, 'is empty');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -198,7 +188,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
// debug('read', '\n' + dumpBlock(drive, block));
|
// debug('read', '\n' + dumpBlock(drive, block));
|
||||||
|
|
||||||
for (let idx = 0; idx < 512; idx++) {
|
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);
|
buffer = buffer.inc(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,7 +205,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
this.debug('write buffer=' + buffer);
|
this.debug('write buffer=' + buffer);
|
||||||
this.debug('write block=$' + toHex(block));
|
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');
|
debug('Drive', drive, 'is empty');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -223,7 +213,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
// debug('write', '\n' + dumpBlock(drive, block));
|
// debug('write', '\n' + dumpBlock(drive, block));
|
||||||
|
|
||||||
for (let idx = 0; idx < 512; idx++) {
|
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);
|
buffer = buffer.inc(1);
|
||||||
}
|
}
|
||||||
state.a = 0;
|
state.a = 0;
|
||||||
|
@ -235,10 +225,10 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
formatDevice(state: CpuState, drive: number) {
|
formatDevice(state: CpuState, drive: number) {
|
||||||
for (let idx = 0; idx < this.disks[drive].length; idx++) {
|
for (let idx = 0; idx < this.disks[drive].blocks.length; idx++) {
|
||||||
this.disks[drive][idx] = new Uint8Array();
|
this.disks[drive].blocks[idx] = new Uint8Array();
|
||||||
for (let jdx = 0; jdx < 512; jdx++) {
|
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
|
default: // Unit 1
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0:
|
case 0:
|
||||||
blocks = this.disks[unit].length;
|
blocks = this.disks[unit].blocks.length;
|
||||||
buffer.writeByte(0xf0); // W/R Block device in drive
|
buffer.writeByte(0xf0); // W/R Block device in drive
|
||||||
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
|
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
|
||||||
buffer.inc(2).writeByte((blocks & 0xff00) >> 8);
|
buffer.inc(2).writeByte((blocks & 0xff00) >> 8);
|
||||||
|
@ -433,33 +423,53 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
||||||
getState() {
|
getState() {
|
||||||
return {
|
return {
|
||||||
disks: this.disks.map(
|
disks: this.disks.map(
|
||||||
(disk) => disk.map(
|
(disk) => {
|
||||||
(block) => new Uint8Array(block)
|
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) {
|
setState(state: SmartPortState) {
|
||||||
this.disks = state.disks.map(
|
this.disks = state.disks.map(
|
||||||
(disk) => disk.map(
|
(disk) => {
|
||||||
(block) => new Uint8Array(block)
|
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) {
|
setBinary(drive: number, name: string, fmt: string, rawData: ArrayBuffer) {
|
||||||
this.disks[drive] = [];
|
const volume = 254;
|
||||||
|
const readOnly = false;
|
||||||
if (fmt == '2mg') {
|
if (fmt == '2mg') {
|
||||||
data = data.slice(64);
|
const { bytes, offset } = read2MGHeader(rawData);
|
||||||
}
|
rawData = rawData.slice(offset, offset + bytes);
|
||||||
for (let idx = 0; idx < data.byteLength; idx += 512) {
|
|
||||||
this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512)));
|
|
||||||
}
|
}
|
||||||
|
const options = {
|
||||||
|
rawData,
|
||||||
|
name,
|
||||||
|
readOnly,
|
||||||
|
volume,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.disks[drive] = createBlockDisk(options);
|
||||||
|
|
||||||
return true;
|
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.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { DiskOptions, BlockDisk, ENCODING_BLOCK } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a `Disk` object for a block volume with block-ordered data.
|
* Returns a `Disk` object for a block volume with block-ordered data.
|
||||||
* @param {*} options the disk image and options
|
* @param options the disk image and options
|
||||||
* @returns {import('./format_utils').Disk}
|
|
||||||
*/
|
*/
|
||||||
export default function BlockVolume(options) {
|
export default function createBlockDisk(options: DiskOptions): BlockDisk {
|
||||||
var { rawData, readOnly, name } = options;
|
const { rawData, readOnly, name } = options;
|
||||||
var disk;
|
|
||||||
|
|
||||||
var blocks = [];
|
if (!rawData) {
|
||||||
blocks = [];
|
throw new Error('Requires rawData');
|
||||||
var offset = 0;
|
}
|
||||||
|
|
||||||
|
const blocks = [];
|
||||||
|
let offset = 0;
|
||||||
while (offset < rawData.byteLength) {
|
while (offset < rawData.byteLength) {
|
||||||
blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200)));
|
blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200)));
|
||||||
offset += 0x200;
|
offset += 0x200;
|
||||||
}
|
}
|
||||||
|
|
||||||
disk = {
|
const disk: BlockDisk = {
|
||||||
|
encoding: ENCODING_BLOCK,
|
||||||
blocks,
|
blocks,
|
||||||
name,
|
name,
|
||||||
readOnly,
|
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,50 +10,56 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { explodeSector13, D13O } from './format_utils';
|
import { explodeSector13, D13O } from './format_utils';
|
||||||
|
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a `Disk` object from DOS 3.2-ordered image data.
|
* Returns a `Disk` object from DOS 3.2-ordered image data.
|
||||||
* @param {*} options the disk image and options
|
* @param options the disk image and options
|
||||||
* @returns {import('./format_utils').Disk}
|
* @returns A nibblized disk
|
||||||
*/
|
*/
|
||||||
export default function DOS13(options) {
|
export default function createDiskFromDOS13(options: DiskOptions) {
|
||||||
var { data, name, rawData, volume, readOnly } = options;
|
const { data, name, rawData, volume, readOnly } = options;
|
||||||
var disk = {
|
const disk: NibbleDisk = {
|
||||||
format: 'd13',
|
format: 'd13',
|
||||||
|
encoding: ENCODING_NIBBLE,
|
||||||
name,
|
name,
|
||||||
volume,
|
volume,
|
||||||
readOnly,
|
readOnly,
|
||||||
tracks: [],
|
tracks: []
|
||||||
trackMap: null,
|
|
||||||
rawTracks: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!data && !rawData) {
|
||||||
|
throw new Error('data or rawData required');
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* DOS 13-sector disks have the physical sectors skewed on the track. The skew
|
* 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:
|
* between physical sectors is 10 (A), resulting in the following physical order:
|
||||||
*
|
*
|
||||||
* 0 A 7 4 1 B 8 5 2 C 9 6 3
|
* 0 A 7 4 1 B 8 5 2 C 9 6 3
|
||||||
*
|
*
|
||||||
* Note that because physical sector == logical sector, this works slightly
|
* Note that because physical sector == logical sector, this works slightly
|
||||||
* differently from the DOS and ProDOS nibblizers.
|
* differently from the DOS and ProDOS nibblizers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
for (var t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
var track = [];
|
let track: number[] = [];
|
||||||
for (var disk_sector = 0; disk_sector < 13; disk_sector++) {
|
for (let disk_sector = 0; disk_sector < 13; disk_sector++) {
|
||||||
var physical_sector = D13O[disk_sector];
|
const physical_sector = D13O[disk_sector];
|
||||||
var sector;
|
let sector: Uint8Array;
|
||||||
if (rawData) {
|
if (rawData) {
|
||||||
var off = (13 * t + physical_sector) * 256;
|
const off = (13 * t + physical_sector) * 256;
|
||||||
sector = new Uint8Array(rawData.slice(off, off + 256));
|
sector = new Uint8Array(rawData.slice(off, off + 256));
|
||||||
} else {
|
} else if (data) {
|
||||||
sector = data[t][physical_sector];
|
sector = data[t][physical_sector];
|
||||||
|
} else {
|
||||||
|
throw new Error('Requires data or rawData');
|
||||||
}
|
}
|
||||||
track = track.concat(
|
track = track.concat(
|
||||||
explodeSector13(volume, t, physical_sector, sector)
|
explodeSector13(volume, t, physical_sector, sector)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
disk.tracks.push(track);
|
disk.tracks.push(new Uint8Array(track));
|
||||||
}
|
}
|
||||||
|
|
||||||
return disk;
|
return disk;
|
|
@ -11,34 +11,37 @@
|
||||||
|
|
||||||
import { explodeSector16, DO } from './format_utils';
|
import { explodeSector16, DO } from './format_utils';
|
||||||
import { bytify } from '../util';
|
import { bytify } from '../util';
|
||||||
|
import { byte } from '../types';
|
||||||
|
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a `Disk` object from DOS-ordered image data.
|
* Returns a `Disk` object from DOS-ordered image data.
|
||||||
* @param {*} options the disk image and options
|
* @param options the disk image and options
|
||||||
* @returns {import('./format_utils').Disk}
|
* @returns A nibblized disk
|
||||||
*/
|
*/
|
||||||
export default function DOS(options) {
|
export default function createDiskFromDOS(options: DiskOptions): NibbleDisk {
|
||||||
var { data, name, rawData, volume, readOnly } = options;
|
const { data, name, rawData, volume, readOnly } = options;
|
||||||
var disk = {
|
const disk: NibbleDisk = {
|
||||||
format: 'dsk',
|
format: 'dsk',
|
||||||
|
encoding: ENCODING_NIBBLE,
|
||||||
name,
|
name,
|
||||||
volume,
|
volume,
|
||||||
readOnly,
|
readOnly,
|
||||||
tracks: [],
|
tracks: [],
|
||||||
trackMap: null,
|
|
||||||
rawTracks: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
var track = [];
|
let track: byte[] = [];
|
||||||
for (var physical_sector = 0; physical_sector < 16; physical_sector++) {
|
for (let physical_sector = 0; physical_sector < 16; physical_sector++) {
|
||||||
const dos_sector = DO[physical_sector];
|
const dos_sector = DO[physical_sector];
|
||||||
var sector;
|
let sector: Uint8Array;
|
||||||
if (rawData) {
|
if (rawData) {
|
||||||
const off = (16 * t + dos_sector) * 256;
|
const off = (16 * t + dos_sector) * 256;
|
||||||
sector = new Uint8Array(rawData.slice(off, off + 256));
|
sector = new Uint8Array(rawData.slice(off, off + 256));
|
||||||
|
} else if (data) {
|
||||||
|
sector = new Uint8Array(data[t][dos_sector]);
|
||||||
} else {
|
} else {
|
||||||
sector = data[t][dos_sector];
|
throw new Error('Requires data or rawData');
|
||||||
}
|
}
|
||||||
track = track.concat(
|
track = track.concat(
|
||||||
explodeSector16(volume, t, physical_sector, sector)
|
explodeSector16(volume, t, physical_sector, sector)
|
|
@ -9,64 +9,10 @@
|
||||||
* implied warranty.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { byte, DiskFormat, memory } from '../types';
|
import { byte, memory } from '../types';
|
||||||
import { base64_decode, base64_encode } from '../base64';
|
import { base64_decode, base64_encode } from '../base64';
|
||||||
import { bytify, debug, toHex } from '../util';
|
import { bytify, debug, toHex } from '../util';
|
||||||
import { GamepadConfiguration } from '../ui/gamepad';
|
import { NibbleDisk, ENCODING_NIBBLE } from './types';
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
|
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
|
||||||
|
@ -74,7 +20,7 @@ export interface Drive {
|
||||||
export const DO = [
|
export const DO = [
|
||||||
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
|
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
|
||||||
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
|
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
|
* DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
|
||||||
|
@ -82,7 +28,7 @@ export const DO = [
|
||||||
export const _DO = [
|
export const _DO = [
|
||||||
0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
|
0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
|
||||||
0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
|
0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
|
* ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
|
||||||
|
@ -90,7 +36,7 @@ export const _DO = [
|
||||||
export const PO = [
|
export const PO = [
|
||||||
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
|
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
|
||||||
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
|
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
|
* ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
|
||||||
|
@ -98,7 +44,7 @@ export const PO = [
|
||||||
export const _PO = [
|
export const _PO = [
|
||||||
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
||||||
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
|
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DOS 13-sector disk physical sector order (index is disk sector, value is
|
* DOS 13-sector disk physical sector order (index is disk sector, value is
|
||||||
|
@ -106,18 +52,18 @@ export const _PO = [
|
||||||
*/
|
*/
|
||||||
export const D13O = [
|
export const D13O = [
|
||||||
0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3
|
0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export const _D13O = [
|
export const _D13O = [
|
||||||
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc
|
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
const _trans53 = [
|
const _trans53 = [
|
||||||
0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
|
0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
|
||||||
0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
|
0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
|
||||||
0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef,
|
0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef,
|
||||||
0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
|
0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
const _trans62 = [
|
const _trans62 = [
|
||||||
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
|
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
|
||||||
|
@ -128,7 +74,7 @@ const _trans62 = [
|
||||||
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
|
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
|
||||||
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
|
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
|
||||||
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
|
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export const detrans62 = [
|
export const detrans62 = [
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
@ -147,10 +93,13 @@ export const detrans62 = [
|
||||||
0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
||||||
0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
||||||
0x00, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
|
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] {
|
export function fourXfour(val: byte): [xx: byte, yy: byte] {
|
||||||
let xx = val & 0xaa;
|
let xx = val & 0xaa;
|
||||||
|
@ -163,11 +112,28 @@ export function fourXfour(val: byte): [xx: byte, yy: byte] {
|
||||||
return [xx, yy];
|
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 {
|
export function defourXfour(xx: byte, yy: byte): byte {
|
||||||
return ((xx << 1) | 0x01) & yy;
|
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 buf = [];
|
||||||
let gap;
|
let gap;
|
||||||
|
|
||||||
|
@ -255,7 +221,17 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m
|
||||||
return buf;
|
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 buf = [];
|
||||||
let gap;
|
let gap;
|
||||||
|
|
||||||
|
@ -354,13 +330,22 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: b
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(flan): Does not work on WOZ disks
|
/**
|
||||||
export function readSector(drive: Drive, track: byte, sector: byte) {
|
* Reads a sector of data from a nibblized disk
|
||||||
const _sector = drive.format == 'po' ? _PO[sector] : _DO[sector];
|
*
|
||||||
|
* 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 val, state = 0;
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
let retry = 0;
|
let retry = 0;
|
||||||
const cur = drive.tracks[track];
|
const cur = disk.tracks[track];
|
||||||
|
|
||||||
function _readNext() {
|
function _readNext() {
|
||||||
const result = cur[idx++];
|
const result = cur[idx++];
|
||||||
|
@ -450,32 +435,46 @@ export function readSector(drive: Drive, track: byte, sector: byte) {
|
||||||
return new Uint8Array();
|
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,
|
// For 'nib', tracks are encoded as strings. For all other formats,
|
||||||
// tracks are arrays of sectors which are encoded as strings.
|
// tracks are arrays of sectors which are encoded as strings.
|
||||||
const data: string[] | string[][] = [];
|
const data: string[] | string[][] = [];
|
||||||
let format = 'dsk';
|
let format = 'dsk';
|
||||||
for (let t = 0; t < cur.tracks.length; t++) {
|
for (let t = 0; t < disk.tracks.length; t++) {
|
||||||
data[t] = [];
|
data[t] = [];
|
||||||
if (cur.format === 'nib') {
|
if (disk.format === 'nib') {
|
||||||
format = 'nib';
|
format = 'nib';
|
||||||
data[t] = base64_encode(cur.tracks[t]);
|
data[t] = base64_encode(disk.tracks[t]);
|
||||||
} else {
|
} else {
|
||||||
for (let s = 0; s < 0x10; s++) {
|
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({
|
return JSON.stringify({
|
||||||
'type': format,
|
'type': format,
|
||||||
'encoding': 'base64',
|
'encoding': 'base64',
|
||||||
'volume': cur.volume,
|
'volume': disk.volume,
|
||||||
'data': data,
|
'data': data,
|
||||||
'readOnly': cur.readOnly,
|
'readOnly': disk.readOnly,
|
||||||
}, undefined, pretty ? ' ' : undefined);
|
}, 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 tracks: memory[] = [];
|
||||||
const json = JSON.parse(data);
|
const json = JSON.parse(data);
|
||||||
const v = json.volume;
|
const v = json.volume;
|
||||||
|
@ -483,20 +482,78 @@ export function jsonDecode(data: string) {
|
||||||
for (let t = 0; t < json.data.length; t++) {
|
for (let t = 0; t < json.data.length; t++) {
|
||||||
let track: byte[] = [];
|
let track: byte[] = [];
|
||||||
for (let s = 0; s < json.data[t].length; s++) {
|
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 sector: string = json.data[t][_s];
|
||||||
const d = base64_decode(sector);
|
const d = base64_decode(sector);
|
||||||
track = track.concat(explodeSector16(v, t, s, d));
|
track = track.concat(explodeSector16(v, t, s, d));
|
||||||
}
|
}
|
||||||
tracks[t] = bytify(track);
|
tracks[t] = bytify(track);
|
||||||
}
|
}
|
||||||
const cur: Drive = {
|
const disk: NibbleDisk = {
|
||||||
volume: v,
|
volume: v,
|
||||||
format: json.type,
|
format: json.type,
|
||||||
|
encoding: ENCODING_NIBBLE,
|
||||||
|
name: json.name,
|
||||||
tracks,
|
tracks,
|
||||||
readOnly,
|
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.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
|
||||||
|
import { memory } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a `Disk` object from raw nibble image data.
|
* Returns a `Disk` object from raw nibble image data.
|
||||||
* @param {*} options the disk image and options
|
* @param options the disk image and options
|
||||||
* @returns {import('./format_utils').Disk}
|
* @returns A nibblized disk
|
||||||
*/
|
*/
|
||||||
export default function Nibble(options) {
|
export default function createDiskFromNibble(options: DiskOptions): NibbleDisk {
|
||||||
var { data, name, rawData, volume, readOnly } = options;
|
const { data, name, rawData, volume, readOnly } = options;
|
||||||
var disk = {
|
const disk: NibbleDisk = {
|
||||||
format: 'nib',
|
format: 'nib',
|
||||||
|
encoding: ENCODING_NIBBLE,
|
||||||
name,
|
name,
|
||||||
volume: volume || 254,
|
volume: volume || 254,
|
||||||
readOnly: readOnly || false,
|
readOnly: readOnly || false,
|
||||||
tracks: [],
|
tracks: []
|
||||||
trackMap: null,
|
|
||||||
rawTracks: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
var track;
|
let track: memory;
|
||||||
if (rawData) {
|
if (rawData) {
|
||||||
var off = t * 0x1a00;
|
const off = t * 0x1a00;
|
||||||
track = new Uint8Array(data.slice(off, off + 0x1a00));
|
track = new Uint8Array(rawData.slice(off, off + 0x1a00));
|
||||||
|
} else if (data) {
|
||||||
|
track = data[t][0];
|
||||||
} else {
|
} else {
|
||||||
track = data[t];
|
throw new Error('Requires data or rawData');
|
||||||
}
|
}
|
||||||
disk.tracks[t] = track;
|
disk.tracks[t] = track;
|
||||||
}
|
}
|
|
@ -11,34 +11,37 @@
|
||||||
|
|
||||||
import { explodeSector16, PO } from './format_utils';
|
import { explodeSector16, PO } from './format_utils';
|
||||||
import { bytify } from '../util';
|
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.
|
* Returns a `Disk` object from ProDOS-ordered image data.
|
||||||
* @param {*} options the disk image and options
|
* @param options the disk image and options
|
||||||
* @returns {import('./format_utils').Disk}
|
* @returns A nibblized disk
|
||||||
*/
|
*/
|
||||||
export default function ProDOS(options) {
|
export default function createDiskFromProDOS(options: DiskOptions) {
|
||||||
var { data, name, rawData, volume, readOnly } = options;
|
const { data, name, rawData, volume, readOnly } = options;
|
||||||
var disk = {
|
const disk: NibbleDisk = {
|
||||||
format: 'nib',
|
format: 'nib',
|
||||||
|
encoding: ENCODING_NIBBLE,
|
||||||
name,
|
name,
|
||||||
volume: volume || 254,
|
volume: volume || 254,
|
||||||
tracks: [],
|
tracks: [],
|
||||||
readOnly: readOnly || false,
|
readOnly: readOnly || false,
|
||||||
trackMap: null,
|
|
||||||
rawTracks: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var physical_track = 0; physical_track < 35; physical_track++) {
|
for (let physical_track = 0; physical_track < 35; physical_track++) {
|
||||||
var track = [];
|
let track: byte[] = [];
|
||||||
for (var physical_sector = 0; physical_sector < 16; physical_sector++) {
|
for (let physical_sector = 0; physical_sector < 16; physical_sector++) {
|
||||||
const prodos_sector = PO[physical_sector];
|
const prodos_sector = PO[physical_sector];
|
||||||
var sector;
|
let sector;
|
||||||
if (rawData) {
|
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));
|
sector = new Uint8Array(rawData.slice(off, off + 256));
|
||||||
} else {
|
} else if (data) {
|
||||||
sector = data[physical_track][prodos_sector];
|
sector = data[physical_track][prodos_sector];
|
||||||
|
} else {
|
||||||
|
throw new Error('Requires data or rawData');
|
||||||
}
|
}
|
||||||
track = track.concat(
|
track = track.concat(
|
||||||
explodeSector16(volume, physical_track, physical_sector, sector)
|
explodeSector16(volume, physical_track, physical_sector, sector)
|
|
@ -18,6 +18,10 @@ export function ProDOSVolume(disk) {
|
||||||
var _bitMap;
|
var _bitMap;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
disk() {
|
||||||
|
return _disk;
|
||||||
|
},
|
||||||
|
|
||||||
blocks() {
|
blocks() {
|
||||||
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 { debug, toHex } from '../util';
|
||||||
|
import { ENCODING_BITSTREAM } from './types';
|
||||||
|
|
||||||
var WOZ_HEADER_START = 0;
|
const WOZ_HEADER_START = 0;
|
||||||
var WOZ_HEADER_SIZE = 12;
|
const WOZ_HEADER_SIZE = 12;
|
||||||
|
|
||||||
var WOZ1_SIGNATURE = 0x315A4F57;
|
const WOZ1_SIGNATURE = 0x315A4F57;
|
||||||
var WOZ2_SIGNATURE = 0x325A4F57;
|
const WOZ2_SIGNATURE = 0x325A4F57;
|
||||||
var WOZ_INTEGRITY_CHECK = 0x0a0d0aff;
|
const WOZ_INTEGRITY_CHECK = 0x0a0d0aff;
|
||||||
|
|
||||||
function stringFromBytes(data, start, end) {
|
function stringFromBytes(data, start, end) {
|
||||||
return String.fromCharCode.apply(
|
return String.fromCharCode.apply(
|
||||||
|
@ -26,11 +27,11 @@ function stringFromBytes(data, start, end) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function grabNibble(bits, offset) {
|
function grabNibble(bits, offset) {
|
||||||
var nibble = 0;
|
let nibble = 0;
|
||||||
var waitForOne = true;
|
let waitForOne = true;
|
||||||
|
|
||||||
while (offset < bits.length) {
|
while (offset < bits.length) {
|
||||||
var bit = bits[offset];
|
const bit = bits[offset];
|
||||||
if (bit) {
|
if (bit) {
|
||||||
nibble = (nibble << 1) | 0x01;
|
nibble = (nibble << 1) | 0x01;
|
||||||
waitForOne = false;
|
waitForOne = false;
|
||||||
|
@ -79,7 +80,7 @@ function InfoChunk(data) {
|
||||||
function TMapChunk(data) {
|
function TMapChunk(data) {
|
||||||
this.trackMap = [];
|
this.trackMap = [];
|
||||||
|
|
||||||
for (var idx = 0; idx < 160; idx++) {
|
for (let idx = 0; idx < 160; idx++) {
|
||||||
this.trackMap.push(data.getUint8(idx));
|
this.trackMap.push(data.getUint8(idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,29 +88,28 @@ function TMapChunk(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TrksChunk(data) {
|
function TrksChunk(data) {
|
||||||
var WOZ_TRACK_SIZE = 6656;
|
const WOZ_TRACK_SIZE = 6656;
|
||||||
var WOZ_TRACK_INFO_BITS = 6648;
|
const WOZ_TRACK_INFO_BITS = 6648;
|
||||||
|
|
||||||
this.rawTracks = [];
|
this.rawTracks = [];
|
||||||
this.tracks = [];
|
this.tracks = [];
|
||||||
for (var trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) {
|
for (let trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) {
|
||||||
var jdx;
|
let track = [];
|
||||||
var track = [];
|
const rawTrack = [];
|
||||||
var rawTrack = [];
|
const slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE);
|
||||||
var slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE);
|
const trackData = new Uint8Array(slice);
|
||||||
var trackData = new Uint8Array(slice);
|
const trackInfo = new DataView(slice);
|
||||||
var trackInfo = new DataView(slice);
|
const trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true);
|
||||||
var trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true);
|
for (let jdx = 0; jdx < trackBitCount; jdx++) {
|
||||||
for (jdx = 0; jdx < trackBitCount; jdx++) {
|
const byteIndex = jdx >> 3;
|
||||||
var byteIndex = jdx >> 3;
|
const bitIndex = 7 - (jdx & 0x07);
|
||||||
var bitIndex = 7 - (jdx & 0x07);
|
|
||||||
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1;
|
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1;
|
||||||
}
|
}
|
||||||
|
|
||||||
track = [];
|
track = [];
|
||||||
var offset = 0;
|
let offset = 0;
|
||||||
while (offset < rawTrack.length) {
|
while (offset < rawTrack.length) {
|
||||||
var result = grabNibble(rawTrack, offset);
|
const result = grabNibble(rawTrack, offset);
|
||||||
if (!result.nibble) { break; }
|
if (!result.nibble) { break; }
|
||||||
track.push(result.nibble);
|
track.push(result.nibble);
|
||||||
offset = result.offset + 1;
|
offset = result.offset + 1;
|
||||||
|
@ -123,12 +123,12 @@ function TrksChunk(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TrksChunk2(data) {
|
function TrksChunk2(data) {
|
||||||
var trackNo;
|
let trackNo;
|
||||||
this.trks = [];
|
this.trks = [];
|
||||||
for (trackNo = 0; trackNo < 160; trackNo++) {
|
for (trackNo = 0; trackNo < 160; trackNo++) {
|
||||||
var startBlock = data.getUint16(trackNo * 8, true);
|
const startBlock = data.getUint16(trackNo * 8, true);
|
||||||
var blockCount = data.getUint16(trackNo * 8 + 2, true);
|
const blockCount = data.getUint16(trackNo * 8 + 2, true);
|
||||||
var bitCount = data.getUint32(trackNo * 8 + 4, true);
|
const bitCount = data.getUint32(trackNo * 8 + 4, true);
|
||||||
if (bitCount === 0) { break; }
|
if (bitCount === 0) { break; }
|
||||||
this.trks.push({
|
this.trks.push({
|
||||||
startBlock: startBlock,
|
startBlock: startBlock,
|
||||||
|
@ -139,42 +139,42 @@ function TrksChunk2(data) {
|
||||||
this.tracks = [];
|
this.tracks = [];
|
||||||
this.rawTracks = [];
|
this.rawTracks = [];
|
||||||
|
|
||||||
var bits = data.buffer;
|
const bits = data.buffer;
|
||||||
for (trackNo = 0; trackNo < this.trks.length; trackNo++) {
|
for (trackNo = 0; trackNo < this.trks.length; trackNo++) {
|
||||||
var trk = this.trks[trackNo];
|
const trk = this.trks[trackNo];
|
||||||
var track = [];
|
let track = [];
|
||||||
var rawTrack = [];
|
const rawTrack = [];
|
||||||
var start = trk.startBlock * 512;
|
const start = trk.startBlock * 512;
|
||||||
var end = start + trk.blockCount * 512;
|
const end = start + trk.blockCount * 512;
|
||||||
var slice = bits.slice(start, end);
|
const slice = bits.slice(start, end);
|
||||||
var trackData = new Uint8Array(slice);
|
const trackData = new Uint8Array(slice);
|
||||||
for (var jdx = 0; jdx < trk.bitCount; jdx++) {
|
for (let jdx = 0; jdx < trk.bitCount; jdx++) {
|
||||||
var byteIndex = jdx >> 3;
|
const byteIndex = jdx >> 3;
|
||||||
var bitIndex = 7 - (jdx & 0x07);
|
const bitIndex = 7 - (jdx & 0x07);
|
||||||
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1;
|
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1;
|
||||||
}
|
}
|
||||||
|
|
||||||
track = [];
|
track = [];
|
||||||
var offset = 0;
|
let offset = 0;
|
||||||
while (offset < rawTrack.length) {
|
while (offset < rawTrack.length) {
|
||||||
var result = grabNibble(rawTrack, offset);
|
const result = grabNibble(rawTrack, offset);
|
||||||
if (!result.nibble) { break; }
|
if (!result.nibble) { break; }
|
||||||
track.push(result.nibble);
|
track.push(result.nibble);
|
||||||
offset = result.offset + 1;
|
offset = result.offset + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tracks[trackNo] = track;
|
this.tracks[trackNo] = new Uint8Array(track);
|
||||||
this.rawTracks[trackNo] = rawTrack;
|
this.rawTracks[trackNo] = new Uint8Array(rawTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetaChunk(data) {
|
function MetaChunk(data) {
|
||||||
var infoStr = stringFromBytes(data, 0, data.byteLength);
|
const infoStr = stringFromBytes(data, 0, data.byteLength);
|
||||||
var parts = infoStr.split('\n');
|
const parts = infoStr.split('\n');
|
||||||
var info = parts.reduce(function(acc, part) {
|
const info = parts.reduce(function(acc, part) {
|
||||||
var subParts = part.split('\t');
|
const subParts = part.split('\t');
|
||||||
acc[subParts[0]] = subParts[1];
|
acc[subParts[0]] = subParts[1];
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -189,19 +189,20 @@ function MetaChunk(data) {
|
||||||
* @param {*} options the disk image and options
|
* @param {*} options the disk image and options
|
||||||
* @returns {import('./format_utils').Disk}
|
* @returns {import('./format_utils').Disk}
|
||||||
*/
|
*/
|
||||||
export default function Woz(options) {
|
export default function createDiskFromWoz(options) {
|
||||||
var { rawData } = options;
|
const { rawData } = options;
|
||||||
var dv = new DataView(rawData, 0);
|
const dv = new DataView(rawData, 0);
|
||||||
var dvOffset = 0;
|
let dvOffset = 0;
|
||||||
var disk = {
|
const disk = {
|
||||||
format: 'woz'
|
format: 'woz',
|
||||||
|
encoding: ENCODING_BITSTREAM,
|
||||||
};
|
};
|
||||||
|
|
||||||
var wozVersion;
|
let wozVersion;
|
||||||
var chunks = {};
|
const chunks = {};
|
||||||
|
|
||||||
function readHeader() {
|
function readHeader() {
|
||||||
var wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true);
|
const wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true);
|
||||||
|
|
||||||
switch (wozSignature) {
|
switch (wozSignature) {
|
||||||
case WOZ1_SIGNATURE:
|
case WOZ1_SIGNATURE:
|
||||||
|
@ -226,9 +227,9 @@ export default function Woz(options) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var type = dv.getUint32(dvOffset, true);
|
const type = dv.getUint32(dvOffset, true);
|
||||||
var size = dv.getUint32(dvOffset + 4, true);
|
const size = dv.getUint32(dvOffset + 4, true);
|
||||||
var data = new DataView(dv.buffer, dvOffset + 8, size);
|
const data = new DataView(dv.buffer, dvOffset + 8, size);
|
||||||
dvOffset += size + 8;
|
dvOffset += size + 8;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -240,7 +241,7 @@ export default function Woz(options) {
|
||||||
|
|
||||||
if (readHeader()) {
|
if (readHeader()) {
|
||||||
dvOffset = WOZ_HEADER_SIZE;
|
dvOffset = WOZ_HEADER_SIZE;
|
||||||
var chunk = readChunk();
|
let chunk = readChunk();
|
||||||
while (chunk) {
|
while (chunk) {
|
||||||
switch (chunk.type) {
|
switch (chunk.type) {
|
||||||
case 0x4F464E49: // INFO
|
case 0x4F464E49: // INFO
|
||||||
|
@ -275,7 +276,7 @@ export default function Woz(options) {
|
||||||
disk.tracks = chunks.trks.tracks;
|
disk.tracks = chunks.trks.tracks;
|
||||||
disk.rawTracks = chunks.trks.rawTracks;
|
disk.rawTracks = chunks.trks.rawTracks;
|
||||||
disk.readOnly = true; //chunks.info.writeProtected === 1;
|
disk.readOnly = true; //chunks.info.writeProtected === 1;
|
||||||
disk.name = chunks.info.title;
|
disk.name = chunks.meta?.title || options.name;
|
||||||
|
|
||||||
return disk;
|
return disk;
|
||||||
}
|
}
|
||||||
|
|
36
js/types.ts
36
js/types.ts
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the members of a constant array as a type. Used as:
|
* Extracts the members of a constant array as a type. Used as:
|
||||||
*
|
*
|
||||||
|
@ -11,7 +10,7 @@ export type MemberOf<T extends ReadonlyArray<unknown>> =
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively extracts all members of a constant array as a type. Used as:
|
* Recursively extracts all members of a constant array as a type. Used as:
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const SOME_ARRAYS = [['a'],['b', 2], 3] as const;
|
* const SOME_ARRAYS = [['a'],['b', 2], 3] as const;
|
||||||
* type SomeArrayValues = DeepMemberOf<typeof SOME_ARRAYS>; // 'a' | 'b' | 2 | 3
|
* type SomeArrayValues = DeepMemberOf<typeof SOME_ARRAYS>; // 'a' | 'b' | 2 | 3
|
||||||
|
@ -23,7 +22,7 @@ export type DeepMemberOf<T extends ReadonlyArray<unknown>> =
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the declared keys of a type by removing `string` and `number`.
|
* Extracts the declared keys of a type by removing `string` and `number`.
|
||||||
*
|
*
|
||||||
* Cribbed from the interwebs:
|
* Cribbed from the interwebs:
|
||||||
* https://github.com/microsoft/TypeScript/issues/25987#issuecomment-408339599
|
* https://github.com/microsoft/TypeScript/issues/25987#issuecomment-408339599
|
||||||
*/
|
*/
|
||||||
|
@ -39,7 +38,7 @@ export type KnownValues<T> = T extends {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replacement for `includes` on constant types that is also a type assertion.
|
* Replacement for `includes` on constant types that is also a type assertion.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const SOME_VALUES = [1, 2, 'a'] as const;
|
* const SOME_VALUES = [1, 2, 'a'] as const;
|
||||||
* let n: number = 1;
|
* let n: number = 1;
|
||||||
|
@ -101,35 +100,6 @@ export interface Card extends Memory, Restorable {
|
||||||
ioSwitch(off: byte, val?: byte): byte | undefined;
|
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 type TapeData = Array<[duration: number, high: boolean]>;
|
||||||
|
|
||||||
export interface Restorable<T = any> {
|
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 { base64_json_parse, base64_json_stringify } from '../base64';
|
||||||
import { Audio, SOUND_ENABLED_OPTION } from './audio';
|
import { Audio, SOUND_ENABLED_OPTION } from './audio';
|
||||||
import DriveLights from './drive_lights';
|
import DriveLights from './drive_lights';
|
||||||
import { byte, DISK_FORMATS, includes, word } from '../types';
|
import { byte, includes, word } from '../types';
|
||||||
import { initGamepad, GamepadConfiguration } from './gamepad';
|
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 KeyBoard from './keyboard';
|
||||||
import Tape, { TAPE_TYPES } from './tape';
|
import Tape, { TAPE_TYPES } from './tape';
|
||||||
|
import type { GamepadConfiguration } from './types';
|
||||||
|
|
||||||
import ApplesoftDump from '../applesoft/decompiler';
|
import ApplesoftDump from '../applesoft/decompiler';
|
||||||
import ApplesoftCompiler from '../applesoft/compiler';
|
import ApplesoftCompiler from '../applesoft/compiler';
|
||||||
|
|
||||||
import { debug, gup, hup } from '../util';
|
import { debug } from '../util';
|
||||||
import { Apple2, Stats } from '../apple2';
|
import { Apple2, Stats } from '../apple2';
|
||||||
import DiskII, { DriveNumber, DRIVE_NUMBERS } from '../cards/disk2';
|
import DiskII from '../cards/disk2';
|
||||||
import SmartPort from '../cards/smartport';
|
|
||||||
import CPU6502 from '../cpu6502';
|
import CPU6502 from '../cpu6502';
|
||||||
import { VideoModes } from '../videomodes';
|
import { VideoModes } from '../videomodes';
|
||||||
import Apple2IO from '../apple2io';
|
import Apple2IO from '../apple2io';
|
||||||
import { JSONDisk } from '../formats/format_utils';
|
import { } from '../formats/format_utils';
|
||||||
import Printer from './printer';
|
import Printer from './printer';
|
||||||
|
|
||||||
import { OptionsModal } from './options_modal';
|
import { OptionsModal } from './options_modal';
|
||||||
|
@ -71,7 +78,7 @@ let stats: Stats;
|
||||||
let vm: VideoModes;
|
let vm: VideoModes;
|
||||||
let tape: Tape;
|
let tape: Tape;
|
||||||
let _disk2: DiskII;
|
let _disk2: DiskII;
|
||||||
let _smartPort: SmartPort;
|
let _massStorage: MassStorage;
|
||||||
let _printer: Printer;
|
let _printer: Printer;
|
||||||
let audio: Audio;
|
let audio: Audio;
|
||||||
let screen: Screen;
|
let screen: Screen;
|
||||||
|
@ -98,9 +105,10 @@ export function compileAppleSoftProgram(program: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openLoad(driveString: string, event: MouseEvent) {
|
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)) {
|
if (event.metaKey && includes(DRIVE_NUMBERS, drive)) {
|
||||||
openLoadHTTP(drive);
|
openLoadHTTP();
|
||||||
} else {
|
} else {
|
||||||
if (disk_cur_cat[drive]) {
|
if (disk_cur_cat[drive]) {
|
||||||
const element = document.querySelector<HTMLSelectElement>('#category_select')!;
|
const element = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||||
|
@ -380,16 +388,25 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
|
||||||
files[drive - 1] = '';
|
files[drive - 1] = '';
|
||||||
document.location.hash = files.join('|');
|
document.location.hash = files.join('|');
|
||||||
|
|
||||||
if (result.byteLength >= 800 * 1024) {
|
if (includes(DISK_FORMATS, ext)) {
|
||||||
if (_smartPort.setBinary(drive, name, ext, result)) {
|
if (result.byteLength >= 800 * 1024) {
|
||||||
initGamepad();
|
if (
|
||||||
}
|
includes(BLOCK_FORMATS, ext) &&
|
||||||
} else {
|
_massStorage.setBinary(drive, name, ext, result)
|
||||||
if (includes(DISK_FORMATS, ext)
|
) {
|
||||||
&& _disk2.setBinary(drive, name, ext, result)) {
|
initGamepad();
|
||||||
initGamepad();
|
} else {
|
||||||
|
openAlert(`Unable to load ${name}`);
|
||||||
|
}
|
||||||
} else {
|
} 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();
|
loadingStop();
|
||||||
|
@ -442,18 +459,24 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
|
||||||
const fileParts = file.split('.');
|
const fileParts = file.split('.');
|
||||||
const ext = fileParts.pop()!.toLowerCase();
|
const ext = fileParts.pop()!.toLowerCase();
|
||||||
const name = decodeURIComponent(fileParts.join('.'));
|
const name = decodeURIComponent(fileParts.join('.'));
|
||||||
if (data.byteLength >= 800 * 1024) {
|
if (includes(DISK_FORMATS, ext)) {
|
||||||
if (_smartPort.setBinary(drive, name, ext, data)) {
|
if (data.byteLength >= 800 * 1024) {
|
||||||
initGamepad();
|
if (
|
||||||
}
|
includes(BLOCK_FORMATS, ext) &&
|
||||||
} else {
|
_massStorage.setBinary(drive, name, ext, data)
|
||||||
if (includes(DISK_FORMATS, ext)) {
|
) {
|
||||||
if (_disk2.setBinary(drive, name, ext, data)) {
|
|
||||||
initGamepad();
|
initGamepad();
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
loadingStop();
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
|
@ -463,8 +486,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLoadHTTP(drive: DriveNumber) {
|
function openLoadHTTP() {
|
||||||
_currentDrive = drive;
|
|
||||||
MicroModal.show('http-modal');
|
MicroModal.show('http-modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -685,7 +707,6 @@ const categorySelect = document.querySelector<HTMLSelectElement>('#category_sele
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
disk_index: DiskDescriptor[];
|
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;
|
_apple2 = apple2;
|
||||||
cpu = _apple2.getCPU();
|
cpu = _apple2.getCPU();
|
||||||
io = _apple2.getIO();
|
io = _apple2.getIO();
|
||||||
|
@ -816,7 +859,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
|
||||||
vm = apple2.getVideoModes();
|
vm = apple2.getVideoModes();
|
||||||
tape = new Tape(io);
|
tape = new Tape(io);
|
||||||
_disk2 = disk2;
|
_disk2 = disk2;
|
||||||
_smartPort = smartPort;
|
_massStorage = massStorage;
|
||||||
_printer = printer;
|
_printer = printer;
|
||||||
_e = e;
|
_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', () => {
|
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 {
|
export default class DriveLights implements Callbacks {
|
||||||
public driveLight(drive: DriveNumber, on: boolean) {
|
public driveLight(drive: DriveNumber, on: boolean) {
|
||||||
|
|
|
@ -10,50 +10,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Apple2IO from '../apple2io';
|
import Apple2IO from '../apple2io';
|
||||||
import { KnownKeys } from '../types';
|
import { BUTTON, ButtonType, GamepadConfiguration } from './types';
|
||||||
|
|
||||||
export let gamepad: Gamepad | null = null;
|
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 = {
|
const DEFAULT_GAMEPAD: GamepadConfiguration = {
|
||||||
'A': 0,
|
'A': 0,
|
||||||
'B': 1,
|
'B': 1,
|
||||||
|
@ -64,7 +24,7 @@ const DEFAULT_GAMEPAD: GamepadConfiguration = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array with 16 entries. For each entry _e_:
|
* An array with 16 entries. For each entry _e_:
|
||||||
*
|
*
|
||||||
* * if _e_ <= 0, then _-e_ is 0 | 1 | 2 and represents a joystick button;
|
* * if _e_ <= 0, then _-e_ is 0 | 1 | 2 and represents a joystick button;
|
||||||
* * if _e_ > 0, then _e_ is a key on the keyboard that is pressed;
|
* * if _e_ > 0, then _e_ is a key on the keyboard that is pressed;
|
||||||
* * if _e_ is undefined, nothing happens.
|
* * if _e_ is undefined, nothing happens.
|
||||||
|
|
|
@ -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;
|
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. */
|
/** Packs a 32-bit integer into a string in little-endian order. */
|
||||||
export function numToString(num: number) {
|
export function numToString(num: number) {
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"jest-image-snapshot": "^4.5.0",
|
"jest-image-snapshot": "^4.5.0",
|
||||||
"node-forge": "^0.10.0",
|
"node-forge": "^0.10.0",
|
||||||
"raw-loader": "^4.0.0",
|
"raw-loader": "^4.0.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
"ts-jest": "^26.5.0",
|
"ts-jest": "^26.5.0",
|
||||||
"ts-loader": "^8.0.15",
|
"ts-loader": "^8.0.15",
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.3",
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Apple II Emulator in JavaScript",
|
"description": "Apple II Emulator in JavaScript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode=production",
|
"build": "rimraf dist/* && webpack --mode=production",
|
||||||
"dev": "webpack serve --mode=development",
|
"dev": "webpack serve --mode=development",
|
||||||
"index": "bin/index > json/disks/index.js",
|
"index": "bin/index > json/disks/index.js",
|
||||||
"lint": "eslint '**/*.js' '**/*.ts'",
|
"lint": "eslint '**/*.js' '**/*.ts'",
|
||||||
"start": "webpack serve --mode=development",
|
"start": "webpack serve --mode=development --progress",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -42,6 +42,7 @@
|
||||||
"jest-image-snapshot": "^4.5.0",
|
"jest-image-snapshot": "^4.5.0",
|
||||||
"node-forge": "^0.10.0",
|
"node-forge": "^0.10.0",
|
||||||
"raw-loader": "^4.0.0",
|
"raw-loader": "^4.0.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
"ts-jest": "^26.5.0",
|
"ts-jest": "^26.5.0",
|
||||||
"ts-loader": "^8.0.15",
|
"ts-loader": "^8.0.15",
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.3",
|
||||||
|
|
|
@ -347,4 +347,4 @@ describe('DOS-13 format', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -284,4 +284,4 @@ describe('DOS format', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -284,4 +284,4 @@ describe('ProDOS format', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { byte } from '../../../../js/types';
|
import { memory } from '../../../../js/types';
|
||||||
|
|
||||||
function generateBytesInOrder() {
|
function generateBytesInOrder() {
|
||||||
const data: byte[][][] = [];
|
const data: memory[][] = [];
|
||||||
for (let t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
const track: byte[][] = [];
|
const track: memory[] = [];
|
||||||
for (let s = 0; s < 13; s++) {
|
for (let s = 0; s < 13; s++) {
|
||||||
const sector: byte[] = [];
|
const sector: memory = new Uint8Array(256);
|
||||||
for (let b = 0; b < 256; b++) {
|
for (let b = 0; b < 256; b++) {
|
||||||
sector[b] = b;
|
sector[b] = b;
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,14 @@ function generateBytesInOrder() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder();
|
export const BYTES_IN_ORDER: memory[][] = generateBytesInOrder();
|
||||||
|
|
||||||
function generateBytesBySector() {
|
function generateBytesBySector() {
|
||||||
const data: byte[][][] = [];
|
const data: memory[][] = [];
|
||||||
for (let t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
const track: byte[][] = [];
|
const track: memory[] = [];
|
||||||
for (let s = 0; s < 13; s++) {
|
for (let s = 0; s < 13; s++) {
|
||||||
const sector: byte[] = [];
|
const sector: memory = new Uint8Array(256);
|
||||||
for (let b = 0; b < 256; b++) {
|
for (let b = 0; b < 256; b++) {
|
||||||
sector[b] = s;
|
sector[b] = s;
|
||||||
}
|
}
|
||||||
|
@ -34,14 +34,14 @@ function generateBytesBySector() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector();
|
export const BYTES_BY_SECTOR: memory[][] = generateBytesBySector();
|
||||||
|
|
||||||
function generateBytesByTrack() {
|
function generateBytesByTrack() {
|
||||||
const data: byte[][][] = [];
|
const data: memory[][] = [];
|
||||||
for (let t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
const track: byte[][] = [];
|
const track: memory[] = [];
|
||||||
for (let s = 0; s < 13; s++) {
|
for (let s = 0; s < 13; s++) {
|
||||||
const sector: byte[] = [];
|
const sector: memory = new Uint8Array(256);
|
||||||
for (let b = 0; b < 256; b++) {
|
for (let b = 0; b < 256; b++) {
|
||||||
sector[b] = t;
|
sector[b] = t;
|
||||||
}
|
}
|
||||||
|
@ -52,4 +52,4 @@ function generateBytesByTrack() {
|
||||||
return data;
|
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() {
|
function generateBytesInOrder() {
|
||||||
const data: byte[][][] = [];
|
const data: memory[][] = [];
|
||||||
for (let t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
const track: byte[][] = [];
|
const track: memory[] = [];
|
||||||
for (let s = 0; s < 16; s++) {
|
for (let s = 0; s < 16; s++) {
|
||||||
const sector: byte[] = [];
|
const sector = new Uint8Array(256);
|
||||||
for (let b = 0; b < 256; b++) {
|
for (let b = 0; b < 256; b++) {
|
||||||
sector[b] = b;
|
sector[b] = b;
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,14 @@ function generateBytesInOrder() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder();
|
export const BYTES_IN_ORDER: memory[][] = generateBytesInOrder();
|
||||||
|
|
||||||
function generateBytesBySector() {
|
function generateBytesBySector() {
|
||||||
const data: byte[][][] = [];
|
const data: memory[][] = [];
|
||||||
for (let t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
const track: byte[][] = [];
|
const track: memory[] = [];
|
||||||
for (let s = 0; s < 16; s++) {
|
for (let s = 0; s < 16; s++) {
|
||||||
const sector: byte[] = [];
|
const sector = new Uint8Array(256);
|
||||||
for (let b = 0; b < 256; b++) {
|
for (let b = 0; b < 256; b++) {
|
||||||
sector[b] = s;
|
sector[b] = s;
|
||||||
}
|
}
|
||||||
|
@ -34,14 +34,14 @@ function generateBytesBySector() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector();
|
export const BYTES_BY_SECTOR: memory[][] = generateBytesBySector();
|
||||||
|
|
||||||
function generateBytesByTrack() {
|
function generateBytesByTrack() {
|
||||||
const data: byte[][][] = [];
|
const data: memory[][] = [];
|
||||||
for (let t = 0; t < 35; t++) {
|
for (let t = 0; t < 35; t++) {
|
||||||
const track: byte[][] = [];
|
const track: memory[] = [];
|
||||||
for (let s = 0; s < 16; s++) {
|
for (let s = 0; s < 16; s++) {
|
||||||
const sector: byte[] = [];
|
const sector = new Uint8Array(256);
|
||||||
for (let b = 0; b < 256; b++) {
|
for (let b = 0; b < 256; b++) {
|
||||||
sector[b] = t;
|
sector[b] = t;
|
||||||
}
|
}
|
||||||
|
@ -52,4 +52,4 @@ function generateBytesByTrack() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BYTES_BY_TRACK: byte[][][] = generateBytesByTrack();
|
export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack();
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
|
"lib": ["DOM", "ES6"],
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
|
||||||
const baseConfig = {
|
const baseConfig = {
|
||||||
devtool: 'source-map',
|
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: {
|
resolve: {
|
||||||
extensions: ['.ts', '.js'],
|
extensions: ['.ts', '.js'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = [
|
const appConfig = merge(baseConfig,
|
||||||
{
|
{
|
||||||
...baseConfig,
|
|
||||||
entry: {
|
entry: {
|
||||||
main2: path.resolve('js/entry2.js'),
|
main2: path.resolve('js/entry2.js'),
|
||||||
main2e: path.resolve('js/entry2e.js')
|
main2e: path.resolve('js/entry2e.js')
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve('dist/'),
|
|
||||||
filename: '[name].bundle.js',
|
|
||||||
chunkFilename: '[name].bundle.js',
|
|
||||||
library: {
|
library: {
|
||||||
name: 'Apple2',
|
name: 'Apple2',
|
||||||
type: 'umd',
|
type: 'umd',
|
||||||
|
@ -47,21 +50,34 @@ module.exports = [
|
||||||
directory: __dirname,
|
directory: __dirname,
|
||||||
},
|
},
|
||||||
devMiddleware: {
|
devMiddleware: {
|
||||||
publicPath: '/dist/',
|
publicPath: 'dist/',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const workletConfig = merge(baseConfig,
|
||||||
{
|
{
|
||||||
...baseConfig,
|
|
||||||
target: false,
|
target: false,
|
||||||
entry: {
|
entry: {
|
||||||
audio_worker: path.resolve('js/ui/audio_worker.ts')
|
audio_worker: path.resolve('js/ui/audio_worker.ts')
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
publicPath: '/dist/',
|
globalObject: 'globalThis',
|
||||||
path: path.resolve('dist/'),
|
},
|
||||||
filename: '[name].bundle.js',
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const workerConfig = merge(baseConfig,
|
||||||
|
{
|
||||||
|
target: false,
|
||||||
|
entry: {
|
||||||
|
format_worker: path.resolve('workers/format.worker.ts')
|
||||||
|
},
|
||||||
|
output: {
|
||||||
globalObject: 'globalThis',
|
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