import { BotAction , Bot , BotTags , isBot , PrecalculatedBot , botAdded , botRemoved , DEFAULT_ENERGY , RuntimeBot , getOriginalObject , ORIGINAL_OBJECT , } from '../bots' ; import { RuntimeBotFactory , RuntimeBotsState , RealtimeEditMode , RuntimeBatcher , } from './RuntimeBot' ; import { AuxVersion } from './AuxVersion' ; import { AuxDevice } from './AuxDevice' ; import { ScriptError , RanOutOfEnergyError } from './AuxResults' ; import { sortBy , sortedIndex , sortedIndexOf , sortedIndexBy } from 'lodash' ; import './PerformanceNowPolyfill' ; import { Observable , Subscription , SubscriptionLike } from 'rxjs' ; import { tap } from 'rxjs/operators' ; import TWEEN from '@tweenjs/tween.js' ; import { v4 as uuidv4 } from 'uuid' ; import stableStringify from '@casual-simulation/fast-json-stable-stringify' ; /**
 * The interval between animation frames in miliseconds when using setInterval().
 */ export const SET_INTERVAL_ANIMATION_FRAME_TIME = 16 ; /**
 * A symbol that can be specified on objects to influence how they are stringified
 * when printed for debug/mock/error purposes.
 */ export const DEBUG_STRING = Symbol ( 'debug_string' ) ; /**
 * Holds global values that need to be accessible from the runtime.
 */ export interface AuxGlobalContext { /**
     * The ordered list of script bots.
     */ bots : RuntimeBot [ ] ; /**
     * The state that the runtime bots occupy.
     */ state : RuntimeBotsState ; /**
     * The version.
     */ version : AuxVersion ; /**
     * The device.
     */ device : AuxDevice ; /**
     * The player bot.
     */ playerBot : RuntimeBot ; /**
     * The current energy that the context has.
     */ energy : number ; /**
     * The number of miliseconds since the session has started.
     */ localTime : number ; /**
     * Whether async API actions should be mocked.
     */ mockAsyncActions : boolean ; /**
     * The global values that the context is using.
     */ global : any ; param action enqueueAction ( action ) : void ; /**
     * Gets the list of actions that have been queued and resets the action queue.
     */ dequeueActions ( ) : BotAction [ ] ; param error enqueueError ( error ) : void ; /**
     * Gets the list of errors that have been queued and resets the error queue.
     */ dequeueErrors ( ) : ScriptError [ ] ; param bot unwrapBot ( bot ) : Bot | BotTags ; param bot createBot ( bot ) : RuntimeBot ; param bot destroyBot ( bot ) : void ; param tag getBotIdsWithListener ( tag ) : string [ ] ; param id param tag param hasListener recordListenerPresense ( id , tag , hasListener ) : void ; param id param info recordBotTimer ( id , info ) : void ; param id param type param timer removeBotTimer ( id , type , timerId ) : void ; param id getBotTimers ( id ) : BotTimer [ ] ; param id getWatchersForBot ( id ) : WatchBotTimer [ ] ; param id getWatchersForPortal ( id ) : WatchPortalTimer [ ] ; /**
     * Gets the list of portal IDs that are being watched.
     */ getWatchedPortals ( ) : Set < string > ; param id param timerId cancelAndRemoveBotTimer ( id , type , timerId ) : void ; param id cancelBotTimers ( id ) : void ; /**
     * Cancels all the timers that bots have created.
     */ cancelAllBotTimers ( ) : void ; param timerId param type cancelAndRemoveTimers ( timerId , type ? ) : void ; /**
     * Gets the number of timers.
     */ getNumberOfActiveTimers ( ) : number ; param Whether param Whether createTask ( unguessableId ? , allowRemoteResolution ? ) : AsyncTask ; param taskId param result param remote resolveTask ( taskId , result , remote ) : boolean ; param taskId param error param remote rejectTask ( taskId , error , remote ) : boolean ; /**
     * Gets a list of timers that contains the amount of time a tag has run for in miliseconds.
     */ getShoutTimers ( ) : { tag : string ; timeMs : number ; } [ ] ; param shout param ms addShoutTime ( shout , ms ) : void ; /**
     * Starts the animation loop for the context.
     */ startAnimationLoop ( ) : SubscriptionLike ; /**
     * Creates a UUID.
     */ uuid ( ) : string ; param func param returnValues setMockReturns ( func , returnValues ) : void ; param func param args param returnValue setMockReturn ( func , args , returnValue ) : void ; param func getNextMockReturn ( func , functionName , args ) : any ; } /**
 * Defines an interface for an asynchronous task.
 */ export interface AsyncTask { /**
     * The ID of the task.
     */ taskId : number | string ; /**
     * Whether the task is allowed to be resolved via a remote action result.
     */ allowRemoteResolution : boolean ; /**
     * The promise that the task contains.
     */ promise : Promise < any > ; /**
     * The function that is used to resolve the task with a result.
     */ resolve : ( val : any ) => void ; /**
     * The function that is used to reject the task with an error.
     */ reject : ( err : any ) => void ; } /**
 * Defines an interface for a timer that was created by a bot (e.g. setTimeout() or setInterval()).
 */ export type BotTimer = TimeoutOrIntervalTimer | AnimationTimer | WatchPortalTimer | WatchBotTimer ; /**
 * Defines an interface for a setTimeout() or setInterval() timer that was created by a bot.
 */ export interface TimeoutOrIntervalTimer { /**
     * The ID of the timer.
     */ timerId : number ; /**
     * The type of the timer.
     */ type : 'timeout' | 'interval' ; } /**
 * Defines an interface for a animation timer that was created by a bot.
 */ export interface AnimationTimer { /**
     * The ID of the timer.
     */ timerId : number ; /**
     * The type of the timer.
     */ type : 'animation' ; /**
     * The tag that the timer is for.
     */ tag : string ; /**
     * The group ID that the timer exists in.
     */ groupId ? : string ; /**
     * A function used to cancel the timer.
     */ cancel : ( ) => void ; } /**
 * Defines an interface for a subscription to watching a set of bots that was created by a bot.
 */ export interface WatchPortalTimer { /**
     * The ID of the timer.
     */ timerId : number ; /**
     * The type of the timer.
     */ type : 'watch_portal' ; /**
     * The tag that the timer is for.
     */ tag : string ; /**
     * The ID of the portal that the timer is for.
     */ portalId : string ; /**
     * The function that should be called when the portal changes.
     */ handler : ( ) => void ; } /**
 * Defines an interface for a subscription to watching a set of bots that was created by a bot.
 */ export interface WatchBotTimer { /**
     * The ID of the timer.
     */ timerId : number ; /**
     * The type of the timer.
     */ type : 'watch_bot' ; /**
     * The tag that the timer was created by.
     */ tag : string ; /**
     * The ID of the bot that the timer is for.
     */ botId : string ; /**
     * The function that should be called when the bot changes.
     */ handler : ( ) => void ; } param context param bot function indexInContext ( context , bot ) { const index = sortedIndexBy ( context . bots , < RuntimeBot > bot , ( sb ) => sb . id ) ; const expected = context . bots . length > index ? context . bots [ index ] : null ; if ( ! ! expected && expected . id === bot . id ) { return index ; } return - 1 ; } param context param bot export function addToContext ( context , ... bots ) { for ( let bot of bots ) { if ( ! ! context . state [ bot . id ] ) { throw new Error ( 'Bot already exists in the context!' ) ; } const index = sortedIndexBy ( context . bots , bot , ( sb ) => sb . id ) ; context . bots . splice ( index , 0 , bot ) ; context . state [ bot . id ] = bot ; } } param context param bots export function removeFromContext ( context , bots , cancelTimers = true ) { for ( let bot of bots ) { const index = indexInContext ( context , bot ) ; if ( index < 0 ) { continue ; } context . bots . splice ( index , 1 ) ; delete context . state [ bot . id ] ; if ( cancelTimers ) { context . cancelBotTimers ( bot . id ) ; } } } param context param bot export function isInContext ( context , bot ) { return indexInContext ( context , bot ) >= 0 ; } /**
 * Defines a global context that stores all information in memory.
 */ export class MemoryGlobalContext implements AuxGlobalContext { /**
     * The ordered list of script bots.
     */ bots : RuntimeBot [ ] = [ ] ; /**
     * The state that the runtime bots occupy.
     */ state : RuntimeBotsState = { } ; /**
     * The list of actions that have been queued.
     */ actions : BotAction [ ] = [ ] ; /**
     * The list of errors that have been queued.
     */ errors : ScriptError [ ] = [ ] ; /**
     * The map of task IDs to tasks.
     */ tasks : Map < number | string , AsyncTask > = new Map ( ) ; /**
     * The version.
     */ version : AuxVersion ; /**
     * The device.
     */ device : AuxDevice ; /**
     * The player bot.
     */ playerBot : RuntimeBot = null ; /**
     * The current energy that the context has.
     */ energy : number = DEFAULT_ENERGY ; /**
     * Whether async API actions should be mocked.
     */ mockAsyncActions : boolean ; global : any = { } ; uuid = uuidv4 ; get localTime ( ) { return performance . now ( ) - this . _startTime ; } private _taskCounter : number = 0 ; private _scriptFactory : RuntimeBotFactory ; private _batcher : RuntimeBatcher ; private _shoutTimers : { [ shout ] : number ; } = { } ; private _listenerMap : Map < string , string [ ] > ; private _botTimerMap : Map < string , BotTimer [ ] > ; private _botWatcherMap : Map < string , WatchBotTimer [ ] > ; private _portalWatcherMap : Map < string , WatchPortalTimer [ ] > ; private _numberOfTimers : number = 0 ; private _startTime : number ; private _animationLoop : Subscription ; private _mocks : Map < any , any [ ] | Map < string , any > > ; param version param device param scriptFactory param batcher constructor ( version , device , scriptFactory , batcher ) { this . version = version ; this . device = device ; this . _scriptFactory = scriptFactory ; this . _batcher = batcher ; this . _listenerMap = new Map ( ) ; this . _botTimerMap = new Map ( ) ; this . _botWatcherMap = new Map ( ) ; this . _portalWatcherMap = new Map ( ) ; this . _mocks = new Map ( ) ; this . _startTime = performance . now ( ) ; } getBotIdsWithListener ( tag ) { const set = this . _listenerMap . get ( tag ) ; if ( ! set ) { return [ ] ; } return set . slice ( ) ; } recordListenerPresense ( id , tag , hasListener ) { let set = this . _listenerMap . get ( tag ) ; if ( ! hasListener && ! set ) { return ; } if ( ! set ) { set = [ ] ; this . _listenerMap . set ( tag , set ) ; } if ( hasListener ) { const index = sortedIndex ( set , id ) ; if ( index < set . length && index >= 0 ) { const current = set [ index ] ; if ( current !== id ) { set . splice ( index , 0 , id ) ; } } else { set . splice ( index , 0 , id ) ; } } else { const index = sortedIndexOf ( set , id ) ; if ( index >= 0 ) { set . splice ( index , 1 ) ; } if ( set . length <= 0 ) { this . _listenerMap . delete ( tag ) ; } } } recordBotTimer ( id , info ) { let list = this . _botTimerMap . get ( id ) ; if ( ! list ) { list = [ ] ; this . _botTimerMap . set ( id , list ) ; } list . push ( info ) ; this . _numberOfTimers += 1 ; if ( info . type === 'watch_bot' ) { let watchers = this . _botWatcherMap . get ( info . botId ) ; if ( ! watchers ) { watchers = [ ] ; this . _botWatcherMap . set ( info . botId , watchers ) ; } watchers . push ( info ) ; } else if ( info . type === 'watch_portal' ) { let watchers = this . _portalWatcherMap . get ( info . portalId ) ; if ( ! watchers ) { watchers = [ ] ; this . _portalWatcherMap . set ( info . portalId , watchers ) ; } watchers . push ( info ) ; } } removeBotTimer ( id , type , timer ) { let list = this . _botTimerMap . get ( id ) ; if ( list ) { let index = list . findIndex ( ( t ) => t . type === type && t . timerId === timer ) ; if ( index >= 0 ) { list . splice ( index , 1 ) ; this . _numberOfTimers = Math . max ( 0 , this . _numberOfTimers - 1 ) ; } } } getBotTimers ( id ) { let timers = this . _botTimerMap . get ( id ) ; if ( timers ) { return timers . slice ( ) ; } return [ ] ; } getWatchersForBot ( id ) { let watchers = this . _botWatcherMap . get ( id ) ; if ( watchers ) { return watchers . slice ( ) ; } return [ ] ; } getWatchersForPortal ( id ) { let watchers = this . _portalWatcherMap . get ( id ) ; if ( watchers ) { return watchers . slice ( ) ; } return [ ] ; } getWatchedPortals ( ) { return new Set ( this . _portalWatcherMap . keys ( ) ) ; } cancelAndRemoveBotTimer ( id , type , timerId ) { let timers = this . _botTimerMap . get ( id ) ; if ( ! timers ) { return ; } for ( let i = 0 ; i < timers . length ; i ++ ) { let timer = timers [ i ] ; if ( timer . timerId === timerId && timer . type === type ) { timers . splice ( i , 1 ) ; this . _clearTimer ( timer ) ; i -= 1 ; } } } cancelBotTimers ( id ) { let list = this . _botTimerMap . get ( id ) ; if ( list ) { this . _clearTimers ( list ) ; } this . _botTimerMap . delete ( id ) ; } cancelAllBotTimers ( ) { for ( let list of this . _botTimerMap . values ( ) ) { this . _clearTimers ( list ) ; } this . _botTimerMap . clear ( ) ; } cancelAndRemoveTimers ( timerId , type ? ) { for ( let list of this . _botTimerMap . values ( ) ) { for ( let i = 0 ; i < list . length ; i ++ ) { const timer = list [ i ] ; if ( timer . timerId === timerId && ( ! type || timer . type === type ) ) { this . _clearTimer ( timer ) ; list . splice ( i , 1 ) ; i -= 1 ; } } } } getNumberOfActiveTimers ( ) { return this . _numberOfTimers ; } private _clearTimers ( list ) { for ( let timer of list ) { this . _clearTimer ( timer ) ; } } private _clearTimer ( timer ) { this . _numberOfTimers = Math . max ( 0 , this . _numberOfTimers - 1 ) ; if ( timer . type === 'timeout' ) { clearTimeout ( timer . timerId ) ; } else if ( timer . type === 'interval' ) { clearInterval ( timer . timerId ) ; } else if ( timer . type === 'animation' ) { timer . cancel ( ) ; } else if ( timer . type === 'watch_bot' ) { let watchers = this . _botWatcherMap . get ( timer . botId ) ; if ( watchers ) { let index = watchers . findIndex ( ( w ) => w . timerId === timer . timerId ) ; if ( index >= 0 ) { watchers . splice ( index , 1 ) ; } } } else if ( timer . type === 'watch_portal' ) { let watchers = this . _portalWatcherMap . get ( timer . portalId ) ; if ( watchers ) { let index = watchers . findIndex ( ( w ) => w . timerId === timer . timerId ) ; if ( index >= 0 ) { watchers . splice ( index , 1 ) ; } } } } param action enqueueAction ( action ) { if ( action . type === 'remote' ) { const index = this . actions . indexOf ( < BotAction > action . event ) ; if ( index >= 0 ) { this . actions [ index ] = action ; } else { this . actions . push ( action ) ; this . _batcher . notifyChange ( ) ; } } else { this . actions . push ( action ) ; this . _batcher . notifyChange ( ) ; } } dequeueActions ( ) { let actions = this . actions ; this . actions = [ ] ; return actions ; } enqueueError ( error ) { if ( error instanceof RanOutOfEnergyError ) { throw error ; } this . errors . push ( error ) ; this . _batcher . notifyChange ( ) ; } dequeueErrors ( ) { let errors = this . errors ; this . errors = [ ] ; return errors ; } param bot unwrapBot ( bot ) { if ( isBot ( bot ) ) { return { id : bot . id , space : bot . space , tags : { ... bot . tags , } , } ; } return bot ; } createBot ( bot ) { const script = this . _scriptFactory . createRuntimeBot ( bot ) || null ; if ( script ) { addToContext ( this , script ) ; if ( script . listeners ) { for ( let key in script . listeners ) { if ( typeof script . listeners [ key ] === 'function' ) { this . recordListenerPresense ( script . id , key , true ) ; } } } } this . enqueueAction ( botAdded ( bot ) ) ; return script ; } param bot destroyBot ( bot ) { const index = indexInContext ( this , bot ) ; if ( index < 0 ) { return ; } const mode = this . _scriptFactory . destroyScriptBot ( bot ) ; if ( mode === RealtimeEditMode . Immediate ) { this . bots . splice ( index , 1 ) ; delete this . state [ bot . id ] ; this . cancelBotTimers ( bot . id ) ; if ( bot . listeners ) { for ( let key in bot . listeners ) { if ( typeof bot . listeners [ key ] === 'function' ) { this . recordListenerPresense ( bot . id , key , false ) ; } } } } this . enqueueAction ( botRemoved ( bot . id ) ) ; } createTask ( unguessableId , allowRemoteResolution ) { let resolve ; let reject ; let promise = new Promise ( ( res , rej ) => { resolve = res ; reject = rej ; } ) ; const task = { taskId : ! unguessableId ? ( this . _taskCounter += 1 ) : this . uuid ( ) , allowRemoteResolution : allowRemoteResolution || false , resolve : resolve , reject : reject , promise , } ; this . tasks . set ( task . taskId , task ) ; return task ; } resolveTask ( taskId , result , remote ) { const task = this . tasks . get ( taskId ) ; if ( task && ( task . allowRemoteResolution || remote === false ) ) { this . tasks . delete ( taskId ) ; task . resolve ( result ) ; return true ; } return false ; } rejectTask ( taskId , error , remote ) { const task = this . tasks . get ( taskId ) ; if ( task && ( task . allowRemoteResolution || remote === false ) ) { this . tasks . delete ( taskId ) ; task . reject ( error ) ; return true ; } return false ; } getShoutTimers ( ) { const keys = Object . keys ( this . _shoutTimers ) ; const list = keys . map ( ( k ) => ( { tag : k , timeMs : this . _shoutTimers [ k ] , } ) ) ; return sortBy ( list , ( timer ) => - timer . timeMs ) ; } addShoutTime ( shout , ms ) { if ( ms < 0 ) { throw new Error ( 'Cannot add negative time to a shout timer.' ) ; } if ( ! ( shout in this . _shoutTimers ) ) { this . _shoutTimers [ shout ] = 0 ; } this . _shoutTimers [ shout ] += ms ; } startAnimationLoop ( ) { if ( ! this . _animationLoop ) { const sub = animationLoop ( ) . pipe ( tap ( ( ) => this . _updateAnimationLoop ( ) ) ) . subscribe ( ) ; this . _animationLoop = new Subscription ( ( ) => { sub . unsubscribe ( ) ; this . _animationLoop = null ; } ) ; } return this . _animationLoop ; } param func param returnValues setMockReturns ( func , returnValues ) { if ( ORIGINAL_OBJECT in func ) { func = func [ ORIGINAL_OBJECT ] ; } this . _mocks . set ( func , returnValues . slice ( ) ) ; } param func param args param returnValue setMockReturn ( func , args , returnValue ) { if ( ORIGINAL_OBJECT in func ) { func = func [ ORIGINAL_OBJECT ] ; } let mocks = this . _mocks . get ( func ) ; let map ; if ( mocks instanceof Map ) { map = mocks ; } else { map = new Map ( ) ; this . _mocks . set ( func , map ) ; } const argJson = stableStringify ( args , { space : 2 } ) ; map . set ( argJson , returnValue ) ; } param func getNextMockReturn ( func , functionName , args ) { if ( ORIGINAL_OBJECT in func ) { func = func [ ORIGINAL_OBJECT ] ; } if ( ! this . _mocks . has ( func ) ) { throw new Error ( `No mask data for function: ${ debugStringifyFunction ( functionName , args ) }` ) ; } let arrayOrMap = this . _mocks . get ( func ) ; if ( arrayOrMap instanceof Map ) { const argJson = stableStringify ( args , { space : 2 } ) ; if ( arrayOrMap . has ( argJson ) ) { return arrayOrMap . get ( argJson ) ; } else { throw new Error ( `No mask data for function (no matching input): ${ debugStringifyFunction ( functionName , args ) }` ) ; } } else { if ( arrayOrMap . length > 0 ) { return arrayOrMap . shift ( ) ; } else { throw new Error ( `No mask data for function (out of return values): ${ debugStringifyFunction ( functionName , args ) }` ) ; } } } private _updateAnimationLoop ( ) { TWEEN . update ( this . localTime ) ; } } param functionName param args returns export function debugStringifyFunction ( functionName , args ) { const argList = args . map ( ( a ) => debugStringify ( a ) ) . join ( ', ' ) ; return `${ functionName }(${ argList })` ; } param value returns export function debugStringify ( value ) { if ( ( typeof value === 'object' || typeof value === 'function' ) && DEBUG_STRING in value ) { return value [ DEBUG_STRING ] ; } return stableStringify ( value , { space : 2 } ) ; } function animationLoop ( ) { return new Observable < void > ( ( observer ) => { let interval = setInterval ( ( ) => { observer . next ( ) ; } , SET_INTERVAL_ANIMATION_FRAME_TIME ) ; return ( ) => { clearInterval ( interval ) ; } ; } ) ; }