Merged God and Buddha, split out Angels, did a huge refactoring, fixed many bugs, and laid groundwork for preloading Worlds.

This commit is contained in:
Nick Winter 2014-05-10 18:24:50 -07:00
parent df88be1ab1
commit 1e18f0b42f
15 changed files with 411 additions and 686 deletions

View file

@ -1,7 +1,5 @@
// There's no reason that this file is in JavaScript instead of CoffeeScript.
// We should convert it and update the brunch config.
// This file is in JavaScript because we can't figure out how to get brunch to compile it bare.
// If we wanted to be more robust, we could use this: https://github.com/padolsey/operative/blob/master/src/operative.js
if(typeof window !== 'undefined' || !self.importScripts)
throw "Attempt to load worker_world into main window instead of web worker.";
@ -31,7 +29,7 @@ if (!Function.prototype.bind) {
};
}
// assign global window so that Brunch's require (in world.js) can go into it
// Assign global window so that Brunch's require (in world.js) can go into it
self.window = self;
self.workerID = "Worker";
@ -42,7 +40,7 @@ var console = {
if(self.logsLogged++ == self.logLimit)
self.postMessage({type: 'console-log', args: ["Log limit " + self.logLimit + " reached; shutting up."], id: self.workerID});
else if(self.logsLogged < self.logLimit) {
args = [].slice.call(arguments);
var args = [].slice.call(arguments);
for(var i = 0; i < args.length; ++i) {
if(args[i] && args[i].constructor) {
if(args[i].constructor.className === "Thang" || args[i].isComponent)
@ -57,10 +55,10 @@ var console = {
}
}
}}; // so that we don't crash when debugging statements happen
console.error = console.info = console.log;
console.error = console.warn = console.info = console.debug = console.log;
self.console = console;
importScripts('/javascripts/world.js');
self.importScripts('/javascripts/world.js');
// We could do way more from this: http://stackoverflow.com/questions/10653809/making-webworkers-a-safe-environment
Object.defineProperty(self, "XMLHttpRequest", {
@ -69,16 +67,17 @@ Object.defineProperty(self, "XMLHttpRequest", {
});
self.transferableSupported = function transferableSupported() {
if (typeof self._transferableSupported !== 'undefined') return self._transferableSupported;
// Not in IE, even in IE 11
try {
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
return ab.byteLength == 0;
return self._transferableSupported = ab.byteLength == 0;
} catch(error) {
return false;
return self._transferableSupported = false;
}
return false;
}
return self._transferableSupported = false;
};
var World = self.require('lib/world/world');
var GoalManager = self.require('lib/world/GoalManager');
@ -182,8 +181,6 @@ self.retrieveValueFromCache = function (thangID, spellID, variableChain, frame)
return spellCache[variableChain.join()];
return undefined;
};
self.updateCache = function (thangID, spellID, variableChain, frame, value) {
var key, keys, currentObject;
keys = [frame,thangID, spellID, variableChain.join()];
@ -198,6 +195,7 @@ self.updateCache = function (thangID, spellID, variableChain, frame, value) {
}
currentObject[keys[keys.length - 1]] = value;
};
self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
var cacheValue;
if (args.frame === self.currentDebugWorldFrame && (cacheValue = self.retrieveValueFromCache(args.currentThangID, args.currentSpellID, args.variableChain, args.frame)))
@ -243,7 +241,7 @@ self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
if (value.CN === "Thang")
{
var thang = self.debugWorld.thangMap[value.id];
value = thang || "<Thang " + value.id + " (non-existent)>"
value = thang || "<Thang " + value.id + " (non-existent)>";
}
else
{
@ -261,7 +259,7 @@ self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
self.enableFlowOnThangSpell(args.currentThangID, args.currentSpellID, args.userCodeMap);
self.setupDebugWorldToRunUntilFrame(args);
self.debugWorld.loadFrames(
retrieveProperty.bind({},args.currentThangID, args.currentSpellID, args.variableChain),
retrieveProperty.bind({}, args.currentThangID, args.currentSpellID, args.variableChain),
self.onDebugWorldError,
self.onDebugWorldProgress,
false,
@ -284,8 +282,8 @@ self.enableFlowOnThangSpell = function (thangID, spellID, userCodeMap) {
}
}
catch (e) {
console.log("there was an error enabling flow on thang spell:" + e)
catch (error) {
console.log("Debug error enabling flow on", thangID, spellID + ":", error.toString() + "\n" + error.stack || error.stackTrace);
}
};
@ -301,9 +299,10 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
if (args.frame != self.currentDebugWorldFrame) self.invalidateCache();
if (!self.debugWorld || userCodeMapHasChanged || args.frame < self.currentDebugWorldFrame) {
try {
self.debugWorld = new World(args.worldName, args.userCodeMap);
self.debugWorld = new World(args.userCodeMap);
if (args.level)
self.debugWorld.loadFromLevel(args.level, true);
self.debugWorld.debugging = true;
self.debugGoalManager = new GoalManager(self.debugWorld);
self.debugGoalManager.setGoals(args.goals);
self.debugGoalManager.setCode(args.userCodeMap);
@ -322,7 +321,7 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
self.onDebugWorldLoaded = function onDebugWorldLoaded() {
console.log("World loaded!");
console.log("Debug world loaded!");
};
self.onDebugWorldError = function onDebugWorldError(error) {
@ -338,26 +337,25 @@ self.onDebugWorldProgress = function onDebugWorldProgress(progress) {
};
self.debugAbort = function () {
if(self.debugWorld && self.debugWorld.name) {
console.log("About to abort:", self.debugWorld.name, typeof self.debugWorld.abort);
if(typeof self.debugWorld !== "undefined")
self.debugWorld.abort();
if(self.debugWorld) {
self.debugWorld.abort();
self.debugWorld = null;
}
self.postMessage({type: 'debugAbort'});
self.postMessage({type: 'debug-abort'});
};
self.runWorld = function runWorld(args) {
self.postedErrors = {};
self.t0 = new Date();
self.firstWorld = args.firstWorld;
self.postedErrors = false;
self.logsLogged = 0;
try {
self.world = new World(args.worldName, args.userCodeMap);
self.world = new World(args.userCodeMap);
if(args.level)
self.world.loadFromLevel(args.level, true);
self.world.preloading = args.preload;
self.world.headless = args.headless;
self.goalManager = new GoalManager(self.world);
self.goalManager.setGoals(args.goals);
self.goalManager.setCode(args.userCodeMap);
@ -369,13 +367,19 @@ self.runWorld = function runWorld(args) {
return;
}
Math.random = self.world.rand.randf; // so user code is predictable
self.postMessage({type: 'start-load-frames'});
self.world.loadFrames(self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress);
};
self.onWorldLoaded = function onWorldLoaded() {
self.goalManager.worldGenerationEnded();
var goalStates = self.goalManager.getGoalStates();
self.postMessage({type: 'end-load-frames', goalStates: goalStates});
var t1 = new Date();
var diff = t1 - self.t0;
if (self.world.headless)
return console.log('Headless simulation completed in ' + diff + 'ms.');
var transferableSupported = self.transferableSupported();
try {
var serialized = self.world.serialize();
@ -386,10 +390,11 @@ self.onWorldLoaded = function onWorldLoaded() {
var t2 = new Date();
//console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
try {
var message = {type: 'new-world', serialized: serialized.serializedWorld, goalStates: goalStates};
if(transferableSupported)
self.postMessage({type: 'new-world', serialized: serialized.serializedWorld, goalStates: self.goalManager.getGoalStates()}, serialized.transferableObjects);
self.postMessage(message, serialized.transferableObjects);
else
self.postMessage({type: 'new-world', serialized: serialized.serializedWorld, goalStates: self.goalManager.getGoalStates()});
self.postMessage(message);
}
catch(error) {
console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
@ -411,7 +416,7 @@ self.onWorldError = function onWorldError(error) {
console.log("Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace);
}
/* We don't actually have the recoverable property any more; hmm
if(!self.firstWorld && !error.recoverable) {
if(!error.recoverable) {
self.abort();
return false;
}
@ -424,18 +429,16 @@ self.onWorldLoadProgress = function onWorldLoadProgress(progress) {
};
self.abort = function abort() {
if(self.world && self.world.name) {
console.log("About to abort:", self.world.name, typeof self.world.abort);
if(typeof self.world !== "undefined")
self.world.abort();
if(self.world) {
self.world.abort();
self.world = null;
}
self.postMessage({type: 'abort'});
};
self.reportIn = function reportIn() {
self.postMessage({type: 'reportIn'});
}
self.postMessage({type: 'report-in'});
};
self.addEventListener('message', function(event) {
self[event.data.func](event.data.args);

201
app/lib/Angel.coffee Normal file
View file

@ -0,0 +1,201 @@
# 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'
CocoClass = require 'lib/CocoClass'
module.exports = class Angel extends CocoClass
@nicks: ['Archer', 'Lana', 'Cyril', 'Pam', 'Cheryl', 'Woodhouse', 'Ray', 'Krieger']
infiniteLoopIntervalDuration: 2500 # check this often
infiniteLoopTimeoutDuration: 7500 # wait this long between checks
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
constructor: (@shared) ->
super()
@say 'Got my wings.'
if window.navigator and (window.navigator.userAgent.search("MSIE") isnt -1 or window.navigator.appName is 'Microsoft Internet Explorer')
@infiniteLoopIntervalDuration *= 10 # since it's so slow to serialize without transferable objects, we can't trust it
@infiniteLoopTimeoutDuration *= 10
@abortTimeoutDuration *= 10
@initialized = false
@running = false
@hireWorker()
@shared.angels.push @
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...
log: (args...) -> console.log "|#{@shared.godNick}'s #{@nick}|", args...
testWorker: =>
@worker.postMessage func: 'reportIn' unless @destroyed
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
@condemnTimeout = _.delay @infinitelyLooped, @infiniteLoopTimeoutDuration
when 'report-in'
clearTimeout @condemnTimeout
when 'end-load-frames'
clearTimeout @condemnTimeout
@beholdGoalStates event.data.goalStates # Work ends here if we're headless.
# We pay attention to certain progress indicators as the world loads.
when 'world-load-progress-changed'
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
when 'console-log'
@log event.data.args...
when 'user-code-problem'
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
# Either the world finished simulating successfully, or we abort the worker.
when 'new-world'
@beholdWorld event.data.serialized, event.data.goalStates
when 'abort'
@say "Aborted.", event.data
clearTimeout @abortTimeout
@aborting = false
@running = false
_.remove @shared.busyAngels, @
@doWork()
else
@log "Received unsupported message:", event.data
beholdGoalStates: (goalStates) ->
return if @aborting
Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates
@finishWork() if @shared.headless
beholdWorld: (serialized, goalStates) ->
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
World.deserialize serialized, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates)
window.BOX2D_ENABLED = true
@shared.lastSerializedWorldFrames = serialized.frames
finishBeholdingWorld: (goalStates) -> (world) =>
return if @aborting
world.findFirstChangedFrame @shared.world
@shared.world = world
errorCount = (t for t in @shared.world.thangs when t.errorsOut).length
Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, errorCount: errorCount, goalStates: goalStates
for scriptNote in @shared.world.scriptNotes
Backbone.Mediator.publish scriptNote.channel, scriptNote.event
@shared.goalManager?.world = world
@finishWork()
finishWork: ->
@shared.firstWorld = false
@running = false
_.remove @shared.busyAngels, @
@doWork()
infinitelyLooped: =>
return if @aborting
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: @shared.firstWorld
@fireWorker()
doWork: ->
return if @aborting
return @say "Not initialized for work yet." unless @initialized
if @shared.workQueue.length
work = @shared.workQueue.shift()
return _.defer @simulateSync, work if work.synchronous
@say "Running world..."
@running = true
@shared.busyAngels.push @
@worker.postMessage func: 'runWorld', args: work
clearTimeout @purgatoryTimer
@purgatoryTimer = setInterval @testWorker, @infiniteLoopIntervalDuration
else
@say "No work to do."
@hireWorker()
abort: ->
return unless @worker and @running
@say "Aborting..."
@running = false
_.remove @shared.busyAngels, @
@abortTimeout = _.delay @fireWorker, @abortTimeoutDuration
@aborting = true
@worker.postMessage func: 'abort'
fireWorker: (rehire=true) =>
@aborting = false
@running = false
_.remove @shared.busyAngels, @
@worker?.removeEventListener 'message', @onWorkerMessage
@worker?.terminate()
@worker = null
clearTimeout @condemnTimeout
clearInterval @purgatoryTimer
@say "Fired worker."
@initialized = false
@work = null
@hireWorker() if rehire
hireWorker: ->
return if @worker
@say "Hiring worker."
@worker = new Worker @shared.workerCode
@worker.addEventListener 'message', @onWorkerMessage
@worker.creationTime = new Date()
#### 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()
work.testWorld = testWorld = new World work.userCodeMap
testWorld.loadFromLevel work.level
if @shared.goalManager
testGM = new @shared.goalManager.constructor @testWorld
testGM.setGoals work.goals
testGM.setCode work.userCodeMap
testGM.worldGenerationWillBegin()
testWorld.setGoalManager testGM
@doSimulateWorld work
console?.profileEnd?() if imitateIE9?
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."
# If performance was really a priority in IE9, we would rework things to be able to skip this step.
goalStates = testGM?.getGoalStates()
serialized = testWorld.serialize().serializedWorld
window.BOX2D_ENABLED = false
World.deserialize serialized, @angelsShare.worldClassMap, @hsared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates)
window.BOX2D_ENABLED = true
@shared.lastSerializedWorldFrames = serialized.frames
doSimulateWorld: (work) ->
work.t1 = now()
Math.random = work.testWorld.rand.randf # so user code is predictable
i = 0
while i < work.testWorld.totalFrames
frame = work.testWorld.getFrame i++
work.testWorld.ended = true
system.finish work.testWorld.thangs for system in work.testWorld.systems
work.t2 = now()

View file

@ -1,241 +0,0 @@
#Sane rewrite of God (a thread pool)
{now} = require 'lib/world/world_utils'
World = require 'lib/world/world'
###
Every Angel has exactly one WebWorker attached to it.
It will call methods inside the webwrker and kill it if it times out.
###
class Angel
@cyanide: 0xDEADBEEF
infiniteLoopIntervalDuration: 7500 # check this often (must be more than the others added)
infiniteLoopTimeoutDuration: 10000 # wait this long when we check
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
constructor: (@id, @shared) ->
console.log @id + ": Creating Angel"
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
@initialized = false
@running = false
@hireWorker()
@shared.angels.push @
testWorker: =>
if @initialized
@worker.postMessage {func: 'reportIn'}
# Are there any errors when webworker isn't loaded properly?
onWorkerMessage: (event) =>
#console.log JSON.stringify event
if @aborting and not
event.data.type is 'abort'
console.log id + " is currently aborting old work."
return
switch event.data.type
when 'start-load-frames'
clearTimeout(@condemnTimeout)
@condemnTimeout = _.delay @infinitelyLooped, @infiniteLoopTimeoutDuration
when 'end-load-frames'
console.log @id + ': No condemn this time.'
clearTimeout(@condemnTimeout)
when 'worker-initialized'
unless @initialized
console.log @id + ": Worker initialized after", ((new Date()) - @worker.creationTime), "ms"
@initialized = true
@doWork()
when 'new-world'
@beholdWorld event.data.serialized, event.data.goalStates
when 'world-load-progress-changed'
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
when 'console-log'
console.log "|" + @id + "|", event.data.args...
when 'user-code-problem'
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
when 'abort'
console.log @id, "aborted."
clearTimeout @abortTimeout
@aborting = false
@running = false
@shared.busyAngels.pop @
@doWork()
when 'reportIn'
clearTimeout @condemnTimeout
else
console.log @id + " received unsupported message:", event.data
beholdWorld: (serialized, goalStates) ->
return if @aborting
unless serialized
# We're only interested in goalStates. (Simulator)
@latestGoalStates = goalStates
Backbone.Mediator.publish('god:goals-calculated', goalStates: goalStates)
@running = false
@shared.busyAngels.pop @
# console.warn "Goal states: " + JSON.stringify(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.deserialize serialized, @shared.worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld(goalStates)
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
finishBeholdingWorld: (goalStates) => (world) =>
return if @aborting
world.findFirstChangedFrame @shared.world
@shared.world = world
errorCount = (t for t in @shared.world.thangs when t.errorsOut).length
Backbone.Mediator.publish('god:new-world-created', world: world, firstWorld: @shared.firstWorld, errorCount: errorCount, goalStates: goalStates)
for scriptNote in @shared.world.scriptNotes
Backbone.Mediator.publish scriptNote.channel, scriptNote.event
@shared.goalManager?.world = world
@running = false
@shared.busyAngels.pop @
@shared.firstWorld = false
@doWork()
infinitelyLooped: =>
unless @aborting
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: @shared.firstWorld
@fireWorker()
workIfIdle: ->
@doWork() unless @running
doWork: =>
#console.log "work."
return if @aborted
console.log @id + " ready and looking for work. WorkQueue length is " + @shared.workQueue.length
if @initialized and @shared.workQueue.length
work = @shared.workQueue.pop()
if work is Angel.cyanide # Kill all other Angels, too
console.log @id + ": 'work is poison'"
@shared.workQueue.push Angel.cyanide
@free()
else
console.log @id + ": Sending the worker to work."
@running = true
@shared.busyAngels.push @
console.log "Running world..."
#console.error "worker.postMessage: " + @worker.postMessage + ", work: " + work
@worker.postMessage func: 'runWorld', args: work
console.log @id + ": Setting interval."
clearTimeout @purgatoryTimer
@purgatoryTimer = setInterval @testWorker, @infiniteLoopIntervalDuration
else
console.log "No work for " + @id
@hireWorker()
abort: =>
if @worker and @running
console.log "Aborting " + @id
@running = false
@shared.busyAngels.pop @
@abortTimeout = _.delay @terminate, @fireWorker, @abortTimeoutDuration
@worker.postMessage func: 'abort'
@aborting = true
@work = null
fireWorker: (rehire=true) =>
@aborting = false
@running = false
@shared.busyAngels.pop @
@worker?.removeEventListener 'message', @onWorkerMessage
@worker?.terminate()
@worker = null
clearTimeout @condemnTimeout
clearInterval @purgatoryTimer
console.log "Fired worker."
@initialized = false
@work = null
@hireWorker() if rehire
hireWorker: ->
unless @worker
console.log @id + ": Hiring worker."
@worker = new Worker @shared.workerCode
@worker.addEventListener 'message', @onWorkerMessage
@worker.creationTime = new Date()
#@worker.postMessage func: 'initialized' else
kill: ->
@fireWorker false
@shared.angels.pop @
clearTimeout @condemnTimeout
clearTimeout @purgatoryTimer
@purgatoryTimer = null
@condemnTimeout = null
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', '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]
# Charlie's Angels are all given access to this.
angelsShare: {
workerCode: '/javascripts/workers/worker_world.js' # Either path or function
workQueue: []
firstWorld: true
world: undefined
goalManager: undefined
worldClassMap: undefined
angels: []
busyAngels: [] # Busy angels will automatically register here.
}
constructor: (options) ->
options ?= {}
@angelsShare.workerCode = options.workerCode if options.workerCode
# ~20MB per idle worker + angel overhead - in this implementation, every Angel maps to 1 worker
angelCount = options.maxAngels ? options.maxWorkerPoolSize ? 2 # How many concurrent Angels/web workers to use at a time
_.delay (=>new Angel @nextID(), @angelsShare), 250 * i for i in [0...angelCount] # Don't generate all Angels at once.
Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
onTomeCast: (e) ->
@createWorld e.spells
setGoalManager: (goalManager) =>
@angelsShare.goalManager = goalManager
setWorldClassMap: (worldClassMap) =>
@angelsShare.worldClassMap = worldClassMap
getUserCodeMap: (spells) ->
userCodeMap = {}
for spellKey, spell of spells
for thangID, spellThang of spell.thangs
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
#console.log userCodeMap
userCodeMap
createWorld: (spells) =>
angel.abort() for angel in @angelsShare.busyAngels # We really only ever want one world calculated per God
#console.log "Level: " + @level
@angelsShare.workQueue.push
worldName: @level.name
userCodeMap: @getUserCodeMap(spells)
level: @level
goals: @angelsShare.goalManager?.getGoals()
angel.workIfIdle() for angel in @angelsShare.angels
destroy: =>
console.log "Destroying Buddha"
@createWorld = -> console.log "CreateWorld already gone."
@angelsShare.workQueue.push Angel.cyanide
angel.kill for angel in @angelsShare.busyAngels
Backbone.Mediator.unsubscribe('tome:cast-spells', @onTomeCast, @)
@angelsShare.goalManager?.destroy()
@angelsShare.goalManager = null
@angelsShare = null

View file

@ -1,16 +1,32 @@
# Template for classes with common functions, like hooking into the Mediator.
utils = require './utils'
classCount = 0
makeScopeName = -> "class-scope-#{classCount++}"
makeScopeName = -> "class-scope-#{classCount++}"
doNothing = ->
module.exports = class CocoClass
@nicks: []
@nicksUsed: {}
@remainingNicks: []
@nextNick: ->
return "CocoClass " + classCount unless @nicks.length
@remainingNicks = if @remainingNicks.length then @remainingNicks else @nicks.slice()
baseNick = @remainingNicks.splice(Math.floor(Math.random() * @remainingNicks.length), 1)[0]
i = 0
while true
nick = if i then "#{baseNick} #{i}" else baseNick
break unless @nicksUsed[nick]
i++
@nicksUsed[nick] = true
nick
subscriptions: {}
shortcuts: {}
# setup/teardown
constructor: ->
@nick = @constructor.nextNick()
@subscriptions = utils.combineAncestralObject(@, 'subscriptions')
@shortcuts = utils.combineAncestralObject(@, 'shortcuts')
@listenToSubscriptions()
@ -24,6 +40,7 @@ module.exports = class CocoClass
@off()
@unsubscribeAll()
@stopListeningToShortcuts()
@constructor.nicksUsed[@nick] = false
@[key] = undefined for key of @
@destroyed = true
@off = doNothing

View file

@ -1,149 +1,88 @@
# 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.)
{now} = require 'lib/world/world_utils'
World = require 'lib/world/world'
CocoClass = require 'lib/CocoClass'
Angel = require 'lib/Angel'
## Uncomment to imitate IE9 (and in world_utils.coffee)
#window.Worker = null
#window.Float32Array = null
# Also uncomment vendor_with_box2d.js in index.html if you want Collision to run and things to move.
module.exports = class God extends CocoClass
@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']
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', '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]
subscriptions:
'tome:cast-spells': 'onTomeCast'
'tome:spell-debug-value-request': 'retrieveValueFromFrame'
'god:new-world-created': 'onNewWorldCreated'
worldWaiting: false # whether we're waiting for a worker to free up and run the world
constructor: (options) ->
@id = God.nextID()
options ?= {}
@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
@workerCode = options.workerCode if options.workerCode?
@angels = []
@firstWorld = true
Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
@retriveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000
Backbone.Mediator.subscribe 'tome:spell-debug-value-request', @retrieveValueFromFrame, @
@fillWorkerPool = _.throttle @fillWorkerPool, 3000, leading: false
@fillWorkerPool()
#TODO: have this as a constructor option
@debugWorker = @createDebugWorker()
@currentUserCodeMap = {}
@retrieveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000
super()
workerCode: '/javascripts/workers/worker_world.js' #Can be a string or a function.
# 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]
destroy: ->
angel.destroy() for angel in @angelsShare.angels
@angelsShare.goalManager?.destroy()
@debugWorker?.terminate()
@debugWorker?.removeEventListener 'message', @onDebugWorkerMessage
super()
setLevel: (@level) ->
setGoalManager: (goalManager) -> @angelsShare.goalManager = goalManager
setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap
onTomeCast: (e) ->
return if @dead
@createWorld e.spells
@createWorld e.spells, e.preload
fillWorkerPool: =>
return unless Worker and not @dead
@workerPool ?= []
if @workerPool.length < @maxWorkerPoolSize
@workerPool.push @createWorker()
if @workerPool.length < @maxWorkerPoolSize
@fillWorkerPool()
getWorker: ->
@fillWorkerPool()
worker = @workerPool?.shift()
return worker if worker
@createWorker()
createWorker: ->
worker = new Worker @workerCode
worker.creationTime = new Date()
worker.addEventListener 'message', @onWorkerMessage(worker)
worker
createDebugWorker: ->
worker = new Worker '/javascripts/workers/worker_world.js'
worker.creationTime = new Date()
worker.addEventListener 'message', @onDebugWorkerMessage
worker
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
onDebugWorkerMessage: (event) =>
worker = event.target
switch event.data.type
when "worker-initialized"
worker.initialized = true
when 'new-debug-world'
console.log "New Debug world!"
when 'console-log'
console.log "|" + @id + "'s debugger|", event.data.args...
when 'debug-value-return'
Backbone.Mediator.publish 'god:debug-value-return', event.data.serialized
getAngel: ->
freeAngel = null
for angel in @angels
if angel.busy
angel.abort()
else
freeAngel ?= angel
return freeAngel.enslave() if freeAngel
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', problem.range?[0]
Backbone.Mediator.publish 'god:user-code-problem', problem: problem
createWorld: (@spells) ->
#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
#console.log "going to run world with code", @getUserCodeMap()
angel.worker.postMessage {func: 'runWorld', args: {
worldName: @level.name
createWorld: (spells, preload=false) ->
console.log "#{@nick}: Let there be light upon #{@level.name}!"
angel.abort() for angel in @angelsShare.busyAngels # We really only ever want one world calculated per God.
@angelsShare.workQueue = []
@angelsShare.workQueue.push
userCodeMap: @getUserCodeMap(spells)
level: @level
firstWorld: @firstWorld
goals: @goalManager?.getGoals()
}}
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
retrieveValueFromFrame: (args) ->
if not args.thangID or not args.spellID or not args.variableChain then return
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
return console.error "Tried to retrieve debug value with no currentUserCodeMap" unless @currentUserCodeMap
@debugWorker ?= @createDebugWorker()
args.frame ?= @world.age / @world.dt
@debugWorker.postMessage
func: 'retrieveValueFromFrame'
args:
worldName: @level.name
userCodeMap: @currentUserCodeMap
level: @level
goals: @goalManager?.getGoals()
@ -152,54 +91,27 @@ module.exports = class God
currentSpellID: args.spellID
variableChain: args.variableChain
#Coffeescript needs getters and setters.
setGoalManager: (@goalManager) =>
createDebugWorker: ->
worker = new Worker '/javascripts/workers/worker_world.js'
worker.addEventListener 'message', @onDebugWorkerMessage
worker
setWorldClassMap: (@worldClassMap) =>
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
beholdWorld: (angel, serialized, goalStates) ->
unless serialized
# We're only interested in goalStates.
@latestGoalStates = goalStates
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."
worldCreation = angel.started
angel.free()
return if @latestWorldCreation? and worldCreation < @latestWorldCreation
@latestWorldCreation = worldCreation
@latestGoalStates = goalStates
console.warn "Goal states: " + JSON.stringify(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.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
finishBeholdingWorld: (newWorld) =>
newWorld.findFirstChangedFrame @world
@world = newWorld
@currentUserCodeMap = @filterUserCodeMapWhenFromWorld @world.userCodeMap
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, team: me.team)
for scriptNote in @world.scriptNotes
Backbone.Mediator.publish scriptNote.channel, scriptNote.event
@goalManager?.world = newWorld
@firstWorld = false
@testWorld = null
unless _.find @angels, 'busy'
@spells = null # Don't hold onto old spells; memory leaks
onNewWorldCreated: (e) ->
@currentUserCodeMap = @filterUserCodeMapWhenFromWorld e.world.userCodeMap
filterUserCodeMapWhenFromWorld: (worldUserCodeMap) ->
newUserCodeMap = {}
for thangName, thang of worldUserCodeMap
newUserCodeMap[thangName] = {}
for spellName,aether of thang
shallowFilteredObject = _.pick aether, ['raw','pure','originalOptions']
for spellName, aether of thang
shallowFilteredObject = _.pick aether, ['raw', 'pure', 'originalOptions']
newUserCodeMap[thangName][spellName] = _.cloneDeep shallowFilteredObject
newUserCodeMap[thangName][spellName] = _.defaults newUserCodeMap[thangName][spellName],
flow: {}
@ -211,165 +123,9 @@ module.exports = class God
style: {}
newUserCodeMap
getUserCodeMap: ->
userCodeMap = {}
for spellKey, spell of @spells
for thangID, spellThang of spell.thangs
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
userCodeMap
destroy: ->
worker.removeEventListener 'message', @onWorkerMessage for worker in @workerPool ? []
angel.destroy() for angel in @angels
@dead = true
Backbone.Mediator.unsubscribe('tome:cast-spells', @onTomeCast, @)
@goalManager?.destroy()
@debugWorker?.terminate()
@debugWorker?.removeEventListener 'message', @onDebugWorkerMessage
@debugWorker ?= null
@currentUserCodeMap = null
@goalManager = null
@fillWorkerPool = null
@simulateWorld = null
@onWorkerMessage = 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
World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @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: 7500 # check this often (must be more than the others added)
infiniteLoopTimeoutDuration: 2500 # 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 = @god.getWorker()
@listen()
enslave: ->
@busy = true
@started = new Date()
@purgatoryTimer = setInterval @testWorker, @infiniteLoopIntervalDuration
@spawnWorker() unless @worker
@
free: ->
@busy = false
@started = null
clearInterval @purgatoryTimer
@purgatoryTimer = null
if @worker
worker = @worker
onWorkerMessage = @onWorkerMessage
_.delay ->
worker.terminate()
worker.removeEventListener 'message', onWorkerMessage
, 3000
@worker = null
@
abort: ->
return unless @worker
@abortTimeout = _.delay @terminate, @abortTimeoutDuration
@worker.postMessage {func: 'abort'}
terminate: =>
@worker?.terminate()
@worker?.removeEventListener 'message', @onWorkerMessage
@worker = null
return if @dead
@free()
@god.angelAborted @
destroy: ->
@dead = true
@finishBeholdingWorld = null
@abort()
@terminate = null
@testWorker = null
@condemnWorker = null
@onWorkerMessage = null
testWorker: =>
unless @worker.initialized
console.warn "Worker", @id, " hadn't even loaded the scripts yet after", @infiniteLoopIntervalDuration, "ms."
return
@worker.postMessage {func: 'reportIn'}
@condemnTimeout = _.delay @condemnWorker, @infiniteLoopTimeoutDuration
condemnWorker: =>
@god.angelInfinitelyLooped @
@abort()
listen: ->
@worker.addEventListener 'message', @onWorkerMessage
onWorkerMessage: (event) =>
switch event.data.type
when 'worker-initialized'
console.log "Worker", @id, "initialized after", ((new Date()) - @worker.creationTime), "ms (we had been waiting for it)"
@worker.initialized = true
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
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.

View file

@ -195,7 +195,7 @@ module.exports = class LevelLoader extends CocoClass
initWorld: ->
return if @initialized
@initialized = true
@world = new World @level.get('name')
@world = new World()
serializedLevel = @level.serialize(@supermodel)
@world.loadFromLevel serializedLevel, false

View file

@ -2,7 +2,7 @@ SuperModel = require 'models/SuperModel'
CocoClass = require 'lib/CocoClass'
LevelLoader = require 'lib/LevelLoader'
GoalManager = require 'lib/world/GoalManager'
God = require 'lib/Buddha'
God = require 'lib/God'
Aether.addGlobal 'Vector', require 'lib/world/vector'
Aether.addGlobal '_', _
@ -16,11 +16,12 @@ module.exports = class Simulator extends CocoClass
@retryDelayInSeconds = 10
@taskURL = '/queue/scoring'
@simulatedByYou = 0
@god = new God maxWorkerPoolSize: 1, maxAngels: 1, workerCode: @options.workerCode # Start loading worker.
@god = new God maxAngels: 1, workerCode: @options.workerCode, headless: true # Start loading worker.
destroy: ->
@off()
@cleanupSimulation()
@god?.destroy()
super()
fetchAndSimulateTask: =>
@ -40,6 +41,7 @@ module.exports = class Simulator extends CocoClass
$.ajax
url: @taskURL
type: "GET"
parse: true
error: @handleFetchTaskError
success: @setupSimulationAndLoadLevel
@ -94,27 +96,20 @@ module.exports = class Simulator extends CocoClass
@simulateAnotherTaskAfterDelay()
assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->
console.log "Assigning world and level"
@world = @levelLoader.world
@level = @levelLoader.level
@levelLoader.destroy()
@levelLoader = null
setupGod: ->
@god.level = @level.serialize @supermodel
@god.setLevel @level.serialize @supermodel
@god.setWorldClassMap @world.classMap
@setupGoalManager()
@setupGodSpells()
setupGoalManager: ->
@god.setGoalManager new GoalManager(@world, @level.get 'goals')
commenceSimulationAndSetupCallback: ->
@god.createWorld @generateSpellsObject()
Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @
Backbone.Mediator.subscribeOnce 'god:new-world-created', @processResults, @
Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processResults, @
#Search for leaks, headless-client only.
if @options.headlessClient and @options.leakTest and not @memwatch?
@ -140,7 +135,6 @@ module.exports = class Simulator extends CocoClass
process.exit()
@hd = new @memwatch.HeapDiff()
onInfiniteLoop: ->
console.warn "Skipping infinitely looping game."
@trigger 'statusUpdate', "Infinite loop detected; grabbing a new game in #{@retryDelayInSeconds} seconds."
@ -150,9 +144,9 @@ module.exports = class Simulator extends CocoClass
taskResults = @formTaskResultsObject simulationResults
@sendResultsBackToServer taskResults
sendResultsBackToServer: (results) =>
sendResultsBackToServer: (results) ->
@trigger 'statusUpdate', 'Simulation completed, sending results back to server!'
console.log "Sending result back to server!"
console.log "Sending result back to server!", results
if @options.headlessClient and @options.testing
return @fetchAndSimulateTask()
@ -161,6 +155,7 @@ module.exports = class Simulator extends CocoClass
url: "/queue/scoring"
data: results
type: "PUT"
parse: true
success: @handleTaskResultsTransferSuccess
error: @handleTaskResultsTransferError
complete: @cleanupAndSimulateAnotherTask
@ -183,8 +178,6 @@ module.exports = class Simulator extends CocoClass
@fetchAndSimulateTask()
cleanupSimulation: ->
@god?.destroy()
@god = null
@world = null
@level = null
@ -227,16 +220,13 @@ module.exports = class Simulator extends CocoClass
else
return 1
setupGodSpells: ->
@generateSpellsObject()
@god.spells = @spells
generateSpellsObject: ->
@currentUserCodeMap = @task.generateSpellKeyToSourceMap()
@spells = {}
for thang in @level.attributes.thangs
continue if @thangIsATemplate thang
@generateSpellKeyToSourceMapPropertiesFromThang thang
@spells
thangIsATemplate: (thang) ->
for component in thang.components
@ -266,7 +256,6 @@ module.exports = class Simulator extends CocoClass
spellKey = spellKeyComponents.join '/'
spellKey
createSpellAndAssignName: (spellKey, spellName) ->
@spells[spellKey] ?= {}
@spells[spellKey].name = spellName
@ -307,9 +296,9 @@ module.exports = class Simulator extends CocoClass
#console.log "creating aether with options", aetherOptions
return new Aether aetherOptions
class SimulationTask
constructor: (@rawData) ->
console.log 'Simulating sessions', (session for session in @getSessions())
@spellKeyToTeamMap = {}
getLevelName: ->

View file

@ -8,7 +8,6 @@ module.exports = class Thang
@remainingThangNames: {}
@nextID: (spriteName, world) ->
Thang.lastIDNums ?= {}
originals = thangNames[spriteName] or [spriteName]
remaining = Thang.remainingThangNames[spriteName]
remaining = Thang.remainingThangNames[spriteName] = originals.slice() unless remaining?.length

View file

@ -15,13 +15,15 @@ module.exports = class World
@className: "World"
age: 0
ended: false
preloading: false # Whether we are just preloading a world in case we soon cast it
debugging: false # Whether we are just rerunning to debug a world we've already cast
headless: false # Whether we are just simulating for goal states instead of all serialized results
apiProperties: ['age', 'dt']
constructor: (name, @userCodeMap, classMap) ->
constructor: (@userCodeMap, classMap) ->
# classMap is needed for deserializing Worlds, Thangs, and other classes
@classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang}
Thang.resetThangIDs()
@name ?= name ? "Unnamed World"
@userCodeMap ?= {}
@thangs = []
@thangMap = {}
@ -89,17 +91,18 @@ module.exports = class World
catch error
# Not an Aether.errors.UserCodeError; maybe we can't recover
@addError error
for error in (@unhandledRuntimeErrors ? [])
return unless errorCallback error # errorCallback tells us whether the error is recoverable
@unhandledRuntimeErrors = []
unless @preloading or @debugging
for error in (@unhandledRuntimeErrors ? [])
return unless errorCallback error # errorCallback tells us whether the error is recoverable
@unhandledRuntimeErrors = []
t2 = now()
if t2 - t1 > PROGRESS_UPDATE_INTERVAL
loadProgressCallback? i / @totalFrames
loadProgressCallback? i / @totalFrames unless @preloading
t1 = t2
if t2 - @t0 > 1000
console.log(' Loaded', i, 'of', @totalFrames, "(+" + (t2 - @t0).toFixed(0) + "ms)")
@t0 = t2
continueFn = =>
continueFn = =>
if loadUntilFrame
@loadFrames(loadedCallback,errorCallback,loadProgressCallback, skipDeferredLoading, loadUntilFrame)
else
@ -109,11 +112,12 @@ module.exports = class World
else
setTimeout(continueFn, 0)
return
unless loadUntilFrame
unless @debugging
@ended = true
system.finish @thangs for system in @systems
loadProgressCallback? 1
loadedCallback()
unless @preloading
loadProgressCallback? 1
loadedCallback()
abort: ->
@aborted = true
@ -262,7 +266,7 @@ module.exports = class World
# Code hotspot; optimize it
if @frames.length < @totalFrames then throw new Error("World Should Be Over Before Serialization")
[transferableObjects, nontransferableObjects] = [0, 0]
o = {name: @name, totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
for thangID, methods of @userCodeMap
@ -355,7 +359,7 @@ module.exports = class World
#console.log "Got special keys and values:", o.specialValuesToKeys, o.specialKeysToValues
perf = {}
perf.t0 = now()
w = new World o.name, o.userCodeMap, classMap
w = new World o.userCodeMap, classMap
[w.totalFrames, w.maxTotalFrames, w.frameRate, w.dt, w.scriptNotes, w.victory] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory]
w[prop] = val for prop, val of o.trackedProperties

View file

@ -214,11 +214,11 @@ module.exports = class SpellView extends View
@createDebugView() unless @debugView
@debugView.thang = @thang
@toolbarView?.toggleFlow false
@updateAether false, true
@updateAether false, false
@highlightCurrentLine()
cast: ->
Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang
cast: (preload=false) ->
Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload
notifySpellChanged: =>
Backbone.Mediator.publish 'tome:spell-changed', spell: @spell
@ -393,7 +393,6 @@ module.exports = class SpellView extends View
# But the error message display was delayed, so now trying:
# - Go after specified delay if a) and not b) or c)
guessWhetherFinished: (aether) ->
return if @autocastDelay > 60000
#@recompileValid = not aether.getAllProblems().length
valid = not aether.getAllProblems().length
cursorPosition = @ace.getCursorPosition()
@ -401,8 +400,11 @@ module.exports = class SpellView extends View
endOfLine = cursorPosition.column >= currentLine.length # just typed a semicolon or brace, for example
beginningOfLine = not currentLine.substr(0, cursorPosition.column).trim().length # uncommenting code, for example
#console.log "finished?", valid, endOfLine, beginningOfLine, cursorPosition, currentLine.length, aether, new Date() - 0, currentLine
if valid and endOfLine or beginningOfLine
@recompile()
if valid and (endOfLine or beginningOfLine)
if @autocastDelay > 60000
null #@cast true
else
@recompile()
#console.log "recompile now!"
#else if not valid
# # if this works, we can get rid of all @recompileValid logic
@ -422,7 +424,6 @@ module.exports = class SpellView extends View
@spellHasChanged = false
onUserCodeProblem: (e) ->
console.log "onUserCodeProblem", e, e.problem.userInfo.methodName is @spell.name, spellThang = _.find @spell.thangs, (spellThang, thangID) -> thangID is e.problem.userInfo.thangID
return @onInfiniteLoop e if e.problem.id is "runtime_InfiniteLoop"
return unless e.problem.userInfo.methodName is @spell.name
return unless spellThang = _.find @spell.thangs, (spellThang, thangID) -> thangID is e.problem.userInfo.thangID

View file

@ -138,10 +138,10 @@ module.exports = class TomeView extends View
onCastSpell: (e) ->
# A single spell is cast.
# Hmm; do we need to make sure other spells are all cast here?
@cast()
@cast e?.preload
cast: ->
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells
cast: (preload=false) ->
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload
onToggleSpellList: (e) ->
@spellList.rerenderEntries()

View file

@ -112,7 +112,7 @@ module.exports = class PlayLevelView extends View
load: ->
@loadStartTime = new Date()
@god = new God()
@god = new God debugWorker: true
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable("team")
getRenderData: ->
@ -168,7 +168,7 @@ module.exports = class PlayLevelView extends View
@grabLevelLoaderData()
team = @getQueryVariable("team") ? @world.teamForPlayer(0)
@loadOpponentTeam(team)
@god.level = @level.serialize @supermodel
@god.setLevel @level.serialize @supermodel
@god.setWorldClassMap @world.classMap
@setTeam team
@initSurface()

View file

@ -106,7 +106,7 @@ module.exports = class SpectateLevelView extends View
spectateMode: true
team: @getQueryVariable("team")
@listenToOnce(@levelLoader, 'loaded-all', @onLevelLoaderLoaded)
@god = new God maxWorkerPoolSize: 1, maxAngels: 1
@god = new God maxAngels: 1
getRenderData: ->
c = super()
@ -155,7 +155,7 @@ module.exports = class SpectateLevelView extends View
#at this point, all requisite data is loaded, and sessions are not denormalized
team = @world.teamForPlayer(0)
@loadOpponentTeam(team)
@god.level = @level.serialize @supermodel
@god.setLevel @level.serialize @supermodel
@god.setWorldClassMap @world.classMap
@setTeam team
@initSurface()

View file

@ -10,7 +10,7 @@ headlessClientPath = "./headless_client/"
options =
workerCode: require headlessClientPath + 'worker_world'
debug: false # Enable logging of ajax calls mainly
testing: true # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting.
testing: false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting.
testFile: require headlessClientPath + 'test.js'
leakTest: false # Install callback that tries to find leaks automatically
exitOnLeak: false # Exit if leak is found. Only useful if leaktest is set to true, obviously.
@ -61,12 +61,6 @@ GLOBAL.tv4 = require('tv4').tv4
GLOBAL.marked = setOptions: ->
GLOBAL.navigator =
# userAgent: "nodejs"
platform: "headless_client"
vendor: "codecombat"
opera: false
store = {}
GLOBAL.localStorage =
getItem: (key) => store[key]
@ -78,9 +72,6 @@ GLOBAL.localStorage =
# since it will replace that.
# (Why is there no easier way?)
hookedLoader = (request, parent, isMain) ->
if request == 'lib/God'
request = 'lib/Buddha'
if request in disable or ~request.indexOf('templates')
console.log 'Ignored ' + request if options.debug
return class fake

View file

@ -44,7 +44,7 @@ betterConsole = () ->
id: self.workerID
# so that we don't crash when debugging statements happen
self.console.error = self.console.info = self.console.log
self.console.error = self.console.warn = self.console.info = self.console.debug = self.console.log
GLOBAL.console = console = self.console
self.console
@ -54,8 +54,11 @@ work = () ->
console.log = ->
World = self.require('lib/world/world');
GoalManager = self.require('lib/world/GoalManager');
World = self.require('lib/world/world')
GoalManager = self.require('lib/world/GoalManager')
Aether.addGlobal('Vector', require('lib/world/vector'))
Aether.addGlobal('_', _)
self.cleanUp = ->
self.world = null
@ -72,8 +75,9 @@ work = () ->
self.logsLogged = 0
try
self.world = new World(args.worldName, args.userCodeMap)
self.world = new World(args.userCodeMap)
self.world.loadFromLevel args.level, true if args.level
self.world.headless = args.headless
self.goalManager = new GoalManager(self.world)
self.goalManager.setGoals args.goals
self.goalManager.setCode args.userCodeMap
@ -93,14 +97,18 @@ work = () ->
self.onWorldLoaded = onWorldLoaded = ->
self.postMessage type: "end-load-frames"
self.goalManager.worldGenerationEnded()
goalStates = self.goalManager.getGoalStates()
self.postMessage type: "end-load-frames", goalStates: goalStates
t1 = new Date()
diff = t1 - self.t0
if (self.world.headless)
return console.log("Headless simulation completed in #{diff}ms.");
transferableSupported = self.transferableSupported()
try
serialized = serializedWorld: undefined # self.world.serialize()
serialized = serializedWorld: self.world.serialize()
transferableSupported = false
catch error
console.log "World serialization error:", error.toString() + "\n" + error.stack or error.stackTrace
@ -108,17 +116,14 @@ work = () ->
# console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
try
message =
type: "new-world"
serialized: serialized.serializedWorld
goalStates: goalStates
if transferableSupported
self.postMessage
type: "new-world"
serialized: serialized.serializedWorld
goalStates: self.goalManager.getGoalStates()
, serialized.transferableObjects
self.postMessage message, serialized.transferableObjects
else
self.postMessage
type: "new-world"
serialized: serialized.serializedWorld
goalStates: self.goalManager.getGoalStates()
self.postMessage message
catch error
console.log "World delivery error:", error.toString() + "\n" + error.stack or error.stackTrace
@ -150,16 +155,16 @@ work = () ->
self.abort = abort = ->
#console.log "Abort called for worker."
if self.world and self.world.name
if self.world
#console.log "About to abort:", self.world.name, typeof self.world.abort
self.world.abort() if typeof self.world isnt "undefined"
self.world.abort()
self.world = null
self.postMessage type: "abort"
self.cleanUp()
self.reportIn = reportIn = ->
console.log "Reporting in."
self.postMessage type: "reportIn"
self.postMessage type: "report-in"
self.addEventListener "message", (event) ->
#console.log JSON.stringify event