mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
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:
parent
df88be1ab1
commit
1e18f0b42f
15 changed files with 411 additions and 686 deletions
|
@ -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
201
app/lib/Angel.coffee
Normal 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()
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue