2014-02-10 20:09:19 -05:00
{ now } = require ' lib/world/world_utils '
World = require ' lib/world/world '
2014-01-03 13:32:13 -05:00
## Uncomment to imitate IE9 (and in world_utils.coffee)
#window.Worker = null
#window.Float32Array = null
2014-02-06 17:00:27 -05:00
# Also uncomment vendor_with_box2d.js in index.html if you want Collision to run and things to move.
2014-01-03 13:32:13 -05:00
module.exports = class God
2014-03-23 19:48:30 -04:00
@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 ' , ' 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 ' ]
2014-01-03 13:32:13 -05:00
@nextID: ->
@lastID = ( if @ lastID ? then @ lastID + 1 else Math . floor ( @ ids . length * Math . random ( ) ) ) % @ ids . length
@ ids [ @ lastID ]
worldWaiting: false # whether we're waiting for a worker to free up and run the world
2014-02-15 20:29:54 -05:00
constructor: (options) ->
2014-01-03 13:32:13 -05:00
@id = God . nextID ( )
2014-02-15 20:38:45 -05:00
options ? = { }
2014-02-15 20:29:54 -05:00
@maxAngels = options . maxAngels ? 2 # How many concurrent web workers to use; if set past 8, make up more names
@maxWorkerPoolSize = options . maxWorkerPoolSize ? 2 # ~20MB per idle worker
2014-05-05 20:37:14 -04:00
@workerCode = options . workerCode if options . workerCode ?
2014-01-03 13:32:13 -05:00
@angels = [ ]
@firstWorld = true
Backbone . Mediator . subscribe ' tome:cast-spells ' , @ onTomeCast , @
2014-05-06 13:06:32 -04:00
@retriveValueFromFrame = _ . throttle @ retrieveValueFromFrame , 1000
Backbone . Mediator . subscribe ' tome:spell-debug-value-request ' , @ retrieveValueFromFrame , @
2014-02-10 20:09:19 -05:00
@fillWorkerPool = _ . throttle @ fillWorkerPool , 3000 , leading: false
@ fillWorkerPool ( )
2014-04-28 18:05:54 -04:00
#TODO: have this as a constructor option
@debugWorker = @ createDebugWorker ( )
2014-05-01 14:12:44 -04:00
@currentUserCodeMap = { }
2014-01-03 13:32:13 -05:00
2014-05-05 20:37:14 -04:00
workerCode: ' /javascripts/workers/worker_world.js ' #Can be a string or a function.
2014-01-03 13:32:13 -05:00
onTomeCast: (e) ->
return if @ dead
2014-05-05 20:37:14 -04:00
@ createWorld e . spells
2014-01-03 13:32:13 -05:00
2014-02-10 20:09:19 -05:00
fillWorkerPool: =>
2014-03-06 18:52:51 -05:00
return unless Worker and not @ dead
2014-02-10 20:09:19 -05:00
@ workerPool ? = [ ]
2014-02-11 17:38:47 -05:00
if @ workerPool . length < @ maxWorkerPoolSize
2014-02-10 20:09:19 -05:00
@ workerPool . push @ createWorker ( )
2014-02-11 17:38:47 -05:00
if @ workerPool . length < @ maxWorkerPoolSize
@ fillWorkerPool ( )
2014-02-10 20:09:19 -05:00
getWorker: ->
@ fillWorkerPool ( )
2014-02-15 20:29:54 -05:00
worker = @ workerPool ? . shift ( )
2014-02-10 20:09:19 -05:00
return worker if worker
@ createWorker ( )
createWorker: ->
2014-05-05 20:37:14 -04:00
worker = new Worker @ workerCode
2014-02-27 23:01:27 -05:00
worker.creationTime = new Date ( )
2014-05-05 20:37:14 -04:00
worker . addEventListener ' message ' , @ onWorkerMessage ( worker )
2014-02-27 23:01:27 -05:00
worker
2014-04-28 18:05:54 -04:00
createDebugWorker: ->
worker = new Worker ' /javascripts/workers/worker_debug.js '
worker.creationTime = new Date ( )
worker . addEventListener ' message ' , @ onDebugWorkerMessage
worker
2014-05-07 14:37:03 -04:00
2014-05-05 20:37:14 -04:00
onWorkerMessage: (worker) =>
unless worker . onMessage ?
worker.onMessage = (event) =>
if event . data . type is ' worker-initialized '
console . log @ id , " worker initialized after " , ( ( new Date ( ) ) - worker . creationTime ) , " ms (before it was needed) "
worker.initialized = true
worker . removeEventListener ' message ' , worker . onMessage
else
console . warn " Received strange word from God: #{ event . data . type } "
worker . onMessage
2014-02-10 20:09:19 -05:00
2014-04-28 18:05:54 -04:00
onDebugWorkerMessage: (event) =>
worker = event . target
switch event . data . type
when " worker-initialized "
worker.initialized = true
when ' new-debug-world '
2014-05-01 14:12:44 -04:00
console . log " New Debug world! "
2014-04-28 18:05:54 -04:00
when ' console-log '
console . log " | " + @ id + " ' s " + @ id + " | " , event . data . args . . .
2014-05-01 19:08:46 -04:00
when ' debug-value-return '
2014-05-06 13:06:32 -04:00
Backbone . Mediator . publish ' god:debug-value-return ' , event . data . serialized
2014-05-07 14:37:03 -04:00
2014-01-03 13:32:13 -05:00
getAngel: ->
2014-02-06 17:00:27 -05:00
freeAngel = null
2014-01-03 13:32:13 -05:00
for angel in @ angels
2014-02-06 17:00:27 -05:00
if angel . busy
angel . abort ( )
else
freeAngel ? = angel
return freeAngel . enslave ( ) if freeAngel
2014-01-03 13:32:13 -05:00
maxedOut = @ angels . length is @ maxAngels
if not maxedOut
angel = new Angel @
@ angels . push angel
return angel . enslave ( )
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
2014-05-06 12:49:04 -04:00
createWorld: (@spells) ->
2014-01-03 13:32:13 -05:00
#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
2014-02-22 15:01:05 -05:00
#console.log "going to run world with code", @getUserCodeMap()
2014-01-03 13:32:13 -05:00
angel . worker . postMessage { func: ' runWorld ' , args: {
2014-02-10 20:09:19 -05:00
worldName: @ level . name
2014-05-05 20:37:14 -04:00
userCodeMap: @ getUserCodeMap ( spells )
2014-01-03 13:32:13 -05:00
level: @ level
firstWorld: @ firstWorld
goals: @ goalManager ? . getGoals ( )
} }
2014-05-06 13:06:32 -04:00
retrieveValueFromFrame: (args) ->
if not args . thangID or not args . spellID or not args . variableChain then return
args . frame ? = @ world . age / @ world . dt
2014-05-01 19:08:46 -04:00
@ debugWorker . postMessage
2014-05-05 16:26:37 -04:00
func: ' retrieveValueFromFrame '
2014-05-01 19:08:46 -04:00
args:
worldName: @ level . name
userCodeMap: @ currentUserCodeMap
level: @ level
firstWorld: @ firstWorld
goals: @ goalManager ? . getGoals ( )
2014-05-06 13:06:32 -04:00
frame: args . frame
currentThangID: args . thangID
2014-05-07 14:37:03 -04:00
currentSpellID: args . spellID
2014-05-06 13:06:32 -04:00
variableChain: args . variableChain
2014-05-06 18:07:06 -04:00
2014-05-05 20:37:14 -04:00
#Coffeescript needs getters and setters.
setGoalManager: (@goalManager) =>
setWorldClassMap: (@worldClassMap) =>
2014-01-03 13:32:13 -05:00
beholdWorld: (angel, serialized, goalStates) ->
2014-05-05 20:37:14 -04:00
unless serialized
# We're only interested in goalStates.
2014-05-06 12:49:04 -04:00
@latestGoalStates = goalStates
2014-05-05 20:37:14 -04:00
Backbone . Mediator . publish ( ' god:goals-calculated ' , goalStates: goalStates , team: me . team )
unless _ . find @ angels , ' busy '
@spells = null # Don't hold onto old spells; memory leaks
return
console . log " Beholding world. "
2014-01-03 13:32:13 -05:00
worldCreation = angel . started
angel . free ( )
return if @ latestWorldCreation ? and worldCreation < @ latestWorldCreation
@latestWorldCreation = worldCreation
@latestGoalStates = goalStates
2014-05-05 20:37:14 -04:00
console . warn " Goal states: " + JSON . stringify ( goalStates )
2014-01-03 13:32:13 -05:00
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
2014-05-05 20:37:14 -04:00
World . deserialize serialized , @ worldClassMap , @ lastSerializedWorldFrames , @ finishBeholdingWorld
2014-01-03 13:32:13 -05:00
window . BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized . frames
2014-02-12 12:55:19 -05:00
finishBeholdingWorld: (newWorld) =>
2014-01-03 13:32:13 -05:00
newWorld . findFirstChangedFrame @ world
@world = newWorld
2014-05-06 13:06:32 -04:00
@currentUserCodeMap = @ filterUserCodeMapWhenFromWorld @ world . userCodeMap
2014-01-03 13:32:13 -05:00
errorCount = ( t for t in @ world . thangs when t . errorsOut ) . length
2014-02-15 18:44:45 -05:00
Backbone . Mediator . publish ( ' god:new-world-created ' , world: @ world , firstWorld: @ firstWorld , errorCount: errorCount , goalStates: @ latestGoalStates , team: me . team )
2014-01-03 13:32:13 -05:00
for scriptNote in @ world . scriptNotes
Backbone . Mediator . publish scriptNote . channel , scriptNote . event
2014-02-06 17:00:27 -05:00
@ goalManager ? . world = newWorld
2014-01-03 13:32:13 -05:00
@firstWorld = false
2014-01-15 16:04:48 -05:00
@testWorld = null
2014-02-06 17:00:27 -05:00
unless _ . find @ angels , ' busy '
@spells = null # Don't hold onto old spells; memory leaks
2014-01-03 13:32:13 -05:00
2014-05-06 13:06:32 -04:00
filterUserCodeMapWhenFromWorld: (worldUserCodeMap) ->
newUserCodeMap = { }
for thangName , thang of worldUserCodeMap
newUserCodeMap [ thangName ] = { }
for spellName , aether of thang
shallowFilteredObject = _ . pick aether , [ ' raw ' , ' pure ' , ' originalOptions ' ]
newUserCodeMap [ thangName ] [ spellName ] = _ . cloneDeep shallowFilteredObject
newUserCodeMap [ thangName ] [ spellName ] = _ . defaults newUserCodeMap [ thangName ] [ spellName ] ,
flow: { }
metrics: { }
2014-05-07 14:37:03 -04:00
problems:
2014-05-06 13:06:32 -04:00
errors: [ ]
infos: [ ]
warnings: [ ]
style: { }
newUserCodeMap
2014-05-07 14:37:03 -04:00
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: ->
2014-02-27 23:01:27 -05:00
worker . removeEventListener ' message ' , @ onWorkerMessage for worker in @ workerPool ? [ ]
2014-01-03 13:32:13 -05:00
angel . destroy ( ) for angel in @ angels
@dead = true
Backbone . Mediator . unsubscribe ( ' tome:cast-spells ' , @ onTomeCast , @ )
2014-02-25 15:59:35 -05:00
@ goalManager ? . destroy ( )
2014-05-06 18:07:06 -04:00
@ debugWorker ? . terminate ( )
@ debugWorker ? . removeEventListener ' message ' , @ onDebugWorkerMessage
@ debugWorker ? = null
@currentUserCodeMap = null
2014-01-03 13:32:13 -05:00
@goalManager = null
2014-02-12 15:41:41 -05:00
@fillWorkerPool = null
@simulateWorld = null
2014-02-27 23:01:27 -05:00
@onWorkerMessage = null
2014-01-03 13:32:13 -05:00
#### 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
2014-05-05 20:37:14 -04:00
World . deserialize serialized , @ worldClassMap , @ lastSerializedWorldFrames , @ finishBeholdingWorld
2014-01-03 13:32:13 -05:00
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
2014-03-02 19:06:22 -05:00
infiniteLoopIntervalDuration: 7500 # check this often (must be more than the others added)
infiniteLoopTimeoutDuration: 2500 # wait this long when we check
2014-01-03 13:32:13 -05:00
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: ->
2014-02-10 20:09:19 -05:00
@worker = @ god . getWorker ( )
2014-01-03 13:32:13 -05:00
@ listen ( )
enslave: ->
@busy = true
@started = new Date ( )
@purgatoryTimer = setInterval @ testWorker , @ infiniteLoopIntervalDuration
2014-02-06 17:00:27 -05:00
@ spawnWorker ( ) unless @ worker
2014-01-03 13:32:13 -05:00
@
free: ->
@busy = false
@started = null
clearInterval @ purgatoryTimer
@purgatoryTimer = null
2014-02-09 21:44:31 -05:00
if @ worker
worker = @ worker
2014-02-17 14:53:52 -05:00
onWorkerMessage = @ onWorkerMessage
_ . delay ->
worker . terminate ( )
worker . removeEventListener ' message ' , onWorkerMessage
2014-03-05 15:00:29 -05:00
, 3000
2014-02-09 21:44:31 -05:00
@worker = null
2014-01-03 13:32:13 -05:00
@
abort: ->
2014-02-11 14:30:47 -05:00
return unless @ worker
2014-01-03 13:32:13 -05:00
@abortTimeout = _ . delay @ terminate , @ abortTimeoutDuration
@ worker . postMessage { func: ' abort ' }
terminate: =>
2014-02-06 20:31:08 -05:00
@ worker ? . terminate ( )
2014-02-12 15:41:41 -05:00
@ worker ? . removeEventListener ' message ' , @ onWorkerMessage
2014-02-06 17:00:27 -05:00
@worker = null
2014-01-03 13:32:13 -05:00
return if @ dead
@ free ( )
@ god . angelAborted @
destroy: ->
@dead = true
2014-02-12 12:55:19 -05:00
@finishBeholdingWorld = null
2014-01-03 13:32:13 -05:00
@ abort ( )
2014-02-12 15:41:41 -05:00
@terminate = null
@testWorker = null
@condemnWorker = null
@onWorkerMessage = null
2014-01-03 13:32:13 -05:00
testWorker: =>
2014-02-27 23:01:27 -05:00
unless @ worker . initialized
2014-05-05 20:37:14 -04:00
console . warn " Worker " , @ id , " hadn ' t even loaded the scripts yet after " , @ infiniteLoopIntervalDuration , " ms. "
2014-02-27 23:01:27 -05:00
return
2014-01-03 13:32:13 -05:00
@ worker . postMessage { func: ' reportIn ' }
@condemnTimeout = _ . delay @ condemnWorker , @ infiniteLoopTimeoutDuration
condemnWorker: =>
@ god . angelInfinitelyLooped @
@ abort ( )
listen: ->
2014-02-12 15:41:41 -05:00
@ worker . addEventListener ' message ' , @ onWorkerMessage
onWorkerMessage: (event) =>
switch event . data . type
2014-02-27 23:01:27 -05:00
when ' worker-initialized '
console . log " Worker " , @ id , " initialized after " , ( ( new Date ( ) ) - @ worker . creationTime ) , " ms (we had been waiting for it) "
2014-05-05 20:37:14 -04:00
@worker.initialized = true
2014-02-12 15:41:41 -05:00
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 @
when ' reportIn '
clearTimeout @ condemnTimeout
else
console . log " Unsupported message: " , event . data