import { TransportLayer , ClientMessage , MessageHandler , ServerMessage } from "emulators/dist/types/protocol/protocol" ; import * as base64 from "base64-js" ; export interface Hardware { readConfig ( ) : string ; sendMessage ( payload ) : void ; addKey ( key , pressed , timeMs ) : void ; mouseMove ( x , y , relative , timeMs ) : void ; mouseButton ( button , pressed , timeMs ) : void ; getFramePayload ( ) : string ; createFile ( path ) : string ; appendFile ( blob ) : string ; closeFile ( ) : string ; } const textDecoder = new TextDecoder ( ) ; class HardwareTransportLayer implements TransportLayer { sessionId : string = Date . now ( ) + "" ; hardware : Hardware ; private alive = true ; private frameWidth = 0 ; private frameHeight = 0 ; private handler : MessageHandler = ( ) => { } ; constructor ( realtime ) { this . hardware = realtime ; } callMain ( ) { this . hardware . sendMessage ( "wc-install\n" + this . sessionId + "\n" ) ; requestAnimationFrame ( this . update . bind ( this ) ) ; } async sendMessageToServer ( name , props ? ) { if ( props === undefined || props ?. sessionId !== this . sessionId ) { return ; } switch ( name ) { case "wc-run" : { let errorMessage = writeFile ( this . hardware , "bundle_0.zip" , props . bundles [ 0 ] ) ; if ( errorMessage . length > 0 ) { console . error ( errorMessage ) ; throw new Error ( errorMessage ) ; } if ( props . bundles [ 1 ] !== undefined ) { errorMessage = writeFile ( this . hardware , "bundle_1.zip" , props . bundles [ 1 ] ) ; if ( errorMessage . length > 0 ) { console . error ( errorMessage ) ; throw new Error ( errorMessage ) ; } } const payload = "wc-run\n" ; this . hardware . sendMessage ( payload ) ; } break ; case "wc-add-key" : { this . hardware . addKey ( props . key , props . pressed ? 1 : 0 , props . timeMs ) ; } break ; case "wc-pause" : { this . hardware . sendMessage ( "wc-pause\n" + this . sessionId + "\n" ) ; } break ; case "wc-resume" : { this . hardware . sendMessage ( "wc-resume\n" + this . sessionId + "\n" ) ; } break ; case "wc-mute" : { this . hardware . sendMessage ( "wc-mute\n" + this . sessionId + "\n" ) ; } break ; case "wc-unmute" : { this . hardware . sendMessage ( "wc-unmute\n" + this . sessionId + "\n" ) ; } break ; case "wc-exit" : { this . alive = false ; this . hardware . sendMessage ( "wc-exit\n" + this . sessionId + "\n" ) ; } break ; case "wc-mouse-move" : { this . hardware . mouseMove ( props . x , props . y , props . relative , props . timeMs ) ; } break ; case "wc-mouse-button" : { this . hardware . mouseButton ( props . button , props . pressed ? 1 : 0 , props . timeMs ) ; } break ; case "wc-pack-fs-to-bundle" : { this . hardware . sendMessage ( "wc-pack-fs-to-bundle\n" + this . sessionId + "\n" ) ; } break ; default : { console . log ( "Unhandled client message (wc):" , name , props ) ; } break ; } } initMessageHandler ( handler ) { this . handler = handler ; } exit ( ) { this . alive = false ; } async onServerMessage ( name , optProps ? ) { const props = optProps || { } ; switch ( name ) { case "ws-server-ready" : { const config = this . hardware . readConfig ( ) ; this . handler ( "ws-config" , { sessionId : this . sessionId , content : config } ) ; } break ; case "ws-sound-init" : { this . handler ( name , props ) ; this . handler ( "ws-server-ready" , { sessionId : this . sessionId } ) ; } break ; case "ws-frame-set-size" : { this . frameWidth = props . width ; this . frameHeight = props . height ; this . handler ( name , props ) ; } break ; case "ws-sound-push" : case "ws-update-lines" : { console . error ( name , "should not be called" ) ; } break ; case "ws-persist" : { props . bundle = decode ( props . bundle ) ; this . handler ( name , props ) ; } break ; case "ws-log" : case "ws-warn" : case "ws-err" : case "ws-stdout" : { if ( props . message !== undefined && props . message !== null && props . message . length > 0 ) { props . message = textDecoder . decode ( decode ( props . message ) ) ; } this . handler ( name , props ) ; } break ; default : { this . handler ( name as ServerMessage , props ) ; } } } private update ( ) { if ( this . alive ) { requestAnimationFrame ( this . update . bind ( this ) ) ; } this . updateFrame ( ) ; } private updateFrame ( ) { if ( this . frameWidth === 0 || this . frameHeight === 0 ) { return ; } const framePayload = this . hardware . getFramePayload ( ) ; if ( framePayload . length === 0 ) { return ; } const framePayloadU8 = decode ( framePayload ) ; if ( framePayloadU8 . length === 0 ) { return ; } const lines = [ ] ; const pitch = this . frameWidth * 3 ; let offset = this . frameHeight ; let upBorder = - 1 ; for ( let line = 0 ; line < this . frameHeight ; ++ line ) { const lastLine = line === this . frameHeight - 1 ; if ( framePayloadU8 [ line ] === 1 && upBorder === - 1 ) { upBorder = line ; } else if ( ( lastLine || framePayloadU8 [ line ] === 0 ) && upBorder !== - 1 ) { const downBorder = framePayloadU8 [ line ] === 1 ? line : line - 1 ; const range = ( downBorder - upBorder + 1 ) * pitch ; const heapu8 = framePayloadU8 . slice ( offset , offset + range ) ; lines . push ( { start : upBorder , heapu8 , } ) ; offset += range ; upBorder = - 1 ; } } this . handler ( "ws-update-lines" , { sessionId : this . sessionId , lines , } ) ; } } export class HardwareTransportLayerFactory { private serverMessageHandler : ( name : string , props ? : { [ key ] : any ; } ) => void = ( ) => { } ; constructor ( ) { ( window as any ) . serverMessage = ( encoded ) => { const json = textDecoder . decode ( decode ( encoded ) ) ; try { const data = JSON . parse ( "{" + json . slice ( 0 , - 1 ) + "}" ) ; this . serverMessageHandler ( data . name , data ) ; } catch ( e ) { console . error ( "Can't parse" , json , e ) ; throw e ; } } ; this . createTransportLayer = this . createTransportLayer . bind ( this ) ; } createTransportLayer ( realtime ) { const transport = new HardwareTransportLayer ( realtime ) ; this . serverMessageHandler = transport . onServerMessage . bind ( transport ) ; transport . callMain ( ) ; return transport ; } } export const hardwareTransportLayerFactory = new HardwareTransportLayerFactory ( ) ; function writeFile ( hardware , path , payload ) { if ( ( hardware as any ) . writeFile !== undefined ) { return ( hardware as any ) . writeFile ( path , encode ( payload ) ) ; } const bufferSize = 4 * 1024 * 1024 ; let error = hardware . createFile ( path ) ; if ( error . length > 0 ) { return error ; } let cursor = 0 ; while ( cursor < payload . length ) { const size = Math . min ( bufferSize , payload . length - cursor ) ; const chunk = payload . subarray ( cursor , cursor + size ) ; error = hardware . appendFile ( encode ( chunk ) ) ; if ( error . length > 0 ) { return error ; } cursor += size ; } error = hardware . closeFile ( ) ; return error ; } function decode ( input ) { return base64 . toByteArray ( input ) ; } function encode ( input ) { return base64 . fromByteArray ( input ) ; }