Merged master into feature/thangload.

This commit is contained in:
Nick Winter 2014-05-12 09:57:31 -07:00
commit 1aa72541ff
39 changed files with 552 additions and 797 deletions

View file

@ -1,7 +1,7 @@
var window = self;
var Global = self;
importScripts("/javascripts/tome_aether.js");
importScripts("/javascripts/lodash.js", "/javascripts/aether.js");
console.log("Aether Tome worker has finished importing scripts.");
var aethers = {};

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.";
@ -14,8 +12,8 @@ if (!Function.prototype.bind) {
throw new TypeError("Function.prototype.bind (Shim) - target is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
@ -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', '/javascripts/lodash.js', '/javascripts/aether.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)))
@ -223,7 +221,7 @@ self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
var flowStates = self.debugWorld.userCodeMap[currentThangID][currentSpellID].flow.states;
//we have to go to the second last flowState as we run the world for one additional frame
//to collect the flow
value = _.last(flowStates[flowStates.length - 2].statements).variables[prop];
value = _.last(flowStates[flowStates.length - 1].statements).variables[prop];
}
catch (e)
{
@ -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
{
@ -260,11 +258,12 @@ self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
};
self.enableFlowOnThangSpell(args.currentThangID, args.currentSpellID, args.userCodeMap);
self.setupDebugWorldToRunUntilFrame(args);
self.debugWorld.loadFramesUntilFrame(
args.frame,
retrieveProperty.bind({},args.currentThangID, args.currentSpellID, args.variableChain),
self.debugWorld.loadFrames(
retrieveProperty.bind({}, args.currentThangID, args.currentSpellID, args.variableChain),
self.onDebugWorldError,
self.onDebugWorldProgress
self.onDebugWorldProgress,
false,
args.frame
);
};
@ -283,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);
}
};
@ -297,12 +296,13 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
var stringifiedUserCodeMap = JSON.stringify(args.userCodeMap);
var userCodeMapHasChanged = ! _.isEqual(self.currentUserCodeMapCopy, stringifiedUserCodeMap);
self.currentUserCodeMapCopy = stringifiedUserCodeMap;
if (!self.debugWorld || userCodeMapHasChanged || args.frame != self.currentDebugWorldFrame) {
self.invalidateCache();
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);
@ -314,32 +314,20 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
return;
}
Math.random = self.debugWorld.rand.randf; // so user code is predictable
self.debugWorld.totalFrames = args.frame; //hack to work around error checking
self.currentDebugWorldFrame = args.frame;
}
self.debugWorld.totalFrames = args.frame; //hack to work around error checking
self.currentDebugWorldFrame = args.frame;
};
self.runDebugWorldUntilFrame = function (args) {
self.setupDebugWorldToRunUntilFrame(args);
self.debugWorld.loadFramesUntilFrame(args.frame, self.onDebugWorldLoaded, self.onDebugWorldError, self.onDebugWorldProgress);
};
self.onDebugWorldLoaded = function onDebugWorldLoaded() {
console.log("World loaded!");
console.log("Debug world loaded!");
};
self.onDebugWorldError = function onDebugWorldError(error) {
if(error instanceof Aether.problems.UserCodeProblem) {
if(!self.debugPostedErrors[error.key]) {
var problem = error.serialize();
self.postMessage({type: 'user-code-problem', problem: problem});
self.debugPostedErrors[error.key] = problem;
}
}
else {
console.log("Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace);
if(!error.isUserCodeProblem) {
console.log("Debug Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace);
}
return true;
};
@ -349,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);
@ -380,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();
@ -397,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);
@ -422,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;
}
@ -435,18 +429,20 @@ 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.finalizePreload = function finalizePreload() {
self.world.finalizePreload(self.onWorldLoaded);
};
self.addEventListener('message', function(event) {
self[event.data.func](event.data.args);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -34,6 +34,7 @@
<script src="/lib/ace/ace.js"></script>
<!--[if IE 9]> <script src="/javascripts/vendor_with_box2d.js"></script> <![endif]-->
<!--[if !IE]><!--> <script src="/javascripts/vendor.js"></script> <!--<![endif]-->
<script src="/javascripts/aether.js"></script>
<script src="/javascripts/app.js"></script> <!-- it's all Backbone! -->
<script>

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

@ -0,0 +1,205 @@
# 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()
finalizePreload: ->
@worker.postMessage func: 'finalizePreload'
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
@work = null
_.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,148 +1,102 @@
# 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 ? 1 # ~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
@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.slice()
@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()
createWorld: (spells, preload=false) ->
console.log "#{@nick}: Let there be light upon #{@level.name}!"
userCodeMap = @getUserCodeMap spells
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()
# We only want one world being simulated, so we abort other angels, unless we had one preloading this very code.
hadPreloader = false
for angel in @angelsShare.busyAngels
isPreloading = angel.running and angel.work.preload and _.isEqual angel.work.userCodeMap, userCodeMap, (a, b) ->
return a.raw is b.raw if a?.raw? and b?.raw?
undefined # Let default equality test suffice.
if not hadPreloader and isPreloading
angel.finalizePreload()
hadPreloader = true
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
angel.abort()
return if hadPreloader
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', @level.name + '!"'
unless Worker? # profiling world simulation is easier on main thread, or we are IE9
setTimeout @simulateWorld, 1
return
angel = @getAngel()
if angel
@worldWaiting = false
else
@worldWaiting = true
return
angel.worker.postMessage {func: 'runWorld', args: {
worldName: @level.name
userCodeMap: @getUserCodeMap(spells)
@angelsShare.workQueue = []
@angelsShare.workQueue.push
userCodeMap: userCodeMap
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()
@ -151,54 +105,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: {}
@ -210,165 +137,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

@ -212,7 +212,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
console.log "World has been initialized from level loader."

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

@ -14,9 +14,9 @@ module.exports = class CastingScreen extends CocoClass
console.error @toString(), "needs a camera." unless @camera
console.error @toString(), "needs a layer." unless @layer
@build()
onCastingBegins: -> @show()
onCastingEnds: -> @hide()
onCastingBegins: (e) -> @show() unless e.preload
onCastingEnds: (e) -> @hide()
toString: -> "<CastingScreen>"

View file

@ -246,6 +246,7 @@ module.exports = class Mark extends CocoClass
size = @sprite.getAverageDimension()
size += 60 if @name is 'selection'
size += 60 if @name is 'repair'
size *= @sprite.scaleFactor
scale = size / {selection: 128, target: 128, repair: 320, highlight: 160}[@name]
if @sprite?.thang.spriteName.search(/(dungeon|indoor).wall/i) isnt -1
scale *= 2

View file

@ -33,7 +33,7 @@ module.exports = class RegionChooser extends CocoClass
@options.camera.dragDisabled = false
restrictRegion: ->
RATIO = 1.56876 # 1848 / 1178
RATIO = 1.56876 # 924 / 589
rect = @options.camera.normalizeBounds([@firstPoint, @secondPoint])
currentRatio = rect.width / rect.height
if currentRatio > RATIO

View file

@ -222,7 +222,7 @@ module.exports = class SpriteBoss extends CocoClass
@world = @options.world = e.world
@play()
onCastSpells: -> @stop()
onCastSpells: (e) -> @stop() unless e.preload
play: ->
sprite.play() for sprite in @spriteArray

View file

@ -65,6 +65,7 @@ module.exports = Surface = class Surface extends CocoClass
'god:new-world-created': 'onNewWorld'
'tome:cast-spells': 'onCastSpells'
'level-set-letterbox': 'onSetLetterbox'
'application:idle-changed': 'onIdleChanged'
shortcuts:
'ctrl+\\, ⌘+\\': 'onToggleDebug'
@ -304,16 +305,34 @@ module.exports = Surface = class Surface extends CocoClass
@spriteBoss.stop()
@playbackOverScreen.show()
@ended = true
@setPaused true
Backbone.Mediator.publish 'surface:playback-ended'
else if @currentFrame < @world.totalFrames and @ended
@spriteBoss.play()
@playbackOverScreen.hide()
@ended = false
@setPaused false
Backbone.Mediator.publish 'surface:playback-restarted'
@lastFrame = @currentFrame
onCastSpells: ->
onIdleChanged: (e) ->
@setPaused e.idle unless @ended
setPaused: (to) ->
# We want to be able to essentially stop rendering the surface if it doesn't need to animate anything.
# If pausing, though, we want to give it enough time to finish any tweens.
performToggle = =>
createjs.Ticker.setFPS if to then 1 else @options.frameRate
@surfacePauseInterval = null
clearTimeout @surfacePauseInterval if @surfacePauseInterval
if to
@surfacePauseInterval = _.delay performToggle, 2000
else
performToggle()
onCastSpells: (e) ->
return if e.preload
@casting = true
@wasPlayingWhenCastingBegan = @playing
Backbone.Mediator.publish 'level-set-playing', { playing: false }
@ -575,8 +594,6 @@ module.exports = Surface = class Surface extends CocoClass
@paths.parent.removeChild @paths
@paths = null
# Screenshot
screenshot: (scale=0.25, format='image/jpeg', quality=0.8, zoom=2) ->
# Quality doesn't work with image/png, just image/jpeg and image/webp
[w, h] = [@camera.canvasWidth, @camera.canvasHeight]
@ -586,6 +603,5 @@ module.exports = Surface = class Surface extends CocoClass
#console.log "Screenshot with scale", scale, "format", format, "quality", quality, "was", Math.floor(imageData.length / 1024), "kB"
screenshot = document.createElement("img")
screenshot.src = imageData
#$('body').append(screenshot)
@stage.uncache()
imageData

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 = {}
@ -71,69 +73,55 @@ module.exports = class World
(@runtimeErrors ?= []).push error
(@unhandledRuntimeErrors ?= []).push error
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading) ->
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) ->
return if @aborted
unless @thangs.length
console.log "Warning: loadFrames called on empty World (no thangs)."
t1 = now()
@t0 ?= t1
if loadUntilFrame
frameToLoadUntil = loadUntilFrame + 1
else
frameToLoadUntil = @totalFrames
i = @frames.length
while i < @totalFrames
while i < frameToLoadUntil
try
@getFrame(i)
++i # increment this after we have succeeded in getting the frame, otherwise we'll have to do that frame again
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 = => @loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
continueFn = =>
if loadUntilFrame
@loadFrames(loadedCallback,errorCallback,loadProgressCallback, skipDeferredLoading, loadUntilFrame)
else
@loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
if skipDeferredLoading
continueFn()
else
setTimeout(continueFn, 0)
return
@ended = true
system.finish @thangs for system in @systems
loadProgressCallback? 1
loadedCallback()
unless @debugging
@ended = true
system.finish @thangs for system in @systems
unless @preloading
loadProgressCallback? 1
loadedCallback()
loadFramesUntilFrame: (frameToLoadUntil, loadedCallback, errorCallback, loadProgressCallback) ->
return if @aborted
unless @thangs.length
console.log "Warning: loadFrames called on empty World"
t1 = now()
@t0 ?= t1
i = @frames.length
while i <= frameToLoadUntil #state is gathered at next frame
try
@getFrame(i)
++i # increment this after we have succeeded in getting the frame, otherwise we'll have to do that frame again
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 = []
t2 = now()
if t2 - t1 > PROGRESS_UPDATE_INTERVAL
loadProgressCallback? i / @totalFrames
t1 = t2
if t2 - @t0 > 1000
console.log(' Loaded', i, 'of', frameToLoadUntil, "(+" + (t2 - @t0).toFixed(0) + "ms)")
@t0 = t2
setTimeout((=> @loadFrames(loadedCallback, errorCallback, loadProgressCallback)), 0)
return
loadProgressCallback? 1
loadedCallback()
finalizePreload: (loadedCallback) ->
@preloading = false
loadedCallback() if @ended
abort: ->
@aborted = true
@ -258,7 +246,7 @@ module.exports = class World
@goalManager.setGoalState(goalID, status)
endWorld: (victory=false, delay=3, tentative=false) ->
@totalFrames = Math.min(@totalFrames, @frames.length + Math.floor(delay / @dt)) - 1 # end a few seconds later
@totalFrames = Math.min(@totalFrames, @frames.length + Math.floor(delay / @dt)) # end a few seconds later
@victory = victory # TODO: should just make this signify the winning superteam
@victoryIsTentative = tentative
status = if @victory then 'won' else 'lost'
@ -282,7 +270,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
@ -375,7 +363,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

@ -60,7 +60,7 @@ UserSchema = c.object {},
colorConfig: c.object {additionalProperties: c.colorConfig()}
aceConfig: c.object {},
language: {type: 'string', 'default': 'javascript', 'enum': ['javascript', 'coffeescript']}
language: {type: 'string', 'default': 'javascript', 'enum': ['javascript', 'coffeescript', 'clojure', 'lua']}
keyBindings: {type: 'string', 'default': 'default', 'enum': ['default', 'vim', 'emacs']}
invisibles: {type: 'boolean', 'default': false}
indentGuides: {type: 'boolean', 'default': false}

View file

@ -27,7 +27,7 @@ block modal-body-content
div.alert.alert-info
strong Enter
| to confirm
canvas(width=1848, height=1178)
canvas(width=924, height=589)
block modal-footer-content
a.btn.btn-primary#done-button Done

View file

@ -27,7 +27,7 @@ button.navbar-toggle.toggle.btn-primary#thangs-palette-toggle(type="button", dat
a(data-i18n="editor.delete") Delete
li#duplicate
a(data-i18n="editor.duplicate") Duplicate
canvas(width=1848, height=1178)#surface
canvas(width=924, height=589)#surface
#canvas-left-gradient.gradient
#canvas-top-gradient.gradient

View file

@ -8,7 +8,7 @@
#tome-view
#canvas-wrapper
canvas(width=1848, height=1178)#surface
canvas(width=924, height=589)#surface
#canvas-left-gradient.gradient
#canvas-top-gradient.gradient

View file

@ -10,6 +10,8 @@ block modal-body-content
select#tome-language(name="language")
option(value="javascript" selected=(language === "javascript")) JavaScript
option(value="coffeescript" selected=(language === "coffeescript")) CoffeeScript
option(value="clojure" selected=(language === "clojure")) Clojure (Experimental)
option(value="lua" selected=(language === "lua")) Lua (Soon)
span.help-block(data-i18n="play_level.editor_config_language_description") Define the programming language you want to code in.
.form-group.select-group
label.control-label(for="tome-key-bindings" data-i18n="play_level.editor_config_keybindings_label") Key Bindings

View file

@ -3,7 +3,7 @@
.level-content
#control-bar-view
#canvas-wrapper
canvas(width=1848, height=1178)#surface
canvas(width=924, height=589)#surface
#canvas-left-gradient.gradient
#canvas-top-gradient.gradient
#gold-view.secret.expanded

View file

@ -78,8 +78,8 @@ module.exports = class WorldSelectModal extends View
showZoomRegion: ->
d = @defaultFromZoom
canvasWidth = 1848 # Dimensions for canvas player. Need these somewhere.
canvasHeight = 1178
canvasWidth = 924 # Dimensions for canvas player. Need these somewhere.
canvasHeight = 589
dimensions = {x: canvasWidth/d.zoom, y: canvasHeight/d.zoom}
dimensions = @surface.camera.surfaceToWorld(dimensions)
width = dimensions.x

View file

@ -3,7 +3,7 @@ ThangType = require '/models/ThangType'
makeButton = -> $('<a class="btn btn-primary btn-xs treema-map-button"><span class="glyphicon glyphicon-screenshot"></span></a>')
shorten = (f) -> parseFloat(f.toFixed(1))
WIDTH = 1848
WIDTH = 924
module.exports.WorldPointNode = class WorldPointNode extends TreemaNode.nodeMap.point2d
constructor: (args...) ->

View file

@ -38,7 +38,7 @@ module.exports = class LadderView extends RootView
super(options)
@level = @supermodel.loadModel(new Level(_id:@levelID), 'level').model
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(levelID), 'your_sessions').model
@teams = []
onLoaded: ->

View file

@ -183,7 +183,8 @@ module.exports = class PlaybackView extends View
onEditEditorConfig: ->
@openModalView(new EditorConfigModal())
onCastSpells: ->
onCastSpells: (e) ->
return if e.preload
@casting = true
@$progressScrubber.slider('disable', true)

View file

@ -57,6 +57,7 @@ module.exports = class CastButtonView extends View
@updateCastButton()
onCastSpells: (e) ->
return if e.preload
@casting = true
Backbone.Mediator.publish 'play-sound', trigger: 'cast', volume: 0.5
@updateCastButton()

View file

@ -15,6 +15,7 @@ module.exports = class DebugView extends View
'god:new-world-created': 'onNewWorld'
'god:debug-value-return': 'handleDebugValue'
'tome:spell-shown': 'changeCurrentThangAndSpell'
'surface:frame-changed': 'onFrameChanged'
events: {}
@ -24,7 +25,6 @@ module.exports = class DebugView extends View
@thang = options.thang
@spell = options.spell
@variableStates = {}
@globals = {Math: Math, _: _, String: String, Number: Number, Array: Array, Object: Object} # ... add more as documented
for className, serializedClass of serializedClasses
@globals[className] = serializedClass
@ -93,13 +93,17 @@ module.exports = class DebugView extends View
onNewWorld: (e) ->
@thang = @options.thang = e.world.thangMap[@thang.id] if @thang
onFrameChanged: (data) ->
@currentFrame = data.frame
update: ->
if @variableChain
Backbone.Mediator.publish 'tome:spell-debug-value-request',
thangID: @thang.id
spellID: @spell.name
variableChain: @variableChain
frame: @currentFrame
@$el.find("code").text "Finding value..."
@$el.show().css(@pos)
else

View file

@ -18,6 +18,8 @@ module.exports = class SpellView extends View
editModes:
'javascript': 'ace/mode/javascript'
'coffeescript': 'ace/mode/coffee'
'clojure': 'ace/mode/clojure'
'lua': 'ace/mode/lua'
keyBindings:
'default': null
@ -51,7 +53,7 @@ module.exports = class SpellView extends View
@session = options.session
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
@spell = options.spell
@problems = {}
@problems = []
@writable = false unless me.team in @spell.permissions.readwrite # TODO: make this do anything
@highlightCurrentLine = _.throttle @highlightCurrentLine, 100
@ -214,11 +216,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
@ -289,30 +291,21 @@ module.exports = class SpellView extends View
callback() for callback in onAnyChange # Then these
@aceDoc.on 'change', @onCodeChangeMetaHandler
setRecompileNeeded: (needed=true) =>
if needed
@recompileNeeded = needed # and @recompileValid # todo, remove if not caring about validity
else
@recompileNeeded = false
setRecompileNeeded: (@recompileNeeded) =>
onCursorActivity: =>
@currentAutocastHandler?()
# Design for a simpler system?
# * Turn off ACE's JSHint worker
# * Keep Aether linting, debounced, on any significant change
# - Don't send runtime errors from in-progress worlds
# - All problems just vanish when you make any change to the code
# * You wouldn't accept any Aether updates/runtime information/errors unless its code was current when you got it
# * Store the last run Aether in each spellThang and use it whenever its code actually is current
# This suffers from the problem that any whitespace/comment changes will lose your info, but what else
# could you do other than somehow maintain a mapping from current to original code locations?
# I guess you could use dynamic markers for problem ranges and keep annotations/alerts in when insignificant
# * Store the last run Aether in each spellThang and use it whenever its code actually is current.
# Use dynamic markers for problem ranges and keep annotations/alerts in when insignificant
# changes happen, but always treat any change in the (trimmed) number of lines as a significant change.
# Ooh, that's pretty nice. Gets you most of the way there and is simple.
# - All problems have a master representation as a Problem, and we can easily generate all Problems from
# any Aether instance. Then when we switch contexts in any way, we clear, recreate, and reapply the Problems.
# * Problem alerts will have their own templated ProblemAlertViews
# * Problem alerts have their own templated ProblemAlertViews.
# * We'll only show the first problem alert, and it will always be at the bottom.
# Annotations and problem ranges can show all, I guess.
# * The editor will reserve space for one annotation as a codeless area.
@ -366,6 +359,7 @@ module.exports = class SpellView extends View
displayAether: (aether) ->
@displayedAether = aether
isCast = not _.isEmpty(aether.metrics) or _.some aether.problems.errors, {type: 'runtime'}
problem.destroy() for problem in @problems # Just in case another problem was added since clearAetherDisplay() ran.
@problems = []
annotations = []
seenProblemKeys = {}
@ -393,22 +387,30 @@ 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()
currentLine = _.string.rtrim(@aceDoc.$lines[cursorPosition.row].replace(/[ \t]*\/\/[^"']*/g, '')) # trim // unless inside "
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()
#console.log "recompile now!"
#else if not valid
# # if this works, we can get rid of all @recompileValid logic
# console.log "not valid, but so we'll wait to do it in", @autocastDelay + "ms"
#else
# console.log "valid but not at end of line; recompile in", @autocastDelay + "ms"
if valid and (endOfLine or beginningOfLine)
if @autocastDelay > 60000
@preload()
else
@recompile()
preload: ->
# Send this code over to the God for preloading, but don't change the cast state.
oldSource = @spell.source
oldSpellThangAethers = {}
for thangID, spellThang of @spell.thangs
oldSpellThangAethers[thangID] = spellThang.aether.serialize() # Get raw, pure, and problems
@spell.transpile @getSource()
@cast true
@spell.source = oldSource
for thangID, spellThang of @spell.thangs
for key, value of oldSpellThangAethers[thangID]
spellThang.aether[key] = value
onSpellChanged: (e) ->
@spellHasChanged = true
@ -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()
@ -205,7 +205,7 @@ module.exports = class TomeView extends View
reloadAllCode: ->
spell.view.reloadCode false for spellKey, spell of @spells when spell.team is me.team or (spell.team in ["common", "neutral", null])
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: false
updateLanguageForAllSpells: ->
spell.updateLanguageAether() for spellKey, spell of @spells

View file

@ -110,7 +110,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: ->
@ -149,7 +149,7 @@ module.exports = class PlayLevelView extends View
@worldInitialized = true
team = @getQueryVariable("team") ? @world.teamForPlayer(0)
@loadOpponentTeam(team)
@god.level = @level.serialize @supermodel
@god.setLevel @level.serialize @supermodel
@god.setWorldClassMap @world.classMap
@setTeam team
@initGoalManager()

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

@ -22,24 +22,24 @@ exports.config =
|(app[\/\\]lib[\/\\]utils.coffee)
|(vendor[\/\\]scripts[\/\\]Box2dWeb-2.1.a.3)
|(vendor[\/\\]scripts[\/\\]string_score.js)
|(bower_components[\/\\]lodash[\/\\]dist[\/\\]lodash.js)
|(bower_components[\/\\]aether[\/\\]build[\/\\]aether.js)
)///
'javascripts/app.js': /^app/
'javascripts/vendor.js': ///^(
vendor[\/\\](?!scripts[\/\\]Box2d)
|bower_components
|bower_components[\/\\](?!aether)
)///
'javascripts/vendor_with_box2d.js': ///^(
vendor[\/\\]
|bower_components # include box2dweb for profiling (and for IE9...)
|bower_components[\/\\](?!aether) # include box2dweb for profiling (and for IE9...)
)///
'javascripts/tome_aether.js': ///^(
'javascripts/lodash.js': ///^(
(bower_components[\/\\]lodash[\/\\]dist[\/\\]lodash.js)
|(bower_components[\/\\]aether[\/\\]build[\/\\]aether.js)
)///
'test/javascripts/test.js': /^test[\/\\](?!vendor)/
'test/javascripts/test-vendor.js': /^test[\/\\](?=vendor)/
'javascripts/aether.js': ///^(
(bower_components[\/\\]aether[\/\\]build[\/\\]aether.js)
)///
# 'test/javascripts/test.js': /^test[\/\\](?!vendor)/
# 'test/javascripts/test-vendor.js': /^test[\/\\](?=vendor)/
order:
before: [
'bower_components/jquery/dist/jquery.js'

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

@ -4,6 +4,8 @@
JASON = require 'jason'
fs = require 'fs'
GLOBAL.Aether = Aether = require 'aether'
GLOBAL._ = _ = require 'lodash'
betterConsole = () ->
@ -44,7 +46,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 +56,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 +77,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 +99,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 +118,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 +157,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
@ -167,8 +174,9 @@ work = () ->
self.postMessage type: "worker-initialized"
world = fs.readFileSync "./public/javascripts/world.js", 'utf8'
worldCode = fs.readFileSync "./public/javascripts/world.js", 'utf8'
lodashCode = fs.readFileSync "./public/javascripts/lodash.js", 'utf8'
aetherCode = fs.readFileSync "./public/javascripts/aether.js", 'utf8'
#window.BOX2D_ENABLED = true;
@ -185,7 +193,9 @@ ret = """
try {
// the world javascript file
#{world};
#{worldCode};
#{lodashCode};
#{aetherCode};
// Don't let user generated code access stuff from our file system!
self.importScripts = importScripts = null;

View file

@ -47,7 +47,7 @@
"mongoose-text-search": "~0.0.2",
"request": "2.12.x",
"tv4": "~1.0.16",
"lodash": "~2.0.0",
"lodash": "~2.4.1",
"underscore.string": "2.3.x",
"async": "0.2.x",
"connect": "2.7.x",

View file

@ -107,6 +107,7 @@ module.exports.setup = (app) ->
else
return res.end()
else
console.log 'password is', user.get('passwordReset')
res.send user.get('passwordReset')
return res.end()
)