2014-05-10 21:24:50 -04:00
# Every Angel has one web worker attached to it. It will call methods inside the worker and kill it if it times out.
# God is the public API; Angels are an implementation detail. Each God can have one or more Angels.
{ now } = require ' lib/world/world_utils '
World = require ' lib/world/world '
2014-11-28 20:49:41 -05:00
CocoClass = require ' core/CocoClass '
2014-09-29 20:31:58 -04:00
GoalManager = require ' lib/world/GoalManager '
2015-04-25 20:29:02 -04:00
{ sendContactMessage } = require ' core/contact '
2014-05-10 21:24:50 -04:00
2015-05-11 21:53:42 -04:00
reportedLoadErrorAlready = false
2014-05-10 21:24:50 -04:00
module.exports = class Angel extends CocoClass
@nicks: [ ' Archer ' , ' Lana ' , ' Cyril ' , ' Pam ' , ' Cheryl ' , ' Woodhouse ' , ' Ray ' , ' Krieger ' ]
2014-06-21 13:56:55 -04:00
infiniteLoopIntervalDuration: 10000 # check this often; must be longer than other two combined
infiniteLoopTimeoutDuration: 7500 # wait this long for a response when checking
2014-05-10 21:24:50 -04:00
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
2014-08-23 00:35:08 -04:00
subscriptions:
2014-08-23 20:26:56 -04:00
' level:flag-updated ' : ' onFlagEvent '
2014-08-26 01:05:24 -04:00
' playback:stop-real-time-playback ' : ' onStopRealTimePlayback '
2015-04-18 22:54:34 -04:00
' level:escape-pressed ' : ' onEscapePressed '
2014-08-23 00:35:08 -04:00
2014-05-10 21:24:50 -04:00
constructor: (@shared) ->
super ( )
@ say ' Got my wings. '
2015-04-10 11:24:19 -04:00
isIE = window . navigator and ( window . navigator . userAgent . search ( ' MSIE ' ) isnt - 1 or window . navigator . appName is ' Microsoft Internet Explorer ' )
2015-11-29 15:30:19 -05:00
slowerSimulations = isIE #or @shared.headless
# Since IE is so slow to serialize without transferable objects, we can't trust it.
# We also noticed the headless_client simulator needing more time. (This does both Simulators, though.) If we need to use lots of headless clients, enable this.
if slowerSimulations
2015-04-10 11:24:19 -04:00
@ infiniteLoopIntervalDuration *= 10
2014-05-10 21:24:50 -04:00
@ infiniteLoopTimeoutDuration *= 10
@ abortTimeoutDuration *= 10
@initialized = false
@running = false
2015-04-25 20:29:02 -04:00
@allLogs = [ ]
2014-05-10 21:24:50 -04:00
@ hireWorker ( )
@ shared . angels . push @
2016-07-08 17:17:07 -04:00
@ listenTo @ shared . gameUIState . get ( ' realTimeInputEvents ' ) , ' add ' , @ onAddRealTimeInputEvent
2014-05-10 21:24:50 -04:00
destroy: ->
@ fireWorker false
_ . remove @ shared . angels , @
super ( )
workIfIdle: ->
@ doWork ( ) unless @ running
# say: debugging stuff, usually off; log: important performance indicators, keep on
say: (args...) -> #@log args...
2015-01-05 13:44:17 -05:00
log: ->
2015-04-25 20:29:02 -04:00
# console.info.apply is undefined in IE9, CoffeeScript splats invocation won't work.
2014-11-26 15:40:16 -05:00
# http://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function
message = " | #{ @ shared . godNick } ' s #{ @ nick } | "
message += " #{ arg } " for arg in arguments
console . info message
2015-04-25 20:29:02 -04:00
@ allLogs . push message
2014-05-10 21:24:50 -04:00
testWorker: =>
2014-05-24 00:24:50 -04:00
return if @ destroyed
clearTimeout @ condemnTimeout
@condemnTimeout = _ . delay @ infinitelyLooped , @ infiniteLoopTimeoutDuration
2014-06-30 22:16:26 -04:00
@ say ' Let \' s give it ' , @ infiniteLoopTimeoutDuration , ' to not loop. '
2014-05-24 00:24:50 -04:00
@ worker . postMessage func: ' reportIn '
2014-05-10 21:24:50 -04:00
onWorkerMessage: (event) =>
return @ say ' Currently aborting old work. ' if @ aborting and event . data . type isnt ' abort '
switch event . data . type
# First step: worker has to load the scripts.
when ' worker-initialized '
unless @ initialized
@ log " Worker initialized after #{ ( new Date ( ) ) - @ worker . creationTime } ms "
@initialized = true
@ doWork ( )
# We watch over the worker as it loads the world frames to make sure it doesn't infinitely loop.
when ' start-load-frames '
clearTimeout @ condemnTimeout
when ' report-in '
2014-06-30 22:16:26 -04:00
@ say ' Worker reported in. '
2014-05-10 21:24:50 -04:00
clearTimeout @ condemnTimeout
when ' end-load-frames '
clearTimeout @ condemnTimeout
2016-06-29 15:40:30 -04:00
@ beholdGoalStates { goalStates: event . data . goalStates , overallStatus: event . data . overallStatus , preload: false , totalFrames: event . data . totalFrames , lastFrameHash: event . data . lastFrameHash , simulationFrameRate: event . data . simulationFrameRate } # Work ends here if we're headless.
2014-09-23 21:21:27 -04:00
when ' end-preload-frames '
clearTimeout @ condemnTimeout
2016-06-29 15:40:30 -04:00
@ beholdGoalStates { goalStates: event . data . goalStates , overallStatus: event . data . overallStatus , preload: true , simulationFrameRate: event . data . simulationFrameRate }
2014-05-10 21:24:50 -04:00
2014-05-26 21:45:00 -04:00
# We have to abort like an infinite loop if we see one of these; they're not really recoverable
when ' non-user-code-problem '
2016-04-25 20:03:08 -04:00
@ publishGodEvent ' non-user-code-problem ' , problem: event . data . problem
2014-05-26 21:45:00 -04:00
if @ shared . firstWorld
2015-04-25 20:29:02 -04:00
@ infinitelyLooped ( false , true ) # For now, this should do roughly the right thing if it happens during load.
2014-05-26 21:45:00 -04:00
else
@ fireWorker ( )
2014-08-22 15:39:29 -04:00
# If it didn't finish simulating successfully, or we abort the worker.
2014-05-10 21:24:50 -04:00
when ' abort '
2014-06-30 22:16:26 -04:00
@ say ' Aborted. ' , event . data
2014-05-10 21:24:50 -04:00
clearTimeout @ abortTimeout
@aborting = false
@running = false
_ . remove @ shared . busyAngels , @
@ doWork ( )
2014-08-22 15:39:29 -04:00
# We pay attention to certain progress indicators as the world loads.
when ' console-log '
@ log event . data . args . . .
when ' user-code-problem '
2016-04-25 20:03:08 -04:00
@ publishGodEvent ' user-code-problem ' , problem: event . data . problem
2014-08-22 15:39:29 -04:00
when ' world-load-progress-changed '
2016-07-28 16:39:58 -04:00
progress = event . data . progress
progress = Math . min ( progress , 0.9 ) if @ work . indefiniteLength
@ publishGodEvent ' world-load-progress-changed ' , { progress }
2014-09-21 17:35:59 -04:00
unless event . data . progress is 1 or @ work . preload or @ work . headless or @ work . synchronous or @ deserializationQueue . length or ( @ shared . firstWorld and not @ shared . spectate )
2014-08-22 15:39:29 -04:00
@ worker . postMessage func: ' serializeFramesSoFar ' # Stream it!
# We have some or all of the frames serialized, so let's send the (partially?) simulated world to the Surface.
when ' some-frames-serialized ' , ' new-world '
2014-08-22 17:59:32 -04:00
deserializationArgs = [ event . data . serialized , event . data . goalStates , event . data . startFrame , event . data . endFrame , @ streamingWorld ]
2014-08-22 15:39:29 -04:00
@ deserializationQueue . push deserializationArgs
if @ deserializationQueue . length is 1
@ beholdWorld deserializationArgs . . .
2014-05-10 21:24:50 -04:00
else
2014-06-30 22:16:26 -04:00
@ log ' Received unsupported message: ' , event . data
2014-05-10 21:24:50 -04:00
2016-06-29 15:40:30 -04:00
beholdGoalStates: ({goalStates, overallStatus, preload, totalFrames, lastFrameHash, simulationFrameRate}) ->
2014-05-10 21:24:50 -04:00
return if @ aborting
2016-06-29 15:40:30 -04:00
event = goalStates: goalStates , preload: preload ? false , overallStatus: overallStatus
2016-04-07 22:06:57 -04:00
event.totalFrames = totalFrames if totalFrames ?
event.lastFrameHash = lastFrameHash if lastFrameHash ?
2016-06-29 15:40:30 -04:00
event.simulationFrameRate = simulationFrameRate if simulationFrameRate ?
2016-04-25 20:03:08 -04:00
@ publishGodEvent ' goals-calculated ' , event
2014-05-10 21:24:50 -04:00
@ finishWork ( ) if @ shared . headless
2014-08-21 19:27:52 -04:00
beholdWorld: (serialized, goalStates, startFrame, endFrame, streamingWorld) ->
2014-05-10 21:24:50 -04:00
return if @ aborting
# Toggle BOX2D_ENABLED during deserialization so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment.
window . BOX2D_ENABLED = false
2016-07-28 16:39:58 -04:00
streamingWorld ? . indefiniteLength = @ work . indefiniteLength
2014-10-23 21:11:12 -04:00
@streamingWorld = World . deserialize serialized , @ shared . worldClassMap , @ shared . lastSerializedWorldFrames , @ finishBeholdingWorld ( goalStates ) , startFrame , endFrame , @ work . level , streamingWorld
2014-05-10 21:24:50 -04:00
window . BOX2D_ENABLED = true
@shared.lastSerializedWorldFrames = serialized . frames
finishBeholdingWorld: (goalStates) -> (world) =>
2014-10-14 14:53:32 -04:00
return if @ aborting or @ destroyed
2014-08-21 19:27:52 -04:00
finished = world . frames . length is world . totalFrames
2016-07-28 16:39:58 -04:00
if @ work ? . indefiniteLength and world . victory ?
finished = true
world.totalFrames = world . frames . length
firstChangedFrame = if @ work . indefiniteLength then 0 else world . findFirstChangedFrame @ shared . world
2016-04-25 20:03:08 -04:00
eventType = if finished then ' new-world-created ' else ' streaming-world-updated '
2014-08-21 19:27:52 -04:00
if finished
@shared.world = world
2016-04-25 20:03:08 -04:00
@ publishGodEvent eventType , world: world , firstWorld: @ shared . firstWorld , goalStates: goalStates , team: me . team , firstChangedFrame: firstChangedFrame , finished: finished
2014-08-22 00:23:45 -04:00
if finished
2014-08-21 19:27:52 -04:00
for scriptNote in @ shared . world . scriptNotes
Backbone . Mediator . publish scriptNote . channel , scriptNote . event
@ shared . goalManager ? . world = world
@ finishWork ( )
else
2014-08-22 15:39:29 -04:00
@ deserializationQueue . shift ( ) # Finished with this deserialization.
if deserializationArgs = @ deserializationQueue [ 0 ] # Start another?
@ beholdWorld deserializationArgs . . .
2014-05-10 21:24:50 -04:00
finishWork: ->
2014-08-22 17:59:32 -04:00
@streamingWorld = null
2014-05-10 21:24:50 -04:00
@shared.firstWorld = false
2014-08-22 15:39:29 -04:00
@deserializationQueue = [ ]
2014-05-10 21:24:50 -04:00
@running = false
_ . remove @ shared . busyAngels , @
2014-09-19 11:11:08 -04:00
clearTimeout @ condemnTimeout
clearInterval @ purgatoryTimer
@condemnTimeout = @purgatoryTimer = null
2014-05-10 21:24:50 -04:00
@ doWork ( )
2014-05-11 20:42:32 -04:00
finalizePreload: ->
2014-06-30 22:16:26 -04:00
@ say ' Finalize preload. '
2014-05-11 20:42:32 -04:00
@ worker . postMessage func: ' finalizePreload '
2014-08-21 19:27:52 -04:00
@work.preload = false
2014-05-11 20:42:32 -04:00
2015-04-25 20:29:02 -04:00
infinitelyLooped: (escaped=false, nonUserCodeProblem=false) =>
2014-06-30 22:16:26 -04:00
@ say ' On infinitely looped! Aborting? ' , @ aborting
2014-05-10 21:24:50 -04:00
return if @ aborting
2014-06-30 22:16:26 -04:00
problem = type: ' runtime ' , level: ' error ' , id: ' runtime_InfiniteLoop ' , message: ' Code never finished. It \' s either really slow or has an infinite loop. '
2015-04-18 22:54:34 -04:00
problem.message = ' Escape pressed; code aborted. ' if escaped
2016-04-25 20:03:08 -04:00
@ publishGodEvent ' user-code-problem ' , problem: problem
@ publishGodEvent ' infinite-loop ' , firstWorld: @ shared . firstWorld , nonUserCodeProblem: nonUserCodeProblem
2015-04-25 20:29:02 -04:00
@ reportLoadError ( ) if nonUserCodeProblem
2014-05-10 21:24:50 -04:00
@ fireWorker ( )
2016-04-25 20:03:08 -04:00
publishGodEvent: (channel, e) ->
# For Simulator. TODO: refactor all the god:* Mediator events to be local events.
@ shared . god . trigger channel , e
e.god = @ shared . god
Backbone . Mediator . publish ' god: ' + channel , e
2015-04-25 20:29:02 -04:00
reportLoadError: ->
2015-05-11 21:53:42 -04:00
return if me . isAdmin ( ) or /dev=true/ . test ( window . location ? . href ? ' ' ) or reportedLoadErrorAlready
reportedLoadErrorAlready = true
2015-04-25 20:29:02 -04:00
context = email: me . get ( ' email ' )
context.message = " Automatic Report - Unable to Load Level \n Logs: \n " + @ allLogs . join ( ' \n ' )
if $ . browser
context.browser = " #{ $ . browser . platform } #{ $ . browser . name } #{ $ . browser . versionNumber } "
context.screenSize = " #{ screen ? . width ? $ ( window ) . width ( ) } x #{ screen ? . height ? $ ( window ) . height ( ) } "
context.subject = " Level Load Error: #{ @ work ? . level ? . name or ' Unknown Level ' } "
context.levelSlug = @ work ? . level ? . slug
2015-05-11 21:53:42 -04:00
sendContactMessage context
2015-04-25 20:29:02 -04:00
2014-05-10 21:24:50 -04:00
doWork: ->
return if @ aborting
2014-06-30 22:16:26 -04:00
return @ say ' Not initialized for work yet. ' unless @ initialized
2014-05-10 21:24:50 -04:00
if @ shared . workQueue . length
2014-05-11 20:42:32 -04:00
@work = @ shared . workQueue . shift ( )
return _ . defer @ simulateSync , @ work if @ work . synchronous
2014-06-30 22:16:26 -04:00
@ say ' Running world... '
2014-05-10 21:24:50 -04:00
@running = true
@ shared . busyAngels . push @
2014-08-22 15:39:29 -04:00
@deserializationQueue = [ ]
2014-05-11 20:42:32 -04:00
@ worker . postMessage func: ' runWorld ' , args: @ work
2014-05-10 21:24:50 -04:00
clearTimeout @ purgatoryTimer
2014-06-30 22:16:26 -04:00
@ say ' Infinite loop timer started at interval of ' , @ infiniteLoopIntervalDuration
2014-05-10 21:24:50 -04:00
@purgatoryTimer = setInterval @ testWorker , @ infiniteLoopIntervalDuration
else
2014-06-30 22:16:26 -04:00
@ say ' No work to do. '
2014-05-10 21:24:50 -04:00
@ hireWorker ( )
abort: ->
return unless @ worker and @ running
2014-06-30 22:16:26 -04:00
@ say ' Aborting... '
2014-05-10 21:24:50 -04:00
@running = false
2014-05-11 20:42:32 -04:00
@work = null
2014-08-22 17:59:32 -04:00
@streamingWorld = null
@deserializationQueue = null
2014-05-10 21:24:50 -04:00
_ . remove @ shared . busyAngels , @
@abortTimeout = _ . delay @ fireWorker , @ abortTimeoutDuration
@aborting = true
@ worker . postMessage func: ' abort '
fireWorker: (rehire=true) =>
2014-10-25 14:47:04 -04:00
return if @ destroyed
2014-05-10 21:24:50 -04:00
@aborting = false
@running = false
_ . remove @ shared . busyAngels , @
@ worker ? . removeEventListener ' message ' , @ onWorkerMessage
@ worker ? . terminate ( )
@worker = null
clearTimeout @ condemnTimeout
clearInterval @ purgatoryTimer
2014-06-30 22:16:26 -04:00
@ say ' Fired worker. '
2014-05-10 21:24:50 -04:00
@initialized = false
@work = null
2014-10-02 18:33:23 -04:00
@streamingWorld = null
@deserializationQueue = null
2014-05-10 21:24:50 -04:00
@ hireWorker ( ) if rehire
hireWorker: ->
2014-09-29 02:24:18 -04:00
unless Worker ?
unless @ initialized
@initialized = true
@ doWork ( )
return null
2014-05-10 21:24:50 -04:00
return if @ worker
2014-06-30 22:16:26 -04:00
@ say ' Hiring worker. '
2014-05-10 21:24:50 -04:00
@worker = new Worker @ shared . workerCode
@ worker . addEventListener ' message ' , @ onWorkerMessage
@worker.creationTime = new Date ( )
2014-08-23 20:26:56 -04:00
onFlagEvent: (e) ->
2014-08-23 00:35:08 -04:00
return unless @ running and @ work . realTime
2014-08-23 20:26:56 -04:00
@ worker . postMessage func: ' addFlagEvent ' , args: e
2014-08-23 00:35:08 -04:00
2016-07-08 17:17:07 -04:00
onAddRealTimeInputEvent: (realTimeInputEvent) ->
return unless @ running and @ work . realTime
@ worker . postMessage func: ' addRealTimeInputEvent ' , args: realTimeInputEvent . toJSON ( )
2014-08-26 01:05:24 -04:00
onStopRealTimePlayback: (e) ->
return unless @ running and @ work . realTime
@work.realTime = false
2015-04-18 22:54:34 -04:00
@lastRealTimeWork = new Date ( )
2014-08-26 01:05:24 -04:00
@ worker . postMessage func: ' stopRealTimePlayback '
2014-05-10 21:24:50 -04:00
2015-04-18 22:54:34 -04:00
onEscapePressed: (e) ->
return unless @ running and not @ work . realTime
return if ( new Date ( ) - @ lastRealTimeWork ) < 1000 # Fires right after onStopRealTimePlayback
@ infinitelyLooped true
2014-05-10 21:24:50 -04:00
#### Synchronous code for running worlds on main thread (profiling / IE9) ####
simulateSync: (work) =>
console ? . profile ? " World Generation #{ ( Math . random ( ) * 1000 ) . toFixed ( 0 ) } " if imitateIE9 ?
work.t0 = now ( )
2016-04-07 22:06:57 -04:00
work.world = testWorld = new World work . userCodeMap
work.world.levelSessionIDs = work . levelSessionIDs
work.world.submissionCount = work . submissionCount
work.world.fixedSeed = work . fixedSeed
work.world.flagHistory = work . flagHistory ? [ ]
work.world.difficulty = work . difficulty
work . world . loadFromLevel work . level
work.world.preloading = work . preload
work.world.headless = work . headless
work.world.realTime = work . realTime
2014-05-10 21:24:50 -04:00
if @ shared . goalManager
2014-09-30 13:15:33 -04:00
testGM = new GoalManager ( testWorld )
2014-05-10 21:24:50 -04:00
testGM . setGoals work . goals
testGM . setCode work . userCodeMap
testGM . worldGenerationWillBegin ( )
testWorld . setGoalManager testGM
@ doSimulateWorld work
console ? . profileEnd ? ( ) if imitateIE9 ?
2014-06-30 22:16:26 -04:00
console . log ' Construction: ' , ( work . t1 - work . t0 ) . toFixed ( 0 ) , ' ms. Simulation: ' , ( work . t2 - work . t1 ) . toFixed ( 0 ) , ' ms -- ' , ( ( work . t2 - work . t1 ) / testWorld . frames . length ) . toFixed ( 3 ) , ' ms per frame, profiled. '
2014-05-10 21:24:50 -04:00
# If performance was really a priority in IE9, we would rework things to be able to skip this step.
goalStates = testGM ? . getGoalStates ( )
2016-04-07 22:06:57 -04:00
work . world . goalManager . worldGenerationEnded ( ) if work . world . ended
if work . headless
2016-06-29 15:40:30 -04:00
simulationFrameRate = work . world . frames . length / ( work . t2 - work . t1 ) * 1000 * 30 / work . world . frameRate
@ beholdGoalStates { goalStates , overallStatus: testGM . checkOverallStatus ( ) , preload: false , totalFrames: work . world . totalFrames , lastFrameHash: work . world . frames [ work . world . totalFrames - 2 ] ? . hash , simulationFrameRate: simulationFrameRate }
2016-04-07 22:06:57 -04:00
return
serialized = world . serialize ( )
2014-05-10 21:24:50 -04:00
window . BOX2D_ENABLED = false
2014-11-26 15:40:16 -05:00
World . deserialize serialized . serializedWorld , @ shared . worldClassMap , @ shared . lastSerializedWorldFrames , @ finishBeholdingWorld ( goalStates ) , serialized . startFrame , serialized . endFrame , work . level
2014-05-10 21:24:50 -04:00
window . BOX2D_ENABLED = true
2014-09-29 02:24:18 -04:00
@shared.lastSerializedWorldFrames = serialized . serializedWorld . frames
2014-05-10 21:24:50 -04:00
doSimulateWorld: (work) ->
work.t1 = now ( )
2016-04-07 22:06:57 -04:00
Math . random = work . world . rand . randf # so user code is predictable
2014-06-30 22:16:26 -04:00
Aether . replaceBuiltin ( ' Math ' , Math )
2014-07-19 23:26:13 -04:00
replacedLoDash = _ . runInContext ( window )
_ [ key ] = replacedLoDash [ key ] for key , val of replacedLoDash
2014-05-10 21:24:50 -04:00
i = 0
2016-04-07 22:06:57 -04:00
while i < work . world . totalFrames
frame = work . world . getFrame i ++
2016-04-25 20:03:08 -04:00
@ publishGodEvent ' world-load-progress-changed ' , progress: 1
2016-04-07 22:06:57 -04:00
work.world.ended = true
system . finish work . world . thangs for system in work . world . systems
2014-05-10 21:24:50 -04:00
work.t2 = now ( )