2014-05-10 21:24:50 -04:00
|
|
|
# Each LevelView or Simulator has a God which listens for spells cast and summons new Angels on the main thread to
|
|
|
|
# oversee simulation of the World on worker threads. The Gods and Angels even have names. It's kind of fun.
|
|
|
|
# (More fun than ThreadPool and WorkerAgentManager and such.)
|
|
|
|
|
2014-02-10 20:09:19 -05:00
|
|
|
{now} = require 'lib/world/world_utils'
|
|
|
|
World = require 'lib/world/world'
|
2014-05-10 21:24:50 -04:00
|
|
|
CocoClass = require 'lib/CocoClass'
|
|
|
|
Angel = require 'lib/Angel'
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
module.exports = class God extends CocoClass
|
2014-06-30 22:16:26 -04:00
|
|
|
@nicks: ['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
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
subscriptions:
|
|
|
|
'tome:cast-spells': 'onTomeCast'
|
|
|
|
'tome:spell-debug-value-request': 'retrieveValueFromFrame'
|
|
|
|
'god:new-world-created': 'onNewWorldCreated'
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-02-15 20:29:54 -05:00
|
|
|
constructor: (options) ->
|
2014-02-15 20:38:45 -05:00
|
|
|
options ?= {}
|
2014-05-10 21:24:50 -04:00
|
|
|
@retrieveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000
|
|
|
|
super()
|
|
|
|
|
|
|
|
# Angels are all given access to this.
|
|
|
|
@angelsShare =
|
|
|
|
workerCode: options.workerCode or '/javascripts/workers/worker_world.js' # Either path or function
|
|
|
|
headless: options.headless # Whether to just simulate the goals, or to deserialize all simulation results
|
|
|
|
godNick: @nick
|
|
|
|
workQueue: []
|
|
|
|
firstWorld: true
|
|
|
|
world: undefined
|
|
|
|
goalManager: undefined
|
|
|
|
worldClassMap: undefined
|
|
|
|
angels: []
|
|
|
|
busyAngels: [] # Busy angels will automatically register here.
|
|
|
|
|
|
|
|
# ~20MB per idle worker + angel overhead - every Angel maps to 1 worker
|
|
|
|
angelCount = options.maxAngels ? 2 # How many concurrent Angels/web workers to use at a time
|
|
|
|
# Don't generate all Angels at once.
|
|
|
|
_.delay (=> new Angel @angelsShare unless @destroyed), 250 * i for i in [0 ... angelCount]
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
destroy: ->
|
2014-05-11 20:42:32 -04:00
|
|
|
angel.destroy() for angel in @angelsShare.angels.slice()
|
2014-05-10 21:24:50 -04:00
|
|
|
@angelsShare.goalManager?.destroy()
|
|
|
|
@debugWorker?.terminate()
|
|
|
|
@debugWorker?.removeEventListener 'message', @onDebugWorkerMessage
|
|
|
|
super()
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
setLevel: (@level) ->
|
2014-05-15 17:54:31 -04:00
|
|
|
setLevelSessionIDs: (@levelSessionIDs) ->
|
2014-05-20 11:00:44 -04:00
|
|
|
setGoalManager: (goalManager) ->
|
|
|
|
@angelsShare.goalManager?.destroy() unless @angelsShare.goalManager is goalManager
|
|
|
|
@angelsShare.goalManager = goalManager
|
2014-05-10 21:24:50 -04:00
|
|
|
setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
onTomeCast: (e) ->
|
|
|
|
@createWorld e.spells, e.preload
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
createWorld: (spells, preload=false) ->
|
2014-05-19 23:12:08 -04:00
|
|
|
console.log "#{@nick}: Let there be light upon #{@level.name}! (preload: #{preload})"
|
2014-05-11 20:42:32 -04:00
|
|
|
userCodeMap = @getUserCodeMap spells
|
|
|
|
|
|
|
|
# We only want one world being simulated, so we abort other angels, unless we had one preloading this very code.
|
|
|
|
hadPreloader = false
|
|
|
|
for angel in @angelsShare.busyAngels
|
|
|
|
isPreloading = angel.running and angel.work.preload and _.isEqual angel.work.userCodeMap, userCodeMap, (a, b) ->
|
|
|
|
return a.raw is b.raw if a?.raw? and b?.raw?
|
|
|
|
undefined # Let default equality test suffice.
|
|
|
|
if not hadPreloader and isPreloading
|
|
|
|
angel.finalizePreload()
|
|
|
|
hadPreloader = true
|
2014-05-19 23:12:08 -04:00
|
|
|
else if preload and angel.running and not angel.work.preload
|
|
|
|
# It's still running for real, so let's not preload.
|
|
|
|
return
|
2014-05-11 20:42:32 -04:00
|
|
|
else
|
|
|
|
angel.abort()
|
|
|
|
return if hadPreloader
|
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
@angelsShare.workQueue = []
|
|
|
|
@angelsShare.workQueue.push
|
2014-05-11 20:42:32 -04:00
|
|
|
userCodeMap: userCodeMap
|
2014-01-03 13:32:13 -05:00
|
|
|
level: @level
|
2014-05-15 17:54:31 -04:00
|
|
|
levelSessionIDs: @levelSessionIDs
|
2014-05-10 21:24:50 -04:00
|
|
|
goals: @angelsShare.goalManager?.getGoals()
|
|
|
|
headless: @angelsShare.headless
|
|
|
|
preload: preload
|
|
|
|
synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9.
|
|
|
|
angel.workIfIdle() for angel in @angelsShare.angels
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
getUserCodeMap: (spells) ->
|
|
|
|
userCodeMap = {}
|
|
|
|
for spellKey, spell of spells
|
|
|
|
for thangID, spellThang of spell.thangs
|
|
|
|
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
|
|
|
|
userCodeMap
|
|
|
|
|
|
|
|
|
|
|
|
#### New stuff related to debugging ####
|
|
|
|
retrieveValueFromFrame: (args) =>
|
|
|
|
return if @destroyed
|
|
|
|
return unless args.thangID and args.spellID and args.variableChain
|
2014-06-30 22:16:26 -04:00
|
|
|
return console.error 'Tried to retrieve debug value with no currentUserCodeMap' unless @currentUserCodeMap
|
2014-05-10 21:24:50 -04:00
|
|
|
@debugWorker ?= @createDebugWorker()
|
2014-05-12 14:16:02 -04:00
|
|
|
args.frame ?= @angelsShare.world.age / @angelsShare.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:
|
|
|
|
userCodeMap: @currentUserCodeMap
|
|
|
|
level: @level
|
2014-05-15 17:54:31 -04:00
|
|
|
levelSessionIDs: @levelSessionIDs
|
2014-05-01 19:08:46 -04:00
|
|
|
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-10 21:24:50 -04:00
|
|
|
createDebugWorker: ->
|
|
|
|
worker = new Worker '/javascripts/workers/worker_world.js'
|
|
|
|
worker.addEventListener 'message', @onDebugWorkerMessage
|
|
|
|
worker
|
2014-05-05 20:37:14 -04:00
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
onDebugWorkerMessage: (event) =>
|
|
|
|
switch event.data.type
|
|
|
|
when 'console-log'
|
|
|
|
console.log "|#{@nick}'s debugger|", event.data.args...
|
|
|
|
when 'debug-value-return'
|
|
|
|
Backbone.Mediator.publish 'god:debug-value-return', event.data.serialized
|
2014-06-18 16:43:03 -04:00
|
|
|
when 'debug-world-load-progress-changed'
|
|
|
|
Backbone.Mediator.publish 'god:debug-world-load-progress-changed', event.data
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
onNewWorldCreated: (e) ->
|
|
|
|
@currentUserCodeMap = @filterUserCodeMapWhenFromWorld e.world.userCodeMap
|
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] = {}
|
2014-05-10 21:24:50 -04:00
|
|
|
for spellName, aether of thang
|
|
|
|
shallowFilteredObject = _.pick aether, ['raw', 'pure', 'originalOptions']
|
2014-05-06 13:06:32 -04:00
|
|
|
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
|
|
|
|
2014-05-10 21:24:50 -04:00
|
|
|
imitateIE9 = false # (and in world_utils.coffee)
|
|
|
|
if imitateIE9
|
|
|
|
window.Worker = null
|
|
|
|
window.Float32Array = null
|
|
|
|
# Also uncomment vendor_with_box2d.js in index.html if you want Collision to run and Thangs to move.
|