diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts index b3df6f6..b8c5262 100644 --- a/js/cards/cffa.ts +++ b/js/cards/cffa.ts @@ -389,6 +389,7 @@ export default class CFFA implements Card, MassStorage, Restorable< (block) => new Uint8Array(block) ), encoding: ENCODING_BLOCK, + format: disk.format, readOnly: disk.readOnly, metadata: { ...disk.metadata }, }; @@ -472,7 +473,7 @@ export default class CFFA implements Card, MassStorage, Restorable< volume, readOnly }; - const disk = createBlockDisk(options); + const disk = createBlockDisk(ext, options); return this.setBlockVolume(drive, disk); } @@ -485,7 +486,7 @@ export default class CFFA implements Card, MassStorage, Restorable< } const { blocks, readOnly } = blockDisk; const { name } = blockDisk.metadata; - let ext; + let ext: '2mg' | 'po'; let data: ArrayBuffer; if (this._metadata[drive]) { ext = '2mg'; diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index e4faa9e..e04cb30 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -2,7 +2,6 @@ import { base64_encode } from '../base64'; import type { byte, Card, - memory, nibble, ReadonlyUint8Array, } from '../types'; @@ -15,16 +14,21 @@ import { DRIVE_NUMBERS, DriveNumber, JSONDisk, - ENCODING_NIBBLE, PROCESS_BINARY, PROCESS_JSON_DISK, PROCESS_JSON, - ENCODING_BITSTREAM, MassStorage, MassStorageData, - DiskMetadata, SupportedSectors, FloppyDisk, + FloppyFormat, + WozDisk, + NibbleDisk, + isNibbleDisk, + isWozDisk, + NoFloppyDisk, + isNoFloppyDisk, + NO_DISK, } from '../formats/types'; import { @@ -212,66 +216,26 @@ export interface Callbacks { } /** Common information for Nibble and WOZ disks. */ -interface BaseDrive { - /** Current disk format. */ - format: NibbleFormat; - /** Current disk volume number. */ - volume: byte; +interface Drive { + /** Whether the drive write protect is on. */ + readOnly: boolean; /** Quarter track position of read/write head. */ track: byte; /** Position of the head on the track. */ head: byte; /** Current active coil in the head stepper motor. */ phase: Phase; - /** Whether the drive write protect is on. */ - readOnly: boolean; /** Whether the drive has been written to since it was loaded. */ dirty: boolean; - /** Metadata about the disk image */ - metadata: DiskMetadata; -} - -/** 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. */ - rawTracks: Uint8Array[]; -} - -/** 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 drive.encoding === ENCODING_NIBBLE; -} - -function isWozDrive(drive: Drive): drive is WozDrive { - return drive.encoding === ENCODING_BITSTREAM; } interface DriveState { - format: NibbleFormat; - encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE; - volume: byte; - tracks: memory[]; + disk: FloppyDisk; + readOnly: boolean; track: byte; head: byte; phase: Phase; - readOnly: boolean; dirty: boolean; - trackMap: number[]; - rawTracks: Uint8Array[]; - metadata: DiskMetadata; } /** State of the controller for saving/restoring. */ @@ -282,73 +246,54 @@ interface State { controllerState: ControllerState; } -function getDriveState(drive: Drive): DriveState { - const result: DriveState = { - format: drive.format, - encoding: drive.encoding, - volume: drive.volume, - tracks: [], - track: drive.track, - head: drive.head, - phase: drive.phase, - readOnly: drive.readOnly, - dirty: drive.dirty, - trackMap: [], - rawTracks: [], - metadata: { ...drive.metadata }, - }; - - if (isNibbleDrive(drive)) { - for (let idx = 0; idx < drive.tracks.length; idx++) { - result.tracks.push(new Uint8Array(drive.tracks[idx])); - } +function getDiskState(disk: NoFloppyDisk): NoFloppyDisk; +function getDiskState(disk: NibbleDisk): NibbleDisk; +function getDiskState(disk: WozDisk): WozDisk; +function getDiskState(disk: FloppyDisk): FloppyDisk; +function getDiskState(disk: FloppyDisk): FloppyDisk { + if (isNoFloppyDisk(disk)) { + const { encoding, metadata, readOnly } = disk; + return { + encoding, + metadata: {...metadata}, + readOnly, + }; } - if (isWozDrive(drive)) { - result.trackMap = [...drive.trackMap]; - for (let idx = 0; idx < drive.rawTracks.length; idx++) { - result.rawTracks.push(new Uint8Array(drive.rawTracks[idx])); - } - } - return result; -} - -function setDriveState(state: DriveState) { - let result: Drive; - if (state.encoding === ENCODING_NIBBLE) { - result = { - format: state.format, - encoding: ENCODING_NIBBLE, - volume: state.volume, + if (isNibbleDisk(disk)) { + const { format, encoding, metadata, readOnly, volume, tracks } = disk; + const result: NibbleDisk = { + format, + encoding, + volume, tracks: [], - track: state.track, - head: state.head, - phase: state.phase, - readOnly: state.readOnly, - dirty: state.dirty, - metadata: { ...state.metadata }, + readOnly, + metadata: { ...metadata }, }; - 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, - track: state.track, - head: state.head, - phase: state.phase, - readOnly: state.readOnly, - dirty: state.dirty, - trackMap: [...state.trackMap], - rawTracks: [], - metadata: { ...state.metadata }, - }; - for (let idx = 0; idx < state.rawTracks.length; idx++) { - result.rawTracks.push(new Uint8Array(state.rawTracks[idx])); + for (let idx = 0; idx < tracks.length; idx++) { + result.tracks.push(new Uint8Array(tracks[idx])); } + return result; } - return result; + + if (isWozDisk(disk)) { + const { format, encoding, metadata, readOnly, trackMap, rawTracks } = disk; + const result: WozDisk = { + format, + encoding, + readOnly, + trackMap: [], + rawTracks: [], + metadata: { ...metadata }, + info: disk.info, + }; + result.trackMap = [...trackMap]; + for (let idx = 0; idx < rawTracks.length; idx++) { + result.rawTracks.push(new Uint8Array(rawTracks[idx])); + } + return result; + } + + throw new Error('Unknown drive state'); } /** @@ -358,27 +303,30 @@ export default class DiskII implements Card, MassStorage { private drives: Record = { 1: { // Drive 1 - format: 'dsk', - encoding: ENCODING_NIBBLE, - volume: 254, - tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, - metadata: { name: 'Disk 1' }, }, 2: { // Drive 2 - format: 'dsk', - encoding: ENCODING_NIBBLE, - volume: 254, - tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, + } + }; + + private disks: Record = { + 1: { + encoding: NO_DISK, + readOnly: false, + metadata: { name: 'Disk 1' }, + }, + 2: { + encoding: NO_DISK, + readOnly: false, metadata: { name: 'Disk 2' }, } }; @@ -393,8 +341,10 @@ export default class DiskII implements Card, MassStorage { private skip = 0; /** Drive off timeout id or null. */ private offTimeout: number | null = null; - /** Current drive object. */ - private cur: Drive; + /** Current drive object. Must only be set by `updateActiveDrive()`. */ + private curDrive: Drive; + /** Current disk object. Must only be set by `updateActiveDrive()`. */ + private curDisk: FloppyDisk; /** Nibbles read this on cycle */ private nibbleCount = 0; @@ -432,17 +382,23 @@ export default class DiskII implements Card, MassStorage { state: 2, }; - this.cur = this.drives[this.state.drive]; + this.updateActiveDrive(); this.initWorker(); } + /** Updates the active drive based on the controller state. */ + private updateActiveDrive() { + this.curDrive = this.drives[this.state.drive]; + this.curDisk = this.disks[this.state.drive]; + } + private debug(..._args: unknown[]) { // debug(..._args); } public head(): number { - return this.cur.head; + return this.curDrive.head; } /** @@ -487,18 +443,18 @@ export default class DiskII implements Card, MassStorage { let workCycles = (cycles - this.lastCycles) * 2; this.lastCycles = cycles; - if (!isWozDrive(this.cur)) { + if (!isWozDisk(this.curDisk)) { return; } const track = - this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; + this.curDisk.rawTracks[this.curDisk.trackMap[this.curDrive.track]] || [0]; const state = this.state; while (workCycles-- > 0) { let pulse: number = 0; if (state.clock === 4) { - pulse = track[this.cur.head]; + pulse = track[this.curDrive.head]; if (!pulse) { // More than 2 zeros can not be read reliably. if (++this.zeros > 2) { @@ -531,7 +487,7 @@ export default class DiskII implements Card, MassStorage { break; case 0xA: // SR state.latch >>= 1; - if (this.cur.readOnly) { + if (this.curDrive.readOnly) { state.latch |= 0x80; } break; @@ -550,12 +506,12 @@ export default class DiskII implements Card, MassStorage { if (state.clock === 4) { if (state.on) { if (state.q7) { - track[this.cur.head] = state.state & 0x8 ? 0x01 : 0x00; + track[this.curDrive.head] = state.state & 0x8 ? 0x01 : 0x00; this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00); } - if (++this.cur.head >= track.length) { - this.cur.head = 0; + if (++this.curDrive.head >= track.length) { + this.curDrive.head = 0; } } } @@ -568,29 +524,29 @@ export default class DiskII implements Card, MassStorage { // Only called for non-WOZ disks private readWriteNext() { - if (!isNibbleDrive(this.cur)) { + if (!isNibbleDisk(this.curDisk)) { return; } const state = this.state; if (state.on && (this.skip || state.q7)) { - const track = this.cur.tracks[this.cur.track >> 2]; + const track = this.curDisk.tracks[this.curDrive.track >> 2]; if (track && track.length) { - if (this.cur.head >= track.length) { - this.cur.head = 0; + if (this.curDrive.head >= track.length) { + this.curDrive.head = 0; } if (state.q7) { - if (!this.cur.readOnly) { - track[this.cur.head] = state.bus; - if (!this.cur.dirty) { + if (!this.curDrive.readOnly) { + track[this.curDrive.head] = state.bus; + if (!this.curDrive.dirty) { this.updateDirty(state.drive, true); } } } else { - state.latch = track[this.cur.head]; + state.latch = track[this.curDrive.head]; } - ++this.cur.head; + ++this.curDrive.head; } } else { state.latch = 0; @@ -619,22 +575,24 @@ export default class DiskII implements Card, MassStorage { this.debug(`phase ${phase}${on ? ' on' : ' off'}`); if (on) { - this.cur.track += PHASE_DELTA[this.cur.phase][phase] * 2; - this.cur.phase = phase; + this.curDrive.track += PHASE_DELTA[this.curDrive.phase][phase] * 2; + this.curDrive.phase = phase; } // The emulator clamps the track to the valid track range available // in the image, but real Disk II drives can seek past track 34 by // at least a half track, usually a full track. Some 3rd party // drives can seek to track 39. - const maxTrack = isNibbleDrive(this.cur) - ? this.cur.tracks.length * 4 - 1 - : this.cur.trackMap.length - 1; - if (this.cur.track > maxTrack) { - this.cur.track = maxTrack; + const maxTrack = isNibbleDisk(this.curDisk) + ? this.curDisk.tracks.length * 4 - 1 + : (isWozDisk(this.curDisk) + ? this.curDisk.trackMap.length - 1 + : 0); + if (this.curDrive.track > maxTrack) { + this.curDrive.track = maxTrack; } - if (this.cur.track < 0x0) { - this.cur.track = 0x0; + if (this.curDrive.track < 0x0) { + this.curDrive.track = 0x0; } // debug( @@ -706,7 +664,7 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVE1: // 0x0a this.debug('Disk 1'); state.drive = 1; - this.cur = this.drives[state.drive]; + this.updateActiveDrive(); if (state.on) { this.callbacks.driveLight(2, false); this.callbacks.driveLight(1, true); @@ -715,7 +673,7 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVE2: // 0x0b this.debug('Disk 2'); state.drive = 2; - this.cur = this.drives[state.drive]; + this.updateActiveDrive(); if (state.on) { this.callbacks.driveLight(1, false); this.callbacks.driveLight(2, true); @@ -727,7 +685,7 @@ export default class DiskII implements Card, MassStorage { if (state.q7) { this.debug('clearing _q6/SHIFT'); } - if (isNibbleDrive(this.cur)) { + if (isNibbleDisk(this.curDisk)) { this.readWriteNext(); } break; @@ -737,9 +695,9 @@ export default class DiskII implements Card, MassStorage { if (state.q7) { this.debug('setting _q6/LOAD'); } - if (isNibbleDrive(this.cur)) { + if (isNibbleDisk(this.curDisk)) { if (readMode && !state.q7) { - if (this.cur.readOnly) { + if (this.curDrive.readOnly) { state.latch = 0xff; this.debug('Setting readOnly'); } else { @@ -812,60 +770,85 @@ export default class DiskII implements Card, MassStorage { state.q7 = false; state.on = false; state.drive = 1; - this.cur = this.drives[state.drive]; } + this.updateActiveDrive(); } tick() { this.moveHead(); } + private getDriveState(drive: DriveNumber): DriveState { + const curDrive = this.drives[drive]; + const curDisk = this.disks[drive]; + const { readOnly, track, head, phase, dirty } = curDrive; + return { + disk: getDiskState(curDisk), + readOnly, + track, + head, + phase, + dirty, + }; + } + getState(): State { const result = { drives: [] as DriveState[], skip: this.skip, controllerState: { ...this.state }, }; - result.drives[1] = getDriveState(this.drives[1]); - result.drives[2] = getDriveState(this.drives[2]); + result.drives[1] = this.getDriveState(1); + result.drives[2] = this.getDriveState(2); return result; } + private setDriveState(drive: DriveNumber, state: DriveState) { + const { track, head, phase, readOnly, dirty } = state; + this.drives[drive] = { + track, + head, + phase, + readOnly, + dirty, + }; + this.disks[drive] = getDiskState(state.disk); + } + + setState(state: State) { this.skip = state.skip; this.state = { ...state.controllerState }; for (const d of DRIVE_NUMBERS) { - this.drives[d] = setDriveState(state.drives[d]); - const { name, side } = state.drives[d].metadata; + this.setDriveState(d, state.drives[d]); + const { name, side } = state.drives[d].disk.metadata; const { dirty } = state.drives[d]; this.callbacks.label(d, name, side); this.callbacks.driveLight(d, this.state.on); this.callbacks.dirty(d, dirty); } - this.cur = this.drives[this.state.drive]; + this.updateActiveDrive(); } getMetadata(driveNo: DriveNumber) { - const drive = this.drives[driveNo]; + const { track, head, phase, readOnly, dirty } = this.drives[driveNo]; return { - format: drive.format, - volume: drive.volume, - track: drive.track, - head: drive.head, - phase: drive.phase, - readOnly: drive.readOnly, - dirty: drive.dirty + track, + head, + phase, + readOnly, + dirty, }; } // TODO(flan): Does not work on WOZ disks - rwts(disk: DriveNumber, track: byte, sector: byte) { - const cur = this.drives[disk]; - if (!isNibbleDrive(cur)) { + rwts(drive: DriveNumber, track: byte, sector: byte) { + const curDisk = this.disks[drive]; + if (!isNibbleDisk(curDisk)) { throw new Error('Can\'t read WOZ disks'); } - return readSector(cur, track, sector); + return readSector(curDisk, track, sector); } /** Sets the data for `drive` from `disk`, which is expected to be JSON. */ @@ -892,11 +875,11 @@ export default class DiskII implements Card, MassStorage { } getJSON(drive: DriveNumber, pretty: boolean = false) { - const cur = this.drives[drive]; - if (!isNibbleDrive(cur)) { + const curDisk = this.disks[drive]; + if (!isNibbleDisk(curDisk)) { throw new Error('Can\'t save WOZ disks to JSON'); } - return jsonEncode(cur, pretty); + return jsonEncode(curDisk, pretty); } setJSON(drive: DriveNumber, json: string) { @@ -916,7 +899,7 @@ export default class DiskII implements Card, MassStorage { return true; } - setBinary(drive: DriveNumber, name: string, fmt: NibbleFormat, rawData: ArrayBuffer) { + setBinary(drive: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) { const readOnly = false; const volume = 254; const options = { @@ -975,27 +958,28 @@ export default class DiskII implements Card, MassStorage { } private insertDisk(drive: DriveNumber, disk: FloppyDisk) { - const cur = this.drives[drive]; - Object.assign(cur, disk); - const { name, side } = cur.metadata; + this.disks[drive] = disk; + this.drives[drive].head = 0; + this.updateActiveDrive(); + const { name, side } = disk.metadata; this.updateDirty(drive, true); this.callbacks.label(drive, name, side); } // TODO(flan): Does not work with WOZ or D13 disks - getBinary(drive: DriveNumber, ext?: NibbleFormat): MassStorageData | null { - const cur = this.drives[drive]; - if (!isNibbleDrive(cur)) { + getBinary(drive: DriveNumber, ext?: Exclude): MassStorageData | null { + const curDisk = this.disks[drive]; + if (!isNibbleDisk(curDisk)) { return null; } - const { format, readOnly, tracks, volume } = cur; - const { name } = cur.metadata; + const { format, readOnly, tracks, volume } = curDisk; + const { name } = curDisk.metadata; const len = format === 'nib' ? tracks.reduce((acc, track) => acc + track.length, 0) : this.sectors * tracks.length * 256; const data = new Uint8Array(len); - ext = ext ?? format; + const extension = ext ?? format; let idx = 0; for (let t = 0; t < tracks.length; t++) { if (ext === 'nib') { @@ -1003,7 +987,7 @@ export default class DiskII implements Card, MassStorage { idx += tracks[t].length; } else { for (let s = 0; s < 0x10; s++) { - const sector = readSector({ ...cur, format: ext }, t, s); + const sector = readSector({ ...curDisk, format: extension }, t, s); data.set(sector, idx); idx += sector.length; } @@ -1011,7 +995,7 @@ export default class DiskII implements Card, MassStorage { } return { - ext, + ext: extension, metadata: { name }, data: data.buffer, readOnly, @@ -1021,18 +1005,18 @@ export default class DiskII implements Card, MassStorage { // TODO(flan): Does not work with WOZ or D13 disks getBase64(drive: DriveNumber) { - const cur = this.drives[drive]; - if (!isNibbleDrive(cur)) { + const curDisk = this.disks[drive]; + if (!isNibbleDisk(curDisk)) { return null; } const data: string[][] | string[] = []; - for (let t = 0; t < cur.tracks.length; t++) { - if (cur.format === 'nib') { - data[t] = base64_encode(cur.tracks[t]); + for (let t = 0; t < curDisk.tracks.length; t++) { + if (isNibbleDisk(curDisk)) { + data[t] = base64_encode(curDisk.tracks[t]); } else { const track: string[] = []; for (let s = 0; s < 0x10; s++) { - track[s] = base64_encode(readSector(cur, t, s)); + track[s] = base64_encode(readSector(curDisk, t, s)); } data[t] = track; } diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 5dccb1c..1137e14 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -1,7 +1,7 @@ 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, BlockFormat, MassStorageData } from '../formats/types'; +import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData, DiskFormat } from '../formats/types'; import CPU6502, { CpuState, flags } from '../cpu6502'; import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg'; import createBlockDisk from '../formats/block'; @@ -129,7 +129,7 @@ export default class SmartPort implements Card, MassStorage, Restor private disks: BlockDisk[] = []; private busy: boolean[] = []; private busyTimeout: ReturnType[] = []; - private ext: string[] = []; + private ext: DiskFormat[] = []; private metadata: Array = []; constructor( @@ -522,6 +522,7 @@ export default class SmartPort implements Card, MassStorage, Restor (block) => new Uint8Array(block) ), encoding: ENCODING_BLOCK, + format: disk.format, readOnly: disk.readOnly, metadata: { ...disk.metadata }, }; @@ -539,6 +540,7 @@ export default class SmartPort implements Card, MassStorage, Restor (block) => new Uint8Array(block) ), encoding: ENCODING_BLOCK, + format: disk.format, readOnly: disk.readOnly, metadata: { ...disk.metadata }, }; @@ -547,7 +549,7 @@ export default class SmartPort implements Card, MassStorage, Restor ); } - setBinary(drive: DriveNumber, name: string, fmt: string, rawData: ArrayBuffer) { + setBinary(drive: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) { let volume = 254; let readOnly = false; if (fmt === '2mg') { @@ -568,7 +570,7 @@ export default class SmartPort implements Card, MassStorage, Restor }; this.ext[drive] = fmt; - this.disks[drive] = createBlockDisk(options); + this.disks[drive] = createBlockDisk(fmt, options); this.callbacks?.label(drive, name); return true; diff --git a/js/components/DiskDragTarget.tsx b/js/components/DiskDragTarget.tsx index 2d2b805..6494795 100644 --- a/js/components/DiskDragTarget.tsx +++ b/js/components/DiskDragTarget.tsx @@ -1,4 +1,4 @@ -import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, MassStorage, NIBBLE_FORMATS } from 'js/formats/types'; +import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, FLOPPY_FORMATS, MassStorage } from 'js/formats/types'; import { h, JSX, RefObject } from 'preact'; import { useEffect, useRef } from 'preact/hooks'; import { loadLocalFile } from './util/files'; @@ -7,7 +7,7 @@ import { spawn } from './util/promises'; export interface DiskDragTargetProps extends JSX.HTMLAttributes { storage: MassStorage | undefined; drive?: DriveNumber; - formats: typeof NIBBLE_FORMATS + formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS; dropRef?: RefObject; diff --git a/js/components/DiskII.tsx b/js/components/DiskII.tsx index f01ae17..400aa83 100644 --- a/js/components/DiskII.tsx +++ b/js/components/DiskII.tsx @@ -7,7 +7,7 @@ import { FileModal } from './FileModal'; import styles from './css/DiskII.module.css'; import { DiskDragTarget } from './DiskDragTarget'; -import { NIBBLE_FORMATS } from 'js/formats/types'; +import { FLOPPY_FORMATS } from 'js/formats/types'; import { DownloadModal } from './DownloadModal'; /** @@ -66,7 +66,7 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => { className={styles.disk} storage={disk2} drive={number} - formats={NIBBLE_FORMATS} + formats={FLOPPY_FORMATS} onError={setError} > diff --git a/js/components/FileModal.tsx b/js/components/FileModal.tsx index 3bb0b91..3a636d7 100644 --- a/js/components/FileModal.tsx +++ b/js/components/FileModal.tsx @@ -1,6 +1,6 @@ import { h, Fragment, JSX } from 'preact'; import { useCallback, useEffect, useState } from 'preact/hooks'; -import { DiskDescriptor, DriveNumber, NibbleFormat, NIBBLE_FORMATS } from '../formats/types'; +import { DiskDescriptor, DriveNumber, FLOPPY_FORMATS, NibbleFormat } from '../formats/types'; import { Modal, ModalContent, ModalFooter } from './Modal'; import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files'; import DiskII from '../cards/disk2'; @@ -15,7 +15,7 @@ import styles from './css/FileModal.module.css'; const DISK_TYPES: FilePickerAcceptType[] = [ { description: 'Disk Images', - accept: { 'application/octet-stream': NIBBLE_FORMATS.map(x => '.' + x) }, + accept: { 'application/octet-stream': FLOPPY_FORMATS.map(x => '.' + x) }, } ]; diff --git a/js/components/debugger/Disks.tsx b/js/components/debugger/Disks.tsx index 11d5e84..b2f535c 100644 --- a/js/components/debugger/Disks.tsx +++ b/js/components/debugger/Disks.tsx @@ -2,7 +2,7 @@ import { h, Fragment } from 'preact'; import { useMemo } from 'preact/hooks'; import cs from 'classnames'; import { Apple2 as Apple2Impl } from 'js/apple2'; -import { BlockDisk, DiskFormat, DriveNumber, MassStorage, NibbleDisk } from 'js/formats/types'; +import { BlockDisk, DiskFormat, DriveNumber, FloppyDisk, isBlockDiskFormat, isNibbleDisk, MassStorage } from 'js/formats/types'; import { slot } from 'js/apple2io'; import DiskII from 'js/cards/disk2'; import SmartPort from 'js/cards/smartport'; @@ -38,7 +38,7 @@ const formatDate = (date: Date) => { * @param disk NibbleDisk or BlockDisk * @returns true if is BlockDisk */ -function isBlockDisk(disk: NibbleDisk | BlockDisk): disk is BlockDisk { +function isBlockDisk(disk: FloppyDisk | BlockDisk): disk is BlockDisk { return !!((disk as BlockDisk).blocks); } @@ -256,7 +256,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { if (massStorageData) { const { data, readOnly, ext } = massStorageData; const { name } = massStorageData.metadata; - let disk: BlockDisk | NibbleDisk | null = null; + let disk: BlockDisk | FloppyDisk | null = null; if (ext === '2mg') { disk = createDiskFrom2MG({ name, @@ -277,8 +277,8 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { } } } - if (!disk) { - disk = createBlockDisk({ + if (!disk && isBlockDiskFormat(ext)) { + disk = createBlockDisk(ext, { name, rawData: data, readOnly, @@ -330,7 +330,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { ); } - } else { + } else if (isNibbleDisk(disk)) { const dos = new DOS33(disk); return (
diff --git a/js/components/util/files.ts b/js/components/util/files.ts index f02d7bd..d365c35 100644 --- a/js/components/util/files.ts +++ b/js/components/util/files.ts @@ -1,18 +1,18 @@ -import { includes, word } from 'js/types'; -import { initGamepad } from 'js/ui/gamepad'; +import Disk2 from 'js/cards/disk2'; +import SmartPort from 'js/cards/smartport'; +import Debugger from 'js/debugger'; import { BlockFormat, BLOCK_FORMATS, DISK_FORMATS, DriveNumber, + FloppyFormat, + FLOPPY_FORMATS, JSONDisk, MassStorage, - NibbleFormat, - NIBBLE_FORMATS, } from 'js/formats/types'; -import Disk2 from 'js/cards/disk2'; -import SmartPort from 'js/cards/smartport'; -import Debugger from 'js/debugger'; +import { includes, word } from 'js/types'; +import { initGamepad } from 'js/ui/gamepad'; type ProgressCallback = (current: number, total: number) => void; @@ -46,8 +46,8 @@ export const getNameAndExtension = (url: string) => { }; export const loadLocalFile = ( - storage: MassStorage, - formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS, + storage: MassStorage, + formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS, number: DriveNumber, file: File, ) => { @@ -94,7 +94,7 @@ export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, fi * @returns true if successful */ export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: File) => { - return loadLocalFile(disk2, NIBBLE_FORMATS, number, file); + return loadLocalFile(disk2, FLOPPY_FORMATS, number, file); }; /** @@ -117,7 +117,7 @@ export const loadJSON = async ( throw new Error(`Error loading: ${response.statusText}`); } const data = await response.json() as JSONDisk; - if (!includes(NIBBLE_FORMATS, data.type)) { + if (!includes(FLOPPY_FORMATS, data.type)) { throw new Error(`Type "${data.type}" not recognized.`); } disk2.setDisk(number, data); @@ -209,7 +209,7 @@ export const loadHttpNibbleFile = async ( return loadJSON(disk2, number, url); } const { name, ext } = getNameAndExtension(url); - if (!includes(NIBBLE_FORMATS, ext)) { + if (!includes(FLOPPY_FORMATS, ext)) { throw new Error(`Extension "${ext}" not recognized.`); } const data = await loadHttpFile(url, signal, onProgress); @@ -241,7 +241,7 @@ export class SmartStorageBroker implements MassStorage { } else { throw new Error(`Unable to load "${name}"`); } - } else if (includes(NIBBLE_FORMATS, ext)) { + } else if (includes(FLOPPY_FORMATS, ext)) { this.disk2.setBinary(drive, name, ext, data); } else { throw new Error(`Unable to load "${name}"`); diff --git a/js/formats/block.ts b/js/formats/block.ts index 1750ea8..97fc1da 100644 --- a/js/formats/block.ts +++ b/js/formats/block.ts @@ -1,10 +1,10 @@ -import { DiskOptions, BlockDisk, ENCODING_BLOCK } from './types'; +import { DiskOptions, BlockDisk, ENCODING_BLOCK, BlockFormat } from './types'; /** * Returns a `Disk` object for a block volume with block-ordered data. * @param options the disk image and options */ -export default function createBlockDisk(options: DiskOptions): BlockDisk { +export default function createBlockDisk(fmt: BlockFormat, options: DiskOptions): BlockDisk { const { rawData, readOnly, name } = options; if (!rawData) { @@ -20,6 +20,7 @@ export default function createBlockDisk(options: DiskOptions): BlockDisk { const disk: BlockDisk = { encoding: ENCODING_BLOCK, + format: fmt, blocks, metadata: { name }, readOnly, diff --git a/js/formats/create_disk.ts b/js/formats/create_disk.ts index 5dc884d..01a28b8 100644 --- a/js/formats/create_disk.ts +++ b/js/formats/create_disk.ts @@ -1,6 +1,6 @@ import { includes, memory } from '../types'; import { base64_decode } from '../base64'; -import { DiskOptions, FloppyDisk, JSONDisk, NibbleFormat, NIBBLE_FORMATS } from './types'; +import { BitstreamFormat, DiskOptions, FloppyDisk, FloppyFormat, JSONDisk, NibbleDisk, NibbleFormat, NIBBLE_FORMATS, WozDisk } from './types'; import createDiskFrom2MG from './2mg'; import createDiskFromD13 from './d13'; import createDiskFromDOS from './do'; @@ -8,13 +8,13 @@ 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): FloppyDisk | null { +/** Creates a `NibbleDisk` from the given format and options. */ +export function createDisk(fmt: NibbleFormat, options: DiskOptions): NibbleDisk | null; +/** Creates a `WozDisk` from the given format and options. */ +export function createDisk(fmt: BitstreamFormat, options: DiskOptions): WozDisk | null; +/** Creates a `FloppyDisk` (either a `NibbleDisk` or a `WozDisk`) from the given format and options. */ +export function createDisk(fmt: FloppyFormat, options: DiskOptions): FloppyDisk | null; +export function createDisk(fmt: FloppyFormat, options: DiskOptions): FloppyDisk | null { let disk: FloppyDisk | null = null; switch (fmt) { @@ -42,7 +42,8 @@ export function createDisk(fmt: NibbleFormat, options: DiskOptions): FloppyDisk return disk; } -export function createDiskFromJsonDisk(disk: JSONDisk): FloppyDisk | null { +/** Creates a NibbleDisk from JSON */ +export function createDiskFromJsonDisk(disk: JSONDisk): NibbleDisk | null { const fmt = disk.type; const readOnly = disk.readOnly; const name = disk.name; diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 0bc409a..a81a57e 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -1,7 +1,7 @@ import { bit, byte, memory } from '../types'; import { base64_decode, base64_encode } from '../base64'; import { bytify, debug, toHex } from '../util'; -import { NibbleDisk, ENCODING_NIBBLE, JSONDisk } from './types'; +import { NibbleDisk, ENCODING_NIBBLE, JSONDisk, isNibbleDiskFormat } from './types'; /** * DOS 3.3 Physical sector order (index is physical sector, value is DOS sector). @@ -550,6 +550,9 @@ export function jsonDecode(data: string): NibbleDisk { } tracks[t] = bytify(track); } + if (!isNibbleDiskFormat(json.type)) { + throw new Error(`JSON disks of type ${json.type} are not supported`); + } const disk: NibbleDisk = { volume: v, format: json.type, diff --git a/js/formats/types.ts b/js/formats/types.ts index 272bc9e..a3553b9 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -64,22 +64,29 @@ export interface Disk { readOnly: boolean; } +export const NO_DISK = 'empty'; export const ENCODING_NIBBLE = 'nibble'; export const ENCODING_BITSTREAM = 'bitstream'; export const ENCODING_BLOCK = 'block'; export interface FloppyDisk extends Disk { - tracks: memory[]; + encoding: typeof ENCODING_NIBBLE | typeof ENCODING_BITSTREAM | typeof NO_DISK; +} + +export interface NoFloppyDisk extends FloppyDisk { + encoding: typeof NO_DISK; } export interface NibbleDisk extends FloppyDisk { encoding: typeof ENCODING_NIBBLE; - format: DiskFormat; + format: Exclude; volume: byte; + tracks: memory[]; } export interface WozDisk extends FloppyDisk { encoding: typeof ENCODING_BITSTREAM; + format: 'woz'; trackMap: number[]; rawTracks: Uint8Array[]; info: InfoChunk | undefined; @@ -87,14 +94,13 @@ export interface WozDisk extends FloppyDisk { export interface BlockDisk extends Disk { encoding: typeof ENCODING_BLOCK; + format: BlockFormat; blocks: Uint8Array[]; } /** - * File types supported by the disk format processors and - * block devices. + * File types supported by floppy devices in nibble mode. */ - export const NIBBLE_FORMATS = [ '2mg', 'd13', @@ -102,21 +108,70 @@ export const NIBBLE_FORMATS = [ 'dsk', 'po', 'nib', - 'woz' ] as const; +/** + * File types supported by floppy devices in bitstream mode. + */ +export const BITSTREAM_FORMATS = [ + 'woz', +] as const; + +/** + * All file types supported by floppy devices. + */ +export const FLOPPY_FORMATS = [ + ...NIBBLE_FORMATS, + ...BITSTREAM_FORMATS, +] as const; + +/** + * File types supported by block devices. + */ export const BLOCK_FORMATS = [ '2mg', 'hdv', 'po', ] as const; -export const DISK_FORMATS = [...NIBBLE_FORMATS, ...BLOCK_FORMATS] as const; +/** + * All supported disk formats. + */ +export const DISK_FORMATS = [ + ...FLOPPY_FORMATS, + ...BLOCK_FORMATS, +] as const; +export type FloppyFormat = MemberOf; export type NibbleFormat = MemberOf; +export type BitstreamFormat = 'woz'; export type BlockFormat = MemberOf; export type DiskFormat = MemberOf; +/** Type guard for nibble disk formats. */ +export function isNibbleDiskFormat(f: DiskFormat): f is NibbleFormat { + return f in NIBBLE_FORMATS; +} + +/** Type guard for block disk formats. */ +export function isBlockDiskFormat(f: DiskFormat): f is BlockFormat { + return f in BLOCK_FORMATS; +} + +export function isNoFloppyDisk(disk: Disk): disk is NoFloppyDisk { + return (disk as NoFloppyDisk)?.encoding === NO_DISK; +} + +/** Type guard for NibbleDisks */ +export function isNibbleDisk(disk: Disk): disk is NibbleDisk { + return (disk as NibbleDisk)?.encoding === ENCODING_NIBBLE; +} + +/** Type guard for NibbleDisks */ +export function isWozDisk(disk: Disk): disk is WozDisk { + return (disk as WozDisk)?.encoding === ENCODING_BITSTREAM; +} + /** * Base format for JSON defined disks */ @@ -180,7 +235,7 @@ export interface ProcessBinaryMessage { type: typeof PROCESS_BINARY; payload: { drive: DriveNumber; - fmt: NibbleFormat; + fmt: FloppyFormat; options: DiskOptions; }; } @@ -227,7 +282,7 @@ export type FormatWorkerResponse = export interface MassStorageData { metadata: DiskMetadata; - ext: string; + ext: DiskFormat; readOnly: boolean; volume?: byte; data: ArrayBuffer; diff --git a/js/formats/woz.ts b/js/formats/woz.ts index c632407..5160cae 100644 --- a/js/formats/woz.ts +++ b/js/formats/woz.ts @@ -293,8 +293,8 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk { const disk: WozDisk = { encoding: ENCODING_BITSTREAM, + format: 'woz', trackMap: tmap?.trackMap || [], - tracks: trks?.tracks || [], rawTracks: trks?.rawTracks || [], readOnly: true, //chunks.info.writeProtected === 1; metadata: { diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 5866824..498b5dc 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -11,10 +11,10 @@ import { DriveNumber, DRIVE_NUMBERS, MassStorage, - NIBBLE_FORMATS, JSONBinaryImage, JSONDisk, - BlockFormat + BlockFormat, + FLOPPY_FORMATS } from '../formats/types'; import { initGamepad } from './gamepad'; import KeyBoard from './keyboard'; @@ -392,7 +392,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) { } } else { if ( - includes(NIBBLE_FORMATS, ext) && + includes(FLOPPY_FORMATS, ext) && _disk2.setBinary(drive, name, ext, result) ) { initGamepad(); @@ -459,7 +459,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) { } } else { if ( - includes(NIBBLE_FORMATS, ext) && + includes(FLOPPY_FORMATS, ext) && _disk2.setBinary(drive, name, ext, data) ) { initGamepad(); diff --git a/test/js/cards/disk2.spec.ts b/test/js/cards/disk2.spec.ts index 38f6b5e..9e31cd0 100644 --- a/test/js/cards/disk2.spec.ts +++ b/test/js/cards/disk2.spec.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import Apple2IO from 'js/apple2io'; import DiskII, { Callbacks } from 'js/cards/disk2'; import CPU6502 from 'js/cpu6502'; -import { DriveNumber } from 'js/formats/types'; +import { DriveNumber, NibbleDisk, WozDisk } from 'js/formats/types'; import { byte } from 'js/types'; import { toHex } from 'js/util'; import { VideoModes } from 'js/videomodes'; @@ -71,7 +71,8 @@ describe('DiskII', () => { state.controllerState.latch = 0x42; state.controllerState.on = true; state.controllerState.q7 = true; - state.drives[2].tracks[14][12] = 0x80; + const disk2 = state.drives[2].disk as NibbleDisk; + disk2.tracks[14][12] = 0x80; state.drives[2].head = 1000; state.drives[2].phase = 3; diskII.setState(state); @@ -478,21 +479,24 @@ describe('DiskII', () => { it('writes a nibble to the disk', () => { const diskII = new DiskII(mockApple2IO, callbacks); diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); - let track0 = diskII.getState().drives[1].tracks[0]; + let disk1 = diskII.getState().drives[1].disk as NibbleDisk; + let track0 = disk1.tracks[0]; expect(track0[0]).toBe(0xFF); diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x8F, 0x80); // write diskII.ioSwitch(0x8C); // shift - track0 = diskII.getState().drives[1].tracks[0]; + disk1 = diskII.getState().drives[1].disk as NibbleDisk; + track0 = disk1.tracks[0]; expect(track0[0]).toBe(0x80); }); it('writes two nibbles to the disk', () => { const diskII = new DiskII(mockApple2IO, callbacks); diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); - let track0 = diskII.getState().drives[1].tracks[0]; + let disk1 = diskII.getState().drives[1].disk as NibbleDisk; + let track0 = disk1.tracks[0]; expect(track0[0]).toBe(0xFF); diskII.ioSwitch(0x89); // turn on the motor @@ -501,7 +505,8 @@ describe('DiskII', () => { diskII.ioSwitch(0x8F, 0x81); // write diskII.ioSwitch(0x8C); // shift - track0 = diskII.getState().drives[1].tracks[0]; + disk1 = diskII.getState().drives[1].disk as NibbleDisk; + track0 = disk1.tracks[0]; expect(track0[0]).toBe(0x80); expect(track0[1]).toBe(0x81); }); @@ -819,10 +824,10 @@ class TestDiskReader { rawTracks() { // NOTE(flan): Hack to access private properties. - const disk = this.diskII as unknown as { cur: { rawTracks: Uint8Array[] } }; + const disk = (this.diskII as unknown as { curDisk: WozDisk }).curDisk; const result: Uint8Array[] = []; - for (let i = 0; i < disk.cur.rawTracks.length; i++) { - result[i] = disk.cur.rawTracks[i].slice(0); + for (let i = 0; i < disk.rawTracks.length; i++) { + result[i] = disk.rawTracks[i].slice(0); } return result; diff --git a/test/js/formats/2mg.spec.ts b/test/js/formats/2mg.spec.ts index 653acaa..350e495 100644 --- a/test/js/formats/2mg.spec.ts +++ b/test/js/formats/2mg.spec.ts @@ -193,6 +193,7 @@ describe('2mg format', () => { metadata: { name: 'Good disk' }, readOnly: false, encoding: ENCODING_BLOCK, + format: 'hdv', }; const image = create2MGFromBlockDisk(header, disk); expect(VALID_PRODOS_IMAGE.buffer).toEqual(image); diff --git a/test/js/formats/woz.spec.ts b/test/js/formats/woz.spec.ts index 9855351..c5a5c5c 100644 --- a/test/js/formats/woz.spec.ts +++ b/test/js/formats/woz.spec.ts @@ -21,16 +21,16 @@ describe('woz', () => { const disk = createDiskFromWoz(options); expect(disk).toEqual({ - metadata: { name: 'Mock Woz 1' }, + metadata: { name: 'Mock Woz 1', side: undefined }, readOnly: true, encoding: ENCODING_BITSTREAM, + format: 'woz', trackMap: mockTMAP, rawTracks: [new Uint8Array([ 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, ])], - tracks: [new Uint8Array([0xD5, 0xAA, 0x96])], info: { bitTiming: 0, bootSector: 0, @@ -64,13 +64,13 @@ describe('woz', () => { }, readOnly: true, encoding: ENCODING_BITSTREAM, + format: 'woz', trackMap: mockTMAP, rawTracks: [new Uint8Array([ 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, ])], - tracks: [new Uint8Array([0xD5, 0xAA, 0x96])], info: { bitTiming: 0, bootSector: 0,