import { App , Option } from './app/app' ; import { AboutWnd , EqualizerWnd , GlobalPaletWnd , SettingWnd } from './app/other_wnd' ; import { AudioManager } from './util/audio_manager' ; import { DomUtil } from './util/dom_util' ; import { JsApp } from './app/js_powered_app' ; import { PadKeyHandler } from './util/pad_key_handler' ; import { GamepadManager } from './util/gamepad_manager' ; import { GlobalSetting } from './app/global_setting' ; import { KeyConfigWnd , GamepadWnd } from './app/key_config_wnd' ; import { StorageUtil } from './util/storage_util' ; import { Util } from './util/util' ; import { WindowManager } from './wnd/window_manager' ; import './util/polyfill' ; import * as JSZip from 'jszip' ; import audioOnImg from './res/audio_on.png' ; import audioOffImg from './res/audio_off.png' ; window . requestAnimationFrame = ( window . requestAnimationFrame || window . mozRequestAnimationFrame || window . msRequestAnimationFrame ) ; class Main { private wndMgr : WindowManager ; private apps : App [ ] = [ ] ; private diskBios : Uint8Array | null = null ; private uninsertedApp : App | null = null ; private keyConfigWnd : KeyConfigWnd | null = null ; private gamepadWnd : GamepadWnd | null = null ; private globalPaletWnd : GlobalPaletWnd | null = null ; private equalizerWnd : EqualizerWnd | null = null ; private settingWnd : SettingWnd | null = null ; private aboutWnd : AboutWnd | null = null ; private muted = false ; private focused = true ; private audioEnabled = false ; constructor ( private root ) { this . wndMgr = new WindowManager ( root ) ; GlobalSetting . loadFromStorage ( ) ; this . setUpAudio ( ) ; this . setUpStartMenu ( ) ; this . setUpFileDrop ( ) ; this . setUpBlur ( ) ; window . addEventListener ( 'resize' , _ => this . wndMgr . onResizeWindow ( ) ) ; } private setUpStartMenu ( ) { const submenuItems = [ { label : 'Open' , click : ( ) => { const input = document . createElement ( 'input' ) ; input . type = 'file' ; input . accept = '.nes,.sav,.zip, application/zip' ; input . onchange = _event => { if ( ! input . value ) return ; const fileList = input . files ; if ( fileList ) this . createAppFromFiles ( fileList , 0 , 0 ) ; input . value = '' ; } ; input . click ( ) ; } , } , { label : 'Global palet' , click : ( ) => this . openGlobalPaletWnd ( ) , } , { label : 'Key config' , click : ( ) => this . openKeyConfigWnd ( ) , } , { label : 'Gamepad' , click : ( ) => this . openGamepadWnd ( ) , disabled : ! GamepadManager . isSupported ( ) , } , { label : 'Equalizer' , click : ( ) => this . openEqualizerWnd ( ) , } , { label : 'Setting' , click : ( ) => this . openSettingWnd ( ) , } , { label : 'About' , click : ( ) => this . openAboutWnd ( ) , } , ] ; this . wndMgr . setUpStartMenu ( '👾' , submenuItems ) ; } private setUpFileDrop ( ) { const dropDesc = document . getElementById ( 'drop-desc' ) ; if ( ! dropDesc ) return ; if ( ! ( window . File && window . FileReader && window . FileList && window . Blob ) ) { dropDesc . style . display = 'none' ; return ; } dropDesc . style . opacity = '0' ; DomUtil . handleFileDrop ( this . root , ( files , x , y ) => { this . createAppFromFiles ( files , x , y ) ; dropDesc . style . display = 'none' ; } ) ; } private async createAppFromFiles ( files , x , y ) { const kTargetExts = new Set ( [ 'nes' , 'bin' , 'fds' , 'sav' ] ) ; type Result = { type : string ; binary : Uint8Array ; fileName : string ; } ; const promises = Array . from ( files ) . map ( file => { return { file , ext : Util . getExt ( file . name ) . toLowerCase ( ) } ; } ) . filter ( ( { file , ext } ) => { if ( ext !== 'js' ) return true ; const jsApp = new JsApp ( this . wndMgr , { title : file . name , centerX : x , centerY : y , onClosed : app => this . removeApp ( app ) , } ) ; jsApp . setFile ( file ) ; this . apps . push ( jsApp ) ; return false ; } ) . map ( async ( { file , ext } ) => { if ( ext === 'zip' ) { const binary = await DomUtil . loadFile ( file ) ; const zip = new JSZip ( ) ; const loadedZip = await zip . loadAsync ( binary ) ; for ( const fileName2 of Object . keys ( loadedZip . files ) ) { const ext2 = Util . getExt ( fileName2 ) . toLowerCase ( ) ; if ( kTargetExts . has ( ext2 ) ) { const unzipped = await loadedZip . files [ fileName2 ] . async ( 'uint8array' ) ; return { type : ext2 , binary : unzipped , fileName : fileName2 } ; } } return Promise . reject ( `No .nes file included: ${ file . name }` ) ; } else if ( kTargetExts . has ( ext ) ) { const binary = await DomUtil . loadFile ( file ) ; return { type : ext , binary , fileName : file . name } ; } else { return Promise . reject ( `Unsupported file: ${ file . name }` ) ; } } ) ; try { const results = await Promise . all ( promises ) ; const typeMap = { } ; results . forEach ( result => { if ( ! typeMap [ result . type ] ) typeMap [ result . type ] = [ ] ; typeMap [ result . type ] . push ( result ) ; } ) ; if ( typeMap . bin ) { this . diskBios = typeMap . bin [ 0 ] . binary ; if ( ! typeMap . fds ) { this . uninsertedApp = this . bootDiskImage ( this . diskBios , null , 'DISK System' , x , y ) ; } } if ( typeMap . nes ) { typeMap . nes . forEach ( file => { this . createAppFromRom ( file . binary , file . fileName , x , y ) ; x += 16 ; y += 16 ; } ) ; } if ( typeMap . fds ) { const diskBios = this . diskBios ; if ( diskBios == null ) { this . wndMgr . showSnackbar ( '.fds needs BIOS file (.bin) for Disk System' ) ; return ; } typeMap . fds . forEach ( file => { if ( this . uninsertedApp != null ) { this . uninsertedApp . setDiskImage ( file . binary ) ; this . uninsertedApp = null ; } else { this . bootDiskImage ( diskBios , file . binary , file . fileName , x , y ) ; } x += 16 ; y += 16 ; } ) ; } if ( typeMap . sav ) { const file = typeMap . sav [ 0 ] ; const app = this . findActiveApp ( ) ; if ( app == null ) { throw ( 'Load save data failed: No active app' ) ; } else { app . loadDataFromBinary ( file . binary ) ; } } } catch ( e ) { this . wndMgr . showSnackbar ( e . toString ( ) ) ; } } private findActiveApp ( ) { for ( const app of this . apps ) { if ( app . isTop ( ) ) return app ; } return null ; } private createAppFromRom ( romData , name , x , y ) { const m = name . match ( /^(.*?)(\s*\(.*\))?\.\w+$/ ) ; const title = m ? m [ 1 ] : name ; const option = { title , centerX : x , centerY : y , onClosed : app2 => { this . removeApp ( app2 ) ; } , } ; const app = new App ( this . wndMgr , option ) ; const result = app . loadRom ( romData ) ; if ( result !== true ) { this . wndMgr . showSnackbar ( `${ name }: ${ result }` ) ; app . close ( ) ; return ; } this . apps . push ( app ) ; } private bootDiskImage ( biosData , diskImage , name , x , y ) { const m = name . match ( /^(.*?)\s*\(.*\)\.\w*$/ ) ; const title = m ? m [ 1 ] : name ; const option = { title , centerX : x , centerY : y , onClosed : app2 => { this . removeApp ( app2 ) ; } , } ; const app = App . create ( this . wndMgr , option ) ; app . bootDiskBios ( biosData ) ; if ( diskImage != null ) app . setDiskImage ( diskImage ) ; this . apps . push ( app ) ; return app ; } private removeApp ( app ) { const index = this . apps . indexOf ( app ) ; if ( index >= 0 ) this . apps . splice ( index , 1 ) ; } private openGlobalPaletWnd ( ) { if ( this . globalPaletWnd == null ) { this . globalPaletWnd = new GlobalPaletWnd ( this . wndMgr , ( ) => { this . globalPaletWnd = null ; } ) ; } else { this . wndMgr . moveToTop ( this . globalPaletWnd ) ; } } private openKeyConfigWnd ( ) { if ( this . keyConfigWnd == null ) { this . keyConfigWnd = new KeyConfigWnd ( this . wndMgr , ( ) => { this . keyConfigWnd = null ; } ) ; } else { this . wndMgr . moveToTop ( this . keyConfigWnd ) ; } } private openGamepadWnd ( ) { if ( ! GamepadManager . isSupported ( ) ) { return ; } if ( this . gamepadWnd == null ) { this . gamepadWnd = new GamepadWnd ( this . wndMgr , ( ) => { this . gamepadWnd = null ; } ) ; } else { this . wndMgr . moveToTop ( this . gamepadWnd ) ; } } private openEqualizerWnd ( ) { if ( this . equalizerWnd == null ) { this . equalizerWnd = new EqualizerWnd ( this . wndMgr , ( ) => { this . equalizerWnd = null ; } ) ; } else { this . wndMgr . moveToTop ( this . equalizerWnd ) ; } } private openSettingWnd ( ) { if ( this . settingWnd == null ) { this . settingWnd = new SettingWnd ( this . wndMgr , ( ) => { this . settingWnd = null ; } ) ; } else { this . wndMgr . moveToTop ( this . settingWnd ) ; } } private openAboutWnd ( ) { if ( this . aboutWnd == null ) { this . aboutWnd = new AboutWnd ( this . wndMgr , ( ) => { this . aboutWnd = null ; } ) ; } else { this . wndMgr . moveToTop ( this . aboutWnd ) ; } } private setUpBlur ( ) { window . addEventListener ( 'blur' , ( ) => { if ( GlobalSetting . muteOnInactive ) this . onFocusChanged ( false ) ; } ) ; window . addEventListener ( 'focus' , ( ) => { if ( GlobalSetting . muteOnInactive ) this . onFocusChanged ( true ) ; } ) ; } private setUpAudio ( ) { const audioContextClass = window . AudioContext || window . webkitAudioContext ; AudioManager . setUp ( audioContextClass ) ; AudioManager . setMasterVolume ( GlobalSetting . volume ) ; const icon = document . getElementById ( 'audio-toggle-icon' ) as HTMLImageElement ; icon . src = audioOffImg ; DomUtil . setStyles ( icon , { visibility : null } ) ; const button = document . getElementById ( 'audio-toggle' ) ; button ?. addEventListener ( 'click' , _event => { let muted ; if ( ! this . audioEnabled ) { AudioManager . enableAudio ( ) ; for ( const app of this . apps ) app . setupAudioManager ( ) ; this . audioEnabled = true ; muted = false ; } else { muted = ! this . muted ; this . setMuted ( muted ) ; } icon . src = muted ? audioOffImg : audioOnImg ; this . wndMgr . setFocus ( ) ; } ) ; } private onFocusChanged ( focused ) { this . focused = focused ; this . updateVolume ( ) ; } private setMuted ( muted ) { this . muted = muted ; this . updateVolume ( ) ; } private updateVolume ( ) { if ( this . focused && ! this . muted ) { AudioManager . setMasterVolume ( GlobalSetting . volume ) ; } else { AudioManager . setMasterVolume ( 0 ) ; } } } window . addEventListener ( 'load' , ( ) => { StorageUtil . setKeyPrefix ( 'nesemu:' ) ; PadKeyHandler . setUp ( ) ; GamepadManager . setUp ( ) ; const root = document . getElementById ( 'nesroot' ) ; if ( root != null ) new Main ( root ) ; } ) ;