codecombat/app/lib/God.coffee

226 lines
9.4 KiB
CoffeeScript
Raw Normal View History

2014-01-03 13:32:13 -05:00
{now} = require './world/world_utils'
## Uncomment to imitate IE9 (and in world_utils.coffee)
#window.Worker = null
#window.Float32Array = null
# (except that we won't have included vendor_with_box2d.js, so Collision won't run, things won't move, etc.)
module.exports = class God
@ids: ['Athena', 'Baldr', 'Crom', 'Dagr', 'Eris', 'Freyja', 'Great Gish', 'Hades', 'Ishtar', 'Janus', 'Khronos', 'Loki', 'Marduk', 'Negafook', 'Odin', 'Poseidon', 'Quetzalcoatl', 'Ra', 'Shiva', 'Thor', 'Umvelinqangi', 'Týr', 'Vishnu', 'Wepwawet', 'Xipe Totec', 'Yahweh', 'Zeus', '上帝', 'Tiamat', '盘古', 'Phoebe', 'Artemis', 'Osiris', "嫦娥", 'Anhur', 'Teshub', 'Enlil', 'Perkele', 'Aether', 'Chaos', 'Hera', 'Iris', 'Theia', 'Uranus', 'Stribog', 'Sabazios', 'Izanagi', 'Ao', 'Tāwhirimātea', 'Tengri', 'Inmar', 'Torngarsuk', 'Centzonhuitznahua', 'Hunab Ku', 'Apollo', 'Helios', 'Thoth', 'Hyperion', 'Alectrona', 'Eos', 'Mitra', 'Saranyu', 'Freyr', 'Koyash', 'Atropos', 'Clotho', 'Lachesis', 'Tyche', 'Skuld', 'Urðr', 'Verðandi', 'Camaxtli', 'Huhetotl', 'Set', 'Anu', 'Allah', 'Anshar', 'Hermes', 'Lugh', 'Brigit', 'Manannan Mac Lir', 'Persephone', 'Mercury', 'Venus', 'Mars', 'Azrael', 'He-Man', 'Anansi', 'Issek', 'Mog', 'Kos', 'Amaterasu Omikami', 'Raijin', 'Susanowo', 'Blind Io', 'The Lady', 'Offler', 'Ptah', 'Anubis', 'Ereshkigal', 'Nergal', 'Thanatos', 'Macaria', 'Angelos', 'Erebus', 'Hecate', 'Hel', 'Orcus', 'Ishtar-Deela Nakh', 'Prometheus', 'Hephaestos', 'Sekhmet', 'Ares', 'Enyo', 'Otrera', 'Pele', 'Hadúr', 'Hachiman', 'Dayisun Tngri', 'Ullr', 'Lua', 'Minerva']
@nextID: ->
@lastID = (if @lastID? then @lastID + 1 else Math.floor(@ids.length * Math.random())) % @ids.length
@ids[@lastID]
maxAngels: 2 # how many concurrent web workers to use; if set past 8, make up more names
worldWaiting: false # whether we're waiting for a worker to free up and run the world
constructor: (@world, @level) ->
@id = God.nextID()
@angels = []
@firstWorld = true
Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
onTomeCast: (e) ->
return if @dead
@spells = e.spells
@createWorld()
getAngel: ->
for angel in @angels
return angel.enslave() unless angel.busy
maxedOut = @angels.length is @maxAngels
if not maxedOut
angel = new Angel @
@angels.push angel
return angel.enslave()
oldestAngel = {started: new Date(2099, 1, 1)}
for angel in @angels
oldestAngel = angel if angel.started < oldestAngel.started
oldestAngel.abort()
null
angelInfinitelyLooped: (angel) ->
return if @dead
problem = type: "runtime", level: "error", id: "runtime_InfiniteLoop", message: "Code never finished. It's either really slow or has an infinite loop."
Backbone.Mediator.publish 'god:user-code-problem', problem: problem
Backbone.Mediator.publish 'god:infinite-loop', firstWorld: @firstWorld
angelAborted: (angel) ->
return unless @worldWaiting and not @dead
@createWorld()
angelUserCodeProblem: (angel, problem) ->
return if @dead
#console.log "UserCodeProblem:", '"' + problem.message + '"', "for", problem.userInfo.thangID, "-", problem.userInfo.methodName, 'at line', problem.ranges?[0][0][0], 'column', problem.ranges?[0][0][1]
Backbone.Mediator.publish 'god:user-code-problem', problem: problem
createWorld: ->
#console.log @id + ': "Let there be light upon', @world.name + '!"'
unless Worker? # profiling world simulation is easier on main thread, or we are IE9
setTimeout @simulateWorld, 1
return
angel = @getAngel()
if angel
@worldWaiting = false
else
@worldWaiting = true
return
angel.worker.postMessage {func: 'runWorld', args: {
worldName: @world.name
userCodeMap: @getUserCodeMap()
level: @level
firstWorld: @firstWorld
goals: @goalManager?.getGoals()
}}
beholdWorld: (angel, serialized, goalStates) ->
worldCreation = angel.started
angel.free()
return if @latestWorldCreation? and worldCreation < @latestWorldCreation
@latestWorldCreation = worldCreation
@latestGoalStates = goalStates
window.BOX2D_ENABLED = false # Flip this off so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment
@world.constructor.deserialize serialized, @world.classMap, @lastSerializedWorldFrames, worldCreation, @finishBeholdingWorld
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
finishBeholdingWorld: (newWorld) =>
newWorld.findFirstChangedFrame @world
@world = newWorld
errorCount = (t for t in @world.thangs when t.errorsOut).length
Backbone.Mediator.publish('god:new-world-created', world: @world, firstWorld: @firstWorld, errorCount: errorCount, goalStates: @latestGoalStates)
for scriptNote in @world.scriptNotes
Backbone.Mediator.publish scriptNote.channel, scriptNote.event
@firstWorld = false
getUserCodeMap: ->
userCodeMap = {}
for spellKey, spell of @spells
for thangID, spellThang of spell.thangs
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
userCodeMap
destroy: ->
angel.destroy() for angel in @angels
@dead = true
Backbone.Mediator.unsubscribe('tome:cast-spells', @onTomeCast, @)
@goalManager = null
#### Bad code for running worlds on main thread (profiling / IE9) ####
simulateWorld: =>
if Worker?
console?.profile? "World Generation #{(Math.random() * 1000).toFixed(0)}"
@t0 = now()
@testWorld = new @world.constructor @world.name, @getUserCodeMap()
@testWorld.loadFromLevel @level
if @goalManager
@testGM = new @goalManager.constructor @testWorld
@testGM.setGoals @goalManager.getGoals()
@testGM.setCode @getUserCodeMap()
@testGM.worldGenerationWillBegin()
@testWorld.setGoalManager @testGM
@doSimulateWorld()
if Worker?
console?.profileEnd?()
console.log "Construction:", (@t1 - @t0).toFixed(0), "ms. Simulation:", (@t2 - @t1).toFixed(0), "ms --", ((@t2 - @t1) / @testWorld.frames.length).toFixed(3), "ms per frame, profiled."
# If performance was really a priority in IE9, we would rework things to be able to skip this step.
@latestGoalStates = @testGM?.getGoalStates()
serialized = @testWorld.serialize().serializedWorld
window.BOX2D_ENABLED = false
@testWorld.constructor.deserialize serialized, @world.classMap, @lastSerializedWorldFrames, @t0, @finishBeholdingWorld
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
doSimulateWorld: ->
@t1 = now()
Math.random = @testWorld.rand.randf # so user code is predictable
i = 0
while i < @testWorld.totalFrames
frame = @testWorld.getFrame i++
@testWorld.ended = true
system.finish @testWorld.thangs for system in @testWorld.systems
@t2 = now()
#### End bad testing code ####
class Angel
@ids: ['Archer', 'Lana', 'Cyril', 'Pam', 'Cheryl', 'Woodhouse', 'Ray', 'Krieger']
@nextID: ->
@lastID = (if @lastID? then @lastID + 1 else Math.floor(@ids.length * Math.random())) % @ids.length
@ids[@lastID]
# https://github.com/codecombat/codecombat/issues/81 -- TODO: we need to wait for worker initialization first
infiniteLoopIntervalDuration: 15000 # check this often (must be more than the others added)
2014-01-03 13:32:13 -05:00
infiniteLoopTimeoutDuration: 1500 # wait this long when we check
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
constructor: (@god) ->
@id = Angel.nextID()
if (navigator.userAgent or navigator.vendor or window.opera).search("MSIE") isnt -1
@infiniteLoopIntervalDuration *= 20 # since it's so slow to serialize without transferable objects, we can't trust it
@infiniteLoopTimeoutDuration *= 20
@abortTimeoutDuration *= 10
@spawnWorker()
spawnWorker: ->
@worker = new Worker '/javascripts/workers/worker_world.js'
@listen()
enslave: ->
@busy = true
@started = new Date()
@purgatoryTimer = setInterval @testWorker, @infiniteLoopIntervalDuration
@
free: ->
@busy = false
@started = null
clearInterval @purgatoryTimer
@purgatoryTimer = null
@
abort: ->
@abortTimeout = _.delay @terminate, @abortTimeoutDuration
@worker.postMessage {func: 'abort'}
terminate: =>
@worker.terminate()
return if @dead
@spawnWorker()
@free()
@god.angelAborted @
destroy: ->
@dead = true
@abort()
testWorker: =>
@worker.postMessage {func: 'reportIn'}
@condemnTimeout = _.delay @condemnWorker, @infiniteLoopTimeoutDuration
condemnWorker: =>
@god.angelInfinitelyLooped @
@abort()
listen: ->
@worker.addEventListener 'message', (event) =>
switch event.data.type
when 'new-world'
@god.beholdWorld @, event.data.serialized, event.data.goalStates
when 'world-load-progress-changed'
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data unless @dead
when 'console-log'
console.log "|" + @god.id + "'s " + @id + "|", event.data.args...
when 'user-code-problem'
@god.angelUserCodeProblem @, event.data.problem
when 'abort'
#console.log @id, "aborted."
clearTimeout @abortTimeout
@free()
@god.angelAborted @
@worker.terminate() if @god.dead
when 'reportIn'
clearTimeout @condemnTimeout
else
console.log "Unsupported message:", event.data