diff --git a/.eslintrc.json b/.eslintrc.json index d60df35..f615dee 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -110,6 +110,14 @@ "env": { "commonjs": true } + }, + { + "files": [ + "workers/*" + ], + "parserOptions": { + "project": "workers/tsconfig.json" + } } ], "ignorePatterns": ["coverage/**/*"] diff --git a/js/cards/cffa.js b/js/cards/cffa.js deleted file mode 100644 index 236d039..0000000 --- a/js/cards/cffa.js +++ /dev/null @@ -1,418 +0,0 @@ -/* Copyright 2010-2019 Will Scullin - * - * 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; - } - } - }; -} diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts new file mode 100644 index 0000000..5e1895c --- /dev/null +++ b/js/cards/cffa.ts @@ -0,0 +1,482 @@ +/* Copyright 2010-2019 Will Scullin + * + * 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 +} + +type Partition = ReturnType +export default class CFFA implements Card, MassStorage, Restorable { + + // 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 = [ + // 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); + } +} diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index ede4450..22d1965 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -9,22 +9,42 @@ * implied warranty. */ -import { base64_decode, base64_encode} from '../base64'; -import { bit, byte, Card, DiskFormat, MemberOf, memory, nibble, rom } from '../types'; +import { base64_encode} from '../base64'; +import type { + bit, + byte, + Card, + memory, + nibble, + rom, +} from '../types'; + +import { + FormatWorkerMessage, + FormatWorkerResponse, + NibbleFormat, + DISK_PROCESSED, + DRIVE_NUMBERS, + DriveNumber, + JSONDisk, + ENCODING_NIBBLE, + PROCESS_BINARY, + PROCESS_JSON_DISK, + PROCESS_JSON, + ENCODING_BITSTREAM, +} from '../formats/types'; + +import { + createDisk, + createDiskFromJsonDisk +} from '../formats/create_disk'; + import { debug, toHex } from '../util'; -import { Disk, JSONDisk, jsonDecode, jsonEncode, readSector } from '../formats/format_utils'; +import { jsonDecode, jsonEncode, readSector } from '../formats/format_utils'; import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2'; - -import _2MG from '../formats/2mg'; -import D13 from '../formats/d13'; -import DOS from '../formats/do'; -import ProDOS from '../formats/po'; -import Woz from '../formats/woz'; -import Nibble from '../formats/nib'; import Apple2IO from '../apple2io'; - /** Softswitch locations */ const LOC = { // Disk II Controller Commands @@ -144,10 +164,6 @@ const PHASE_DELTA = [ [-2, -1, 0, 1], [1, -2, -1, 0] ] as const; - -export const DRIVE_NUMBERS = [1, 2] as const; -export type DriveNumber = MemberOf; - export interface Callbacks { driveLight: (drive: DriveNumber, on: boolean) => void; dirty: (drive: DriveNumber, dirty: boolean) => void; @@ -155,9 +171,10 @@ export interface Callbacks { } /** Common information for Nibble and WOZ disks. */ + interface BaseDrive { /** Current disk format. */ - format: DiskFormat, + format: NibbleFormat, /** Current disk volume number. */ volume: byte, /** Displayed disk name */ @@ -176,6 +193,8 @@ interface BaseDrive { /** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */ interface WozDrive extends BaseDrive { + /** Woz encoding */ + encoding: typeof ENCODING_BITSTREAM /** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */ trackMap: byte[]; /** Unique track bitstreams. The index is arbitrary; it is NOT the track number. */ @@ -184,19 +203,25 @@ interface WozDrive extends BaseDrive { /** Nibble format track data. */ interface NibbleDrive extends BaseDrive { + /** Nibble encoding */ + encoding: typeof ENCODING_NIBBLE /** Nibble data. The index is the track number. */ tracks: memory[]; } type Drive = WozDrive | NibbleDrive; -function isNibbleDrive(drive: Drive): drive is NibbleDrive { - return 'tracks' in drive; +function isNibbleDrive(drive: Drive): drive is NibbleDrive { + return drive.encoding === ENCODING_NIBBLE; +} + +function isWozDrive(drive: Drive): drive is WozDrive { + return drive.encoding === ENCODING_BITSTREAM; } -// Does not support WOZ disks interface DriveState { - format: DiskFormat, + format: NibbleFormat, + encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE volume: byte, name: string, tracks: memory[], @@ -205,6 +230,8 @@ interface DriveState { phase: Phase, readOnly: boolean, dirty: boolean, + trackMap: number[], + rawTracks: bit[][], } interface State { @@ -216,10 +243,10 @@ interface State { drive: DriveNumber; } -// TODO(flan): Does not work for WOZ disks function getDriveState(drive: Drive): DriveState { const result: DriveState = { format: drive.format, + encoding: drive.encoding, volume: drive.volume, name: drive.name, tracks: [], @@ -227,34 +254,61 @@ function getDriveState(drive: Drive): DriveState { head: drive.head, phase: drive.phase, readOnly: drive.readOnly, - dirty: drive.dirty + dirty: drive.dirty, + trackMap: [], + rawTracks: [], }; - if (!isNibbleDrive(drive)) { - throw Error('No tracks.'); + + if (isNibbleDrive(drive)) { + for (let idx = 0; idx < drive.tracks.length; idx++) { + result.tracks.push(new Uint8Array(drive.tracks[idx])); + } } - for (let idx = 0; idx < drive.tracks.length; idx++) { - result.tracks.push(new Uint8Array(drive.tracks[idx])); + if (isWozDrive(drive)) { + result.trackMap = [...drive.trackMap]; + for (let idx = 0; idx < drive.rawTracks.length; idx++) { + result.rawTracks.push([...drive.rawTracks[idx]]); + } } return result; } -// TODO(flan): Does not work for WOZ disks function setDriveState(state: DriveState) { - const result: Drive = { - format: state.format, - volume: state.volume, - name: state.name, - tracks: [] as memory[], - track: state.track, - head: state.head, - phase: state.phase, - readOnly: state.readOnly, - dirty: state.dirty - }; - for (let idx = 0; idx < state.tracks.length; idx++) { - result.tracks!.push(new Uint8Array(state.tracks[idx])); + let result: Drive; + if (state.encoding === ENCODING_NIBBLE) { + result = { + format: state.format, + encoding: ENCODING_NIBBLE, + volume: state.volume, + name: state.name, + tracks: [], + track: state.track, + head: state.head, + phase: state.phase, + readOnly: state.readOnly, + dirty: state.dirty, + }; + for (let idx = 0; idx < state.tracks.length; idx++) { + result.tracks.push(new Uint8Array(state.tracks[idx])); + } + } else { + result = { + format: state.format, + encoding: ENCODING_BITSTREAM, + volume: state.volume, + name: state.name, + track: state.track, + head: state.head, + phase: state.phase, + readOnly: state.readOnly, + dirty: state.dirty, + trackMap: [...state.trackMap], + rawTracks: [], + }; + for (let idx = 0; idx < state.rawTracks.length; idx++) { + result.rawTracks.push([...state.rawTracks[idx]]); + } } - return result; } @@ -266,6 +320,7 @@ export default class DiskII implements Card { private drives: Drive[] = [ { // Drive 1 format: 'dsk', + encoding: ENCODING_NIBBLE, volume: 254, name: 'Disk 1', tracks: [], @@ -273,10 +328,11 @@ export default class DiskII implements Card { head: 0, phase: 0, readOnly: false, - dirty: false + dirty: false, }, { // Drive 2 format: 'dsk', + encoding: ENCODING_NIBBLE, volume: 254, name: 'Disk 2', tracks: [], @@ -284,7 +340,7 @@ export default class DiskII implements Card { head: 0, phase: 0, readOnly: false, - dirty: false + dirty: false, }]; private skip = 0; @@ -307,6 +363,9 @@ export default class DiskII implements Card { /** Current drive object. */ private cur = this.drives[this.drive - 1]; + /** Nibbles read this on cycle */ + private nibbleCount = 0; + /** Q0-Q3: Coil states. */ private q = [false, false, false, false]; @@ -328,37 +387,37 @@ export default class DiskII implements Card { /** Contents of the P6 ROM. */ private sequencerRom: typeof SEQUENCER_ROM_16 | typeof SEQUENCER_ROM_13; + private worker: Worker; + /** Builds a new Disk ][ card. */ constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) { + this.debug('Disk ]['); + this.lastCycles = this.io.cycles(); this.bootstrapRom = this.sectors == 16 ? BOOTSTRAP_ROM_16 : BOOTSTRAP_ROM_13; this.sequencerRom = this.sectors == 16 ? SEQUENCER_ROM_16 : SEQUENCER_ROM_13; - this.init(); + this.initWorker(); } private debug(..._args: any[]) { // debug.apply(this, arguments); } - private init() { - this.debug('Disk ]['); - } - // Only used for WOZ disks private moveHead() { - if (isNibbleDrive(this.cur)) { - return; - } - const track: bit[] = - this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; - const cycles = this.io.cycles(); // Spin the disk the number of elapsed cycles since last call let workCycles = (cycles - this.lastCycles) * 2; this.lastCycles = cycles; + if (!isWozDrive(this.cur)) { + return; + } + const track: bit[] = + this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; + while (workCycles-- > 0) { let pulse: bit = 0; if (this.clock == 4) { @@ -531,6 +590,7 @@ export default class DiskII implements Card { this.debug('Drive Off'); this.on = false; this.callbacks.driveLight(this.drive, false); + this.debug('nibbles read', this.nibbleCount); }, 1000); } } @@ -543,6 +603,7 @@ export default class DiskII implements Card { } if (!this.on) { this.debug('Drive On'); + this.nibbleCount = 0; this.on = true; this.lastCycles = this.io.cycles(); this.callbacks.driveLight(this.drive, true); @@ -619,6 +680,9 @@ export default class DiskII implements Card { // also cause conflicts with the disk controller commands. if ((off & 0x01) === 0) { result = this.latch; + if (result & 0x80) { + this.nibbleCount++; + } } else { result = 0; } @@ -665,7 +729,6 @@ export default class DiskII implements Card { this.moveHead(); } - // TODO(flan): Does not work for WOZ disks getState() { const result = { drives: [] as DriveState[], @@ -682,7 +745,6 @@ export default class DiskII implements Card { return result; } - // TODO(flan): Does not work for WOZ disks setState(state: State) { this.skip = state.skip; this.latch = state.latch; @@ -699,22 +761,17 @@ export default class DiskII implements Card { this.cur = this.drives[this.drive - 1]; } - // TODO(flan): Does not work for WOZ disks getMetadata(driveNo: DriveNumber) { const drive = this.drives[driveNo - 1]; - if (isNibbleDrive(drive)) { - return { - format: drive.format, - volume: drive.volume, - track: drive.track, - head: drive.head, - phase: drive.phase, - readOnly: drive.readOnly, - dirty: drive.dirty - }; - } else { - return null; - } + return { + format: drive.format, + volume: drive.volume, + track: drive.track, + head: drive.head, + phase: drive.phase, + readOnly: drive.readOnly, + dirty: drive.dirty + }; } // TODO(flan): Does not work on WOZ disks @@ -728,63 +785,28 @@ export default class DiskII implements Card { /** Sets the data for `drive` from `disk`, which is expected to be JSON. */ // TODO(flan): This implementation is not very safe. - setDisk(drive: DriveNumber, disk: JSONDisk) { - const fmt = disk.type; - const readOnly = disk.readOnly; - const name = disk.name; - - let data: memory[] | memory[][]; - if (disk.encoding == 'base64') { - data = []; - for (let t = 0; t < disk.data.length; t++) { - if (fmt == 'nib') { - data[t] = base64_decode(disk.data[t] as string); - } else { - data[t] = []; - for (let s = 0; s < disk.data[t].length; s++) { - data[t][s] = base64_decode(disk.data[t][s] as string); - } - } - } + setDisk(drive: DriveNumber, jsonDisk: JSONDisk) { + if (this.worker) { + const message: FormatWorkerMessage = { + type: PROCESS_JSON_DISK, + payload: { + drive, + jsonDisk + }, + }; + this.worker.postMessage(message); + return true; } else { - data = disk.data; + const disk = createDiskFromJsonDisk(jsonDisk); + if (disk) { + const cur = this.drives[drive - 1]; + Object.assign(cur, disk); + this.updateDirty(drive, false); + this.callbacks.label(drive, disk.name); + return true; + } } - const cur = this.drives[drive - 1]; - - // var v = (fmt === 'dsk' ? data[0x11][0x00][0x06] : 0xfe); - // if (v == 0x00) { - const volume = disk.volume || 0xfe; - // } - - const options = { - volume, - readOnly, - name, - data - }; - - let newDisk: Disk; - switch (fmt) { - case 'd13': - newDisk = D13(options); - break; - case 'do': - case 'dsk': - newDisk = DOS(options); - break; - case 'nib': - newDisk = Nibble(options); - break; - case 'po': - newDisk = ProDOS(options); - break; - default: - return false; - } - - Object.assign(cur, newDisk); - this.updateDirty(drive, false); - this.callbacks.label(drive, name); + return false; } getJSON(drive: DriveNumber, pretty: boolean = false) { @@ -795,52 +817,78 @@ export default class DiskII implements Card { return jsonEncode(cur, pretty); } - setJSON(drive: DriveNumber, data: string) { - const cur = this.drives[drive - 1]; - Object.assign(cur, jsonDecode(data)); + setJSON(drive: DriveNumber, json: string) { + if (this.worker) { + const message: FormatWorkerMessage = { + type: PROCESS_JSON, + payload: { + drive, + json + }, + }; + this.worker.postMessage(message); + } else { + const cur = this.drives[drive - 1]; + Object.assign(cur, jsonDecode(json)); + } return true; } - setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: ArrayBuffer) { - let disk; - const cur = this.drives[drive - 1]; + setBinary(drive: DriveNumber, name: string, fmt: NibbleFormat, rawData: ArrayBuffer) { const readOnly = false; const volume = 254; const options = { name, rawData, readOnly, - volume + volume, }; - switch (fmt) { - case '2mg': - disk = _2MG(options); - break; - case 'd13': - disk = D13(options); - break; - case 'do': - case 'dsk': - disk = DOS(options); - break; - case 'nib': - disk = Nibble(options); - break; - case 'po': - disk = ProDOS(options); - break; - case 'woz': - disk = Woz(options); - break; - default: - return false; - } + if (this.worker) { + const message: FormatWorkerMessage = { + type: PROCESS_BINARY, + payload: { + drive, + fmt, + options, + } + }; + this.worker.postMessage(message); - Object.assign(cur, disk); - this.updateDirty(drive, true); - this.callbacks.label(this.drive, name); - return true; + return true; + } else { + const disk = createDisk(fmt, options); + if (disk) { + const cur = this.drives[drive - 1]; + Object.assign(cur, disk); + this.updateDirty(drive, true); + this.callbacks.label(this.drive, name); + + return true; + } + } + return false; + } + + initWorker() { + this.worker = new Worker('dist/format_worker.bundle.js'); + + this.worker.addEventListener('message', (message: MessageEvent) => { + 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 diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 04a8827..c567678 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -12,16 +12,13 @@ import { debug, toHex } from '../util'; import { rom as smartPortRom } from '../roms/cards/smartport'; import { Card, Restorable, byte, word, rom } from '../types'; +import { MassStorage, BlockDisk, ENCODING_BLOCK } from '../formats/types'; import CPU6502, { CpuState, flags } from '../cpu6502'; - -type SmartDisk = Uint8Array[]; - -interface BlockDevice { - blocks: Uint8Array -} +import { read2MGHeader } from '../formats/2mg'; +import createBlockDisk from '../formats/block'; export interface SmartPortState { - disks: SmartDisk[] + disks: BlockDisk[] } export interface SmartPortOptions { @@ -100,10 +97,10 @@ const ADDRESS_LO = 0x44; const BLOCK_LO = 0x46; // const BLOCK_HI = 0x47; -export default class SmartPort implements Card, Restorable { +export default class SmartPort implements Card, MassStorage, Restorable { private rom: rom; - private disks: SmartDisk[] = []; + private disks: BlockDisk[] = []; constructor(private cpu: CPU6502, options: SmartPortOptions) { if (options?.block) { @@ -117,13 +114,6 @@ export default class SmartPort implements Card, Restorable { } } - private decodeDisk(unit: number, disk: BlockDevice) { - this.disks[unit] = []; - for (let idx = 0; idx < disk.blocks.length; idx++) { - this.disks[unit][idx] = new Uint8Array(disk.blocks[idx]); - } - } - private debug(..._args: any[]) { // debug.apply(this, arguments); } @@ -131,16 +121,16 @@ export default class SmartPort implements Card, Restorable { /* * dumpBlock */ - /* - dumpBlock(drive, block) { - const result = ''; - const b; - const jdx; - for (const idx = 0; idx < 32; idx++) { + dumpBlock(drive: number, block: number) { + let result = ''; + let b; + let jdx; + + for (let idx = 0; idx < 32; idx++) { result += toHex(idx << 4, 4) + ': '; for (jdx = 0; jdx < 16; jdx++) { - b = disks[drive][block][idx * 16 + jdx]; + b = this.disks[drive].blocks[block][idx * 16 + jdx]; if (jdx == 8) { result += ' '; } @@ -148,7 +138,7 @@ export default class SmartPort implements Card, Restorable { } result += ' '; for (jdx = 0; jdx < 16; jdx++) { - b = disks[drive][block][idx * 16 + jdx] & 0x7f; + b = this.disks[drive].blocks[block][idx * 16 + jdx] & 0x7f; if (jdx == 8) { result += ' '; } @@ -162,14 +152,14 @@ export default class SmartPort implements Card, Restorable { } return result; } -*/ + /* * getDeviceInfo */ getDeviceInfo(state: CpuState, drive: number) { if (this.disks[drive]) { - const blocks = this.disks[drive].length; + const blocks = this.disks[drive].blocks.length; state.x = blocks & 0xff; state.y = blocks >> 8; @@ -190,7 +180,7 @@ export default class SmartPort implements Card, Restorable { this.debug('read buffer=' + buffer); this.debug('read block=$' + toHex(block)); - if (!this.disks[drive] || !this.disks[drive].length) { + if (!this.disks[drive]?.blocks.length) { debug('Drive', drive, 'is empty'); return; } @@ -198,7 +188,7 @@ export default class SmartPort implements Card, Restorable { // debug('read', '\n' + dumpBlock(drive, block)); for (let idx = 0; idx < 512; idx++) { - buffer.writeByte(this.disks[drive][block][idx]); + buffer.writeByte(this.disks[drive].blocks[block][idx]); buffer = buffer.inc(1); } @@ -215,7 +205,7 @@ export default class SmartPort implements Card, Restorable { this.debug('write buffer=' + buffer); this.debug('write block=$' + toHex(block)); - if (!this.disks[drive] || !this.disks[drive].length) { + if (!this.disks[drive]?.blocks.length) { debug('Drive', drive, 'is empty'); return; } @@ -223,7 +213,7 @@ export default class SmartPort implements Card, Restorable { // debug('write', '\n' + dumpBlock(drive, block)); for (let idx = 0; idx < 512; idx++) { - this.disks[drive][block][idx] = buffer.readByte(); + this.disks[drive].blocks[block][idx] = buffer.readByte(); buffer = buffer.inc(1); } state.a = 0; @@ -235,10 +225,10 @@ export default class SmartPort implements Card, Restorable { */ formatDevice(state: CpuState, drive: number) { - for (let idx = 0; idx < this.disks[drive].length; idx++) { - this.disks[drive][idx] = new Uint8Array(); + for (let idx = 0; idx < this.disks[drive].blocks.length; idx++) { + this.disks[drive].blocks[idx] = new Uint8Array(); for (let jdx = 0; jdx < 512; jdx++) { - this.disks[drive][idx][jdx] = 0; + this.disks[drive].blocks[idx][jdx] = 0; } } @@ -371,7 +361,7 @@ export default class SmartPort implements Card, Restorable { default: // Unit 1 switch (status) { case 0: - blocks = this.disks[unit].length; + blocks = this.disks[unit].blocks.length; buffer.writeByte(0xf0); // W/R Block device in drive buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks buffer.inc(2).writeByte((blocks & 0xff00) >> 8); @@ -433,33 +423,53 @@ export default class SmartPort implements Card, Restorable { getState() { return { disks: this.disks.map( - (disk) => disk.map( - (block) => new Uint8Array(block) - ) + (disk) => { + const result: BlockDisk = { + blocks: disk.blocks.map( + (block) => new Uint8Array(block) + ), + encoding: ENCODING_BLOCK, + readOnly: disk.readOnly, + name: disk.name, + }; + return result; + } ) }; } setState(state: SmartPortState) { this.disks = state.disks.map( - (disk) => disk.map( - (block) => new Uint8Array(block) - ) + (disk) => { + const result: BlockDisk = { + blocks: disk.blocks.map( + (block) => new Uint8Array(block) + ), + encoding: ENCODING_BLOCK, + readOnly: disk.readOnly, + name: disk.name, + }; + return result; + } ); } - setBinary(drive: number, _name: string, fmt: string, data: ArrayBuffer) { - this.disks[drive] = []; + setBinary(drive: number, name: string, fmt: string, rawData: ArrayBuffer) { + const volume = 254; + const readOnly = false; if (fmt == '2mg') { - data = data.slice(64); - } - for (let idx = 0; idx < data.byteLength; idx += 512) { - this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512))); + const { bytes, offset } = read2MGHeader(rawData); + rawData = rawData.slice(offset, offset + bytes); } + const options = { + rawData, + name, + readOnly, + volume, + }; + + this.disks[drive] = createBlockDisk(options); + return true; } - - setDisk(drive: number, json: BlockDevice) { - this.decodeDisk(drive, json); - } } diff --git a/js/formats/2mg.js b/js/formats/2mg.js deleted file mode 100644 index d60c621..0000000 --- a/js/formats/2mg.js +++ /dev/null @@ -1,87 +0,0 @@ -/* Copyright 2010-2019 Will Scullin - * - * 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; -} diff --git a/js/formats/2mg.ts b/js/formats/2mg.ts new file mode 100644 index 0000000..3e2f3e3 --- /dev/null +++ b/js/formats/2mg.ts @@ -0,0 +1,95 @@ +/* Copyright 2010-2019 Will Scullin + * + * 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; +} diff --git a/js/formats/block.js b/js/formats/block.ts similarity index 66% rename from js/formats/block.js rename to js/formats/block.ts index f4f5f24..89053b8 100644 --- a/js/formats/block.js +++ b/js/formats/block.ts @@ -9,24 +9,28 @@ * implied warranty. */ +import { DiskOptions, BlockDisk, ENCODING_BLOCK } from './types'; + /** * Returns a `Disk` object for a block volume with block-ordered data. - * @param {*} options the disk image and options - * @returns {import('./format_utils').Disk} + * @param options the disk image and options */ -export default function BlockVolume(options) { - var { rawData, readOnly, name } = options; - var disk; +export default function createBlockDisk(options: DiskOptions): BlockDisk { + const { rawData, readOnly, name } = options; - var blocks = []; - blocks = []; - var offset = 0; + if (!rawData) { + throw new Error('Requires rawData'); + } + + const blocks = []; + let offset = 0; while (offset < rawData.byteLength) { blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200))); offset += 0x200; } - disk = { + const disk: BlockDisk = { + encoding: ENCODING_BLOCK, blocks, name, readOnly, diff --git a/js/formats/create_disk.ts b/js/formats/create_disk.ts new file mode 100644 index 0000000..58feaa6 --- /dev/null +++ b/js/formats/create_disk.ts @@ -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; + } +} + diff --git a/js/formats/d13.js b/js/formats/d13.ts similarity index 61% rename from js/formats/d13.js rename to js/formats/d13.ts index 2259864..fd2a879 100644 --- a/js/formats/d13.js +++ b/js/formats/d13.ts @@ -10,50 +10,56 @@ */ import { explodeSector13, D13O } from './format_utils'; +import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types'; /** * Returns a `Disk` object from DOS 3.2-ordered image data. - * @param {*} options the disk image and options - * @returns {import('./format_utils').Disk} + * @param options the disk image and options + * @returns A nibblized disk */ -export default function DOS13(options) { - var { data, name, rawData, volume, readOnly } = options; - var disk = { +export default function createDiskFromDOS13(options: DiskOptions) { + const { data, name, rawData, volume, readOnly } = options; + const disk: NibbleDisk = { format: 'd13', + encoding: ENCODING_NIBBLE, name, volume, readOnly, - tracks: [], - trackMap: null, - rawTracks: null + tracks: [] }; + if (!data && !rawData) { + throw new Error('data or rawData required'); + } + /* * DOS 13-sector disks have the physical sectors skewed on the track. The skew * between physical sectors is 10 (A), resulting in the following physical order: - * + * * 0 A 7 4 1 B 8 5 2 C 9 6 3 - * + * * Note that because physical sector == logical sector, this works slightly * differently from the DOS and ProDOS nibblizers. */ - for (var t = 0; t < 35; t++) { - var track = []; - for (var disk_sector = 0; disk_sector < 13; disk_sector++) { - var physical_sector = D13O[disk_sector]; - var sector; + for (let t = 0; t < 35; t++) { + let track: number[] = []; + for (let disk_sector = 0; disk_sector < 13; disk_sector++) { + const physical_sector = D13O[disk_sector]; + let sector: Uint8Array; if (rawData) { - var off = (13 * t + physical_sector) * 256; + const off = (13 * t + physical_sector) * 256; sector = new Uint8Array(rawData.slice(off, off + 256)); - } else { + } else if (data) { sector = data[t][physical_sector]; + } else { + throw new Error('Requires data or rawData'); } track = track.concat( explodeSector13(volume, t, physical_sector, sector) ); } - disk.tracks.push(track); + disk.tracks.push(new Uint8Array(track)); } return disk; diff --git a/js/formats/do.js b/js/formats/do.ts similarity index 63% rename from js/formats/do.js rename to js/formats/do.ts index 00857e8..5092fee 100644 --- a/js/formats/do.js +++ b/js/formats/do.ts @@ -11,34 +11,37 @@ import { explodeSector16, DO } from './format_utils'; import { bytify } from '../util'; +import { byte } from '../types'; +import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types'; /** * Returns a `Disk` object from DOS-ordered image data. - * @param {*} options the disk image and options - * @returns {import('./format_utils').Disk} + * @param options the disk image and options + * @returns A nibblized disk */ -export default function DOS(options) { - var { data, name, rawData, volume, readOnly } = options; - var disk = { +export default function createDiskFromDOS(options: DiskOptions): NibbleDisk { + const { data, name, rawData, volume, readOnly } = options; + const disk: NibbleDisk = { format: 'dsk', + encoding: ENCODING_NIBBLE, name, volume, readOnly, tracks: [], - trackMap: null, - rawTracks: null }; - for (var t = 0; t < 35; t++) { - var track = []; - for (var physical_sector = 0; physical_sector < 16; physical_sector++) { + for (let t = 0; t < 35; t++) { + let track: byte[] = []; + for (let physical_sector = 0; physical_sector < 16; physical_sector++) { const dos_sector = DO[physical_sector]; - var sector; + let sector: Uint8Array; if (rawData) { const off = (16 * t + dos_sector) * 256; sector = new Uint8Array(rawData.slice(off, off + 256)); + } else if (data) { + sector = new Uint8Array(data[t][dos_sector]); } else { - sector = data[t][dos_sector]; + throw new Error('Requires data or rawData'); } track = track.concat( explodeSector16(volume, t, physical_sector, sector) diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 6b73008..2934a63 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -9,64 +9,10 @@ * implied warranty. */ -import { byte, DiskFormat, memory } from '../types'; +import { byte, memory } from '../types'; import { base64_decode, base64_encode } from '../base64'; import { bytify, debug, toHex } from '../util'; -import { GamepadConfiguration } from '../ui/gamepad'; - -export interface Disk { - format: DiskFormat - name: string - volume: byte - tracks: memory[] - readOnly: boolean -} - -/** - * Base format for JSON defined disks - */ -export class JSONDiskBase { - type: DiskFormat - name: string - disk?: number - category?: string - writeProtected?: boolean - volume: byte - readOnly: boolean - gamepad?: GamepadConfiguration -} - -/** - * JSON Disk format with base64 encoded tracks - */ - -export interface Base64JSONDisk extends JSONDiskBase { - encoding: 'base64' - data: string[] -} - -/** - * JSON Disk format with byte array tracks - */ - -export interface BinaryJSONDisk extends JSONDiskBase { - encoding: 'binary' - data: memory[][] -} - -/** - * General JSON Disk format - */ - -export type JSONDisk = Base64JSONDisk | BinaryJSONDisk; - -export interface Drive { - format: DiskFormat - volume: byte - tracks: memory[] - readOnly: boolean - dirty: boolean -} +import { NibbleDisk, ENCODING_NIBBLE } from './types'; /** * DOS 3.3 Physical sector order (index is physical sector, value is DOS sector). @@ -74,7 +20,7 @@ export interface Drive { export const DO = [ 0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4, 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF -]; +] as const; /** * DOS 3.3 Logical sector order (index is DOS sector, value is physical sector). @@ -82,7 +28,7 @@ export const DO = [ export const _DO = [ 0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1, 0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF -]; +] as const; /** * ProDOS Physical sector order (index is physical sector, value is ProDOS sector). @@ -90,7 +36,7 @@ export const _DO = [ export const PO = [ 0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb, 0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf -]; +] as const; /** * ProDOS Logical sector order (index is ProDOS sector, value is physical sector). @@ -98,7 +44,7 @@ export const PO = [ export const _PO = [ 0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe, 0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf -]; +] as const; /** * DOS 13-sector disk physical sector order (index is disk sector, value is @@ -106,18 +52,18 @@ export const _PO = [ */ export const D13O = [ 0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3 -]; +] as const; export const _D13O = [ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc -]; +] as const; const _trans53 = [ 0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba, 0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb, 0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef, 0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff -]; +] as const; const _trans62 = [ 0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6, @@ -128,7 +74,7 @@ const _trans62 = [ 0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff -]; +] as const; export const detrans62 = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -147,10 +93,13 @@ export const detrans62 = [ 0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x00, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F -]; +] as const; /** - * From Beneath Apple DOS + * Converts a byte into its 4x4 encoded representation + * + * @param val byte to encode. + * @returns A two byte array of representing the 4x4 encoding. */ export function fourXfour(val: byte): [xx: byte, yy: byte] { let xx = val & 0xaa; @@ -163,11 +112,28 @@ export function fourXfour(val: byte): [xx: byte, yy: byte] { return [xx, yy]; } +/** + * Converts 2 4x4 encoded bytes into a byte value + * + * @param xx First encoded byte. + * @param yy Second encoded byte. + * @returns The decoded value. + */ export function defourXfour(xx: byte, yy: byte): byte { return ((xx << 1) | 0x01) & yy; } -export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory) { +/** + * Converts a raw sector into a nibblized representation to be combined into a + * nibblized 16 sector track. + * + * @param volume volume number + * @param track track number + * @param sector sector number + * @param data sector data + * @returns a nibblized representation of the sector data + */ +export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory): byte[] { let buf = []; let gap; @@ -255,7 +221,17 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m return buf; } -export function explodeSector13(volume: byte, track: byte, sector: byte, data: byte[]) { +/** + * Converts a raw sector into a nibblized representation to be combined into + * a nibblized 13 sector track. + * + * @param volume volume number + * @param track track number + * @param sector sector number + * @param data sector data + * @returns a nibblized representation of the sector data + */ +export function explodeSector13(volume: byte, track: byte, sector: byte, data: memory): byte[] { let buf = []; let gap; @@ -354,13 +330,22 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: b return buf; } -// TODO(flan): Does not work on WOZ disks -export function readSector(drive: Drive, track: byte, sector: byte) { - const _sector = drive.format == 'po' ? _PO[sector] : _DO[sector]; +/** + * Reads a sector of data from a nibblized disk + * + * TODO(flan): Does not work on WOZ disks + * + * @param disk Nibble disk + * @param track track number to read + * @param sector sector number to read + * @returns An array of sector data bytes. + */ +export function readSector(disk: NibbleDisk, track: byte, sector: byte): memory { + const _sector = disk.format == 'po' ? _PO[sector] : _DO[sector]; let val, state = 0; let idx = 0; let retry = 0; - const cur = drive.tracks[track]; + const cur = disk.tracks[track]; function _readNext() { const result = cur[idx++]; @@ -450,32 +435,46 @@ export function readSector(drive: Drive, track: byte, sector: byte) { return new Uint8Array(); } -export function jsonEncode(cur: Drive, pretty: boolean) { +/** + * Convert a nibblized disk into a JSON string for storage. + * + * @param disk Nibblized disk + * @param pretty Whether to format the output string + * @returns A JSON string representing the disk + */ +export function jsonEncode(disk: NibbleDisk, pretty: boolean): string { // For 'nib', tracks are encoded as strings. For all other formats, // tracks are arrays of sectors which are encoded as strings. const data: string[] | string[][] = []; let format = 'dsk'; - for (let t = 0; t < cur.tracks.length; t++) { + for (let t = 0; t < disk.tracks.length; t++) { data[t] = []; - if (cur.format === 'nib') { + if (disk.format === 'nib') { format = 'nib'; - data[t] = base64_encode(cur.tracks[t]); + data[t] = base64_encode(disk.tracks[t]); } else { for (let s = 0; s < 0x10; s++) { - (data[t] as string[])[s] = base64_encode(readSector(cur, t, s)); + (data[t] as string[])[s] = base64_encode(readSector(disk, t, s)); } } } return JSON.stringify({ 'type': format, 'encoding': 'base64', - 'volume': cur.volume, + 'volume': disk.volume, 'data': data, - 'readOnly': cur.readOnly, + 'readOnly': disk.readOnly, }, undefined, pretty ? ' ' : undefined); } -export function jsonDecode(data: string) { +/** + * Convert a JSON string into a nibblized disk. + * + * @param data JSON string representing a disk image, created by [jsonEncode]. + * @returns A nibblized disk + */ + +export function jsonDecode(data: string): NibbleDisk { const tracks: memory[] = []; const json = JSON.parse(data); const v = json.volume; @@ -483,20 +482,78 @@ export function jsonDecode(data: string) { for (let t = 0; t < json.data.length; t++) { let track: byte[] = []; for (let s = 0; s < json.data[t].length; s++) { - const _s = 15 - s; + const _s = json.type == 'po' ? PO[s] : DO[s]; const sector: string = json.data[t][_s]; const d = base64_decode(sector); track = track.concat(explodeSector16(v, t, s, d)); } tracks[t] = bytify(track); } - const cur: Drive = { + const disk: NibbleDisk = { volume: v, format: json.type, + encoding: ENCODING_NIBBLE, + name: json.name, tracks, readOnly, - dirty: false, }; - return cur; + return disk; +} + +export function analyseDisk(disk: NibbleDisk) { + for (let track = 0; track < 35; track++) { + let outStr = `${toHex(track)}: `; + let val, state = 0; + let idx = 0; + const cur = disk.tracks[track]; + + const _readNext = () => { + const result = cur[idx++]; + return result; + }; + + const _skipBytes = (count: number) => { + idx += count; + }; + + let t = 0, s = 0, v = 0, checkSum; + while (idx < cur.length) { + switch (state) { + case 0: + val = _readNext(); + state = (val === 0xd5) ? 1 : 0; + break; + case 1: + val = _readNext(); + state = (val === 0xaa) ? 2 : 0; + break; + case 2: + val = _readNext(); + state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0); + break; + case 3: // Address + v = defourXfour(_readNext(), _readNext()); // Volume + t = defourXfour(_readNext(), _readNext()); + s = defourXfour(_readNext(), _readNext()); + checkSum = defourXfour(_readNext(), _readNext()); + if (checkSum != (v ^ t ^ s)) { + debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum)); + } else { + outStr += toHex(s, 1); + } + _skipBytes(3); // Skip footer + state = 0; + break; + case 4: // Valid header + _skipBytes(0x159); // Skip data, checksum and footer + state = 0; + break; + default: + break; + } + } + debug(outStr); + } + return new Uint8Array(); } diff --git a/js/formats/nib.js b/js/formats/nib.ts similarity index 54% rename from js/formats/nib.js rename to js/formats/nib.ts index 91437ad..83c0366 100644 --- a/js/formats/nib.js +++ b/js/formats/nib.ts @@ -9,30 +9,34 @@ * implied warranty. */ +import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types'; +import { memory } from '../types'; + /** * Returns a `Disk` object from raw nibble image data. - * @param {*} options the disk image and options - * @returns {import('./format_utils').Disk} + * @param options the disk image and options + * @returns A nibblized disk */ -export default function Nibble(options) { - var { data, name, rawData, volume, readOnly } = options; - var disk = { +export default function createDiskFromNibble(options: DiskOptions): NibbleDisk { + const { data, name, rawData, volume, readOnly } = options; + const disk: NibbleDisk = { format: 'nib', + encoding: ENCODING_NIBBLE, name, volume: volume || 254, readOnly: readOnly || false, - tracks: [], - trackMap: null, - rawTracks: null + tracks: [] }; - for (var t = 0; t < 35; t++) { - var track; + for (let t = 0; t < 35; t++) { + let track: memory; if (rawData) { - var off = t * 0x1a00; - track = new Uint8Array(data.slice(off, off + 0x1a00)); + const off = t * 0x1a00; + track = new Uint8Array(rawData.slice(off, off + 0x1a00)); + } else if (data) { + track = data[t][0]; } else { - track = data[t]; + throw new Error('Requires data or rawData'); } disk.tracks[t] = track; } diff --git a/js/formats/po.js b/js/formats/po.ts similarity index 64% rename from js/formats/po.js rename to js/formats/po.ts index 1bec268..016cf16 100644 --- a/js/formats/po.js +++ b/js/formats/po.ts @@ -11,34 +11,37 @@ import { explodeSector16, PO } from './format_utils'; import { bytify } from '../util'; +import type { byte } from '../types'; +import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types'; /** * Returns a `Disk` object from ProDOS-ordered image data. - * @param {*} options the disk image and options - * @returns {import('./format_utils').Disk} + * @param options the disk image and options + * @returns A nibblized disk */ -export default function ProDOS(options) { - var { data, name, rawData, volume, readOnly } = options; - var disk = { +export default function createDiskFromProDOS(options: DiskOptions) { + const { data, name, rawData, volume, readOnly } = options; + const disk: NibbleDisk = { format: 'nib', + encoding: ENCODING_NIBBLE, name, volume: volume || 254, tracks: [], readOnly: readOnly || false, - trackMap: null, - rawTracks: null }; - for (var physical_track = 0; physical_track < 35; physical_track++) { - var track = []; - for (var physical_sector = 0; physical_sector < 16; physical_sector++) { + for (let physical_track = 0; physical_track < 35; physical_track++) { + let track: byte[] = []; + for (let physical_sector = 0; physical_sector < 16; physical_sector++) { const prodos_sector = PO[physical_sector]; - var sector; + let sector; if (rawData) { - var off = (16 * physical_track + prodos_sector) * 256; + const off = (16 * physical_track + prodos_sector) * 256; sector = new Uint8Array(rawData.slice(off, off + 256)); - } else { + } else if (data) { sector = data[physical_track][prodos_sector]; + } else { + throw new Error('Requires data or rawData'); } track = track.concat( explodeSector16(volume, physical_track, physical_sector, sector) diff --git a/js/formats/prodos/index.js b/js/formats/prodos/index.js index 734c907..bf7c7bf 100644 --- a/js/formats/prodos/index.js +++ b/js/formats/prodos/index.js @@ -18,6 +18,10 @@ export function ProDOSVolume(disk) { var _bitMap; return { + disk() { + return _disk; + }, + blocks() { return _disk.blocks; }, diff --git a/js/formats/types.ts b/js/formats/types.ts new file mode 100644 index 0000000..c11bbf1 --- /dev/null +++ b/js/formats/types.ts @@ -0,0 +1,193 @@ +/* Copyright 2021 Will Scullin + * + * 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; + +/** + * 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; +export type BlockFormat = MemberOf; +export type DiskFormat = MemberOf; + +/** + * 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 +} diff --git a/js/formats/woz.js b/js/formats/woz.js index 74258f5..c960c88 100644 --- a/js/formats/woz.js +++ b/js/formats/woz.js @@ -10,13 +10,14 @@ */ import { debug, toHex } from '../util'; +import { ENCODING_BITSTREAM } from './types'; -var WOZ_HEADER_START = 0; -var WOZ_HEADER_SIZE = 12; +const WOZ_HEADER_START = 0; +const WOZ_HEADER_SIZE = 12; -var WOZ1_SIGNATURE = 0x315A4F57; -var WOZ2_SIGNATURE = 0x325A4F57; -var WOZ_INTEGRITY_CHECK = 0x0a0d0aff; +const WOZ1_SIGNATURE = 0x315A4F57; +const WOZ2_SIGNATURE = 0x325A4F57; +const WOZ_INTEGRITY_CHECK = 0x0a0d0aff; function stringFromBytes(data, start, end) { return String.fromCharCode.apply( @@ -26,11 +27,11 @@ function stringFromBytes(data, start, end) { } function grabNibble(bits, offset) { - var nibble = 0; - var waitForOne = true; + let nibble = 0; + let waitForOne = true; while (offset < bits.length) { - var bit = bits[offset]; + const bit = bits[offset]; if (bit) { nibble = (nibble << 1) | 0x01; waitForOne = false; @@ -79,7 +80,7 @@ function InfoChunk(data) { function TMapChunk(data) { this.trackMap = []; - for (var idx = 0; idx < 160; idx++) { + for (let idx = 0; idx < 160; idx++) { this.trackMap.push(data.getUint8(idx)); } @@ -87,29 +88,28 @@ function TMapChunk(data) { } function TrksChunk(data) { - var WOZ_TRACK_SIZE = 6656; - var WOZ_TRACK_INFO_BITS = 6648; + const WOZ_TRACK_SIZE = 6656; + const WOZ_TRACK_INFO_BITS = 6648; this.rawTracks = []; this.tracks = []; - for (var trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) { - var jdx; - var track = []; - var rawTrack = []; - var slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE); - var trackData = new Uint8Array(slice); - var trackInfo = new DataView(slice); - var trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true); - for (jdx = 0; jdx < trackBitCount; jdx++) { - var byteIndex = jdx >> 3; - var bitIndex = 7 - (jdx & 0x07); + for (let trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) { + let track = []; + const rawTrack = []; + const slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE); + const trackData = new Uint8Array(slice); + const trackInfo = new DataView(slice); + const trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true); + for (let jdx = 0; jdx < trackBitCount; jdx++) { + const byteIndex = jdx >> 3; + const bitIndex = 7 - (jdx & 0x07); rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1; } track = []; - var offset = 0; + let offset = 0; while (offset < rawTrack.length) { - var result = grabNibble(rawTrack, offset); + const result = grabNibble(rawTrack, offset); if (!result.nibble) { break; } track.push(result.nibble); offset = result.offset + 1; @@ -123,12 +123,12 @@ function TrksChunk(data) { } function TrksChunk2(data) { - var trackNo; + let trackNo; this.trks = []; for (trackNo = 0; trackNo < 160; trackNo++) { - var startBlock = data.getUint16(trackNo * 8, true); - var blockCount = data.getUint16(trackNo * 8 + 2, true); - var bitCount = data.getUint32(trackNo * 8 + 4, true); + const startBlock = data.getUint16(trackNo * 8, true); + const blockCount = data.getUint16(trackNo * 8 + 2, true); + const bitCount = data.getUint32(trackNo * 8 + 4, true); if (bitCount === 0) { break; } this.trks.push({ startBlock: startBlock, @@ -139,42 +139,42 @@ function TrksChunk2(data) { this.tracks = []; this.rawTracks = []; - var bits = data.buffer; + const bits = data.buffer; for (trackNo = 0; trackNo < this.trks.length; trackNo++) { - var trk = this.trks[trackNo]; - var track = []; - var rawTrack = []; - var start = trk.startBlock * 512; - var end = start + trk.blockCount * 512; - var slice = bits.slice(start, end); - var trackData = new Uint8Array(slice); - for (var jdx = 0; jdx < trk.bitCount; jdx++) { - var byteIndex = jdx >> 3; - var bitIndex = 7 - (jdx & 0x07); + const trk = this.trks[trackNo]; + let track = []; + const rawTrack = []; + const start = trk.startBlock * 512; + const end = start + trk.blockCount * 512; + const slice = bits.slice(start, end); + const trackData = new Uint8Array(slice); + for (let jdx = 0; jdx < trk.bitCount; jdx++) { + const byteIndex = jdx >> 3; + const bitIndex = 7 - (jdx & 0x07); rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1; } track = []; - var offset = 0; + let offset = 0; while (offset < rawTrack.length) { - var result = grabNibble(rawTrack, offset); + const result = grabNibble(rawTrack, offset); if (!result.nibble) { break; } track.push(result.nibble); offset = result.offset + 1; } - this.tracks[trackNo] = track; - this.rawTracks[trackNo] = rawTrack; + this.tracks[trackNo] = new Uint8Array(track); + this.rawTracks[trackNo] = new Uint8Array(rawTrack); } return this; } function MetaChunk(data) { - var infoStr = stringFromBytes(data, 0, data.byteLength); - var parts = infoStr.split('\n'); - var info = parts.reduce(function(acc, part) { - var subParts = part.split('\t'); + const infoStr = stringFromBytes(data, 0, data.byteLength); + const parts = infoStr.split('\n'); + const info = parts.reduce(function(acc, part) { + const subParts = part.split('\t'); acc[subParts[0]] = subParts[1]; return acc; }, {}); @@ -189,19 +189,20 @@ function MetaChunk(data) { * @param {*} options the disk image and options * @returns {import('./format_utils').Disk} */ -export default function Woz(options) { - var { rawData } = options; - var dv = new DataView(rawData, 0); - var dvOffset = 0; - var disk = { - format: 'woz' +export default function createDiskFromWoz(options) { + const { rawData } = options; + const dv = new DataView(rawData, 0); + let dvOffset = 0; + const disk = { + format: 'woz', + encoding: ENCODING_BITSTREAM, }; - var wozVersion; - var chunks = {}; + let wozVersion; + const chunks = {}; function readHeader() { - var wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true); + const wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true); switch (wozSignature) { case WOZ1_SIGNATURE: @@ -226,9 +227,9 @@ export default function Woz(options) { return null; } - var type = dv.getUint32(dvOffset, true); - var size = dv.getUint32(dvOffset + 4, true); - var data = new DataView(dv.buffer, dvOffset + 8, size); + const type = dv.getUint32(dvOffset, true); + const size = dv.getUint32(dvOffset + 4, true); + const data = new DataView(dv.buffer, dvOffset + 8, size); dvOffset += size + 8; return { @@ -240,7 +241,7 @@ export default function Woz(options) { if (readHeader()) { dvOffset = WOZ_HEADER_SIZE; - var chunk = readChunk(); + let chunk = readChunk(); while (chunk) { switch (chunk.type) { case 0x4F464E49: // INFO @@ -275,7 +276,7 @@ export default function Woz(options) { disk.tracks = chunks.trks.tracks; disk.rawTracks = chunks.trks.rawTracks; disk.readOnly = true; //chunks.info.writeProtected === 1; - disk.name = chunks.info.title; + disk.name = chunks.meta?.title || options.name; return disk; } diff --git a/js/types.ts b/js/types.ts index eb5dbc3..3e47499 100644 --- a/js/types.ts +++ b/js/types.ts @@ -1,4 +1,3 @@ - /** * Extracts the members of a constant array as a type. Used as: * @@ -11,7 +10,7 @@ export type MemberOf> = /** * Recursively extracts all members of a constant array as a type. Used as: - * + * * @example * const SOME_ARRAYS = [['a'],['b', 2], 3] as const; * type SomeArrayValues = DeepMemberOf; // 'a' | 'b' | 2 | 3 @@ -23,7 +22,7 @@ export type DeepMemberOf> = /** * Extracts the declared keys of a type by removing `string` and `number`. - * + * * Cribbed from the interwebs: * https://github.com/microsoft/TypeScript/issues/25987#issuecomment-408339599 */ @@ -39,7 +38,7 @@ export type KnownValues = T extends { /** * Replacement for `includes` on constant types that is also a type assertion. - * + * * @example * const SOME_VALUES = [1, 2, 'a'] as const; * let n: number = 1; @@ -101,35 +100,6 @@ export interface Card extends Memory, Restorable { ioSwitch(off: byte, val?: byte): byte | undefined; } -export const DISK_FORMATS = [ - '2mg', - 'd13', - 'do', - 'dsk', - 'hdv', - 'po', - 'nib', - 'woz' -] as const; - -export type DiskFormat = MemberOf; - -export interface Drive { - format: DiskFormat, - volume: number, - tracks: Array, - trackMap: unknown, -} - -export interface DiskIIDrive extends Drive { - rawTracks: unknown, - track: number, - head: number, - phase: number, - readOnly: boolean, - dirty: boolean, -} - export type TapeData = Array<[duration: number, high: boolean]>; export interface Restorable { diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 3e707e6..f95fd46 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -3,22 +3,29 @@ import MicroModal from 'micromodal'; import { base64_json_parse, base64_json_stringify } from '../base64'; import { Audio, SOUND_ENABLED_OPTION } from './audio'; import DriveLights from './drive_lights'; -import { byte, DISK_FORMATS, includes, word } from '../types'; -import { initGamepad, GamepadConfiguration } from './gamepad'; +import { byte, includes, word } from '../types'; +import { BLOCK_FORMATS, MassStorage, NIBBLE_FORMATS } from '../formats/types'; +import { + DISK_FORMATS, + DriveNumber, + DRIVE_NUMBERS, + JSONDisk +} from '../formats/types'; +import { initGamepad } from './gamepad'; import KeyBoard from './keyboard'; import Tape, { TAPE_TYPES } from './tape'; +import type { GamepadConfiguration } from './types'; import ApplesoftDump from '../applesoft/decompiler'; import ApplesoftCompiler from '../applesoft/compiler'; -import { debug, gup, hup } from '../util'; +import { debug } from '../util'; import { Apple2, Stats } from '../apple2'; -import DiskII, { DriveNumber, DRIVE_NUMBERS } from '../cards/disk2'; -import SmartPort from '../cards/smartport'; +import DiskII from '../cards/disk2'; import CPU6502 from '../cpu6502'; import { VideoModes } from '../videomodes'; import Apple2IO from '../apple2io'; -import { JSONDisk } from '../formats/format_utils'; +import { } from '../formats/format_utils'; import Printer from './printer'; import { OptionsModal } from './options_modal'; @@ -71,7 +78,7 @@ let stats: Stats; let vm: VideoModes; let tape: Tape; let _disk2: DiskII; -let _smartPort: SmartPort; +let _massStorage: MassStorage; let _printer: Printer; let audio: Audio; let screen: Screen; @@ -98,9 +105,10 @@ export function compileAppleSoftProgram(program: string) { } export function openLoad(driveString: string, event: MouseEvent) { - const drive = parseInt(driveString, 10); + const drive = parseInt(driveString, 10) as DriveNumber; + _currentDrive = drive; if (event.metaKey && includes(DRIVE_NUMBERS, drive)) { - openLoadHTTP(drive); + openLoadHTTP(); } else { if (disk_cur_cat[drive]) { const element = document.querySelector('#category_select')!; @@ -380,16 +388,25 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) { files[drive - 1] = ''; document.location.hash = files.join('|'); - if (result.byteLength >= 800 * 1024) { - if (_smartPort.setBinary(drive, name, ext, result)) { - initGamepad(); - } - } else { - if (includes(DISK_FORMATS, ext) - && _disk2.setBinary(drive, name, ext, result)) { - initGamepad(); + if (includes(DISK_FORMATS, ext)) { + if (result.byteLength >= 800 * 1024) { + if ( + includes(BLOCK_FORMATS, ext) && + _massStorage.setBinary(drive, name, ext, result) + ) { + initGamepad(); + } else { + openAlert(`Unable to load ${name}`); + } } else { - openAlert(`Unable to load ${name}`); + if ( + includes(NIBBLE_FORMATS, ext) && + _disk2.setBinary(drive, name, ext, result) + ) { + initGamepad(); + } else { + openAlert(`Unable to load ${name}`); + } } } loadingStop(); @@ -442,18 +459,24 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) { const fileParts = file.split('.'); const ext = fileParts.pop()!.toLowerCase(); const name = decodeURIComponent(fileParts.join('.')); - if (data.byteLength >= 800 * 1024) { - if (_smartPort.setBinary(drive, name, ext, data)) { - initGamepad(); - } - } else { - if (includes(DISK_FORMATS, ext)) { - if (_disk2.setBinary(drive, name, ext, data)) { + if (includes(DISK_FORMATS, ext)) { + if (data.byteLength >= 800 * 1024) { + if ( + includes(BLOCK_FORMATS, ext) && + _massStorage.setBinary(drive, name, ext, data) + ) { initGamepad(); } } else { - throw new Error(`Extension ${ext} not recognized.`); + if ( + includes(NIBBLE_FORMATS, ext) && + _disk2.setBinary(drive, name, ext, data) + ) { + initGamepad(); + } } + } else { + throw new Error(`Extension ${ext} not recognized.`); } loadingStop(); }).catch(function (error) { @@ -463,8 +486,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) { } } -function openLoadHTTP(drive: DriveNumber) { - _currentDrive = drive; +function openLoadHTTP() { MicroModal.show('http-modal'); } @@ -685,7 +707,6 @@ const categorySelect = document.querySelector('#category_sele declare global { interface Window { disk_index: DiskDescriptor[]; - e: boolean; } } @@ -808,7 +829,29 @@ declare global { } } -function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: Printer, e: boolean) { +/** + * Returns the value of a query parameter or the empty string if it does not + * exist. + * @param name the parameter name. Note that `name` must not have any RegExp + * meta-characters except '[' and ']' or it will fail. + */ + +function gup(name: string) { + const params = new URLSearchParams(window.location.search); + return params.get(name); +} + +/** Returns the URL fragment. */ +function hup() { + const regex = new RegExp('#(.*)'); + const results = regex.exec(window.location.hash); + if (!results) + return ''; + else + return results[1]; +} + +function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) { _apple2 = apple2; cpu = _apple2.getCPU(); io = _apple2.getIO(); @@ -816,7 +859,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: vm = apple2.getVideoModes(); tape = new Tape(io); _disk2 = disk2; - _smartPort = smartPort; + _massStorage = massStorage; _printer = printer; _e = e; @@ -905,8 +948,8 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: ); } -export function initUI(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: Printer, e: boolean) { +export function initUI(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) { window.addEventListener('load', () => { - onLoaded(apple2, disk2, smartPort, printer, e); + onLoaded(apple2, disk2, massStorage, printer, e); }); } diff --git a/js/ui/drive_lights.ts b/js/ui/drive_lights.ts index 905b98c..01cdfde 100644 --- a/js/ui/drive_lights.ts +++ b/js/ui/drive_lights.ts @@ -1,4 +1,5 @@ -import { Callbacks, DriveNumber } from '../cards/disk2'; +import { Callbacks } from '../cards/disk2'; +import type { DriveNumber } from '../formats/types'; export default class DriveLights implements Callbacks { public driveLight(drive: DriveNumber, on: boolean) { diff --git a/js/ui/gamepad.ts b/js/ui/gamepad.ts index e40dd06..e229113 100644 --- a/js/ui/gamepad.ts +++ b/js/ui/gamepad.ts @@ -10,50 +10,10 @@ */ import Apple2IO from '../apple2io'; -import { KnownKeys } from '../types'; +import { BUTTON, ButtonType, GamepadConfiguration } from './types'; export let gamepad: Gamepad | null = null; -const BUTTON = { - // Buttons - 'A': 0, - 'B': 1, - 'X': 2, - 'Y': 3, - - // Triggers - 'L1': 4, - 'R1': 5, - - // Analog stick buttons - 'L3': 6, - 'R3': 7, - - // Special - 'START': 8, - 'SELECT': 9, - 'LOGO': 10, - - // D pad - 'UP': 11, - 'DOWN': 12, - 'LEFT': 13, - 'RIGHT': 14 -} as const; - -type ButtonType = KnownKeys; - -/** - * A `GamepadConfiguration` maps buttons on the controller to Apple Paddle - * buttons or keys on the keyboard. If the value is a number, it must be - * 0 | 1 | 2 and will map to the corresponding paddle button. If the value - * is a string, the _first_ character of the string is used as a key to - * press on the keyboard. - */ -export type GamepadConfiguration = { - [K in ButtonType]?: 0 | 1 | 2 | string; -}; - const DEFAULT_GAMEPAD: GamepadConfiguration = { 'A': 0, 'B': 1, @@ -64,7 +24,7 @@ const DEFAULT_GAMEPAD: GamepadConfiguration = { /** * 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 a key on the keyboard that is pressed; * * if _e_ is undefined, nothing happens. diff --git a/js/ui/types.ts b/js/ui/types.ts new file mode 100644 index 0000000..bf5345a --- /dev/null +++ b/js/ui/types.ts @@ -0,0 +1,52 @@ +/* Copyright 2010-2021 Will Scullin + * + * 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; + +/** + * 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; +}; diff --git a/js/util.ts b/js/util.ts index 738cb78..dbcf93a 100644 --- a/js/util.ts +++ b/js/util.ts @@ -91,27 +91,6 @@ export function toBinary(v: byte) { return result; } -/** - * Returns the value of a query parameter or the empty string if it does not - * exist. - * @param name the parameter name. Note that `name` must not have any RegExp - * meta-characters except '[' and ']' or it will fail. - */ -export function gup(name: string) { - const params = new URLSearchParams(window.location.search); - return params.get(name); -} - -/** Returns the URL fragment. */ -export function hup() { - const regex = new RegExp('#(.*)'); - const results = regex.exec(window.location.hash); - if (!results) - return ''; - else - return results[1]; -} - /** Packs a 32-bit integer into a string in little-endian order. */ export function numToString(num: number) { let result = ''; diff --git a/package-lock.json b/package-lock.json index 683c6a9..459c7ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "jest-image-snapshot": "^4.5.0", "node-forge": "^0.10.0", "raw-loader": "^4.0.0", + "rimraf": "^3.0.2", "ts-jest": "^26.5.0", "ts-loader": "^8.0.15", "typescript": "^4.1.3", diff --git a/package.json b/package.json index 41582b2..856c1ca 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,11 @@ "version": "0.0.1", "description": "Apple II Emulator in JavaScript", "scripts": { - "build": "webpack --mode=production", + "build": "rimraf dist/* && webpack --mode=production", "dev": "webpack serve --mode=development", "index": "bin/index > json/disks/index.js", "lint": "eslint '**/*.js' '**/*.ts'", - "start": "webpack serve --mode=development", + "start": "webpack serve --mode=development --progress", "test": "jest" }, "engines": { @@ -42,6 +42,7 @@ "jest-image-snapshot": "^4.5.0", "node-forge": "^0.10.0", "raw-loader": "^4.0.0", + "rimraf": "^3.0.2", "ts-jest": "^26.5.0", "ts-loader": "^8.0.15", "typescript": "^4.1.3", diff --git a/test/js/formats/d13.spec.ts b/test/js/formats/d13.spec.ts index 81959c6..b430259 100644 --- a/test/js/formats/d13.spec.ts +++ b/test/js/formats/d13.spec.ts @@ -347,4 +347,4 @@ describe('DOS-13 format', () => { } } }); -}); \ No newline at end of file +}); diff --git a/test/js/formats/do.spec.ts b/test/js/formats/do.spec.ts index cea92c0..fe1f94e 100644 --- a/test/js/formats/do.spec.ts +++ b/test/js/formats/do.spec.ts @@ -284,4 +284,4 @@ describe('DOS format', () => { } } }); -}); \ No newline at end of file +}); diff --git a/test/js/formats/po.spec.ts b/test/js/formats/po.spec.ts index e9e2854..ad18438 100644 --- a/test/js/formats/po.spec.ts +++ b/test/js/formats/po.spec.ts @@ -284,4 +284,4 @@ describe('ProDOS format', () => { } } }); -}); \ No newline at end of file +}); diff --git a/test/js/formats/testdata/13sector.ts b/test/js/formats/testdata/13sector.ts index b7c7722..e77376a 100644 --- a/test/js/formats/testdata/13sector.ts +++ b/test/js/formats/testdata/13sector.ts @@ -1,11 +1,11 @@ -import { byte } from '../../../../js/types'; +import { memory } from '../../../../js/types'; function generateBytesInOrder() { - const data: byte[][][] = []; + const data: memory[][] = []; for (let t = 0; t < 35; t++) { - const track: byte[][] = []; + const track: memory[] = []; for (let s = 0; s < 13; s++) { - const sector: byte[] = []; + const sector: memory = new Uint8Array(256); for (let b = 0; b < 256; b++) { sector[b] = b; } @@ -16,14 +16,14 @@ function generateBytesInOrder() { return data; } -export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder(); +export const BYTES_IN_ORDER: memory[][] = generateBytesInOrder(); function generateBytesBySector() { - const data: byte[][][] = []; + const data: memory[][] = []; for (let t = 0; t < 35; t++) { - const track: byte[][] = []; + const track: memory[] = []; for (let s = 0; s < 13; s++) { - const sector: byte[] = []; + const sector: memory = new Uint8Array(256); for (let b = 0; b < 256; b++) { sector[b] = s; } @@ -34,14 +34,14 @@ function generateBytesBySector() { return data; } -export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector(); +export const BYTES_BY_SECTOR: memory[][] = generateBytesBySector(); function generateBytesByTrack() { - const data: byte[][][] = []; + const data: memory[][] = []; for (let t = 0; t < 35; t++) { - const track: byte[][] = []; + const track: memory[] = []; for (let s = 0; s < 13; s++) { - const sector: byte[] = []; + const sector: memory = new Uint8Array(256); for (let b = 0; b < 256; b++) { sector[b] = t; } @@ -52,4 +52,4 @@ function generateBytesByTrack() { return data; } -export const BYTES_BY_TRACK: byte[][][] = generateBytesByTrack(); +export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack(); diff --git a/test/js/formats/testdata/16sector.ts b/test/js/formats/testdata/16sector.ts index 1d82948..defc009 100644 --- a/test/js/formats/testdata/16sector.ts +++ b/test/js/formats/testdata/16sector.ts @@ -1,11 +1,11 @@ -import { byte } from '../../../../js/types'; +import { memory } from '../../../../js/types'; function generateBytesInOrder() { - const data: byte[][][] = []; + const data: memory[][] = []; for (let t = 0; t < 35; t++) { - const track: byte[][] = []; + const track: memory[] = []; for (let s = 0; s < 16; s++) { - const sector: byte[] = []; + const sector = new Uint8Array(256); for (let b = 0; b < 256; b++) { sector[b] = b; } @@ -16,14 +16,14 @@ function generateBytesInOrder() { return data; } -export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder(); +export const BYTES_IN_ORDER: memory[][] = generateBytesInOrder(); function generateBytesBySector() { - const data: byte[][][] = []; + const data: memory[][] = []; for (let t = 0; t < 35; t++) { - const track: byte[][] = []; + const track: memory[] = []; for (let s = 0; s < 16; s++) { - const sector: byte[] = []; + const sector = new Uint8Array(256); for (let b = 0; b < 256; b++) { sector[b] = s; } @@ -34,14 +34,14 @@ function generateBytesBySector() { return data; } -export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector(); +export const BYTES_BY_SECTOR: memory[][] = generateBytesBySector(); function generateBytesByTrack() { - const data: byte[][][] = []; + const data: memory[][] = []; for (let t = 0; t < 35; t++) { - const track: byte[][] = []; + const track: memory[] = []; for (let s = 0; s < 16; s++) { - const sector: byte[] = []; + const sector = new Uint8Array(256); for (let b = 0; b < 256; b++) { sector[b] = t; } @@ -52,4 +52,4 @@ function generateBytesByTrack() { return data; } -export const BYTES_BY_TRACK: byte[][][] = generateBytesByTrack(); +export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack(); diff --git a/tsconfig.json b/tsconfig.json index fcd4df0..b21c14f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "target": "es6", + "lib": ["DOM", "ES6"], "noImplicitAny": true, "noImplicitThis": true, "noUnusedLocals": true, diff --git a/webpack.config.js b/webpack.config.js index 69f5d15..c1ef889 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const { merge } = require('webpack-merge'); const baseConfig = { devtool: 'source-map', @@ -16,22 +17,24 @@ const baseConfig = { }, ], }, + output: { + publicPath: 'dist/', + path: path.resolve('dist/'), + filename: '[name].bundle.js', + chunkFilename: '[name].bundle.js', + }, resolve: { extensions: ['.ts', '.js'], }, }; -module.exports = [ +const appConfig = merge(baseConfig, { - ...baseConfig, entry: { main2: path.resolve('js/entry2.js'), main2e: path.resolve('js/entry2e.js') }, output: { - path: path.resolve('dist/'), - filename: '[name].bundle.js', - chunkFilename: '[name].bundle.js', library: { name: 'Apple2', type: 'umd', @@ -47,21 +50,34 @@ module.exports = [ directory: __dirname, }, devMiddleware: { - publicPath: '/dist/', + publicPath: 'dist/', }, }, - }, + } +); + +const workletConfig = merge(baseConfig, { - ...baseConfig, target: false, entry: { audio_worker: path.resolve('js/ui/audio_worker.ts') }, output: { - publicPath: '/dist/', - path: path.resolve('dist/'), - filename: '[name].bundle.js', + globalObject: 'globalThis', + }, + } +); + +const workerConfig = merge(baseConfig, + { + target: false, + entry: { + format_worker: path.resolve('workers/format.worker.ts') + }, + output: { globalObject: 'globalThis', }, }, -]; +); + +exports.default = [appConfig, workletConfig, workerConfig]; diff --git a/workers/format.worker.ts b/workers/format.worker.ts new file mode 100644 index 0000000..bb03cfc --- /dev/null +++ b/workers/format.worker.ts @@ -0,0 +1,67 @@ +/* Copyright 2021 Will Scullin + * + * 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) => { + 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); +}); diff --git a/workers/tsconfig.json b/workers/tsconfig.json new file mode 100644 index 0000000..d98b9f8 --- /dev/null +++ b/workers/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "lib": ["ES6", "WebWorker"] + }, + "include": [ + "./**/*" + ] +}