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
2014-01-15 16:04:48 -05:00
@testWorld = null
2014-01-03 13:32:13 -05:00
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 ]
2014-01-06 15:47:50 -05:00
# 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