import * as React from "react" ; import * as fs from "fs-extra" ; import * as path from "path" ; import { ipcRenderer , shell } from "electron" ; import { CONSTANTS , IPC_COMMANDS } from "../constants" ; import { getDiskImageSize } from "../utils/disk-image-size" ; import { CardStart } from "./card-start" ; import { StartMenu } from "./start-menu" ; import { CardSettings } from "./card-settings" ; import { EmulatorInfo } from "./emulator-info" ; import { CardDrive } from "./card-drive" ; import { getStatePath } from "./utils/get-state-path" ; export interface EmulatorState { currentUiCard : string ; emulator ? : any ; scale : number ; floppyFile ? : File ; isBootingFresh : boolean ; isCursorCaptured : boolean ; isInfoDisplayed : boolean ; isRunning : boolean ; } export class Emulator extends React . Component < { } , EmulatorState > { private isQuitting = false ; private isResetting = false ; constructor ( props ) { super ( props ) ; this . startEmulator = this . startEmulator . bind ( this ) ; this . stopEmulator = this . stopEmulator . bind ( this ) ; this . restartEmulator = this . restartEmulator . bind ( this ) ; this . resetEmulator = this . resetEmulator . bind ( this ) ; this . bootFromScratch = this . bootFromScratch . bind ( this ) ; this . state = { isBootingFresh : false , isCursorCaptured : false , isRunning : false , currentUiCard : "start" , isInfoDisplayed : true , scale : 1 , } ; this . setupInputListeners ( ) ; this . setupIpcListeners ( ) ; this . setupUnloadListeners ( ) ; if ( document . location . hash . includes ( "AUTO_START" ) ) { this . startEmulator ( ) ; } } /**
     * We want to capture and release the mouse at appropriate times.
     */ public setupInputListeners ( ) { document . onkeydown = ( evt ) => { const { isCursorCaptured } = this . state ; evt = evt || window . event ; if ( evt . keyCode === 27 ) { if ( isCursorCaptured ) { this . unlockMouse ( ) ; } else { this . lockMouse ( ) ; } evt . stopPropagation ( ) ; } } ; document . addEventListener ( "click" , ( ) => { const { isRunning } = this . state ; if ( isRunning ) { this . lockMouse ( ) ; } } ) ; } /**
     * Save the emulator's state to disk during exit.
     */ public setupUnloadListeners ( ) { const handleClose = async ( ) => { await this . saveState ( ) ; console . log ( `Unload: Now done, quitting again.` ) ; this . isQuitting = true ; setImmediate ( ( ) => { ipcRenderer . invoke ( IPC_COMMANDS . APP_QUIT ) ; } ) ; } ; window . onbeforeunload = ( event ) => { if ( this . isQuitting || this . isResetting ) { console . log ( `Unload: Not preventing` ) ; return ; } console . log ( `Unload: Preventing to first save state` ) ; handleClose ( ) ; event . preventDefault ( ) ; event . returnValue = false ; } ; } /**
     * Setup the various IPC messages sent to the renderer
     * from the main process
     */ public setupIpcListeners ( ) { ipcRenderer . on ( IPC_COMMANDS . MACHINE_CTRL_ALT_DEL , ( ) => { this . sendKeys ( [ 0x1d , 0x38 , 0x53 , ] ) ; } ) ; ipcRenderer . on ( IPC_COMMANDS . MACHINE_ALT_F4 , ( ) => { this . sendKeys ( [ 0x38 , 0x3e , ] ) ; } ) ; ipcRenderer . on ( IPC_COMMANDS . MACHINE_ALT_ENTER , ( ) => { this . sendKeys ( [ 0x38 , 0 , ] ) ; } ) ; ipcRenderer . on ( IPC_COMMANDS . MACHINE_ESC , ( ) => { this . sendKeys ( [ 0x18 , ] ) ; } ) ; ipcRenderer . on ( IPC_COMMANDS . MACHINE_STOP , this . stopEmulator ) ; ipcRenderer . on ( IPC_COMMANDS . MACHINE_RESET , this . resetEmulator ) ; ipcRenderer . on ( IPC_COMMANDS . MACHINE_START , this . startEmulator ) ; ipcRenderer . on ( IPC_COMMANDS . MACHINE_RESTART , this . restartEmulator ) ; ipcRenderer . on ( IPC_COMMANDS . TOGGLE_INFO , ( ) => { this . setState ( { isInfoDisplayed : ! this . state . isInfoDisplayed } ) ; } ) ; ipcRenderer . on ( IPC_COMMANDS . SHOW_DISK_IMAGE , ( ) => { this . showDiskImage ( ) ; } ) ; ipcRenderer . on ( IPC_COMMANDS . ZOOM_IN , ( ) => { this . setScale ( this . state . scale * 1.2 ) ; } ) ; ipcRenderer . on ( IPC_COMMANDS . ZOOM_OUT , ( ) => { this . setScale ( this . state . scale * 0.8 ) ; } ) ; ipcRenderer . on ( IPC_COMMANDS . ZOOM_RESET , ( ) => { this . setScale ( 1 ) ; } ) ; } /**
     * If the emulator isn't running, this is rendering the, erm, UI.
     *
     * 🤡
     */ public renderUI ( ) { const { isRunning , currentUiCard , floppyFile } = this . state ; if ( isRunning ) { return null ; } let card ; if ( currentUiCard === "settings" ) { card = ( < CardSettings setFloppy = { ( floppyFile ) => this . setState ( { floppyFile } ) } bootFromScratch = { this . bootFromScratch } floppy = { floppyFile } / > ) ; } else if ( currentUiCard === "drive" ) { card = < CardDrive showDiskImage = { this . showDiskImage } />; } else { card = < CardStart startEmulator = { this . startEmulator } />; } return ( < > { card } < StartMenu navigate = { ( target ) => this . setState ( { currentUiCard : target } ) } /> < />); } public render ( ) { return ( < > { this . renderInfo ( ) } { this . renderUI ( ) } < div id = "emulator" > < div > < /div> < canvas > < /canvas> < /div> < />); } public renderInfo ( ) { if ( ! this . state . isInfoDisplayed ) { return null ; } return ( < EmulatorInfo emulator = { this . state . emulator } toggleInfo = { ( ) => { this . setState ( { isInfoDisplayed : ! this . state . isInfoDisplayed } ) ; } } />); } public bootFromScratch ( ) { this . setState ( { isBootingFresh : true } ) ; this . startEmulator ( ) ; } public showDiskImage ( ) { const imagePath = path . join ( __dirname , "../../images/windows95.img" ) ; console . log ( `Showing disk image in ${ imagePath }` ) ; `` ; shell . showItemInFolder ( imagePath ) ; } private async startEmulator ( ) { document . body . classList . remove ( "paused" ) ; const imageSize = await getDiskImageSize ( ) ; const options = { memory_size : 128 * 1024 * 1024 , video_memory_size : 32 * 1024 * 1024 , screen_container : document . getElementById ( "emulator" ) , bios : { url : "../../bios/seabios.bin" , } , vga_bios : { url : "../../bios/vgabios.bin" , } , hda : { url : "../../images/windows95.img" , async : true , size : imageSize , } , fda : { buffer : this . state . floppyFile , } , boot_order : 0x132 , } ; console . log ( `🚜 Starting emulator with options` , options ) ; window [ "emulator" ] = new V86Starter ( options ) ; this . setState ( { emulator : window [ "emulator" ] , isRunning : true , } ) ; ipcRenderer . send ( IPC_COMMANDS . MACHINE_STARTED ) ; setTimeout ( async ( ) => { if ( ! this . state . isBootingFresh ) { this . restoreState ( ) ; } this . lockMouse ( ) ; this . state . emulator . run ( ) ; } , 500 ) ; } private restartEmulator ( ) { if ( this . state . emulator && this . state . isRunning ) { console . log ( `🚜 Restarting emulator` ) ; this . state . emulator . restart ( ) ; } else { console . log ( `🚜 Restarting emulator failed: Emulator not running` ) ; } } private async stopEmulator ( ) { const { emulator , isRunning } = this . state ; if ( ! emulator || ! isRunning ) { return ; } console . log ( `🚜 Stopping emulator` ) ; await this . saveState ( ) ; this . unlockMouse ( ) ; emulator . stop ( ) ; this . setState ( { isRunning : false } ) ; document . body . classList . add ( "paused" ) ; ipcRenderer . send ( IPC_COMMANDS . MACHINE_STOPPED ) ; } private async resetEmulator ( ) { this . isResetting = true ; document . location . hash = `#AUTO_START` ; document . location . reload ( ) ; } private async saveState ( ) { const { emulator } = this . state ; const statePath = await getStatePath ( ) ; return new Promise ( ( resolve ) => { if ( ! emulator || ! emulator . save_state ) { console . log ( `restoreState: No emulator present` ) ; return resolve ( ) ; } emulator . save_state ( async ( error , newState ) => { if ( error ) { console . warn ( `saveState: Could not save state` , error ) ; return resolve ( ) ; } await fs . outputFile ( statePath , Buffer . from ( newState ) ) ; console . log ( `saveState: Saved state to ${ statePath }` ) ; resolve ( ) ; } ) ; } ) ; } private async restoreState ( ) { const { emulator } = this . state ; const state = await this . getState ( ) ; if ( ! state ) { console . log ( `restoreState: No state present, not restoring.` ) ; } if ( ! emulator ) { console . log ( `restoreState: No emulator present` ) ; } try { this . state . emulator . restore_state ( state ) ; } catch ( error ) { console . log ( `State: Could not read state file. Maybe none exists?` , error ) ; } } private async getState ( ) { const expectedStatePath = await getStatePath ( ) ; const statePath = fs . existsSync ( expectedStatePath ) ? expectedStatePath : CONSTANTS . DEFAULT_STATE_PATH ; if ( fs . existsSync ( statePath ) ) { return fs . readFileSync ( statePath ) . buffer ; } return null ; } private unlockMouse ( ) { const { emulator } = this . state ; this . setState ( { isCursorCaptured : false } ) ; if ( emulator ) { emulator . mouse_set_status ( false ) ; } document . exitPointerLock ( ) ; } private lockMouse ( ) { const { emulator } = this . state ; if ( emulator ) { this . setState ( { isCursorCaptured : true } ) ; emulator . mouse_set_status ( true ) ; emulator . lock_mouse ( ) ; } else { console . warn ( `Emulator: Tried to lock mouse, but no emulator or not running` ) ; } } private setScale ( target ) { const { emulator , isRunning } = this . state ; if ( emulator && isRunning ) { emulator . screen_set_scale ( target ) ; this . setState ( { scale : target } ) ; } } private sendKeys ( codes ) { if ( this . state . emulator && this . state . isRunning ) { const scancodes = codes ; for ( const scancode of scancodes ) { scancodes . push ( scancode | 0x80 ) ; } this . state . emulator . keyboard_send_scancodes ( scancodes ) ; } } }