From d55afa77b75725acfea8c1bbb696d82526a44b79 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Thu, 21 Aug 2014 16:27:52 -0700
Subject: [PATCH 01/21] Began streaming a mighty stream.

---
 .../javascripts/workers/aether_worker.js      |  2 +-
 .../javascripts/workers/worker_world.js       | 27 +++++++-
 app/initialize.coffee                         |  3 +-
 app/lib/Angel.coffee                          | 39 +++++++----
 app/lib/LinkedInHandler.coffee                |  2 +-
 app/lib/services/linkedin.coffee              |  2 +-
 app/lib/surface/SpriteBoss.coffee             |  1 +
 app/lib/surface/Surface.coffee                |  3 +-
 app/lib/world/world.coffee                    | 65 ++++++++++++-------
 app/models/Level.coffee                       |  2 +-
 app/models/ThangType.coffee                   |  6 +-
 app/schemas/subscriptions/world.coffee        |  3 +
 app/views/play/SpectateView.coffee            |  1 +
 app/views/play/level/LevelPlaybackView.coffee |  5 +-
 app/views/play/level/PlayLevelView.coffee     |  1 +
 app/views/play/level/tome/SpellView.coffee    |  4 +-
 16 files changed, 115 insertions(+), 51 deletions(-)

diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js
index a75469a29..a1d8e3038 100644
--- a/app/assets/javascripts/workers/aether_worker.js
+++ b/app/assets/javascripts/workers/aether_worker.js
@@ -2,7 +2,7 @@ var window = self;
 var Global = self;
 
 importScripts("/javascripts/lodash.js", "/javascripts/aether.js");
-console.log("Aether Tome worker has finished importing scripts.");
+//console.log("Aether Tome worker has finished importing scripts.");
 var aethers = {};
 
 var createAether = function (spellKey, options) 
diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js
index dd1142eff..7df4e9bf4 100644
--- a/app/assets/javascripts/workers/worker_world.js
+++ b/app/assets/javascripts/workers/worker_world.js
@@ -300,7 +300,7 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
         }
         Math.random = self.debugWorld.rand.randf;  // so user code is predictable
         Aether.replaceBuiltin("Math", Math);
-        replacedLoDash = _.runInContext(self);
+        var replacedLoDash = _.runInContext(self);
         for(var key in replacedLoDash)
           _[key] = replacedLoDash[key];
     }
@@ -358,13 +358,34 @@ self.runWorld = function runWorld(args) {
   }
   Math.random = self.world.rand.randf;  // so user code is predictable
   Aether.replaceBuiltin("Math", Math);
-  replacedLoDash = _.runInContext(self);
+  var replacedLoDash = _.runInContext(self);
   for(var key in replacedLoDash)
     _[key] = replacedLoDash[key];
   self.postMessage({type: 'start-load-frames'});
   self.world.loadFrames(self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress);
 };
 
+self.serializeFramesSoFar = function serializeFramesSoFar() {
+  if(!self.world) return console.error("hmm, no world when we went to serialize some frames?");
+  var goalStates = self.goalManager.getGoalStates();
+  var transferableSupported = self.transferableSupported();
+  var serialized = self.world.serializeFramesSoFar();
+  if(!serialized) {
+    console.log("Tried to serialize some frames, but none have been simulated since last time; still at", self.world.framesSerializedSoFar);
+    return;
+  }
+  try {
+    var message = {type: 'some-frames-serialized', serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
+    if(transferableSupported)
+      self.postMessage(message, serialized.transferableObjects);
+    else
+      self.postMessage(message);
+  }
+  catch(error) {
+    console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
+  }
+};
+
 self.onWorldLoaded = function onWorldLoaded() {
   self.goalManager.worldGenerationEnded();
   var goalStates = self.goalManager.getGoalStates();
@@ -384,7 +405,7 @@ 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};
+    var message = {type: 'new-world', serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
     if(transferableSupported)
       self.postMessage(message, serialized.transferableObjects);
     else
diff --git a/app/initialize.coffee b/app/initialize.coffee
index a27c1f7a2..f2ef617ce 100644
--- a/app/initialize.coffee
+++ b/app/initialize.coffee
@@ -27,7 +27,8 @@ init = ->
   # Set up Backbone.Mediator schemas
   setUpDefinitions()
   setUpChannels()
-  Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
+  #Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
+  Backbone.Mediator.setValidationEnabled false  # STREAM: Should change back
   app.initialize()
   Backbone.history.start({ pushState: true })
   handleNormalUrls()
diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee
index 3b5f59a75..0095a42be 100644
--- a/app/lib/Angel.coffee
+++ b/app/lib/Angel.coffee
@@ -67,6 +67,8 @@ module.exports = class Angel extends CocoClass
       # 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
+        unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializingStreamingFrames
+          @worker.postMessage func: 'serializeFramesSoFar'  # Stream it!
       when 'console-log'
         @log event.data.args...
       when 'user-code-problem'
@@ -80,9 +82,16 @@ module.exports = class Angel extends CocoClass
         else
           @fireWorker()
 
+      # We have some of the frames serialized, so let's send the partially simulated world to the Surface.
+      when 'some-frames-serialized'
+        console.log "angel received some frames", event.data.serialized, "with goals", event.data.goalStates, "and streaming into world", @shared.streamingWorld
+        @deserializingStreamingFrames = true
+        @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
+
       # Either the world finished simulating successfully, or we abort the worker.
       when 'new-world'
-        @beholdWorld event.data.serialized, event.data.goalStates
+        console.log "angel received alll frames", event.data.serialized
+        @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame
       when 'abort'
         @say 'Aborted.', event.data
         clearTimeout @abortTimeout
@@ -99,26 +108,33 @@ module.exports = class Angel extends CocoClass
     Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates
     @finishWork() if @shared.headless
 
-  beholdWorld: (serialized, goalStates) ->
+  beholdWorld: (serialized, goalStates, startFrame, endFrame, streamingWorld) ->
     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)
+    World.deserialize serialized, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates), startFrame, endFrame, streamingWorld
     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, team: me.team
-    for scriptNote in @shared.world.scriptNotes
-      Backbone.Mediator.publish scriptNote.channel, scriptNote.event
-    @shared.goalManager?.world = world
-    @finishWork()
+    finished = world.frames.length is world.totalFrames
+    if finished
+      world.findFirstChangedFrame @shared.world
+      @shared.world = world
+      Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team if @shared.firstWorld
+      for scriptNote in @shared.world.scriptNotes
+        Backbone.Mediator.publish scriptNote.channel, scriptNote.event
+      @shared.goalManager?.world = world
+      @finishWork()
+    else
+      @shared.streamingWorld = world
+      #Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
+      #Backbone.Mediator.publish 'god:streaming-world-updated', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
+      @deserializingStreamingFrames = false
 
   finishWork: ->
+    @shared.streamingWorld = null
     @shared.firstWorld = false
     @running = false
     _.remove @shared.busyAngels, @
@@ -127,6 +143,7 @@ module.exports = class Angel extends CocoClass
   finalizePreload: ->
     @say 'Finalize preload.'
     @worker.postMessage func: 'finalizePreload'
+    @work.preload = false
 
   infinitelyLooped: =>
     @say 'On infinitely looped! Aborting?', @aborting
diff --git a/app/lib/LinkedInHandler.coffee b/app/lib/LinkedInHandler.coffee
index ed322f2c2..2a6be9a9e 100644
--- a/app/lib/LinkedInHandler.coffee
+++ b/app/lib/LinkedInHandler.coffee
@@ -14,7 +14,7 @@ module.exports = LinkedInHandler = class LinkedInHandler extends CocoClass
   onLinkedInLoaded: (e) ->
     IN.Event.on IN, 'auth', @onLinkedInAuth
 
-  onLinkedInAuth: (e) => console.log 'Authorized with LinkedIn'
+  onLinkedInAuth: (e) => #console.log 'Authorized with LinkedIn'
 
   constructEmployerAgreementObject: (cb) =>
     IN.API.Profile('me')
diff --git a/app/lib/services/linkedin.coffee b/app/lib/services/linkedin.coffee
index 9a6f0b37f..9134d77bb 100644
--- a/app/lib/services/linkedin.coffee
+++ b/app/lib/services/linkedin.coffee
@@ -1,6 +1,6 @@
 module.exports = initializeLinkedIn = ->
   window.linkedInAsyncInit = ->
-    console.log 'Linkedin async init success!'
+    #console.log 'Linkedin async init success!'
     Backbone.Mediator.publish 'linkedin-loaded'
 
   linkedInSnippet =
diff --git a/app/lib/surface/SpriteBoss.coffee b/app/lib/surface/SpriteBoss.coffee
index c5fd17c19..06830036f 100644
--- a/app/lib/surface/SpriteBoss.coffee
+++ b/app/lib/surface/SpriteBoss.coffee
@@ -19,6 +19,7 @@ module.exports = class SpriteBoss extends CocoClass
     'level-lock-select': 'onSetLockSelect'
     'level:restarted': 'onLevelRestarted'
     'god:new-world-created': 'onNewWorld'
+    'god:streaming-world-updated': 'onNewWorld'
     'camera:dragged': 'onCameraDragged'
     'sprite:loaded': -> @update(true)
 
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index bd6cf7df1..248eb9b7e 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -63,6 +63,7 @@ module.exports = Surface = class Surface extends CocoClass
     'level-set-surface-camera': 'onSetCamera'
     'level:restarted': 'onLevelRestarted'
     'god:new-world-created': 'onNewWorld'
+    'god:streaming-world-updated': 'onNewWorld'
     'tome:cast-spells': 'onCastSpells'
     'level-set-letterbox': 'onSetLetterbox'
     'application:idle-changed': 'onIdleChanged'
@@ -413,7 +414,7 @@ module.exports = Surface = class Surface extends CocoClass
     @surfaceLayer.addChild @cameraBorder = new CameraBorder bounds: @camera.bounds
     @screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight
     @spriteBoss = new SpriteBoss camera: @camera, surfaceLayer: @surfaceLayer, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible
-    @castingScreen ?= new CastingScreen camera: @camera, layer: @screenLayer
+    #@castingScreen ?= new CastingScreen camera: @camera, layer: @screenLayer  # Not needed with world streaming.
     @playbackOverScreen ?= new PlaybackOverScreen camera: @camera, layer: @screenLayer
     @stage.enableMouseOver(10)
     @stage.addEventListener 'stagemousemove', @onMouseMove
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index de540b991..dc52f1f72 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -10,7 +10,7 @@ WorldScriptNote = require './world_script_note'
 {now, consolidateThangs, typedArraySupport} = require './world_utils'
 Component = require 'lib/world/component'
 System = require 'lib/world/system'
-PROGRESS_UPDATE_INTERVAL = 200
+PROGRESS_UPDATE_INTERVAL = 100
 DESERIALIZATION_INTERVAL = 20
 ITEM_ORIGINAL = '53e12043b82921000051cdf9'
 
@@ -21,11 +21,13 @@ module.exports = class World
   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
+  framesSerializedSoFar: 0
   apiProperties: ['age', 'dt']
   constructor: (@userCodeMap, classMap) ->
     # classMap is needed for deserializing Worlds, Thangs, and other classes
     @classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment}
     Thang.resetThangIDs()
+    @aRandomID = Math.random()
 
     @userCodeMap ?= {}
     @thangs = []
@@ -66,7 +68,6 @@ module.exports = class World
     @thangMap[thang.id] = thang
 
   thangDialogueSounds: ->
-    if @frames.length < @totalFrames then throw new Error('World should be over before grabbing dialogue')
     [sounds, seen] = [[], {}]
     for frame in @frames
       for thangID, state of frame.thangStateMap
@@ -287,9 +288,17 @@ module.exports = class World
   addTrackedProperties: (props...) ->
     @trackedProperties = (@trackedProperties ? []).concat props
 
-  serialize: ->
+  serializeFramesSoFar: ->
+    return null if @frames.length is @framesSerializedSoFar
+    serialized = @serialize @framesSerializedSoFar, @frames.length
+    @framesSerializedSoFar = @frames.length
+    serialized
+
+  serialize: (startFrame=0, endFrame=null) ->
     # Code hotspot; optimize it
-    if @frames.length < @totalFrames then throw new Error('World Should Be Over Before Serialization')
+    if not endFrame? and @frames.length < @totalFrames then throw new Error('World Should Be Over Before Serialization')
+    endFrame ?= @totalFrames
+    console.log "... world serializing frames from", startFrame, "to", endFrame
     [transferableObjects, nontransferableObjects] = [0, 0]
     o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
     o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
@@ -305,19 +314,20 @@ module.exports = class World
     o.trackedPropertiesPerThangKeys = []
     o.trackedPropertiesPerThangTypes = []
     trackedPropertiesPerThangValues = []  # We won't send these, just the offsets and the storage buffer
-    o.trackedPropertiesPerThangValuesOffsets = []  # Needed to reconstruct ArrayBufferViews on other end, since Firefox has bugs transfering those: https://bugzilla.mozilla.org/show_bug.cgi?id=841904 and https://bugzilla.mozilla.org/show_bug.cgi?id=861925
+    o.trackedPropertiesPerThangValuesOffsets = []  # Needed to reconstruct ArrayBufferViews on other end, since Firefox has bugs transfering those: https://bugzilla.mozilla.org/show_bug.cgi?id=841904 and https://bugzilla.mozilla.org/show_bug.cgi?id=861925  # Actually, as of January 2014, it should be fixed.
     transferableStorageBytesNeeded = 0
-    nFrames = @frames.length
+    nFrames = endFrame - startFrame
+    streaming = nFrames < @totalFrames
     for thang in @thangs
       # Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles).
       # Check both, since sometimes people mark stateless Thangs but don't change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
-      continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean)
+      continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean) and not streaming
       o.trackedPropertiesThangIDs.push thang.id
       trackedPropertiesIndices = []
       trackedPropertiesKeys = []
       trackedPropertiesTypes = []
       for used, propIndex in thang.trackedPropertiesUsed
-        continue unless used
+        continue unless used or streaming
         trackedPropertiesIndices.push propIndex
         trackedPropertiesKeys.push thang.trackedPropertiesKeys[propIndex]
         trackedPropertiesTypes.push thang.trackedPropertiesTypes[propIndex]
@@ -357,8 +367,8 @@ module.exports = class World
 
     t1 = now()
     o.frameHashes = []
-    for frame, frameIndex in @frames
-      o.frameHashes.push frame.serialize(frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
+    for frameIndex in [startFrame ... endFrame]
+      o.frameHashes.push @frames[frameIndex].serialize(frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
     t2 = now()
 
     unless typedArraySupport
@@ -368,28 +378,29 @@ module.exports = class World
           flattened.push value
       o.storageBuffer = flattened
 
-    #console.log 'Allocating memory:', (t1 - t0).toFixed(0), 'ms; assigning values:', (t2 - t1).toFixed(0), 'ms, so', ((t2 - t1) / @frames.length).toFixed(3), 'ms per frame'
+    #console.log 'Allocating memory:', (t1 - t0).toFixed(0), 'ms; assigning values:', (t2 - t1).toFixed(0), 'ms, so', ((t2 - t1) / nFrames).toFixed(3), 'ms per frame for', nFrames, 'frames'
     #console.log 'Got', transferableObjects, 'transferable objects and', nontransferableObjects, 'nontransferable; stored', transferableStorageBytesNeeded, 'bytes transferably'
 
     o.thangs = (t.serialize() for t in @thangs.concat(@extraneousThangs ? []))
     o.scriptNotes = (sn.serialize() for sn in @scriptNotes)
     if o.scriptNotes.length > 200
       console.log 'Whoa, serializing a lot of WorldScriptNotes here:', o.scriptNotes.length
-    {serializedWorld: o, transferableObjects: [o.storageBuffer]}
+    {serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame}
 
-  @deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback) ->
+  @deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, streamingWorld) ->
     # Code hotspot; optimize it
     #console.log 'Deserializing', o, 'length', JSON.stringify(o).length
     #console.log JSON.stringify(o)
     #console.log 'Got special keys and values:', o.specialValuesToKeys, o.specialKeysToValues
     perf = {}
     perf.t0 = now()
-    w = new World o.userCodeMap, classMap
+    nFrames = endFrame - startFrame
+    w = streamingWorld ? 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
 
     perf.t1 = now()
-    w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs)
+    w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs)  # TODO: just do the new ones?
     w.setThang thang for thang in w.thangs
     w.scriptNotes = (WorldScriptNote.deserialize(sn, w, classMap) for sn in o.scriptNotes)
     perf.t2 = now()
@@ -400,7 +411,7 @@ module.exports = class World
       o.trackedPropertiesPerThangValues.push (trackedPropertiesValues = [])
       trackedPropertiesValuesOffsets = o.trackedPropertiesPerThangValuesOffsets[thangIndex]
       for type, propIndex in trackedPropertyTypes
-        storage = ThangState.createArrayForType(type, o.totalFrames, o.storageBuffer, trackedPropertiesValuesOffsets[propIndex])[0]
+        storage = ThangState.createArrayForType(type, nFrames, o.storageBuffer, trackedPropertiesValuesOffsets[propIndex])[0]
         unless typedArraySupport
           # This could be more efficient
           i = trackedPropertiesValuesOffsets[propIndex]
@@ -409,26 +420,30 @@ module.exports = class World
     perf.t3 = now()
 
     perf.batches = 0
-    w.frames = []
-    _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf
+    w.frames = [] unless streamingWorld
+    clearTimeout @deserializationTimeout if @deserializationTimeout
+    @deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
 
   # Spread deserialization out across multiple calls so the interface stays responsive
-  @deserializeSomeFrames: (o, w, finishedWorldCallback, perf) =>
+  @deserializeSomeFrames: (o, w, finishedWorldCallback, perf, startFrame, endFrame) =>
     ++perf.batches
     startTime = now()
-    for frameIndex in [w.frames.length ... o.totalFrames]
+    for frameIndex in [w.frames.length ... endFrame]
       w.frames.push WorldFrame.deserialize(w, frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesThangs, o.trackedPropertiesPerThangKeys, o.trackedPropertiesPerThangTypes, o.trackedPropertiesPerThangValues, o.specialKeysToValues, o.frameHashes[frameIndex])
       if (now() - startTime) > DESERIALIZATION_INTERVAL
-        _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf
+        console.log "  Deserialization not finished, let's do it again soon. Have:", w.frames.length, ", wanted from", startFrame, "to", endFrame
+        @deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
         return
-    @finishDeserializing w, finishedWorldCallback, perf
+    @deserializationTimeout = null
+    @finishDeserializing w, finishedWorldCallback, perf, startFrame, endFrame
 
-  @finishDeserializing: (w, finishedWorldCallback, perf) ->
+  @finishDeserializing: (w, finishedWorldCallback, perf, startFrame, endFrame) ->
     perf.t4 = now()
+    nFrames = endFrame - startFrame
     w.ended = true
-    w.getFrame(w.totalFrames - 1).restoreState()
+    w.getFrame(endFrame - 1).restoreState()
     perf.t5 = now()
-    console.log 'Deserialization:', (perf.t5 - perf.t0).toFixed(0) + 'ms (' + ((perf.t5 - perf.t0) / w.frames.length).toFixed(3) + 'ms per frame).', perf.batches, 'batches.'
+    console.log 'Deserialization:', (perf.t5 - perf.t0).toFixed(0) + 'ms (' + ((perf.t5 - perf.t0) / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches.'
     if false
       console.log '  Deserializing--constructing new World:', (perf.t1 - perf.t0).toFixed(2) + 'ms'
       console.log '  Deserializing--Thangs and ScriptNotes:', (perf.t2 - perf.t1).toFixed(2) + 'ms'
diff --git a/app/models/Level.coffee b/app/models/Level.coffee
index ad3458692..da61d9d65 100644
--- a/app/models/Level.coffee
+++ b/app/models/Level.coffee
@@ -111,7 +111,7 @@ module.exports = class Level extends CocoModel
       visit = (c) ->
         return if c in sorted
         lc = _.find levelComponents, {original: c.original}
-        console.error thang.id or thang.name, 'couldn\'t find lc for', c, 'of', levelComponents unless lc
+        #console.error thang.id or thang.name, 'couldn\'t find lc for', c, 'of', levelComponents unless lc  # STREAM: uncomment
         return unless lc
         if lc.name is 'Programmable'
           # Programmable always comes last
diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee
index 24e191d29..8dbaae541 100644
--- a/app/models/ThangType.coffee
+++ b/app/models/ThangType.coffee
@@ -188,7 +188,7 @@ module.exports = class ThangType extends CocoModel
     portrait = if portrait then '(Portrait)' else ''
     name = _.string.rpad @get('name'), 20
     time = _.string.lpad '' + new Date().getTime() - startTime, 6
-    console.debug "Built sheet:  #{name} #{time}ms  #{kind}  #{portrait}"
+    #console.debug "Built sheet:  #{name} #{time}ms  #{kind}  #{portrait}"  # STREAM: uncomment
 
   spriteSheetKey: (options) ->
     colorConfigs = []
@@ -276,7 +276,7 @@ module.exports = class ThangType extends CocoModel
       @get('components') or [],
       (compRef) -> compRef.original is LevelComponent.ItemID)
     return itemComponentRef?.config?.slots or []
-    
+
   getFrontFacingStats: ->
     stats = []
     for component in @get('components') or []
@@ -317,4 +317,4 @@ module.exports = class ThangType extends CocoModel
         snippets = config.programmableSnippets
         if snippets.length
           stats.push { name: 'Snippets', value: snippets.join(', ') }
-    stats
\ No newline at end of file
+    stats
diff --git a/app/schemas/subscriptions/world.coffee b/app/schemas/subscriptions/world.coffee
index 9cb7487f0..0eb748c5e 100644
--- a/app/schemas/subscriptions/world.coffee
+++ b/app/schemas/subscriptions/world.coffee
@@ -11,5 +11,8 @@ module.exports =
   'god:new-world-created':
     {} # TODO schema
 
+  'god:streaming-world-updated':
+    {} # TODO schema
+
   'god:world-load-progress-changed':
     {} # TODO schema
diff --git a/app/views/play/SpectateView.coffee b/app/views/play/SpectateView.coffee
index bda2cce7e..8c23d779d 100644
--- a/app/views/play/SpectateView.coffee
+++ b/app/views/play/SpectateView.coffee
@@ -55,6 +55,7 @@ module.exports = class SpectateLevelView extends RootView
     'surface:world-set-up': 'onSurfaceSetUpNewWorld'
     'level:set-team': 'setTeam'
     'god:new-world-created': 'loadSoundsForWorld'
+    'god:streaming-world-updated': 'loadSoundsForWorld'
     'next-game-pressed': 'onNextGamePressed'
     'level:started': 'onLevelStarted'
     'level:loading-view-unveiled': 'onLoadingViewUnveiled'
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index eb619ce06..acf283df9 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -22,6 +22,7 @@ module.exports = class LevelPlaybackView extends CocoView
     'level-toggle-grid': 'onToggleGrid'
     'surface:frame-changed': 'onFrameChanged'
     'god:new-world-created': 'onNewWorld'
+    'god:streaming-world-updated': 'onNewWorld'  # Maybe?
     'level-set-letterbox': 'onSetLetterbox'
     'tome:cast-spells': 'onCastSpells'
 
@@ -329,7 +330,9 @@ module.exports = class LevelPlaybackView extends CocoView
     return if @shouldIgnore()
     Backbone.Mediator.publish 'level-set-time', ratio: ratio, scrubDuration: duration
 
-  shouldIgnore: -> return @disabled or @casting or false
+  shouldIgnore: ->
+    #return @disabled or @casting or false  # STREAM: figure this out
+    return false
 
   onTogglePlay: (e) ->
     e?.preventDefault()
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 604625705..3bc4a6fc5 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -53,6 +53,7 @@ module.exports = class PlayLevelView extends RootView
     'level-disable-controls': 'onDisableControls'
     'level-enable-controls': 'onEnableControls'
     'god:new-world-created': 'onNewWorld'
+    'god:streaming-world-updated': 'onNewWorld'
     'god:infinite-loop': 'onInfiniteLoop'
     'level-reload-from-data': 'onLevelReloadFromData'
     'level-reload-thang-type': 'onLevelReloadThangType'
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 024b60ee0..8b3659247 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -128,10 +128,10 @@ module.exports = class SpellView extends CocoView
       # passEvent: true  # https://github.com/ajaxorg/ace/blob/master/lib/ace/keyboard/keybinding.js#L114
       # No easy way to selectively cancel shift+space, since we don't get access to the event.
       # Maybe we could temporarily set ourselves to read-only if we somehow know that a script is active?
-      exec: => 
+      exec: =>
         if @scriptRunning
           Backbone.Mediator.publish 'level:shift-space-pressed'
-        else 
+        else
           @ace.insert ' '
 
     addCommand

From bd380e4a092cc68bfc2c623c220b52d09bf5842d Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Thu, 21 Aug 2014 17:30:46 -0700
Subject: [PATCH 02/21] World streaming is starting to work!

---
 app/lib/Angel.coffee             |  2 +-
 app/lib/surface/Surface.coffee   |  1 +
 app/lib/world/world.coffee       | 17 +++++++++++------
 app/lib/world/world_frame.coffee |  4 ++--
 4 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee
index 0095a42be..199c144f4 100644
--- a/app/lib/Angel.coffee
+++ b/app/lib/Angel.coffee
@@ -130,7 +130,7 @@ module.exports = class Angel extends CocoClass
     else
       @shared.streamingWorld = world
       #Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
-      #Backbone.Mediator.publish 'god:streaming-world-updated', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
+      Backbone.Mediator.publish 'god:streaming-world-updated', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
       @deserializingStreamingFrames = false
 
   finishWork: ->
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 248eb9b7e..d9ce76b93 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -608,6 +608,7 @@ module.exports = Surface = class Surface extends CocoClass
     if current - Math.floor(current) > 0.01
       next = Math.ceil current
       ratio = current % 1
+      console.log "trying to restore partial state between", current, next, "of frames", @world.frames.length, "frames" unless @world.frames[next]
       @world.frames[next].restorePartialState ratio if next > 1
     frame.clearEvents() if parseInt(@currentFrame) is parseInt(@lastFrame)
     @spriteBoss.updateSounds() if parseInt(@currentFrame) isnt parseInt(@lastFrame)
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index dc52f1f72..b7fc52249 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -321,13 +321,13 @@ module.exports = class World
     for thang in @thangs
       # Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles).
       # Check both, since sometimes people mark stateless Thangs but don't change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
-      continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean) and not streaming
+      continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean)# and not streaming
       o.trackedPropertiesThangIDs.push thang.id
       trackedPropertiesIndices = []
       trackedPropertiesKeys = []
       trackedPropertiesTypes = []
       for used, propIndex in thang.trackedPropertiesUsed
-        continue unless used or streaming
+        continue unless used# or streaming
         trackedPropertiesIndices.push propIndex
         trackedPropertiesKeys.push thang.trackedPropertiesKeys[propIndex]
         trackedPropertiesTypes.push thang.trackedPropertiesTypes[propIndex]
@@ -368,7 +368,7 @@ module.exports = class World
     t1 = now()
     o.frameHashes = []
     for frameIndex in [startFrame ... endFrame]
-      o.frameHashes.push @frames[frameIndex].serialize(frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
+      o.frameHashes.push @frames[frameIndex].serialize(frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
     t2 = now()
 
     unless typedArraySupport
@@ -400,8 +400,13 @@ module.exports = class World
     w[prop] = val for prop, val of o.trackedProperties
 
     perf.t1 = now()
-    w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs)  # TODO: just do the new ones?
-    w.setThang thang for thang in w.thangs
+    if w.thangs.length
+      for thang in o.thangs when not w.thangMap[thang.id]
+        w.thangs.push Thang.deserialize(thang, w, classMap)
+        w.setThang thang
+    else
+      w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs)
+      w.setThang thang for thang in w.thangs
     w.scriptNotes = (WorldScriptNote.deserialize(sn, w, classMap) for sn in o.scriptNotes)
     perf.t2 = now()
 
@@ -429,7 +434,7 @@ module.exports = class World
     ++perf.batches
     startTime = now()
     for frameIndex in [w.frames.length ... endFrame]
-      w.frames.push WorldFrame.deserialize(w, frameIndex, o.trackedPropertiesThangIDs, o.trackedPropertiesThangs, o.trackedPropertiesPerThangKeys, o.trackedPropertiesPerThangTypes, o.trackedPropertiesPerThangValues, o.specialKeysToValues, o.frameHashes[frameIndex])
+      w.frames.push WorldFrame.deserialize(w, frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesThangs, o.trackedPropertiesPerThangKeys, o.trackedPropertiesPerThangTypes, o.trackedPropertiesPerThangValues, o.specialKeysToValues, o.frameHashes[frameIndex - startFrame], w.dt * frameIndex)
       if (now() - startTime) > DESERIALIZATION_INTERVAL
         console.log "  Deserialization not finished, let's do it again soon. Have:", w.frames.length, ", wanted from", startFrame, "to", endFrame
         @deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
diff --git a/app/lib/world/world_frame.coffee b/app/lib/world/world_frame.coffee
index 2ab99f153..d1386b718 100644
--- a/app/lib/world/world_frame.coffee
+++ b/app/lib/world/world_frame.coffee
@@ -60,9 +60,9 @@ module.exports = class WorldFrame
         thangState.serialize(frameIndex, trackedPropertiesPerThangIndices[thangIndex], trackedPropertiesPerThangTypes[thangIndex], trackedPropertiesPerThangValues[thangIndex], specialValuesToKeys, specialKeysToValues)
     @hash
 
-  @deserialize: (world, frameIndex, trackedPropertiesThangIDs, trackedPropertiesThangs, trackedPropertiesPerThangKeys, trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, specialKeysToValues, hash) ->
+  @deserialize: (world, frameIndex, trackedPropertiesThangIDs, trackedPropertiesThangs, trackedPropertiesPerThangKeys, trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, specialKeysToValues, hash, age) ->
     # Optimize
-    wf = new WorldFrame null, world.dt * frameIndex
+    wf = new WorldFrame null, age
     wf.world = world
     wf.hash = hash
     for thangID, thangIndex in trackedPropertiesThangIDs

From 9b31e28536b299e66ee29993c29b88015830ca1f Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Thu, 21 Aug 2014 21:23:45 -0700
Subject: [PATCH 03/21] More improvements to world streaming.

---
 .../javascripts/workers/worker_world.js       | 44 ++++++-------
 app/lib/Angel.coffee                          | 19 +++---
 app/lib/surface/Surface.coffee                | 61 ++++++++-----------
 app/lib/world/thang_state.coffee              |  2 +
 app/lib/world/world.coffee                    | 29 ++++-----
 app/views/play/level/LevelPlaybackView.coffee | 18 ++----
 6 files changed, 72 insertions(+), 101 deletions(-)

diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js
index 7df4e9bf4..b4f1f5e6f 100644
--- a/app/assets/javascripts/workers/worker_world.js
+++ b/app/assets/javascripts/workers/worker_world.js
@@ -367,29 +367,18 @@ self.runWorld = function runWorld(args) {
 
 self.serializeFramesSoFar = function serializeFramesSoFar() {
   if(!self.world) return console.error("hmm, no world when we went to serialize some frames?");
-  var goalStates = self.goalManager.getGoalStates();
-  var transferableSupported = self.transferableSupported();
-  var serialized = self.world.serializeFramesSoFar();
-  if(!serialized) {
-    console.log("Tried to serialize some frames, but none have been simulated since last time; still at", self.world.framesSerializedSoFar);
-    return;
-  }
-  try {
-    var message = {type: 'some-frames-serialized', serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
-    if(transferableSupported)
-      self.postMessage(message, serialized.transferableObjects);
-    else
-      self.postMessage(message);
-  }
-  catch(error) {
-    console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
-  }
+  if(self.world.framesSerializedSoFar == self.world.frames.length) return;
+  self.onWorldLoaded();
+  self.world.framesSerializedSoFar = self.world.frames.length;
 };
 
 self.onWorldLoaded = function onWorldLoaded() {
-  self.goalManager.worldGenerationEnded();
+  if(self.world.framesSerializedSoFar == self.world.frames.length) return;
+  if(self.world.ended)
+    self.goalManager.worldGenerationEnded();
   var goalStates = self.goalManager.getGoalStates();
-  self.postMessage({type: 'end-load-frames', goalStates: goalStates});
+  if(self.world.ended)
+    self.postMessage({type: 'end-load-frames', goalStates: goalStates});
   var t1 = new Date();
   var diff = t1 - self.t0;
   if (self.world.headless)
@@ -402,10 +391,12 @@ self.onWorldLoaded = function onWorldLoaded() {
   catch(error) {
     console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
   }
+
   var t2 = new Date();
   //console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
+  var messageType = self.world.ended ? 'new-world' : 'some-frames-serialized';
   try {
-    var message = {type: 'new-world', serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
+    var message = {type: messageType, serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
     if(transferableSupported)
       self.postMessage(message, serialized.transferableObjects);
     else
@@ -414,11 +405,14 @@ self.onWorldLoaded = function onWorldLoaded() {
   catch(error) {
     console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
   }
-  var t3 = new Date();
-  console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation   :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery     :", (t3 - t2) + "ms");
-  self.world.goalManager.destroy();
-  self.world.destroy();
-  self.world = null;
+
+  if(self.world.ended) {
+    var t3 = new Date();
+    console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation   :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery     :", (t3 - t2) + "ms");
+    self.world.goalManager.destroy();
+    self.world.destroy();
+    self.world = null;
+  }
 };
 
 self.onWorldError = function onWorldError(error) {
diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee
index 199c144f4..af28428c7 100644
--- a/app/lib/Angel.coffee
+++ b/app/lib/Angel.coffee
@@ -67,7 +67,7 @@ module.exports = class Angel extends CocoClass
       # 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
-        unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializingStreamingFrames
+        unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializingStreamingFrames or @shared.firstWorld
           @worker.postMessage func: 'serializeFramesSoFar'  # Stream it!
       when 'console-log'
         @log event.data.args...
@@ -84,14 +84,14 @@ module.exports = class Angel extends CocoClass
 
       # We have some of the frames serialized, so let's send the partially simulated world to the Surface.
       when 'some-frames-serialized'
-        console.log "angel received some frames", event.data.serialized, "with goals", event.data.goalStates, "and streaming into world", @shared.streamingWorld
+        #console.log "angel received some frames", event.data.serialized, "with goals", event.data.goalStates, "and streaming into world", @shared.streamingWorld
         @deserializingStreamingFrames = true
         @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
 
       # Either the world finished simulating successfully, or we abort the worker.
       when 'new-world'
-        console.log "angel received alll frames", event.data.serialized
-        @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame
+        #console.log "angel received alll frames", event.data.serialized, "and have streaming world", @shared.streamingWorld
+        @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
       when 'abort'
         @say 'Aborted.', event.data
         clearTimeout @abortTimeout
@@ -118,24 +118,25 @@ module.exports = class Angel extends CocoClass
 
   finishBeholdingWorld: (goalStates) -> (world) =>
     return if @aborting
+    @shared.streamingWorld = world
     finished = world.frames.length is world.totalFrames
+    firstChangedFrame = world.findFirstChangedFrame @shared.world
+    eventType = if finished then 'god:new-world-created' else 'god:streaming-world-updated'
     if finished
-      world.findFirstChangedFrame @shared.world
       @shared.world = world
-      Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team if @shared.firstWorld
+    Backbone.Mediator.publish eventType, world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team, firstChangedFrame: firstChangedFrame
+    if finished
       for scriptNote in @shared.world.scriptNotes
         Backbone.Mediator.publish scriptNote.channel, scriptNote.event
       @shared.goalManager?.world = world
       @finishWork()
     else
-      @shared.streamingWorld = world
-      #Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
-      Backbone.Mediator.publish 'god:streaming-world-updated', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
       @deserializingStreamingFrames = false
 
   finishWork: ->
     @shared.streamingWorld = null
     @shared.firstWorld = false
+    @deserializingStreamingFrames = false
     @running = false
     _.remove @shared.busyAngels, @
     @doWork()
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index d9ce76b93..1e01b3885 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -205,10 +205,11 @@ module.exports = Surface = class Surface extends CocoClass
 
     @scrubbingTo = Math.min(Math.round(progress * @world.totalFrames), @world.totalFrames)
     @scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5))
+    ease = if @fastForwarding then createjs.Ease.linear else createjs.Ease.sineInOut
     if scrubDuration
       t = createjs.Tween
         .get(@)
-        .to({currentFrame: @scrubbingTo}, scrubDuration, createjs.Ease.sineInOut)
+        .to({currentFrame: @scrubbingTo}, scrubDuration, ease)
         .call(onTweenEnd)
       t.addEventListener('change', @onFramesScrubbed)
     else
@@ -354,48 +355,36 @@ module.exports = Surface = class Surface extends CocoClass
     return if e.preload
     @setPaused false if @ended
     @casting = true
-    @wasPlayingWhenCastingBegan = @playing
-    Backbone.Mediator.publish 'level-set-playing', {playing: false}
-    @setPlayingCalled = false # don't overwrite playing settings if they changed by, say, scripts
-
-    if @coordinateDisplay?
-      @surfaceTextLayer.removeChild @coordinateDisplay
-      @coordinateDisplay.destroy()
-
-    createjs.Tween.removeTweens(@surfaceLayer)
-    createjs.Tween.get(@surfaceLayer).to({alpha: 0.9}, 1000, createjs.Ease.getPowOut(4.0))
+    @setPlayingCalled = false  # Don't overwrite playing settings if they changed by, say, scripts.
+    @frameBeforeCast = @currentFrame
+    @currentFrame = 0
 
   onNewWorld: (event) ->
     return unless event.world.name is @world.name
     @casting = false
-    if @ended and not @wasPlayingWhenCastingBegan
-      @setPaused true
-    else
-      @spriteBoss.play()
+    @spriteBoss.play()
 
     # This has a tendency to break scripts that are waiting for playback to change when the level is loaded
     # so only run it after the first world is created.
-    Backbone.Mediator.publish 'level-set-playing', {playing: @wasPlayingWhenCastingBegan} unless event.firstWorld or @setPlayingCalled
+    Backbone.Mediator.publish 'level-set-playing', {playing: true} unless event.firstWorld or @setPlayingCalled
 
-    fastForwardTo = null
-    if @playing
-      fastForwardTo = Math.min event.world.firstChangedFrame, @currentFrame
-      @currentFrame = 0
-
-    createjs.Tween.removeTweens(@surfaceLayer)
-    f = =>
-      @setWorld event.world
-      @onFrameChanged(true)
-      if fastForwardTo and @playing
-        fastForwardToRatio = fastForwardTo / @world.totalFrames
-        fastForwardToTime = fastForwardTo * @world.dt
-        fastForwardSpeed = Math.max 4, fastForwardToTime / 3
-        @setProgress fastForwardToRatio, 1000 * fastForwardToTime / fastForwardSpeed
+    @setWorld event.world
+    @onFrameChanged(true)
+    if @playing and ffToFrame = Math.min event.firstChangedFrame, @frameBeforeCast, event.world.frames.length
+      ffToRatio = ffToFrame / @world.totalFrames
+      ffToTime = ffToFrame * @world.dt
+      ffSpeed = Math.max 4, ffToTime / 3
+      ffInterval = 1000 * (ffToFrame - @currentFrame) / @options.frameRate
+      ffScrubDuration = 1000 * ffToTime / ffSpeed
+      ffScrubDuration = Math.min(ffScrubDuration, ffInterval)
+      ffFactor = ffInterval / ffScrubDuration
+      if ffFactor > 2
+        createjs.Tween.removeTweens(@)
+        @scrubbingTo = null
         @fastForwarding = true
-    createjs.Tween.get(@surfaceLayer)
-      .to({alpha: 0.0}, 50)
-      .call(f)
-      .to({alpha: 1.0}, 2000, createjs.Ease.getPowOut(2.0))
+        @setProgress ffToRatio, ffScrubDuration
+      else
+        createjs.Tween.removeTweens(@)
 
   # initialization
 
@@ -414,7 +403,7 @@ module.exports = Surface = class Surface extends CocoClass
     @surfaceLayer.addChild @cameraBorder = new CameraBorder bounds: @camera.bounds
     @screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight
     @spriteBoss = new SpriteBoss camera: @camera, surfaceLayer: @surfaceLayer, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible
-    #@castingScreen ?= new CastingScreen camera: @camera, layer: @screenLayer  # Not needed with world streaming.
+    #@castingScreen ?= new CastingScreen camera: @camera, layer: @screenLayer  # STREAM: Not needed with world streaming.
     @playbackOverScreen ?= new PlaybackOverScreen camera: @camera, layer: @screenLayer
     @stage.enableMouseOver(10)
     @stage.addEventListener 'stagemousemove', @onMouseMove
@@ -616,7 +605,7 @@ module.exports = Surface = class Surface extends CocoClass
   updateState: (frameChanged) ->
     # world state must have been restored in @restoreWorldState
     @camera.updateZoom()
-    @spriteBoss.update frameChanged unless @casting
+    @spriteBoss.update frameChanged
     @dimmer?.setSprites @spriteBoss.sprites
 
   drawCurrentFrame: (e) ->
diff --git a/app/lib/world/thang_state.coffee b/app/lib/world/thang_state.coffee
index 611194cc3..473224b58 100644
--- a/app/lib/world/thang_state.coffee
+++ b/app/lib/world/thang_state.coffee
@@ -47,6 +47,7 @@ module.exports = class ThangState
       value = @specialKeysToValues[specialKey]
     else if type is 'Thang'
       specialKey = storage[@frameIndex]
+      console.error "couldn't find world from thang", @thang, "for", @specialKeysToValues[specialKey] unless @thang.world
       value = @thang.world.getThangByID @specialKeysToValues[specialKey]
     else if type is 'array'
       specialKey = storage[@frameIndex]
@@ -164,6 +165,7 @@ module.exports = class ThangState
     # Optimize like no tomorrow--most performance-sensitive part of the whole app, called once per WorldFrame per Thang per trackedProperty, blocking the UI
     ts = new ThangState
     ts.thang = thang
+    console.error "couldn't find thang!", @ unless thang
     ts.frameIndex = frameIndex
     ts.trackedPropertyKeys = trackedPropertyKeys
     ts.trackedPropertyTypes = trackedPropertyTypes
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index b7fc52249..07be18e5b 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -27,7 +27,6 @@ module.exports = class World
     # classMap is needed for deserializing Worlds, Thangs, and other classes
     @classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment}
     Thang.resetThangIDs()
-    @aRandomID = Math.random()
 
     @userCodeMap ?= {}
     @thangs = []
@@ -288,16 +287,10 @@ module.exports = class World
   addTrackedProperties: (props...) ->
     @trackedProperties = (@trackedProperties ? []).concat props
 
-  serializeFramesSoFar: ->
-    return null if @frames.length is @framesSerializedSoFar
-    serialized = @serialize @framesSerializedSoFar, @frames.length
-    @framesSerializedSoFar = @frames.length
-    serialized
-
-  serialize: (startFrame=0, endFrame=null) ->
+  serialize: ->
     # Code hotspot; optimize it
-    if not endFrame? and @frames.length < @totalFrames then throw new Error('World Should Be Over Before Serialization')
-    endFrame ?= @totalFrames
+    startFrame = @framesSerializedSoFar
+    endFrame = @frames.length
     console.log "... world serializing frames from", startFrame, "to", endFrame
     [transferableObjects, nontransferableObjects] = [0, 0]
     o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
@@ -321,7 +314,7 @@ module.exports = class World
     for thang in @thangs
       # Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles).
       # Check both, since sometimes people mark stateless Thangs but don't change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
-      continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean)# and not streaming
+      continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean) and not streaming
       o.trackedPropertiesThangIDs.push thang.id
       trackedPropertiesIndices = []
       trackedPropertiesKeys = []
@@ -401,8 +394,8 @@ module.exports = class World
 
     perf.t1 = now()
     if w.thangs.length
-      for thang in o.thangs when not w.thangMap[thang.id]
-        w.thangs.push Thang.deserialize(thang, w, classMap)
+      for thangConfig in o.thangs when not w.thangMap[thangConfig.id]
+        w.thangs.push thang = Thang.deserialize(thangConfig, w, classMap)
         w.setThang thang
     else
       w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs)
@@ -458,16 +451,16 @@ module.exports = class World
     finishedWorldCallback w
 
   findFirstChangedFrame: (oldWorld) ->
-    return @firstChangedFrame = 0 unless oldWorld
+    return 0 unless oldWorld
     for newFrame, i in @frames
       oldFrame = oldWorld.frames[i]
-      break unless oldFrame and newFrame.hash is oldFrame.hash
-    @firstChangedFrame = i
+      break unless oldFrame and ((newFrame.hash is oldFrame.hash) or not newFrame.hash? or not oldFrame.hash?)  # undefined gets in there when streaming at the last frame of each batch for some reason
+    firstChangedFrame = i
     if @frames[i]
-      console.log 'First changed frame is', @firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
+      console.log 'First changed frame is', firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
     else
       console.log 'No frames were changed out of all', @frames.length
-    @firstChangedFrame
+    firstChangedFrame
 
   pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) ->
     # Optimized
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index acf283df9..4fb24f8ef 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -24,7 +24,6 @@ module.exports = class LevelPlaybackView extends CocoView
     'god:new-world-created': 'onNewWorld'
     'god:streaming-world-updated': 'onNewWorld'  # Maybe?
     'level-set-letterbox': 'onSetLetterbox'
-    'tome:cast-spells': 'onCastSpells'
 
   events:
     'click #debug-toggle': 'onToggleDebug'
@@ -151,10 +150,10 @@ module.exports = class LevelPlaybackView extends CocoView
     @barWidth = $('.progress', @$el).width()
 
   onNewWorld: (e) ->
-    @totalTime = e.world.totalFrames / e.world.frameRate
-    pct = parseInt(100 * e.world.totalFrames / e.world.maxTotalFrames) + '%'
+    @totalLoadedTime = e.world.frames.length * e.world.dt
+    @totalTime = e.world.totalFrames * e.world.dt
+    pct = parseInt(100 * e.world.frames.length / e.world.maxTotalFrames) + '%'
     @barWidth = $('.progress', @$el).css('width', pct).show().width()
-    @casting = false
     $('.scrubber .progress', @$el).slider('enable', true)
     @newTime = 0
     @currentTime = 0
@@ -189,11 +188,6 @@ module.exports = class LevelPlaybackView extends CocoView
   onViewKeyboardShortcuts: ->
     @openModalView new KeyboardShortcutsModal()
 
-  onCastSpells: (e) ->
-    return if e.preload
-    @casting = true
-    @$progressScrubber.slider('disable', true)
-
   onDisableControls: (e) ->
     if not e.controls or 'playback' in e.controls
       @disabled = true
@@ -279,7 +273,7 @@ module.exports = class LevelPlaybackView extends CocoView
       @timePopup.show()
 
   updateProgress: (progress) ->
-    $('.scrubber .progress-bar', @$el).css('width', "#{progress*100}%")
+    $('.scrubber .progress-bar', @$el).css('width', "#{progress * 100 * @totalTime / @totalLoadedTime}%")
 
   updatePlayButton: (progress) ->
     if progress >= 0.99 and @lastProgress < 0.99
@@ -330,9 +324,7 @@ module.exports = class LevelPlaybackView extends CocoView
     return if @shouldIgnore()
     Backbone.Mediator.publish 'level-set-time', ratio: ratio, scrubDuration: duration
 
-  shouldIgnore: ->
-    #return @disabled or @casting or false  # STREAM: figure this out
-    return false
+  shouldIgnore: -> return @disabled
 
   onTogglePlay: (e) ->
     e?.preventDefault()

From 611ecbf4703454eedcca4a175ce6705f8f429c25 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Fri, 22 Aug 2014 12:39:29 -0700
Subject: [PATCH 04/21] Fixed many issues with world streaming.

---
 app/lib/Angel.coffee             | 45 ++++++++++++++++----------------
 app/lib/surface/Surface.coffee   |  2 +-
 app/lib/world/thang.coffee       |  6 ++++-
 app/lib/world/thang_state.coffee | 13 +++++++--
 app/lib/world/world.coffee       | 40 +++++++++++++++-------------
 5 files changed, 62 insertions(+), 44 deletions(-)

diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee
index af28428c7..55188a301 100644
--- a/app/lib/Angel.coffee
+++ b/app/lib/Angel.coffee
@@ -64,16 +64,6 @@ module.exports = class Angel extends CocoClass
         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
-        unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializingStreamingFrames or @shared.firstWorld
-          @worker.postMessage func: 'serializeFramesSoFar'  # Stream it!
-      when 'console-log'
-        @log event.data.args...
-      when 'user-code-problem'
-        Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
-
       # We have to abort like an infinite loop if we see one of these; they're not really recoverable
       when 'non-user-code-problem'
         Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem
@@ -82,16 +72,7 @@ module.exports = class Angel extends CocoClass
         else
           @fireWorker()
 
-      # We have some of the frames serialized, so let's send the partially simulated world to the Surface.
-      when 'some-frames-serialized'
-        #console.log "angel received some frames", event.data.serialized, "with goals", event.data.goalStates, "and streaming into world", @shared.streamingWorld
-        @deserializingStreamingFrames = true
-        @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
-
-      # Either the world finished simulating successfully, or we abort the worker.
-      when 'new-world'
-        #console.log "angel received alll frames", event.data.serialized, "and have streaming world", @shared.streamingWorld
-        @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
+      # If it didn't finish simulating successfully, or we abort the worker.
       when 'abort'
         @say 'Aborted.', event.data
         clearTimeout @abortTimeout
@@ -100,6 +81,23 @@ module.exports = class Angel extends CocoClass
         _.remove @shared.busyAngels, @
         @doWork()
 
+      # We pay attention to certain progress indicators as the world loads.
+      when 'console-log'
+        @log event.data.args...
+      when 'user-code-problem'
+        Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
+      when 'world-load-progress-changed'
+        Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
+        unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializationQueue.length or @shared.firstWorld
+          @worker.postMessage func: 'serializeFramesSoFar'  # Stream it!
+
+      # We have some or all of the frames serialized, so let's send the (partially?) simulated world to the Surface.
+      when 'some-frames-serialized', 'new-world'
+        deserializationArgs = [event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld]
+        @deserializationQueue.push deserializationArgs
+        if @deserializationQueue.length is 1
+          @beholdWorld deserializationArgs...
+
       else
         @log 'Received unsupported message:', event.data
 
@@ -131,12 +129,14 @@ module.exports = class Angel extends CocoClass
       @shared.goalManager?.world = world
       @finishWork()
     else
-      @deserializingStreamingFrames = false
+      @deserializationQueue.shift()  # Finished with this deserialization.
+      if deserializationArgs = @deserializationQueue[0]  # Start another?
+        @beholdWorld deserializationArgs...
 
   finishWork: ->
     @shared.streamingWorld = null
     @shared.firstWorld = false
-    @deserializingStreamingFrames = false
+    @deserializationQueue = []
     @running = false
     _.remove @shared.busyAngels, @
     @doWork()
@@ -163,6 +163,7 @@ module.exports = class Angel extends CocoClass
       @say 'Running world...'
       @running = true
       @shared.busyAngels.push @
+      @deserializationQueue = []
       @worker.postMessage func: 'runWorld', args: @work
       clearTimeout @purgatoryTimer
       @say 'Infinite loop timer started at interval of', @infiniteLoopIntervalDuration
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 1e01b3885..4b773f0fe 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -378,7 +378,7 @@ module.exports = Surface = class Surface extends CocoClass
       ffScrubDuration = 1000 * ffToTime / ffSpeed
       ffScrubDuration = Math.min(ffScrubDuration, ffInterval)
       ffFactor = ffInterval / ffScrubDuration
-      if ffFactor > 2
+      if ffFactor > 1.5
         createjs.Tween.removeTweens(@)
         @scrubbingTo = null
         @fastForwarding = true
diff --git a/app/lib/world/thang.coffee b/app/lib/world/thang.coffee
index 64eb698c6..d3379db9f 100644
--- a/app/lib/world/thang.coffee
+++ b/app/lib/world/thang.coffee
@@ -85,7 +85,7 @@ module.exports = class Thang
           throw new Error "Two types were specified for trackable property #{prop}: #{oldType} and #{type}."
 
   keepTrackedProperty: (prop) ->
-    # Hmm; can we do this faster?
+    # Wish we could do this faster, but I can't think of how.
     propIndex = @trackedPropertiesKeys.indexOf prop
     if propIndex isnt -1
       @trackedPropertiesUsed[propIndex] = true
@@ -147,6 +147,8 @@ module.exports = class Thang
     for trackedFinalProperty in @trackedFinalProperties ? []
       # TODO: take some (but not all) of serialize logic from ThangState to handle other types
       o.finalState[trackedFinalProperty] = @[trackedFinalProperty]
+    # Since we might keep tracked properties later during streaming, we need to know which we think are unused.
+    o.unusedTrackedPropertyKeys = (@trackedPropertiesKeys[propIndex] for used, propIndex in @trackedPropertiesUsed when not used)
     o
 
   @deserialize: (o, world, classMap) ->
@@ -154,6 +156,8 @@ module.exports = class Thang
     for [componentClassName, componentConfig] in o.components
       componentClass = classMap[componentClassName]
       t.addComponents [componentClass, componentConfig]
+    t.unusedTrackedPropertyKeys = o.unusedTrackedPropertyKeys
+    t.unusedTrackedPropertyValues = (t[prop] for prop in o.unusedTrackedPropertyKeys)
     for prop, val of o.finalState
       # TODO: take some (but not all) of deserialize logic from ThangState to handle other types
       t[prop] = val
diff --git a/app/lib/world/thang_state.coffee b/app/lib/world/thang_state.coffee
index 473224b58..86caa1a64 100644
--- a/app/lib/world/thang_state.coffee
+++ b/app/lib/world/thang_state.coffee
@@ -65,7 +65,10 @@ module.exports = class ThangState
     # Get the property, whether we have it stored in @props or in @trackedPropertyValues. Optimize it.
     # Figured based on http://jsperf.com/object-vs-array-vs-native-linked-list/13 that it should be faster with small arrays to do the indexOf reads (each up to 24x faster) than to do a single object read, and then we don't have to maintain an extra @props object; just keep array
     propIndex = @trackedPropertyKeys.indexOf prop
-    return null if propIndex is -1
+    if propIndex is -1
+      initialPropIndex = @thang.unusedTrackedPropertyKeys.indexOf prop
+      return null if initialPropIndex is -1
+      return @thang.unusedTrackedPropertyValues[initialPropIndex]
     value = @props[propIndex]
     return value if value isnt undefined or @hasRestored
     return @props[propIndex] = @getStoredProp propIndex
@@ -74,22 +77,28 @@ module.exports = class ThangState
     # Restore trackedProperties' values to @thang, retrieving them from @trackedPropertyValues if needed. Optimize it.
     return @ if @thang._state is @ and not @thang.partialState
     unless @hasRestored  # Restoring in a deserialized World for first time
+      for prop, propIndex in @thang.unusedTrackedPropertyKeys when @trackedPropertyKeys.indexOf(prop) is -1
+        @thang[prop] = @thang.unusedTrackedPropertyValues[propIndex]
       props = []
       for prop, propIndex in @trackedPropertyKeys
         type = @trackedPropertyTypes[propIndex]
         storage = @trackedPropertyValues[propIndex]
-        props.push(@thang[prop] = @getStoredProp propIndex, type, storage)
+        props.push @thang[prop] = @getStoredProp propIndex, type, storage
         #console.log @frameIndex, @thang.id, prop, propIndex, type, storage, 'got', @thang[prop]
       @props = props
       @trackedPropertyTypes = @trackedPropertyValues = @specialKeysToValues = null  # leave @trackedPropertyKeys for indexing
       @hasRestored = true
     else  # Restoring later times
+      for prop, propIndex in @thang.unusedTrackedPropertyKeys when @trackedPropertyKeys.indexOf(prop) is -1
+        @thang[prop] = @thang.unusedTrackedPropertyValues[propIndex]
       for prop, propIndex in @trackedPropertyKeys
         @thang[prop] = @props[propIndex]
     @thang.partialState = false
     @
 
   restorePartial: (ratio) ->
+    # Don't think we need to worry about unusedTrackedPropertyValues here.
+    # If it's not tracked yet, it'll very rarely partially change between frames; we can afford to miss the first one.
     inverse = 1 - ratio
     for prop, propIndex in @trackedPropertyKeys when prop is 'pos' or prop is 'rotation'
       if @hasRestored
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index 07be18e5b..3c51abb8a 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -11,7 +11,7 @@ WorldScriptNote = require './world_script_note'
 Component = require 'lib/world/component'
 System = require 'lib/world/system'
 PROGRESS_UPDATE_INTERVAL = 100
-DESERIALIZATION_INTERVAL = 20
+DESERIALIZATION_INTERVAL = 10
 ITEM_ORIGINAL = '53e12043b82921000051cdf9'
 
 module.exports = class World
@@ -291,7 +291,7 @@ module.exports = class World
     # Code hotspot; optimize it
     startFrame = @framesSerializedSoFar
     endFrame = @frames.length
-    console.log "... world serializing frames from", startFrame, "to", endFrame
+    #console.log "... world serializing frames from", startFrame, "to", endFrame
     [transferableObjects, nontransferableObjects] = [0, 0]
     o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
     o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
@@ -307,20 +307,21 @@ module.exports = class World
     o.trackedPropertiesPerThangKeys = []
     o.trackedPropertiesPerThangTypes = []
     trackedPropertiesPerThangValues = []  # We won't send these, just the offsets and the storage buffer
-    o.trackedPropertiesPerThangValuesOffsets = []  # Needed to reconstruct ArrayBufferViews on other end, since Firefox has bugs transfering those: https://bugzilla.mozilla.org/show_bug.cgi?id=841904 and https://bugzilla.mozilla.org/show_bug.cgi?id=861925  # Actually, as of January 2014, it should be fixed.
+    o.trackedPropertiesPerThangValuesOffsets = []  # Needed to reconstruct ArrayBufferViews on other end, since Firefox has bugs transfering those: https://bugzilla.mozilla.org/show_bug.cgi?id=841904 and https://bugzilla.mozilla.org/show_bug.cgi?id=861925  # Actually, as of January 2014, it should be fixed. So we could try to undo the workaround.
     transferableStorageBytesNeeded = 0
     nFrames = endFrame - startFrame
     streaming = nFrames < @totalFrames
     for thang in @thangs
       # Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles).
-      # Check both, since sometimes people mark stateless Thangs but don't change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
-      continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean) and not streaming
+      # Check both, since sometimes people mark stateless Thangs but then change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
+      # If streaming the world, a thang marked stateless that actually change will get messed up. I think.
+      continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean)
       o.trackedPropertiesThangIDs.push thang.id
       trackedPropertiesIndices = []
       trackedPropertiesKeys = []
       trackedPropertiesTypes = []
       for used, propIndex in thang.trackedPropertiesUsed
-        continue unless used# or streaming
+        continue unless used
         trackedPropertiesIndices.push propIndex
         trackedPropertiesKeys.push thang.trackedPropertiesKeys[propIndex]
         trackedPropertiesTypes.push thang.trackedPropertiesTypes[propIndex]
@@ -418,6 +419,7 @@ module.exports = class World
     perf.t3 = now()
 
     perf.batches = 0
+    perf.framesCPUTime = 0
     w.frames = [] unless streamingWorld
     clearTimeout @deserializationTimeout if @deserializationTimeout
     @deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
@@ -428,26 +430,27 @@ module.exports = class World
     startTime = now()
     for frameIndex in [w.frames.length ... endFrame]
       w.frames.push WorldFrame.deserialize(w, frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesThangs, o.trackedPropertiesPerThangKeys, o.trackedPropertiesPerThangTypes, o.trackedPropertiesPerThangValues, o.specialKeysToValues, o.frameHashes[frameIndex - startFrame], w.dt * frameIndex)
-      if (now() - startTime) > DESERIALIZATION_INTERVAL
-        console.log "  Deserialization not finished, let's do it again soon. Have:", w.frames.length, ", wanted from", startFrame, "to", endFrame
+      elapsed = now() - startTime
+      if elapsed > DESERIALIZATION_INTERVAL and frameIndex < endFrame - 1
+        #console.log "  Deserialization not finished, let's do it again soon. Have:", w.frames.length, ", wanted from", startFrame, "to", endFrame
+        perf.framesCPUTime += elapsed
         @deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
         return
     @deserializationTimeout = null
+    perf.framesCPUTime += elapsed
     @finishDeserializing w, finishedWorldCallback, perf, startFrame, endFrame
 
   @finishDeserializing: (w, finishedWorldCallback, perf, startFrame, endFrame) ->
     perf.t4 = now()
-    nFrames = endFrame - startFrame
     w.ended = true
-    w.getFrame(endFrame - 1).restoreState()
-    perf.t5 = now()
-    console.log 'Deserialization:', (perf.t5 - perf.t0).toFixed(0) + 'ms (' + ((perf.t5 - perf.t0) / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches.'
+    nFrames = endFrame - startFrame
+    totalCPUTime = perf.t3 - perf.t0 + perf.framesCPUTime
+    console.log 'Deserialization:', totalCPUTime.toFixed(0) + 'ms (' + (totalCPUTime / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches. Did', startFrame, 'to', endFrame, 'in', (perf.t4 - perf.t0).toFixed(0) + 'ms wall clock time.'
     if false
       console.log '  Deserializing--constructing new World:', (perf.t1 - perf.t0).toFixed(2) + 'ms'
       console.log '  Deserializing--Thangs and ScriptNotes:', (perf.t2 - perf.t1).toFixed(2) + 'ms'
       console.log '  Deserializing--reallocating memory:', (perf.t3 - perf.t2).toFixed(2) + 'ms'
-      console.log '  Deserializing--WorldFrames:', (perf.t4 - perf.t3).toFixed(2) + 'ms'
-      console.log '  Deserializing--restoring last WorldFrame:', (perf.t5 - perf.t4).toFixed(2) + 'ms'
+      console.log '  Deserializing--WorldFrames:', (perf.t4 - perf.t3).toFixed(2) + 'ms wall clock time,', (perf.framesCPUTime).toFixed(2) + 'ms CPU time'
     finishedWorldCallback w
 
   findFirstChangedFrame: (oldWorld) ->
@@ -456,10 +459,11 @@ module.exports = class World
       oldFrame = oldWorld.frames[i]
       break unless oldFrame and ((newFrame.hash is oldFrame.hash) or not newFrame.hash? or not oldFrame.hash?)  # undefined gets in there when streaming at the last frame of each batch for some reason
     firstChangedFrame = i
-    if @frames[i]
-      console.log 'First changed frame is', firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
-    else
-      console.log 'No frames were changed out of all', @frames.length
+    if @frames.length is @totalFrames
+      if @frames[i]
+        console.log 'First changed frame is', firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
+      else
+        console.log 'No frames were changed out of all', @frames.length
     firstChangedFrame
 
   pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) ->

From 92f0a68dd1cf260d93cfae79a348e1b1f2d92015 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Fri, 22 Aug 2014 14:59:32 -0700
Subject: [PATCH 05/21] World streaming is getting there!

---
 app/assets/javascripts/workers/worker_world.js | 2 +-
 app/lib/Angel.coffee                           | 8 +++++---
 app/lib/surface/Surface.coffee                 | 2 +-
 app/lib/world/world.coffee                     | 4 ++--
 app/views/play/level/LevelHUDView.coffee       | 2 ++
 app/views/play/level/LevelPlaybackView.coffee  | 5 ++---
 6 files changed, 13 insertions(+), 10 deletions(-)

diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js
index b4f1f5e6f..b9b73aaac 100644
--- a/app/assets/javascripts/workers/worker_world.js
+++ b/app/assets/javascripts/workers/worker_world.js
@@ -366,7 +366,7 @@ self.runWorld = function runWorld(args) {
 };
 
 self.serializeFramesSoFar = function serializeFramesSoFar() {
-  if(!self.world) return console.error("hmm, no world when we went to serialize some frames?");
+  if(!self.world) return;  // We probably got this message late, after delivering the world.
   if(self.world.framesSerializedSoFar == self.world.frames.length) return;
   self.onWorldLoaded();
   self.world.framesSerializedSoFar = self.world.frames.length;
diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee
index 55188a301..9abbb0426 100644
--- a/app/lib/Angel.coffee
+++ b/app/lib/Angel.coffee
@@ -93,7 +93,7 @@ module.exports = class Angel extends CocoClass
 
       # We have some or all of the frames serialized, so let's send the (partially?) simulated world to the Surface.
       when 'some-frames-serialized', 'new-world'
-        deserializationArgs = [event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld]
+        deserializationArgs = [event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @streamingWorld]
         @deserializationQueue.push deserializationArgs
         if @deserializationQueue.length is 1
           @beholdWorld deserializationArgs...
@@ -116,7 +116,7 @@ module.exports = class Angel extends CocoClass
 
   finishBeholdingWorld: (goalStates) -> (world) =>
     return if @aborting
-    @shared.streamingWorld = world
+    @streamingWorld = world
     finished = world.frames.length is world.totalFrames
     firstChangedFrame = world.findFirstChangedFrame @shared.world
     eventType = if finished then 'god:new-world-created' else 'god:streaming-world-updated'
@@ -134,7 +134,7 @@ module.exports = class Angel extends CocoClass
         @beholdWorld deserializationArgs...
 
   finishWork: ->
-    @shared.streamingWorld = null
+    @streamingWorld = null
     @shared.firstWorld = false
     @deserializationQueue = []
     @running = false
@@ -177,6 +177,8 @@ module.exports = class Angel extends CocoClass
     @say 'Aborting...'
     @running = false
     @work = null
+    @streamingWorld = null
+    @deserializationQueue = null
     _.remove @shared.busyAngels, @
     @abortTimeout = _.delay @fireWorker, @abortTimeoutDuration
     @aborting = true
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 4b773f0fe..27d6c0605 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -246,7 +246,7 @@ module.exports = Surface = class Surface extends CocoClass
   getCurrentFrame: ->
     return Math.max(0, Math.min(Math.floor(@currentFrame), @world.frames.length - 1))
 
-  getProgress: -> @currentFrame / @world.totalFrames
+  getProgress: -> @currentFrame / @world.frames.length
 
   onLevelRestarted: (e) ->
     @setProgress 0, 0
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index 3c51abb8a..5042fb02f 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -94,7 +94,7 @@ module.exports = class World
     else
       frameToLoadUntil = @totalFrames
     i = @frames.length
-    while i < frameToLoadUntil
+    while i < frameToLoadUntil and i < @totalFrames
       if @debugging
         for thang in @thangs when thang.isProgrammable
           userCode = @userCodeMap[thang.id] ? {}
@@ -291,7 +291,7 @@ module.exports = class World
     # Code hotspot; optimize it
     startFrame = @framesSerializedSoFar
     endFrame = @frames.length
-    #console.log "... world serializing frames from", startFrame, "to", endFrame
+    #console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames
     [transferableObjects, nontransferableObjects] = [0, 0]
     o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
     o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
diff --git a/app/views/play/level/LevelHUDView.coffee b/app/views/play/level/LevelHUDView.coffee
index 6daa1d8d9..a273712b2 100644
--- a/app/views/play/level/LevelHUDView.coffee
+++ b/app/views/play/level/LevelHUDView.coffee
@@ -70,6 +70,8 @@ module.exports = class LevelHUDView extends CocoView
     @thang = e.world.thangMap[@thang.id] if @thang
     if hadThang and not @thang
       @setThang null, null
+    else if @thang
+      @createActions()  # Make sure it updates its actions.
 
   setThang: (thang, thangType) ->
     unless @speaker
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index 4fb24f8ef..6538af114 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -150,8 +150,7 @@ module.exports = class LevelPlaybackView extends CocoView
     @barWidth = $('.progress', @$el).width()
 
   onNewWorld: (e) ->
-    @totalLoadedTime = e.world.frames.length * e.world.dt
-    @totalTime = e.world.totalFrames * e.world.dt
+    @totalTime = e.world.frames.length * e.world.dt
     pct = parseInt(100 * e.world.frames.length / e.world.maxTotalFrames) + '%'
     @barWidth = $('.progress', @$el).css('width', pct).show().width()
     $('.scrubber .progress', @$el).slider('enable', true)
@@ -273,7 +272,7 @@ module.exports = class LevelPlaybackView extends CocoView
       @timePopup.show()
 
   updateProgress: (progress) ->
-    $('.scrubber .progress-bar', @$el).css('width', "#{progress * 100 * @totalTime / @totalLoadedTime}%")
+    $('.scrubber .progress-bar', @$el).css('width', "#{progress * 100}%")
 
   updatePlayButton: (progress) ->
     if progress >= 0.99 and @lastProgress < 0.99

From ee41796b7330b3054523c6d5554458eddcb62267 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Fri, 22 Aug 2014 15:32:23 -0700
Subject: [PATCH 06/21] Fixed some wonkiness with the playback scrubbing while
 streaming worlds.

---
 app/lib/surface/Surface.coffee                |  2 +-
 app/styles/play/level/playback.sass           |  3 +-
 app/views/play/level/LevelPlaybackView.coffee | 35 ++++++++++---------
 3 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 27d6c0605..eeb3ce345 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -357,7 +357,7 @@ module.exports = Surface = class Surface extends CocoClass
     @casting = true
     @setPlayingCalled = false  # Don't overwrite playing settings if they changed by, say, scripts.
     @frameBeforeCast = @currentFrame
-    @currentFrame = 0
+    @setProgress 0
 
   onNewWorld: (event) ->
     return unless event.world.name is @world.name
diff --git a/app/styles/play/level/playback.sass b/app/styles/play/level/playback.sass
index ebeff9846..e40145362 100644
--- a/app/styles/play/level/playback.sass
+++ b/app/styles/play/level/playback.sass
@@ -97,12 +97,13 @@
       background-image: none
       border-radius: 0
       border: 0
+      //@include transition(width .2s linear)  // can't do this because handle then jitters
 
       &.disabled
         cursor: default
 
       .progress-bar
-        @include transition(width .0s linear)
+        @include transition(width 0s linear)
         position: relative
         pointer-events: none
         // Remove gradient background in favor of solid fill
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index 6538af114..92363fcc7 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -114,6 +114,15 @@ module.exports = class LevelPlaybackView extends CocoView
     ua = navigator.userAgent.toLowerCase()
     if /safari/.test(ua) and not /chrome/.test(ua)
       @$el.find('.toggle-fullscreen').hide()
+    @timePopup ?= new HoverPopup
+    t = $.i18n.t
+    @second = t 'units.second'
+    @seconds = t 'units.seconds'
+    @minute = t 'units.minute'
+    @minutes = t 'units.minutes'
+    @goto = t 'play_level.time_goto'
+    @current = t 'play_level.time_current'
+    @total = t 'play_level.time_total'
 
   updatePopupContent: ->
     @timePopup?.updateContent "<h2>#{@timeToString @newTime}</h2>#{@formatTime(@current, @currentTime)}<br/>#{@formatTime(@total, @totalTime)}"
@@ -150,23 +159,16 @@ module.exports = class LevelPlaybackView extends CocoView
     @barWidth = $('.progress', @$el).width()
 
   onNewWorld: (e) ->
-    @totalTime = e.world.frames.length * e.world.dt
-    pct = parseInt(100 * e.world.frames.length / e.world.maxTotalFrames) + '%'
+    @updateBarWidth e.world.frames.length, e.world.maxTotalFrames, e.world.dt
+
+  updateBarWidth: (loadedFrameCount, maxTotalFrames, dt) ->
+    @totalTime = loadedFrameCount * dt
+    pct = parseInt(100 * loadedFrameCount / maxTotalFrames) + '%'
     @barWidth = $('.progress', @$el).css('width', pct).show().width()
     $('.scrubber .progress', @$el).slider('enable', true)
     @newTime = 0
     @currentTime = 0
-
-    @timePopup ?= new HoverPopup
-
-    t = $.i18n.t
-    @second = t 'units.second'
-    @seconds = t 'units.seconds'
-    @minute = t 'units.minute'
-    @minutes = t 'units.minutes'
-    @goto = t 'play_level.time_goto'
-    @current = t 'play_level.time_current'
-    @total = t 'play_level.time_total'
+    @lastLoadedFrameCount = loadedFrameCount
 
   onToggleDebug: ->
     return if @shouldIgnore()
@@ -249,7 +251,7 @@ module.exports = class LevelPlaybackView extends CocoView
       # @currentTime = @totalTime if Math.abs(@totalTime - @currentTime) < 0.04
       @updatePopupContent() if @timePopup?.shown
 
-      @updateProgress(e.progress)
+      @updateProgress(e.progress, e.world)
       @updatePlayButton(e.progress)
     @lastProgress = e.progress
 
@@ -271,7 +273,9 @@ module.exports = class LevelPlaybackView extends CocoView
     if @timePopup and Math.abs(@currentTime - @newTime) < 1 and not @timePopup.shown
       @timePopup.show()
 
-  updateProgress: (progress) ->
+  updateProgress: (progress, world) ->
+    if world.frames.length isnt @lastLoadedFrameCount
+      @updateBarWidth world.frames.length, world.maxTotalFrames, world.dt
     $('.scrubber .progress-bar', @$el).css('width', "#{progress * 100}%")
 
   updatePlayButton: (progress) ->
@@ -293,7 +297,6 @@ module.exports = class LevelPlaybackView extends CocoView
 
   hookUpScrubber: ->
     @sliderIncrements = 500  # max slider width before we skip pixels
-    @clickingSlider = false  # whether the mouse has been pressed down without moving
     @$progressScrubber.slider(
       max: @sliderIncrements
       animate: 'slow'

From 6fee881efdd979c7a4ecbedc172ddbaa52ece21a Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Fri, 22 Aug 2014 17:11:40 -0700
Subject: [PATCH 07/21] Fixed a few more issues with playback.

---
 app/lib/surface/Surface.coffee      | 27 +++++++++++++--------------
 app/styles/play/level/playback.sass |  5 +++--
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index eeb3ce345..5d3425e1a 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -116,7 +116,7 @@ module.exports = Surface = class Surface extends CocoClass
 
   setWorld: (@world) ->
     @worldLoaded = true
-    lastFrame = Math.min(@getCurrentFrame(), @world.totalFrames - 1)
+    lastFrame = Math.min(@getCurrentFrame(), @world.frames.length - 1)
     @world.getFrame(lastFrame).restoreState() unless @options.choosing
     @spriteBoss.world = @world
 
@@ -203,7 +203,7 @@ module.exports = Surface = class Surface extends CocoClass
       createjs.Tween.removeTweens(@)
       @currentFrame = @scrubbingTo
 
-    @scrubbingTo = Math.min(Math.round(progress * @world.totalFrames), @world.totalFrames)
+    @scrubbingTo = Math.min(Math.round(progress * @world.frames.length), @world.frames.length)
     @scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5))
     ease = if @fastForwarding then createjs.Ease.linear else createjs.Ease.sineInOut
     if scrubDuration
@@ -289,25 +289,25 @@ module.exports = Surface = class Surface extends CocoClass
     if @playing and @currentFrame >= (@world.totalFrames - 5)
       @currentFrame = 0
     if @fastForwarding and not @playing
-      @setProgress @currentFrame / @world.totalFrames
+      @setProgress @currentFrame / @world.frames.length
 
   onSetTime: (e) ->
     toFrame = @currentFrame
     if e.time?
-      @worldLifespan = @world.totalFrames / @world.frameRate
+      @worldLifespan = @world.frames.length / @world.frameRate
       e.ratio = e.time / @worldLifespan
     if e.ratio?
-      toFrame = @world.totalFrames * e.ratio
+      toFrame = @world.frames.length * e.ratio
     if e.frameOffset
       toFrame += e.frameOffset
     if e.ratioOffset
-      toFrame += @world.totalFrames * e.ratioOffset
+      toFrame += @world.frames.length * e.ratioOffset
     unless _.isNumber(toFrame) and not _.isNaN(toFrame)
       return console.error('set-time event', e, 'produced invalid target frame', toFrame)
-    @setProgress(toFrame / @world.totalFrames, e.scrubDuration)
+    @setProgress(toFrame / @world.frames.length, e.scrubDuration)
 
   onFrameChanged: (force) ->
-    @currentFrame = Math.min(@currentFrame, @world.totalFrames)
+    @currentFrame = Math.min(@currentFrame, @world.frames.length)
     @debugDisplay?.updateFrame @currentFrame
     return if @currentFrame is @lastFrame and not force
     progress = @getProgress()
@@ -319,7 +319,7 @@ module.exports = Surface = class Surface extends CocoClass
       world: @world
     )
 
-    if @lastFrame < @world.totalFrames and @currentFrame >= @world.totalFrames - 1
+    if @lastFrame < @world.frames.length and @currentFrame >= @world.totalFrames - 1
       @ended = true
       @setPaused true
       Backbone.Mediator.publish 'surface:playback-ended'
@@ -371,7 +371,7 @@ module.exports = Surface = class Surface extends CocoClass
     @setWorld event.world
     @onFrameChanged(true)
     if @playing and ffToFrame = Math.min event.firstChangedFrame, @frameBeforeCast, event.world.frames.length
-      ffToRatio = ffToFrame / @world.totalFrames
+      ffToRatio = ffToFrame / @world.frames.length
       ffToTime = ffToFrame * @world.dt
       ffSpeed = Math.max 4, ffToTime / 3
       ffInterval = 1000 * (ffToFrame - @currentFrame) / @options.frameRate
@@ -559,7 +559,7 @@ module.exports = Surface = class Surface extends CocoClass
     # seems to be a bug where only one object can register with the Ticker...
     oldFrame = @currentFrame
     oldWorldFrame = Math.floor oldFrame
-    lastFrame = @world.totalFrames - 1
+    lastFrame = @world.frames.length - 1
     while true
       Dropper.tick()
       @trailmaster.tick() if @trailmaster
@@ -593,11 +593,10 @@ module.exports = Surface = class Surface extends CocoClass
   restoreWorldState: ->
     frame = @world.getFrame(@getCurrentFrame())
     frame.restoreState()
-    current = Math.max(0, Math.min(@currentFrame, @world.totalFrames - 1))
-    if current - Math.floor(current) > 0.01
+    current = Math.max(0, Math.min(@currentFrame, @world.frames.length - 1))
+    if current - Math.floor(current) > 0.01 and Math.ceil(current) < @world.frames.length - 1
       next = Math.ceil current
       ratio = current % 1
-      console.log "trying to restore partial state between", current, next, "of frames", @world.frames.length, "frames" unless @world.frames[next]
       @world.frames[next].restorePartialState ratio if next > 1
     frame.clearEvents() if parseInt(@currentFrame) is parseInt(@lastFrame)
     @spriteBoss.updateSounds() if parseInt(@currentFrame) isnt parseInt(@lastFrame)
diff --git a/app/styles/play/level/playback.sass b/app/styles/play/level/playback.sass
index e40145362..b67e8ee28 100644
--- a/app/styles/play/level/playback.sass
+++ b/app/styles/play/level/playback.sass
@@ -97,13 +97,14 @@
       background-image: none
       border-radius: 0
       border: 0
-      //@include transition(width .2s linear)  // can't do this because handle then jitters
+      // Can't do this transition because handle then jitters, but would be good for streaming.
+      //@include transition(width .2s linear)
 
       &.disabled
         cursor: default
 
       .progress-bar
-        @include transition(width 0s linear)
+        @include transition(width .0s linear)
         position: relative
         pointer-events: none
         // Remove gradient background in favor of solid fill

From 9c0869e56658b9430caa6fd54603cff879b286dd Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Fri, 22 Aug 2014 21:35:08 -0700
Subject: [PATCH 08/21] Press Ctrl+Shift+Enter to cast in real-time, and click
 on the ground to use your wizard as a flag. OMG, it not only works, but it is
 really fun!

---
 .../javascripts/workers/worker_world.js       |  6 ++
 app/lib/Angel.coffee                          |  8 ++
 app/lib/God.coffee                            |  5 +-
 app/lib/LevelBus.coffee                       |  1 +
 app/lib/surface/CastingScreen.coffee          | 81 -------------------
 app/lib/surface/Surface.coffee                |  3 -
 app/lib/world/world.coffee                    | 35 +++++++-
 app/views/play/level/tome/SpellView.coffee    | 16 ++--
 app/views/play/level/tome/TomeView.coffee     |  6 +-
 9 files changed, 63 insertions(+), 98 deletions(-)
 delete mode 100644 app/lib/surface/CastingScreen.coffee

diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js
index b9b73aaac..d45a91e2e 100644
--- a/app/assets/javascripts/workers/worker_world.js
+++ b/app/assets/javascripts/workers/worker_world.js
@@ -346,6 +346,7 @@ self.runWorld = function runWorld(args) {
       self.world.loadFromLevel(args.level, true);
     self.world.preloading = args.preload;
     self.world.headless = args.headless;
+    self.world.realTime = args.realTime;
     self.goalManager = new GoalManager(self.world);
     self.goalManager.setGoals(args.goals);
     self.goalManager.setCode(args.userCodeMap);
@@ -458,6 +459,11 @@ self.finalizePreload = function finalizePreload() {
   self.world.finalizePreload(self.onWorldLoaded);
 };
 
+self.updateFlags = function updateFlags(flags) {
+  if(!self.world || self.world.framesSerializedSoFar == self.world.frames.length) return;
+  self.world.updateFlags(flags);
+};
+
 self.addEventListener('message', function(event) {
   self[event.data.func](event.data.args);
 });
diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee
index 9abbb0426..fa9fb75cf 100644
--- a/app/lib/Angel.coffee
+++ b/app/lib/Angel.coffee
@@ -12,6 +12,9 @@ module.exports = class Angel extends CocoClass
   infiniteLoopTimeoutDuration: 7500  # wait this long for a response when checking
   abortTimeoutDuration: 500  # give in-process or dying workers this long to give up
 
+  subscriptions:
+    'self-wizard:target-changed': 'onSelfWizardTargetChanged'
+
   constructor: (@shared) ->
     super()
     @say 'Got my wings.'
@@ -205,6 +208,11 @@ module.exports = class Angel extends CocoClass
     @worker.addEventListener 'message', @onWorkerMessage
     @worker.creationTime = new Date()
 
+  onSelfWizardTargetChanged: (e) ->
+    return unless @running and @work.realTime
+    targetPos = e.sender.targetPos
+    @worker.postMessage func: 'updateFlags', args: [{type: 'wizard', targetPos: targetPos}]
+
 
   #### Synchronous code for running worlds on main thread (profiling / IE9) ####
   simulateSync: (work) =>
diff --git a/app/lib/God.coffee b/app/lib/God.coffee
index ffe2ad726..3af511bc6 100644
--- a/app/lib/God.coffee
+++ b/app/lib/God.coffee
@@ -53,9 +53,9 @@ module.exports = class God extends CocoClass
   setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap
 
   onTomeCast: (e) ->
-    @createWorld e.spells, e.preload
+    @createWorld e.spells, e.preload, e.realTime
 
-  createWorld: (spells, preload=false) ->
+  createWorld: (spells, preload=false, realTime=false) ->
     console.log "#{@nick}: Let there be light upon #{@level.name}! (preload: #{preload})"
     userCodeMap = @getUserCodeMap spells
 
@@ -84,6 +84,7 @@ module.exports = class God extends CocoClass
       headless: @angelsShare.headless
       preload: preload
       synchronous: not Worker?  # Profiling world simulation is easier on main thread, or we are IE9.
+      realTime: realTime
     angel.workIfIdle() for angel in @angelsShare.angels
 
   getUserCodeMap: (spells) ->
diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee
index 7ea0837c1..9631c3476 100644
--- a/app/lib/LevelBus.coffee
+++ b/app/lib/LevelBus.coffee
@@ -197,6 +197,7 @@ module.exports = class LevelBus extends Bus
   onNewGoalStates: ({goalStates})->
     state = @session.get 'state'
     unless utils.kindaEqual state.goalStates, goalStates # Only save when goals really change
+      return console.error("Somehow trying to save null goal states!", goalStates) if _.find(goalStates, (gs) -> not gs.status)
       state.goalStates = goalStates
       @session.set 'state', state
       @changedSessionProperties.state = true
diff --git a/app/lib/surface/CastingScreen.coffee b/app/lib/surface/CastingScreen.coffee
deleted file mode 100644
index 63eea858e..000000000
--- a/app/lib/surface/CastingScreen.coffee
+++ /dev/null
@@ -1,81 +0,0 @@
-CocoClass = require 'lib/CocoClass'
-
-module.exports = class CastingScreen extends CocoClass
-  subscriptions:
-    'tome:cast-spells': 'onCastingBegins'
-    'god:new-world-created': 'onCastingEnds'
-    'god:world-load-progress-changed': 'onWorldLoadProgressChanged'
-
-  constructor: (options) ->
-    super()
-    options ?= {}
-    @camera = options.camera
-    @layer = options.layer
-    console.error @toString(), 'needs a camera.' unless @camera
-    console.error @toString(), 'needs a layer.' unless @layer
-    @build()
-
-  onCastingBegins: (e) -> @show() unless e.preload
-  onCastingEnds: (e) -> @hide()
-
-  toString: -> '<CastingScreen>'
-
-  build: ->
-    @dimLayer = new createjs.Container()
-    @dimLayer.mouseEnabled = @dimLayer.mouseChildren = false
-    @dimLayer.layerIndex = -11
-    @dimLayer.addChild @dimScreen = new createjs.Shape()
-    @dimScreen.graphics.beginFill('rgba(0,0,0,0.5)').rect 0, 0, @camera.canvasWidth, @camera.canvasHeight
-    @dimLayer.alpha = 0
-    @layer.addChild @dimLayer
-    @dimLayer.addChild @makeProgressBar()
-    @dimLayer.addChild @makeCastingText()
-
-  onWorldLoadProgressChanged: (e) ->
-    if new Date().getTime() - @t0 > 500
-      createjs.Tween.removeTweens @progressBar
-      createjs.Tween.get(@progressBar).to({scaleX: e.progress}, 200)
-
-  makeProgressBar: ->
-    BAR_PIXEL_HEIGHT = 3
-    BAR_PCT_WIDTH = .75
-    pixelWidth = parseInt(@camera.canvasWidth * BAR_PCT_WIDTH)
-    pixelMargin = (@camera.canvasWidth - (@camera.canvasWidth * BAR_PCT_WIDTH)) / 2
-    barY = 3 * (@camera.canvasHeight / 5)
-
-    g = new createjs.Graphics()
-    g.beginFill(createjs.Graphics.getRGB(255, 255, 255))
-    g.drawRoundRect(0, 0, pixelWidth, BAR_PIXEL_HEIGHT, 3)
-    @progressBar = new createjs.Shape(g)
-    @progressBar.x = pixelMargin
-    @progressBar.y = barY
-    @progressBar.scaleX = 0
-    @dimLayer.addChild(@progressBar)
-
-  makeCastingText: ->
-    size = @camera.canvasHeight / 15
-    text = new createjs.Text('Casting', "#{size}px cursive", '#aaaaaa')
-    text.regX = text.getMeasuredWidth() / 2
-    text.regY = text.getMeasuredHeight() / 2
-    text.x = @camera.canvasWidth / 2
-    text.y = @camera.canvasHeight / 2
-    @text = text
-    return text
-
-  show: ->
-    return if @showing
-    @showing = true
-    @t0 = new Date().getTime()
-
-    @progressBar.scaleX = 0
-    @dimLayer.alpha = 0
-    createjs.Tween.removeTweens @dimLayer
-    createjs.Tween.get(@dimLayer).to({alpha: 1}, 500)
-
-  hide: ->
-    return unless @showing
-    @showing = false
-
-    createjs.Tween.removeTweens @progressBar
-    createjs.Tween.removeTweens @dimLayer
-    createjs.Tween.get(@dimLayer).to({alpha: 0}, 500)
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 5d3425e1a..dddbd983d 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -8,7 +8,6 @@ CameraBorder = require './CameraBorder'
 Layer = require './Layer'
 Letterbox = require './Letterbox'
 Dimmer = require './Dimmer'
-CastingScreen = require './CastingScreen'
 PlaybackOverScreen = require './PlaybackOverScreen'
 DebugDisplay = require './DebugDisplay'
 CoordinateDisplay = require './CoordinateDisplay'
@@ -97,7 +96,6 @@ module.exports = Surface = class Surface extends CocoClass
     @spriteBoss.destroy()
     @chooser?.destroy()
     @dimmer?.destroy()
-    @castingScreen?.destroy()
     @playbackOverScreen?.destroy()
     @stage.clear()
     @musicPlayer?.destroy()
@@ -403,7 +401,6 @@ module.exports = Surface = class Surface extends CocoClass
     @surfaceLayer.addChild @cameraBorder = new CameraBorder bounds: @camera.bounds
     @screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight
     @spriteBoss = new SpriteBoss camera: @camera, surfaceLayer: @surfaceLayer, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible
-    #@castingScreen ?= new CastingScreen camera: @camera, layer: @screenLayer  # STREAM: Not needed with world streaming.
     @playbackOverScreen ?= new PlaybackOverScreen camera: @camera, layer: @screenLayer
     @stage.enableMouseOver(10)
     @stage.addEventListener 'stagemousemove', @onMouseMove
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index 5042fb02f..9e20be655 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -12,6 +12,8 @@ Component = require 'lib/world/component'
 System = require 'lib/world/system'
 PROGRESS_UPDATE_INTERVAL = 100
 DESERIALIZATION_INTERVAL = 10
+REAL_TIME_BUFFER_MIN = 2 * PROGRESS_UPDATE_INTERVAL
+REAL_TIME_BUFFER_MAX = 5 * PROGRESS_UPDATE_INTERVAL
 ITEM_ORIGINAL = '53e12043b82921000051cdf9'
 
 module.exports = class World
@@ -89,6 +91,8 @@ module.exports = class World
       console.log 'Warning: loadFrames called on empty World (no thangs).'
     t1 = now()
     @t0 ?= t1
+    @worldLoadStartTime ?= t1
+    @lastRealTimeUpdate ?= 0
     if loadUntilFrame
       frameToLoadUntil = loadUntilFrame + 1
     else
@@ -112,8 +116,16 @@ module.exports = class World
           return unless errorCallback error  # errorCallback tells us whether the error is recoverable
         @unhandledRuntimeErrors = []
       t2 = now()
-      if t2 - t1 > PROGRESS_UPDATE_INTERVAL
-        loadProgressCallback? i / @totalFrames unless @preloading
+      if @realTime
+        shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
+        shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
+      else
+        shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL
+        shouldDelayRealTimeSimulation = false
+      if shouldUpdateProgress or shouldDelayRealTimeSimulation
+        if shouldUpdateProgress
+          @lastRealTimeUpdate = i * @dt if @realTime
+          loadProgressCallback? i / @totalFrames unless @preloading
         t1 = t2
         if t2 - @t0 > 1000
           console.log '  Loaded', i, 'of', @totalFrames, '(+' + (t2 - @t0).toFixed(0) + 'ms)'
@@ -127,7 +139,8 @@ module.exports = class World
         if skipDeferredLoading
           continueFn()
         else
-          setTimeout(continueFn, 0)
+          delay = if shouldDelayRealTimeSimulation then PROGRESS_UPDATE_INTERVAL else 0
+          setTimeout(continueFn, delay)
         return
     unless @debugging
       @ended = true
@@ -136,6 +149,19 @@ module.exports = class World
       loadProgressCallback? 1
       loadedCallback()
 
+  shouldDelayRealTimeSimulation: (t) ->
+    return false unless @realTime
+    timeSinceStart = t - @worldLoadStartTime
+    timeLoaded = @frames.length * @dt * 1000
+    timeBuffered = timeLoaded - timeSinceStart
+    timeBuffered > REAL_TIME_BUFFER_MAX
+
+  shouldUpdateRealTimePlayback: (t) ->
+    return false unless @realTime
+    timeSinceStart = t - @worldLoadStartTime
+    remainingBuffer = @lastRealTimeUpdate * 1000 - timeSinceStart
+    remainingBuffer < REAL_TIME_BUFFER_MIN
+
   finalizePreload: (loadedCallback) ->
     @preloading = false
     loadedCallback() if @ended
@@ -143,6 +169,9 @@ module.exports = class World
   abort: ->
     @aborted = true
 
+  updateFlags: (@flags) ->
+    console.log "updated flags", @flags
+
   loadFromLevel: (level, willSimulate=true) ->
     @levelComponents = level.levelComponents
     @thangTypes = level.thangTypes
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 8b3659247..fed8a313a 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -114,6 +114,10 @@ module.exports = class SpellView extends CocoView
       name: 'run-code'
       bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'}
       exec: -> Backbone.Mediator.publish 'tome:manual-cast', {}
+    addCommand
+      name: 'run-code-real-time'
+      bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}
+      exec: -> Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
     addCommand
       name: 'no-op'
       bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'}
@@ -269,8 +273,8 @@ module.exports = class SpellView extends CocoView
     # @addZatannaSnippets()
     @highlightCurrentLine()
 
-  cast: (preload=false) ->
-    Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload
+  cast: (preload=false, realTime=false) ->
+    Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload, realTime: realTime
 
   notifySpellChanged: =>
     Backbone.Mediator.publish 'tome:spell-changed', spell: @spell
@@ -285,7 +289,7 @@ module.exports = class SpellView extends CocoView
 
   onManualCast: (e) ->
     cast = @$el.parent().length
-    @recompile cast
+    @recompile cast, e.realTime
     @focus() if cast
 
   onCodeReload: (e) ->
@@ -299,12 +303,12 @@ module.exports = class SpellView extends CocoView
   recompileIfNeeded: =>
     @recompile() if @recompileNeeded
 
-  recompile: (cast=true) ->
+  recompile: (cast=true, realTime=false) ->
     @setRecompileNeeded false
     return if @spell.source is @getSource()
     @spell.transpile @getSource()
     @updateAether true, false
-    @cast() if cast
+    @cast(false, realTime) if cast
     @notifySpellChanged()
 
   updateACEText: (source) ->
@@ -472,7 +476,7 @@ module.exports = class SpellView extends CocoView
   onSessionWillSave: (e) ->
     return unless @spellHasChanged
     setTimeout(=>
-      unless @spellHasChanged
+      unless @destroyed or @spellHasChanged
         @$el.find('.save-status').finish().show().fadeOut(2000)
     , 1000)
     @spellHasChanged = false
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index fa20c5c45..92e634dd9 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -152,10 +152,10 @@ module.exports = class TomeView extends CocoView
   onCastSpell: (e) ->
     # A single spell is cast.
     # Hmm; do we need to make sure other spells are all cast here?
-    @cast e?.preload
+    @cast e?.preload, e?.realTime
 
-  cast: (preload=false) ->
-    Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload
+  cast: (preload=false, realTime=false) ->
+    Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime
 
   onToggleSpellList: (e) ->
     @spellList.rerenderEntries()

From 0431f709543abf7cf64f6c4078df3a27395db649 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sat, 23 Aug 2014 09:52:05 -0700
Subject: [PATCH 09/21] Improved Surface playback fast-forwarding.

---
 app/lib/surface/Surface.coffee | 34 +++++++++++++---------------------
 1 file changed, 13 insertions(+), 21 deletions(-)

diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index dddbd983d..6fcfa2e13 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -189,12 +189,12 @@ module.exports = Surface = class Surface extends CocoClass
   setProgress: (progress, scrubDuration=500) ->
     progress = Math.max(Math.min(progress, 1), 0.0)
 
+    @fastForwardingToFrame = null
     @scrubbing = true
     onTweenEnd = =>
       @scrubbingTo = null
       @scrubbing = false
       @scrubbingPlaybackSpeed = null
-      @fastForwarding = false
 
     if @scrubbingTo?
       # cut to the chase for existing tween
@@ -203,11 +203,10 @@ module.exports = Surface = class Surface extends CocoClass
 
     @scrubbingTo = Math.min(Math.round(progress * @world.frames.length), @world.frames.length)
     @scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5))
-    ease = if @fastForwarding then createjs.Ease.linear else createjs.Ease.sineInOut
     if scrubDuration
       t = createjs.Tween
         .get(@)
-        .to({currentFrame: @scrubbingTo}, scrubDuration, ease)
+        .to({currentFrame: @scrubbingTo}, scrubDuration, createjs.Ease.sineInOut)
         .call(onTweenEnd)
       t.addEventListener('change', @onFramesScrubbed)
     else
@@ -286,8 +285,8 @@ module.exports = Surface = class Surface extends CocoClass
     @setPlayingCalled = true
     if @playing and @currentFrame >= (@world.totalFrames - 5)
       @currentFrame = 0
-    if @fastForwarding and not @playing
-      @setProgress @currentFrame / @world.frames.length
+    if @fastForwardingToFrame and not @playing
+      @fastForwardingToFrame = null
 
   onSetTime: (e) ->
     toFrame = @currentFrame
@@ -368,21 +367,9 @@ module.exports = Surface = class Surface extends CocoClass
 
     @setWorld event.world
     @onFrameChanged(true)
-    if @playing and ffToFrame = Math.min event.firstChangedFrame, @frameBeforeCast, event.world.frames.length
-      ffToRatio = ffToFrame / @world.frames.length
-      ffToTime = ffToFrame * @world.dt
-      ffSpeed = Math.max 4, ffToTime / 3
-      ffInterval = 1000 * (ffToFrame - @currentFrame) / @options.frameRate
-      ffScrubDuration = 1000 * ffToTime / ffSpeed
-      ffScrubDuration = Math.min(ffScrubDuration, ffInterval)
-      ffFactor = ffInterval / ffScrubDuration
-      if ffFactor > 1.5
-        createjs.Tween.removeTweens(@)
-        @scrubbingTo = null
-        @fastForwarding = true
-        @setProgress ffToRatio, ffScrubDuration
-      else
-        createjs.Tween.removeTweens(@)
+    if @playing and (ffToFrame = Math.min(event.firstChangedFrame, @frameBeforeCast, event.world.frames.length)) and ffToFrame > @currentFrame
+      @fastForwardingToFrame = ffToFrame
+      @fastForwardingSpeed = Math.max 4, 4 * 90 / (@world.maxTotalFrames * @world.dt)
 
   # initialization
 
@@ -563,7 +550,12 @@ module.exports = Surface = class Surface extends CocoClass
       # Skip some frame updates unless we're playing and not at end (or we haven't drawn much yet)
       frameAdvanced = (@playing and @currentFrame < lastFrame) or @totalFramesDrawn < 2
       if frameAdvanced and @playing
-        @currentFrame += @world.frameRate / @options.frameRate
+        advanceBy = @world.frameRate / @options.frameRate
+        if @fastForwardingToFrame and @currentFrame < @fastForwardingToFrame - advanceBy
+          advanceBy = Math.min(@currentFrame + advanceBy * @fastForwardingSpeed, @fastForwardingToFrame) - @currentFrame
+        else if @fastForwardingToFrame
+          @fastForwardingToFrame = @fastForwardingSpeed = null
+        @currentFrame += advanceBy
         @currentFrame = Math.min @currentFrame, lastFrame
       newWorldFrame = Math.floor @currentFrame
       worldFrameAdvanced = newWorldFrame isnt oldWorldFrame

From f2adb7ec7f51f0755b9e8ae7ca34ccc88ded7959 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sat, 23 Aug 2014 11:16:02 -0700
Subject: [PATCH 10/21] Skipping serialization/deserialization of needless
 Programmable clones.

---
 app/lib/God.coffee | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/lib/God.coffee b/app/lib/God.coffee
index 3af511bc6..754db721f 100644
--- a/app/lib/God.coffee
+++ b/app/lib/God.coffee
@@ -91,6 +91,7 @@ module.exports = class God extends CocoClass
     userCodeMap = {}
     for spellKey, spell of spells
       for thangID, spellThang of spell.thangs
+        continue if spellThang.thang.programmableMethods[spell.name].cloneOf
         (userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
     userCodeMap
 

From c79541f669ab6cd102357cf49a221e15f2c6e6f9 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sat, 23 Aug 2014 13:54:52 -0700
Subject: [PATCH 11/21] Full-screen playback during real-time mode.

---
 app/lib/LevelBus.coffee                       |  3 +-
 app/lib/surface/Surface.coffee                | 29 ++++++++++++++++---
 app/schemas/subscriptions/play.coffee         |  8 +++++
 app/styles/play/level.sass                    | 25 +++++++++++++++-
 app/styles/play/spectate.sass                 |  4 +--
 app/views/play/level/LevelPlaybackView.coffee | 23 ++++++++++++---
 app/views/play/level/PlayLevelView.coffee     | 11 +++++++
 7 files changed, 91 insertions(+), 12 deletions(-)

diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee
index 9631c3476..d79451500 100644
--- a/app/lib/LevelBus.coffee
+++ b/app/lib/LevelBus.coffee
@@ -196,7 +196,8 @@ module.exports = class LevelBus extends Bus
 
   onNewGoalStates: ({goalStates})->
     state = @session.get 'state'
-    unless utils.kindaEqual state.goalStates, goalStates # Only save when goals really change
+    unless utils.kindaEqual state.goalStates, goalStates  # Only save when goals really change
+      # TODO: this log doesn't capture when null-status goals are being set during world streaming. Where can they be coming from?
       return console.error("Somehow trying to save null goal states!", goalStates) if _.find(goalStates, (gs) -> not gs.status)
       state.goalStates = goalStates
       @session.set 'state', state
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 6fcfa2e13..91e243f0c 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -67,6 +67,8 @@ module.exports = Surface = class Surface extends CocoClass
     'level-set-letterbox': 'onSetLetterbox'
     'application:idle-changed': 'onIdleChanged'
     'camera:zoom-updated': 'onZoomUpdated'
+    'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
+    'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
 
   shortcuts:
     'ctrl+\\, ⌘+\\': 'onToggleDebug'
@@ -82,7 +84,7 @@ module.exports = Surface = class Surface extends CocoClass
     @options = _.extend(@options, givenOptions) if givenOptions
     @initEasel()
     @initAudio()
-    @onResize = _.debounce @onResize, 500
+    @onResize = _.debounce @onResize, 250
     $(window).on 'resize', @onResize
     if @world.ended
       _.defer => @setWorld @world
@@ -367,7 +369,8 @@ module.exports = Surface = class Surface extends CocoClass
 
     @setWorld event.world
     @onFrameChanged(true)
-    if @playing and (ffToFrame = Math.min(event.firstChangedFrame, @frameBeforeCast, event.world.frames.length)) and ffToFrame > @currentFrame
+    fastForwardBuffer = 2  # Make sure that real-time playback doesn't need to buffer more than this many seconds.
+    if @playing and (ffToFrame = Math.min(event.firstChangedFrame, @frameBeforeCast, event.world.frames.length)) and ffToFrame > @currentFrame + fastForwardBuffer * @world.frameRate
       @fastForwardingToFrame = ffToFrame
       @fastForwardingSpeed = Math.max 4, 4 * 90 / (@world.maxTotalFrames * @world.dt)
 
@@ -402,8 +405,17 @@ module.exports = Surface = class Surface extends CocoClass
   onResize: (e) =>
     oldWidth = parseInt @canvas.attr('width'), 10
     oldHeight = parseInt @canvas.attr('height'), 10
-    newWidth = @canvas.width()
-    newHeight = @canvas.height()
+    aspectRatio = oldWidth / oldHeight
+    pageWidth = $('#page-container').width() - 17  # 17px nano scroll bar
+    if @realTime
+      pageHeight = $('#page-container').height() - $('#control-bar-view').outerHeight() - $('#playback-view').outerHeight()
+      newWidth = Math.min pageWidth, pageHeight * aspectRatio
+      newHeight = newWidth / aspectRatio
+    else
+      newWidth = 0.55 * pageWidth
+      newHeight = newWidth / aspectRatio
+    @canvas.width newWidth
+    @canvas.height newHeight
     return unless newWidth > 0 and newHeight > 0
     #if InstallTrigger?  # Firefox rendering performance goes down as canvas size goes up
     #  newWidth = Math.min 924, newWidth
@@ -600,6 +612,15 @@ module.exports = Surface = class Surface extends CocoClass
     ++@totalFramesDrawn
     @stage.update e
 
+  # Real-time playback
+  onRealTimePlaybackStarted: (e) ->
+    @realTime = true
+    @onResize()
+
+  onRealTimePlaybackEnded: (e) ->
+    @realTime = false
+    @onResize()
+
   # paths - TODO: move to SpriteBoss? but only update on frame drawing instead of on every frame update?
 
   updatePaths: ->
diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee
index 4576450de..51f81f992 100644
--- a/app/schemas/subscriptions/play.coffee
+++ b/app/schemas/subscriptions/play.coffee
@@ -105,6 +105,14 @@ module.exports =
   'playback:manually-scrubbed':
     {} # TODO schema
 
+  'playback:real-time-playback-started':
+    type: 'object'
+    additionalProperties: false
+
+  'playback:real-time-playback-ended':
+    type: 'object'
+    additionalProperties: false
+
   'change:editor-config':
     {} # TODO schema
 
diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index b0d52ea6f..ea3fc3e5a 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -10,6 +10,28 @@ body.is-playing
   margin: 0 auto
   @include user-select(none)
 
+  &.real-time
+    // Hmm, somehow the #page-container is cutting us off by ~17px on the right, looks a bit off.
+
+    #canvas-wrapper
+      width: 100%
+      canvas#surface
+        margin: 0 auto
+    #control-bar-view, #playback-view
+      width: 100%
+    #code-area, #thang-hud, #goals-view
+      display: none
+      visibility: hidden
+    #gold-view
+      right: 1%
+    #control-bar-view .title
+      left: 0
+      width: 100%
+      text-align: center
+
+    .level-content
+      margin: 0px auto
+
   .level-content
     position: relative
 
@@ -17,12 +39,13 @@ body.is-playing
     width: 55%
     position: relative
     overflow: hidden
+    @include transition(0.5s ease-out)
     
   canvas#surface
     background-color: #333
-    width: 100%
     display: block
     z-index: 1
+    @include transition(0.5s ease-out)
     
   min-width: 1024px
   position: relative
diff --git a/app/styles/play/spectate.sass b/app/styles/play/spectate.sass
index 43d44e320..24d18f42f 100644
--- a/app/styles/play/spectate.sass
+++ b/app/styles/play/spectate.sass
@@ -19,11 +19,11 @@
     .title
       position: absolute
       display: inline-block
-      margin-left: 50%
-      right: 0
       color: #BEBEBE
       line-height: 15px
       left: 0
+      width: 100%
+      text-align: center
 
   max-width: 1920px
   margin: 0 auto
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index 92363fcc7..c9601cefd 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -24,6 +24,7 @@ module.exports = class LevelPlaybackView extends CocoView
     'god:new-world-created': 'onNewWorld'
     'god:streaming-world-updated': 'onNewWorld'  # Maybe?
     'level-set-letterbox': 'onSetLetterbox'
+    'tome:cast-spells': 'onTomeCast'
 
   events:
     'click #debug-toggle': 'onToggleDebug'
@@ -151,10 +152,19 @@ module.exports = class LevelPlaybackView extends CocoView
     @$el.find('#music-button').toggleClass('music-on', me.get('music'))
 
   onSetLetterbox: (e) ->
-    buttons = @$el.find '#play-button, .scrubber-handle'
-    buttons.css 'visibility', if e.on then 'hidden' else 'visible'
+    @togglePlaybackControls !e.on
     @disabled = e.on
 
+  togglePlaybackControls: (to) ->
+    buttons = @$el.find '#play-button, .scrubber-handle'
+    buttons.css 'visibility', if to then 'visible' else 'hidden'
+
+  onTomeCast: (e) ->
+    return unless e.realTime
+    @realTime = true
+    @togglePlaybackControls false
+    Backbone.Mediator.publish 'playback:real-time-playback-started', {}
+
   onWindowResize: (s...) =>
     @barWidth = $('.progress', @$el).width()
 
@@ -276,11 +286,16 @@ module.exports = class LevelPlaybackView extends CocoView
   updateProgress: (progress, world) ->
     if world.frames.length isnt @lastLoadedFrameCount
       @updateBarWidth world.frames.length, world.maxTotalFrames, world.dt
+    @worldCompletelyLoaded = world.frames.length is world.totalFrames
     $('.scrubber .progress-bar', @$el).css('width', "#{progress * 100}%")
 
   updatePlayButton: (progress) ->
-    if progress >= 0.99 and @lastProgress < 0.99
+    if @worldCompletelyLoaded and progress >= 0.99 and @lastProgress < 0.99
       $('#play-button').removeClass('playing').removeClass('paused').addClass('ended')
+      if @realTime
+        @realTime = false
+        @togglePlaybackControls true
+        Backbone.Mediator.publish 'playback:real-time-playback-ended', {}
     if progress < 0.99 and @lastProgress >= 0.99
       b = $('#play-button').removeClass('ended')
       if @playing then b.addClass('playing') else b.addClass('paused')
@@ -326,7 +341,7 @@ module.exports = class LevelPlaybackView extends CocoView
     return if @shouldIgnore()
     Backbone.Mediator.publish 'level-set-time', ratio: ratio, scrubDuration: duration
 
-  shouldIgnore: -> return @disabled
+  shouldIgnore: -> return @disabled or @realTime
 
   onTogglePlay: (e) ->
     e?.preventDefault()
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 3bc4a6fc5..1a5225d97 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -64,6 +64,8 @@ module.exports = class PlayLevelView extends RootView
     'level:set-team': 'setTeam'
     'level:started': 'onLevelStarted'
     'level:loading-view-unveiled': 'onLoadingViewUnveiled'
+    'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
+    'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
 
   events:
     'click #level-done-button': 'onDonePressed'
@@ -513,6 +515,15 @@ module.exports = class PlayLevelView extends RootView
       continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers')
       AudioPlayer.preloadSoundReference sound
 
+  # Real-time playback
+  onRealTimePlaybackStarted: (e) ->
+    @$el.addClass 'real-time'
+    @onWindowResize()
+
+  onRealTimePlaybackEnded: (e) ->
+    @$el.removeClass 'real-time'
+    @onWindowResize()
+
   destroy: ->
     @levelLoader?.destroy()
     @surface?.destroy()

From 8777a87774739bfa6dfc72b3085e3f672a3bc686 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sat, 23 Aug 2014 14:31:38 -0700
Subject: [PATCH 12/21] Made a real-time playback button.

---
 app/styles/play/level/tome/cast_button.sass     |  9 ++++++++-
 app/templates/play/level/tome/cast_button.jade  |  4 +++-
 app/views/play/level/tome/CastButtonView.coffee | 13 +++++++++----
 app/views/play/level/tome/SpellView.coffee      | 13 ++++++++-----
 4 files changed, 28 insertions(+), 11 deletions(-)

diff --git a/app/styles/play/level/tome/cast_button.sass b/app/styles/play/level/tome/cast_button.sass
index 87afdad55..38e71cc3d 100644
--- a/app/styles/play/level/tome/cast_button.sass
+++ b/app/styles/play/level/tome/cast_button.sass
@@ -82,7 +82,9 @@
       height: 40px
 
     .cast-button
-      width: 100%
+      width: 80%
+      width: -webkit-calc(100% - 40px)
+      width: calc(100% - 40px)
       border-top-left-radius: 6px
       border-bottom-left-radius: 6px
 
@@ -92,3 +94,8 @@
         font-size: 14px
       @media screen and (max-width: 1024px)
         font-size: 12px
+
+    .cast-real-time-button
+      width: 20%
+      width: -webkit-calc(40px)
+      width: calc(40px)
diff --git a/app/templates/play/level/tome/cast_button.jade b/app/templates/play/level/tome/cast_button.jade
index cbaf3079b..a94b388ac 100644
--- a/app/templates/play/level/tome/cast_button.jade
+++ b/app/templates/play/level/tome/cast_button.jade
@@ -1,3 +1,5 @@
-div.btn-group.btn-group-lg.cast-button-group.dropup
+div.btn-group.btn-group-lg.cast-button-group
   .button-progress-overlay
   button.btn.btn-inverse.banner.cast-button(title=castShortcutVerbose + ": Cast current spell", data-i18n="play_level.tome_cast_button_cast") Spell Cast
+  button.btn.btn-inverse.banner.cast-real-time-button(title=castRealTimeShortcutVerbose + ": Cast in real time") 
+    i.glyphicon.glyphicon-play
\ No newline at end of file
diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee
index fe82e4c28..5a426f92d 100644
--- a/app/views/play/level/tome/CastButtonView.coffee
+++ b/app/views/play/level/tome/CastButtonView.coffee
@@ -8,7 +8,7 @@ module.exports = class CastButtonView extends CocoView
 
   events:
     'click .cast-button': 'onCastButtonClick'
-    'click .autocast-delays a': 'onCastOptionsClick'
+    'click .cast-real-time-button': 'onCastRealTimeButtonClick'
 
   subscriptions:
     'tome:spell-changed': 'onSpellChanged'
@@ -22,10 +22,12 @@ module.exports = class CastButtonView extends CocoView
     @levelID = options.levelID
     @castShortcut = '⇧↵'
     @castShortcutVerbose = 'Shift+Enter'
+    @castRealTimeShortcutVerbose = 'Ctrl+Shift+Enter'
 
   getRenderData: (context={}) ->
     context = super context
     context.castShortcutVerbose = @castShortcutVerbose
+    context.castRealTimeShortcutVerbose = @castRealTimeShortcutVerbose
     context
 
   afterRender: ->
@@ -34,17 +36,20 @@ module.exports = class CastButtonView extends CocoView
     @castButtonGroup = $('.cast-button-group', @$el)
     @castOptions = $('.autocast-delays', @$el)
     delay = me.get('autocastDelay')
-    delay ?= 5000
-    unless @levelID in ['rescue-mission', 'grab-the-mushroom', 'drink-me', 'its-a-trap', 'break-the-prison', 'taunt', 'cowardly-taunt', 'commanding-followers', 'mobile-artillery']
-      delay = 90019001
+    delay ?= 90019001
     @setAutocastDelay delay
 
   attachTo: (spellView) ->
     @$el.detach().prependTo(spellView.toolbarView.$el).show()
 
   onCastButtonClick: (e) ->
+    console.log "cas fast yo"
     Backbone.Mediator.publish 'tome:manual-cast', {}
 
+  onCastRealTimeButtonClick: (e) ->
+    console.log "cas real time yo"
+    Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
+
   onCastOptionsClick: (e) =>
     Backbone.Mediator.publish 'tome:focus-editor'
     @castButtonGroup.removeClass 'open'
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index fed8a313a..deb831a5e 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -305,11 +305,14 @@ module.exports = class SpellView extends CocoView
 
   recompile: (cast=true, realTime=false) ->
     @setRecompileNeeded false
-    return if @spell.source is @getSource()
-    @spell.transpile @getSource()
-    @updateAether true, false
-    @cast(false, realTime) if cast
-    @notifySpellChanged()
+    hasChanged = @spell.source isnt @getSource()
+    if hasChanged
+      @spell.transpile @getSource()
+      @updateAether true, false
+    if cast and (hasChanged or realTime)
+      @cast(false, realTime)
+    if hasChanged
+      @notifySpellChanged()
 
   updateACEText: (source) ->
     @eventsSuppressed = true

From 2e23247521d2ff0956e45d26a905b5356069f61f Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sat, 23 Aug 2014 14:55:58 -0700
Subject: [PATCH 13/21] Fixed #811, a bug where you could pause the world if
 you clicked on a letterboxed playback scrubber.

---
 app/views/play/level/LevelPlaybackView.coffee | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index c9601cefd..921dcc532 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -37,7 +37,7 @@ module.exports = class LevelPlaybackView extends CocoView
     'click #zoom-out-button': -> Backbone.Mediator.publish('camera-zoom-out') unless @shouldIgnore()
     'click #volume-button': 'onToggleVolume'
     'click #play-button': 'onTogglePlay'
-    'click': -> Backbone.Mediator.publish 'tome:focus-editor'
+    'click': -> Backbone.Mediator.publish 'tome:focus-editor' unless @realTime
     'mouseenter #timeProgress': 'onProgressEnter'
     'mouseleave #timeProgress': 'onProgressLeave'
     'mousemove #timeProgress': 'onProgressHover'
@@ -316,15 +316,18 @@ module.exports = class LevelPlaybackView extends CocoView
       max: @sliderIncrements
       animate: 'slow'
       slide: (event, ui) =>
+        return if @shouldIgnore()
         @scrubTo ui.value / @sliderIncrements
         @slideCount += 1
 
       start: (event, ui) =>
+        return if @shouldIgnore()
         @slideCount = 0
         @wasPlaying = @playing
         Backbone.Mediator.publish 'level-set-playing', {playing: false}
 
       stop: (event, ui) =>
+        return if @shouldIgnore()
         @actualProgress = ui.value / @sliderIncrements
         Backbone.Mediator.publish 'playback:manually-scrubbed', ratio: @actualProgress
         Backbone.Mediator.publish 'level-set-playing', {playing: @wasPlaying}

From f59084e6511404e2c552560df38d709dcb519fde Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sat, 23 Aug 2014 17:26:56 -0700
Subject: [PATCH 14/21] Sending flag history to the World for to make flag
 Thangs during simulation.

---
 .../javascripts/workers/worker_world.js       |  4 +--
 app/lib/Angel.coffee                          |  7 ++--
 app/lib/surface/CoordinateDisplay.coffee      |  4 ---
 app/lib/surface/Label.coffee                  | 13 ++++++++
 app/lib/surface/Surface.coffee                | 30 ++++++++++++++++-
 app/lib/surface/WizardSprite.coffee           | 10 ++----
 app/lib/world/world.coffee                    |  5 +--
 app/schemas/subscriptions/play.coffee         | 33 +++++++++++++++++++
 app/styles/base.sass                          |  4 ---
 app/styles/play/level.sass                    |  5 ++-
 app/views/play/level/PlayLevelView.coffee     |  5 ++-
 .../play/level/tome/CastButtonView.coffee     |  2 --
 12 files changed, 93 insertions(+), 29 deletions(-)

diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js
index d45a91e2e..03a100fd3 100644
--- a/app/assets/javascripts/workers/worker_world.js
+++ b/app/assets/javascripts/workers/worker_world.js
@@ -459,9 +459,9 @@ self.finalizePreload = function finalizePreload() {
   self.world.finalizePreload(self.onWorldLoaded);
 };
 
-self.updateFlags = function updateFlags(flags) {
+self.addFlagEvent = function addFlagEvent(flagEvent) {
   if(!self.world || self.world.framesSerializedSoFar == self.world.frames.length) return;
-  self.world.updateFlags(flags);
+  self.world.addFlagEvent(flagEvent);
 };
 
 self.addEventListener('message', function(event) {
diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee
index fa9fb75cf..59547ca16 100644
--- a/app/lib/Angel.coffee
+++ b/app/lib/Angel.coffee
@@ -13,7 +13,7 @@ module.exports = class Angel extends CocoClass
   abortTimeoutDuration: 500  # give in-process or dying workers this long to give up
 
   subscriptions:
-    'self-wizard:target-changed': 'onSelfWizardTargetChanged'
+    'level:flag-updated': 'onFlagEvent'
 
   constructor: (@shared) ->
     super()
@@ -208,10 +208,9 @@ module.exports = class Angel extends CocoClass
     @worker.addEventListener 'message', @onWorkerMessage
     @worker.creationTime = new Date()
 
-  onSelfWizardTargetChanged: (e) ->
+  onFlagEvent: (e) ->
     return unless @running and @work.realTime
-    targetPos = e.sender.targetPos
-    @worker.postMessage func: 'updateFlags', args: [{type: 'wizard', targetPos: targetPos}]
+    @worker.postMessage func: 'addFlagEvent', args: e
 
 
   #### Synchronous code for running worlds on main thread (profiling / IE9) ####
diff --git a/app/lib/surface/CoordinateDisplay.coffee b/app/lib/surface/CoordinateDisplay.coffee
index f650e09b1..155f7e7ac 100644
--- a/app/lib/surface/CoordinateDisplay.coffee
+++ b/app/lib/surface/CoordinateDisplay.coffee
@@ -35,10 +35,6 @@ module.exports = class CoordinateDisplay extends createjs.Container
   onMouseOut: (e) -> @mouseInBounds = false
 
   onMouseMove: (e) ->
-    if @mouseInBounds and key.shift
-      $('#surface').addClass('flag-cursor') unless $('#surface').hasClass('flag-cursor')
-    else if @mouseInBounds
-      $('#surface').removeClass('flag-cursor') if $('#surface').hasClass('flag-cursor')
     wop = @camera.screenToWorld x: e.x, y: e.y
     wop.x = Math.round(wop.x)
     wop.y = Math.round(wop.y)
diff --git a/app/lib/surface/Label.coffee b/app/lib/surface/Label.coffee
index 959c92c13..30829d372 100644
--- a/app/lib/surface/Label.coffee
+++ b/app/lib/surface/Label.coffee
@@ -37,6 +37,8 @@ module.exports = class Label extends CocoClass
   build: ->
     @layer.removeChild @background if @background
     @layer.removeChild @label if @label
+    @label = null
+    @background = null
     return unless @text  # null or '' should both be skipped
     o = @buildLabelOptions()
     @layer.addChild @label = @buildLabel o
@@ -53,6 +55,17 @@ module.exports = class Label extends CocoClass
     @label.y = @background.y = @sprite.imageObject.y + offset.y
     null
 
+  show: ->
+    return unless @label
+    @layer.addChild @label
+    @layer.addChild @background
+    @layer.updateLayerOrder()
+
+  hide: ->
+    return unless @label
+    @layer.removeChild @background
+    @layer.removeChild @label
+
   buildLabelOptions: ->
     o = {}
     st = {dialogue: 'D', say: 'S', name: 'N'}[@style]
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 91e243f0c..72604c0a0 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -69,6 +69,7 @@ module.exports = Surface = class Surface extends CocoClass
     'camera:zoom-updated': 'onZoomUpdated'
     'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
     'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
+    'level:flag-selected': 'onFlagSelected'
 
   shortcuts:
     'ctrl+\\, ⌘+\\': 'onToggleDebug'
@@ -526,7 +527,9 @@ module.exports = Surface = class Surface extends CocoClass
   onMouseDown: (e) =>
     return if @disabled
     onBackground = not @stage.hitTest e.stageX, e.stageY
-    Backbone.Mediator.publish 'surface:stage-mouse-down', onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e
+    event = onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e
+    Backbone.Mediator.publish 'surface:stage-mouse-down', event
+    @placeFlag event if @realTime
 
   onMouseUp: (e) =>
     return if @disabled
@@ -616,10 +619,35 @@ module.exports = Surface = class Surface extends CocoClass
   onRealTimePlaybackStarted: (e) ->
     @realTime = true
     @onResize()
+    @spriteBoss.selfWizardSprite?.imageObject.visible = false
+    @flags = {}
+    @flagHistory = []
 
   onRealTimePlaybackEnded: (e) ->
     @realTime = false
     @onResize()
+    @spriteBoss.selfWizardSprite?.imageObject.visible = true
+
+  onFlagSelected: (e) ->
+    @canvas.addClass("flag-selected")
+    @flagColor = e.color
+
+  placeFlag: (e) ->
+    return unless @flagColor
+    wop = @camera.screenToWorld x: e.x, y: e.y
+    targetPos = x: wop.x, y: wop.y
+    flag = player: me.id, team: me.team, color: @flagColor, targetPos: targetPos, time: @world.dt * @world.frames.length + 1, active: true
+    @flags[@flagColor] = flag
+    @flagHistory.push flag
+    Backbone.Mediator.publish 'level:flag-updated', flag
+    console.log 'trying to place flag at', @world.dt * @currentFrame, 'and think it will happen by', flag.time
+
+  removeFlag: (e) ->
+    delete @flags[e.color]
+    console.log e.color, 'deleted'
+    flag = player: me.id, team: me.team, color: e.color, time: @world.dt * @world.frames.length + 1, active: false
+    @flagHistory.push flag
+    Backbone.Mediator.publish 'level:flag-updated', flag
 
   # paths - TODO: move to SpriteBoss? but only update on frame drawing instead of on every frame update?
 
diff --git a/app/lib/surface/WizardSprite.coffee b/app/lib/surface/WizardSprite.coffee
index 5d929147c..3ba5f68af 100644
--- a/app/lib/surface/WizardSprite.coffee
+++ b/app/lib/surface/WizardSprite.coffee
@@ -35,7 +35,6 @@ module.exports = class WizardSprite extends IndieSprite
     @targetPos = @thang.pos
     if @isSelf
       @setNameLabel me.displayName()
-      @setColorHue me.get('wizardColor1')
     else if options.name
       @setNameLabel options.name
 
@@ -66,7 +65,6 @@ module.exports = class WizardSprite extends IndieSprite
       continue if playerID is me.id  # ignore changes for self wizard sprite
       @setNameLabel state.name
       continue unless state.wizard?
-      @setColorHue state.wizard.wizardColor1
       if targetID = state.wizard.targetSprite
         return console.warn 'Wizard Sprite couldn\'t find target sprite', targetID unless targetID of @options.sprites
         @setTarget @options.sprites[targetID]
@@ -91,17 +89,13 @@ module.exports = class WizardSprite extends IndieSprite
     @imageObject.scaleX = @imageObject.scaleY = @imageObject.alpha = 0
     createjs.Tween.get(@imageObject)
       .to({scaleX: 1, scaleY: 1, alpha: 1}, 1000, createjs.Ease.getPowInOut(2.2))
+    @labels.name?.show()
 
   animateOut: (callback) ->
     tween = createjs.Tween.get(@imageObject)
       .to({scaleX: 0, scaleY: 0, alpha: 0}, 1000, createjs.Ease.getPowInOut(2.2))
     tween.call(callback) if callback
-
-  setColorHue: (newColorHue) ->
-    # TODO: is this needed any more?
-    return if @colorHue is newColorHue
-    @colorHue = newColorHue
-    #@updateColorFilters()
+    @labels.name?.hide()
 
   setEditing: (@editing) ->
     if @editing
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index 9e20be655..ecaa78043 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -36,6 +36,7 @@ module.exports = class World
     @systems = []
     @systemMap = {}
     @scriptNotes = []
+    @flagHistory = []
     @rand = new Rand 0  # Existence System may change this seed
     @frames = [new WorldFrame(@, 0)]
 
@@ -169,8 +170,8 @@ module.exports = class World
   abort: ->
     @aborted = true
 
-  updateFlags: (@flags) ->
-    console.log "updated flags", @flags
+  addFlagEvent: (flagEvent) ->
+    @flagHistory.push flagEvent
 
   loadFromLevel: (level, willSimulate=true) ->
     @levelComponents = level.levelComponents
diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee
index 51f81f992..e91420204 100644
--- a/app/schemas/subscriptions/play.coffee
+++ b/app/schemas/subscriptions/play.coffee
@@ -57,6 +57,39 @@ module.exports =
   'level:victory-hidden':
     {} # TODO schema
 
+  'level:flag-selected':
+    type: 'object'
+    additionalProperties: false
+    properties:
+      color:
+        type: 'string'
+        enum: ['red', 'green', 'blue']
+
+  'level:flag-updated':
+    type: 'object'
+    additionalProperties: false
+    required: ['player', 'color', 'time', 'active']
+    properties:
+      player:
+        type: 'string'
+      team:
+        type: 'string'
+      color:
+        type: 'string'
+        enum: ['red', 'green', 'blue']
+      time:
+        type: 'number'
+        minimum: 0
+      active:
+        type: 'boolean'
+      targetPos:
+        type: 'object'
+        additionalProperties: false
+        required: ['x', 'y']
+        properties:
+          x: {type: 'number'}
+          y: {type: 'number'}
+
   'next-game-pressed':
     {} # TODO schema
 
diff --git a/app/styles/base.sass b/app/styles/base.sass
index 332aaa624..a00d037ee 100644
--- a/app/styles/base.sass
+++ b/app/styles/base.sass
@@ -225,10 +225,6 @@ table.table
 .ui-slider-handle
   border: 1px solid black !important
 
-.flag-cursor
-  cursor: crosshair
-
-
 // Fonts
 
 .header-font
diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index ea3fc3e5a..6105e6424 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -46,7 +46,10 @@ body.is-playing
     display: block
     z-index: 1
     @include transition(0.5s ease-out)
-    
+
+    &.flag-selected
+      cursor: crosshair
+
   min-width: 1024px
   position: relative
 
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 1a5225d97..2eb1ddb38 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -72,6 +72,9 @@ module.exports = class PlayLevelView extends RootView
 
   shortcuts:
     'ctrl+s': 'onCtrlS'
+    'r': -> Backbone.Mediator.publish 'level:flag-selected', color: 'red'
+    'g': -> Backbone.Mediator.publish 'level:flag-selected', color: 'green'
+    'b': -> Backbone.Mediator.publish 'level:flag-selected', color: 'blue'
 
   # Initial Setup #############################################################
 
@@ -517,7 +520,7 @@ module.exports = class PlayLevelView extends RootView
 
   # Real-time playback
   onRealTimePlaybackStarted: (e) ->
-    @$el.addClass 'real-time'
+    @$el.addClass('real-time').focus()
     @onWindowResize()
 
   onRealTimePlaybackEnded: (e) ->
diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee
index 5a426f92d..3b310f2f4 100644
--- a/app/views/play/level/tome/CastButtonView.coffee
+++ b/app/views/play/level/tome/CastButtonView.coffee
@@ -43,11 +43,9 @@ module.exports = class CastButtonView extends CocoView
     @$el.detach().prependTo(spellView.toolbarView.$el).show()
 
   onCastButtonClick: (e) ->
-    console.log "cas fast yo"
     Backbone.Mediator.publish 'tome:manual-cast', {}
 
   onCastRealTimeButtonClick: (e) ->
-    console.log "cas real time yo"
     Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
 
   onCastOptionsClick: (e) =>

From 2dca4d72fc3a46ed410dd053fa8b2a63a311d966 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sat, 23 Aug 2014 19:00:35 -0700
Subject: [PATCH 15/21] Rudimentary flag buttons are in.

---
 app/lib/surface/Surface.coffee                | 32 ++-------
 app/lib/surface/WizardSprite.coffee           |  4 ++
 app/schemas/subscriptions/play.coffee         |  8 +--
 app/styles/play/level/gold.sass               |  3 +-
 app/styles/play/level/level-flags-view.sass   | 29 ++++++++
 app/templates/play/level.jade                 |  2 +
 .../play/level/level-flags-view.jade          |  9 +++
 app/views/play/level/LevelFlagsView.coffee    | 66 +++++++++++++++++++
 app/views/play/level/PlayLevelView.coffee     |  5 +-
 9 files changed, 123 insertions(+), 35 deletions(-)
 create mode 100644 app/styles/play/level/level-flags-view.sass
 create mode 100644 app/templates/play/level/level-flags-view.jade
 create mode 100644 app/views/play/level/LevelFlagsView.coffee

diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 72604c0a0..dd0f8d1e7 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -69,7 +69,6 @@ module.exports = Surface = class Surface extends CocoClass
     'camera:zoom-updated': 'onZoomUpdated'
     'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
     'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
-    'level:flag-selected': 'onFlagSelected'
 
   shortcuts:
     'ctrl+\\, ⌘+\\': 'onToggleDebug'
@@ -527,9 +526,9 @@ module.exports = Surface = class Surface extends CocoClass
   onMouseDown: (e) =>
     return if @disabled
     onBackground = not @stage.hitTest e.stageX, e.stageY
-    event = onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e
+    worldPos = @camera.screenToWorld x: e.stageX, y: e.stageY
+    event = onBackground: onBackground, x: e.stageX, y: e.stageY, originalEvent: e, worldPos: worldPos
     Backbone.Mediator.publish 'surface:stage-mouse-down', event
-    @placeFlag event if @realTime
 
   onMouseUp: (e) =>
     return if @disabled
@@ -619,35 +618,16 @@ module.exports = Surface = class Surface extends CocoClass
   onRealTimePlaybackStarted: (e) ->
     @realTime = true
     @onResize()
-    @spriteBoss.selfWizardSprite?.imageObject.visible = false
-    @flags = {}
-    @flagHistory = []
+    @spriteBoss.selfWizardSprite?.toggle false
 
   onRealTimePlaybackEnded: (e) ->
     @realTime = false
     @onResize()
-    @spriteBoss.selfWizardSprite?.imageObject.visible = true
+    @spriteBoss.selfWizardSprite?.toggle true
+    @canvas.removeClass 'flag-selected'
 
   onFlagSelected: (e) ->
-    @canvas.addClass("flag-selected")
-    @flagColor = e.color
-
-  placeFlag: (e) ->
-    return unless @flagColor
-    wop = @camera.screenToWorld x: e.x, y: e.y
-    targetPos = x: wop.x, y: wop.y
-    flag = player: me.id, team: me.team, color: @flagColor, targetPos: targetPos, time: @world.dt * @world.frames.length + 1, active: true
-    @flags[@flagColor] = flag
-    @flagHistory.push flag
-    Backbone.Mediator.publish 'level:flag-updated', flag
-    console.log 'trying to place flag at', @world.dt * @currentFrame, 'and think it will happen by', flag.time
-
-  removeFlag: (e) ->
-    delete @flags[e.color]
-    console.log e.color, 'deleted'
-    flag = player: me.id, team: me.team, color: e.color, time: @world.dt * @world.frames.length + 1, active: false
-    @flagHistory.push flag
-    Backbone.Mediator.publish 'level:flag-updated', flag
+    @canvas.toggleClass 'flag-selected', Boolean(e.color)
 
   # paths - TODO: move to SpriteBoss? but only update on frame drawing instead of on every frame update?
 
diff --git a/app/lib/surface/WizardSprite.coffee b/app/lib/surface/WizardSprite.coffee
index 3ba5f68af..c562abd1c 100644
--- a/app/lib/surface/WizardSprite.coffee
+++ b/app/lib/surface/WizardSprite.coffee
@@ -58,6 +58,10 @@ module.exports = class WizardSprite extends IndieSprite
       name += " (#{@options.codeLanguage})"  # TODO: move on second line, capitalize properly
     super name
 
+  toggle: (to) ->
+    @imageObject?.visible = to
+    @labels.name?[if to then 'show' else 'hide']()
+
   onPlayerStatesChanged: (e) ->
     for playerID, state of e.states
       continue unless playerID is @thang.id
diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee
index e91420204..b80467250 100644
--- a/app/schemas/subscriptions/play.coffee
+++ b/app/schemas/subscriptions/play.coffee
@@ -61,9 +61,9 @@ module.exports =
     type: 'object'
     additionalProperties: false
     properties:
-      color:
+      color:  # omitted if we've deselected
         type: 'string'
-        enum: ['red', 'green', 'blue']
+        enum: ['green', 'black', 'violet']
 
   'level:flag-updated':
     type: 'object'
@@ -76,13 +76,13 @@ module.exports =
         type: 'string'
       color:
         type: 'string'
-        enum: ['red', 'green', 'blue']
+        enum: ['green', 'black', 'violet']
       time:
         type: 'number'
         minimum: 0
       active:
         type: 'boolean'
-      targetPos:
+      pos:
         type: 'object'
         additionalProperties: false
         required: ['x', 'y']
diff --git a/app/styles/play/level/gold.sass b/app/styles/play/level/gold.sass
index c1efa04bb..283f5b7f9 100644
--- a/app/styles/play/level/gold.sass
+++ b/app/styles/play/level/gold.sass
@@ -6,9 +6,8 @@
   position: absolute
   right: 46%
   top: 42px
-  user-select: none
-  -webkit-user-select: none
   @include transition(box-shadow .2s linear)
+  @include user-select(none)
   padding: 4px
   background: transparent url(/images/level/gold_background.png) no-repeat
   background-size: 100% 100%
diff --git a/app/styles/play/level/level-flags-view.sass b/app/styles/play/level/level-flags-view.sass
new file mode 100644
index 000000000..c5204dbb1
--- /dev/null
+++ b/app/styles/play/level/level-flags-view.sass
@@ -0,0 +1,29 @@
+@import "app/styles/mixins"
+@import "app/styles/bootstrap/mixins"
+
+#level-flags-view
+  display: none
+  position: absolute
+  top: 42px
+  left: 1%
+  @include transition(box-shadow .2s linear)
+  @include user-select(none)
+  padding: 4px
+  background: transparent url(/images/level/gold_background.png) no-repeat
+  background-size: 100% 100%
+  border-radius: 4px
+
+  &:hover
+    box-shadow: 2px 2px 2px black
+
+  .flag-button
+    margin: 3px
+    font-size: 14px
+
+    &.green-flag
+      color: darkgreen
+    &.black-flag
+      color: black
+    &.violet-flag
+      color: violet
+ 
diff --git a/app/templates/play/level.jade b/app/templates/play/level.jade
index a4d9d4fd9..ac9b335f9 100644
--- a/app/templates/play/level.jade
+++ b/app/templates/play/level.jade
@@ -13,6 +13,8 @@
     #canvas-top-gradient.gradient
     #goals-view.secret
   
+  #level-flags-view.secret
+
   #gold-view.secret.expanded
 
   #level-chat-view
diff --git a/app/templates/play/level/level-flags-view.jade b/app/templates/play/level/level-flags-view.jade
new file mode 100644
index 000000000..036ebf6c1
--- /dev/null
+++ b/app/templates/play/level/level-flags-view.jade
@@ -0,0 +1,9 @@
+button.flag-button.btn.btn-xs.green-flag(title="g: Place a green flag")
+  span.glyphicon.glyphicon-flag
+  | g
+button.flag-button.btn.btn-xs.black-flag(title="b: Place a black flag")
+  span.glyphicon.glyphicon-flag
+  | b
+button.flag-button.btn.btn-xs.violet-flag(title="v: Place a violet flag")
+  span.glyphicon.glyphicon-flag
+  | v
diff --git a/app/views/play/level/LevelFlagsView.coffee b/app/views/play/level/LevelFlagsView.coffee
new file mode 100644
index 000000000..c6db86516
--- /dev/null
+++ b/app/views/play/level/LevelFlagsView.coffee
@@ -0,0 +1,66 @@
+CocoView = require 'views/kinds/CocoView'
+template = require 'templates/play/level/level-flags-view'
+{me} = require 'lib/auth'
+
+module.exports = class LevelFlagsView extends CocoView
+  id: 'level-flags-view'
+  template: template
+
+  subscriptions:
+    'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
+    'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
+    'surface:stage-mouse-down': 'onStageMouseDown'
+    'god:new-world-created': 'onNewWorld'
+    'god:streaming-world-updated': 'onNewWorld'
+
+  events:
+    'click .green-flag': -> @onFlagSelected color: 'green', source: 'button'
+    'click .black-flag': -> @onFlagSelected color: 'black', source: 'button'
+    'click .violet-flag': -> @onFlagSelected color: 'violet', source: 'button'
+
+  shortcuts:
+    'g': -> @onFlagSelected color: 'green', source: 'shortcut'
+    'b': -> @onFlagSelected color: 'black', source: 'shortcut'
+    'v': -> @onFlagSelected color: 'violet', source: 'shortcut'
+    'esc': -> @onFlagSelected color: null, source: 'shortcut'
+
+  constructor: (options) ->
+    super options
+    @world = options.world
+
+  onRealTimePlaybackStarted: (e) ->
+    @realTime = true
+    @$el.show()
+    @flags = {}
+    @flagHistory = []
+
+  onRealTimePlaybackEnded: (e) ->
+    @realTime = false
+    @$el.hide()
+
+  onFlagSelected: (e) ->
+    color = if e.source is 'button' and e.color is @flagColor then null else e.color
+    @flagColor = color
+    Backbone.Mediator.publish 'level:flag-selected', color: color
+    @$el.find('.flag-button').removeClass('active')
+    @$el.find(".#{color}-flag").addClass('active') if color
+
+  onStageMouseDown: (e) ->
+    return unless @flagColor and @realTime
+    pos = x: e.worldPos.x, y: e.worldPos.y
+    flag = player: me.id, team: me.team, color: @flagColor, pos: pos, time: @world.dt * @world.frames.length + 1, active: true
+    @flags[@flagColor] = flag
+    @flagHistory.push flag
+    Backbone.Mediator.publish 'level:flag-updated', flag
+    console.log 'trying to place flag at', @world.age, 'and think it will happen by', flag.time
+
+  removeFlag: (e) ->
+    delete @flags[e.color]
+    console.log e.color, 'deleted'
+    flag = player: me.id, team: me.team, color: e.color, time: @world.dt * @world.frames.length + 1, active: false
+    @flagHistory.push flag
+    Backbone.Mediator.publish 'level:flag-updated', flag
+
+  onNewWorld: (event) ->
+    return unless event.world.name is @world.name
+    @world = event.world
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 2eb1ddb38..ec479b504 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -29,6 +29,7 @@ HUDView = require './LevelHUDView'
 ControlBarView = require './ControlBarView'
 LevelPlaybackView = require './LevelPlaybackView'
 GoalsView = require './LevelGoalsView'
+LevelFlagsView = require './LevelFlagsView'
 GoldView = require './LevelGoldView'
 VictoryModal = require './modal/VictoryModal'
 InfiniteLoopModal = require './modal/InfiniteLoopModal'
@@ -72,9 +73,6 @@ module.exports = class PlayLevelView extends RootView
 
   shortcuts:
     'ctrl+s': 'onCtrlS'
-    'r': -> Backbone.Mediator.publish 'level:flag-selected', color: 'red'
-    'g': -> Backbone.Mediator.publish 'level:flag-selected', color: 'green'
-    'b': -> Backbone.Mediator.publish 'level:flag-selected', color: 'blue'
 
   # Initial Setup #############################################################
 
@@ -238,6 +236,7 @@ module.exports = class PlayLevelView extends RootView
     @insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel
     @insertSubView new LevelPlaybackView session: @session
     @insertSubView new GoalsView {}
+    @insertSubView new LevelFlagsView world: @world
     @insertSubView new GoldView {}
     @insertSubView new HUDView {}
     @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session

From be07f9cfb935756c41b040a1a0eef5f19402c771 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sat, 23 Aug 2014 22:24:00 -0700
Subject: [PATCH 16/21] Many improvements to real-time streaming and flags.

---
 .../javascripts/workers/worker_world.js       |  2 +-
 app/lib/surface/Surface.coffee                | 16 ++++-
 app/lib/world/world.coffee                    | 65 ++++++++++---------
 app/views/play/level/LevelFlagsView.coffee    |  4 +-
 app/views/play/level/LevelPlaybackView.coffee |  2 +
 5 files changed, 54 insertions(+), 35 deletions(-)

diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js
index 03a100fd3..c7a6ee560 100644
--- a/app/assets/javascripts/workers/worker_world.js
+++ b/app/assets/javascripts/workers/worker_world.js
@@ -460,7 +460,7 @@ self.finalizePreload = function finalizePreload() {
 };
 
 self.addFlagEvent = function addFlagEvent(flagEvent) {
-  if(!self.world || self.world.framesSerializedSoFar == self.world.frames.length) return;
+  if(!self.world) return;
   self.world.addFlagEvent(flagEvent);
 };
 
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index dd0f8d1e7..553cede03 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -69,6 +69,7 @@ module.exports = Surface = class Surface extends CocoClass
     'camera:zoom-updated': 'onZoomUpdated'
     'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
     'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
+    #'god:world-load-progress-changed': -> console.log 'it is actually', @world.age
 
   shortcuts:
     'ctrl+\\, ⌘+\\': 'onToggleDebug'
@@ -123,7 +124,7 @@ module.exports = Surface = class Surface extends CocoClass
     @showLevel()
     @updateState true if @loaded
     # TODO: synchronize both ways of choosing whether to show coords (@world via UI System or @options via World Select modal)
-    if @world.showCoordinates and @options.coords
+    if @world.showCoordinates and @options.coords and not @coordinateDisplay
       @coordinateDisplay = new CoordinateDisplay camera: @camera
       @surfaceTextLayer.addChild @coordinateDisplay
     @onFrameChanged()
@@ -369,10 +370,19 @@ module.exports = Surface = class Surface extends CocoClass
 
     @setWorld event.world
     @onFrameChanged(true)
-    fastForwardBuffer = 2  # Make sure that real-time playback doesn't need to buffer more than this many seconds.
-    if @playing and (ffToFrame = Math.min(event.firstChangedFrame, @frameBeforeCast, event.world.frames.length)) and ffToFrame > @currentFrame + fastForwardBuffer * @world.frameRate
+    fastForwardBuffer = 2
+    if @playing and not @realTime and (ffToFrame = Math.min(event.firstChangedFrame, @frameBeforeCast, @world.frames.length)) and ffToFrame > @currentFrame + fastForwardBuffer * @world.frameRate
       @fastForwardingToFrame = ffToFrame
       @fastForwardingSpeed = Math.max 4, 4 * 90 / (@world.maxTotalFrames * @world.dt)
+    else if @realTime
+      lag = (@world.frames.length - 1) * @world.dt - @world.age
+      intendedLag = @world.realTimeBufferMax + @world.dt
+      if lag > intendedLag * 1.2
+        @fastForwardingToFrame = @world.frames.length - @world.realTimeBufferMax * @world.frameRate
+        @fastForwardingSpeed = lag / intendedLag
+      else
+        @fastForwardingToFrame = @fastForwardingSpeed = null
+      #console.log "on new world, lag", lag, "intended lag", intendedLag, "fastForwardingToFrame", @fastForwardingToFrame, "speed", @fastForwardingSpeed, "cause we are at", @world.age, "of", @world.frames.length * @world.dt
 
   # initialization
 
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index ecaa78043..8fa6fc99f 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -13,7 +13,8 @@ System = require 'lib/world/system'
 PROGRESS_UPDATE_INTERVAL = 100
 DESERIALIZATION_INTERVAL = 10
 REAL_TIME_BUFFER_MIN = 2 * PROGRESS_UPDATE_INTERVAL
-REAL_TIME_BUFFER_MAX = 5 * PROGRESS_UPDATE_INTERVAL
+REAL_TIME_BUFFER_MAX = 3 * PROGRESS_UPDATE_INTERVAL
+REAL_TIME_BUFFERED_WAIT_INTERVAL = 0.5 * PROGRESS_UPDATE_INTERVAL
 ITEM_ORIGINAL = '53e12043b82921000051cdf9'
 
 module.exports = class World
@@ -25,6 +26,7 @@ module.exports = class World
   headless: false  # Whether we are just simulating for goal states instead of all serialized results
   framesSerializedSoFar: 0
   apiProperties: ['age', 'dt']
+  realTimeBufferMax: REAL_TIME_BUFFER_MAX / 1000
   constructor: (@userCodeMap, classMap) ->
     # classMap is needed for deserializing Worlds, Thangs, and other classes
     @classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment}
@@ -100,6 +102,35 @@ module.exports = class World
       frameToLoadUntil = @totalFrames
     i = @frames.length
     while i < frameToLoadUntil and i < @totalFrames
+      t2 = now()
+      if @realTime
+        shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
+        shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
+      else
+        shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL
+        shouldDelayRealTimeSimulation = false
+      if shouldUpdateProgress or shouldDelayRealTimeSimulation
+        if shouldUpdateProgress
+          @lastRealTimeUpdate = i * @dt if @realTime
+          #console.log 'we think it is now', (t2 - @worldLoadStartTime) / 1000, 'so delivering', @lastRealTimeUpdate
+          loadProgressCallback? i / @totalFrames unless @preloading
+        t1 = t2
+        if t2 - @t0 > 1000
+          console.log '  Loaded', i, 'of', @totalFrames, '(+' + (t2 - @t0).toFixed(0) + 'ms)' unless @realTime
+          @t0 = t2
+        continueFn = =>
+          return if @destroyed
+          if loadUntilFrame
+            @loadFrames(loadedCallback,errorCallback,loadProgressCallback, skipDeferredLoading, loadUntilFrame)
+          else
+            @loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
+        if skipDeferredLoading
+          continueFn()
+        else
+          delay = if shouldDelayRealTimeSimulation then REAL_TIME_BUFFERED_WAIT_INTERVAL else 0
+          setTimeout(continueFn, delay)
+        return
+
       if @debugging
         for thang in @thangs when thang.isProgrammable
           userCode = @userCodeMap[thang.id] ? {}
@@ -116,33 +147,7 @@ module.exports = class World
         for error in (@unhandledRuntimeErrors ? [])
           return unless errorCallback error  # errorCallback tells us whether the error is recoverable
         @unhandledRuntimeErrors = []
-      t2 = now()
-      if @realTime
-        shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
-        shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
-      else
-        shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL
-        shouldDelayRealTimeSimulation = false
-      if shouldUpdateProgress or shouldDelayRealTimeSimulation
-        if shouldUpdateProgress
-          @lastRealTimeUpdate = i * @dt if @realTime
-          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 = =>
-          return if @destroyed
-          if loadUntilFrame
-            @loadFrames(loadedCallback,errorCallback,loadProgressCallback, skipDeferredLoading, loadUntilFrame)
-          else
-            @loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
-        if skipDeferredLoading
-          continueFn()
-        else
-          delay = if shouldDelayRealTimeSimulation then PROGRESS_UPDATE_INTERVAL else 0
-          setTimeout(continueFn, delay)
-        return
+
     unless @debugging
       @ended = true
       system.finish @thangs for system in @systems
@@ -159,6 +164,8 @@ module.exports = class World
 
   shouldUpdateRealTimePlayback: (t) ->
     return false unless @realTime
+    return false if @frames.length * @dt is @lastRealTimeUpdate
+    timeLoaded = @frames.length * @dt * 1000
     timeSinceStart = t - @worldLoadStartTime
     remainingBuffer = @lastRealTimeUpdate * 1000 - timeSinceStart
     remainingBuffer < REAL_TIME_BUFFER_MIN
@@ -475,7 +482,7 @@ module.exports = class World
     w.ended = true
     nFrames = endFrame - startFrame
     totalCPUTime = perf.t3 - perf.t0 + perf.framesCPUTime
-    console.log 'Deserialization:', totalCPUTime.toFixed(0) + 'ms (' + (totalCPUTime / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches. Did', startFrame, 'to', endFrame, 'in', (perf.t4 - perf.t0).toFixed(0) + 'ms wall clock time.'
+    #console.log 'Deserialization:', totalCPUTime.toFixed(0) + 'ms (' + (totalCPUTime / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches. Did', startFrame, 'to', endFrame, 'in', (perf.t4 - perf.t0).toFixed(0) + 'ms wall clock time.'
     if false
       console.log '  Deserializing--constructing new World:', (perf.t1 - perf.t0).toFixed(2) + 'ms'
       console.log '  Deserializing--Thangs and ScriptNotes:', (perf.t2 - perf.t1).toFixed(2) + 'ms'
diff --git a/app/views/play/level/LevelFlagsView.coffee b/app/views/play/level/LevelFlagsView.coffee
index c6db86516..c9d68c960 100644
--- a/app/views/play/level/LevelFlagsView.coffee
+++ b/app/views/play/level/LevelFlagsView.coffee
@@ -48,7 +48,7 @@ module.exports = class LevelFlagsView extends CocoView
   onStageMouseDown: (e) ->
     return unless @flagColor and @realTime
     pos = x: e.worldPos.x, y: e.worldPos.y
-    flag = player: me.id, team: me.team, color: @flagColor, pos: pos, time: @world.dt * @world.frames.length + 1, active: true
+    flag = player: me.id, team: me.team, color: @flagColor, pos: pos, time: @world.dt * @world.frames.length, active: true
     @flags[@flagColor] = flag
     @flagHistory.push flag
     Backbone.Mediator.publish 'level:flag-updated', flag
@@ -57,7 +57,7 @@ module.exports = class LevelFlagsView extends CocoView
   removeFlag: (e) ->
     delete @flags[e.color]
     console.log e.color, 'deleted'
-    flag = player: me.id, team: me.team, color: e.color, time: @world.dt * @world.frames.length + 1, active: false
+    flag = player: me.id, team: me.team, color: e.color, time: @world.dt * @world.frames.length, active: false
     @flagHistory.push flag
     Backbone.Mediator.publish 'level:flag-updated', flag
 
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index 921dcc532..87f07a885 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -152,6 +152,7 @@ module.exports = class LevelPlaybackView extends CocoView
     @$el.find('#music-button').toggleClass('music-on', me.get('music'))
 
   onSetLetterbox: (e) ->
+    return if @realTime
     @togglePlaybackControls !e.on
     @disabled = e.on
 
@@ -211,6 +212,7 @@ module.exports = class LevelPlaybackView extends CocoView
     $('#volume-button', @$el).removeClass('disabled')
 
   onEnableControls: (e) ->
+    return if @realTime
     if not e.controls or 'playback' in e.controls
       @disabled = false
       $('button', @$el).removeClass('disabled')

From f44d802934165aba4a9d644b1a768140507cce1d Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sun, 24 Aug 2014 12:33:46 -0700
Subject: [PATCH 17/21] Fixed a couple bugs. Flags are now colored.

---
 app/lib/LevelLoader.coffee                    |  7 ++++---
 app/lib/scripts/ScriptManager.coffee          |  1 +
 app/lib/surface/SpriteBoss.coffee             |  2 +-
 app/lib/world/thang.coffee                    | 15 ++++++++++++---
 app/styles/play/level.sass                    |  4 ++--
 app/styles/play/spectate.sass                 |  4 ++--
 app/views/kinds/SearchView.coffee             |  2 +-
 app/views/play/level/LevelPlaybackView.coffee | 14 ++++++++++----
 app/views/play/level/ThangAvatarView.coffee   |  2 +-
 9 files changed, 34 insertions(+), 17 deletions(-)

diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index 48a980831..861dd0b22 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -70,13 +70,13 @@ module.exports = class LevelLoader extends CocoClass
     @listenToOnce @session, 'sync', ->
       @session.url = -> '/db/level.session/' + @id
       @loadDependenciesForSession(@session)
-    
+
     if @opponentSessionID
       opponentSession = new LevelSession().setURL "/db/level_session/#{@opponentSessionID}"
       @opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session')
       @opponentSession = @opponentSessionResource.model
       @listenToOnce @opponentSession, 'sync', @loadDependenciesForSession
-      
+
   loadDependenciesForSession: (session) ->
     if heroConfig = session.get('heroConfig')
       url = "/db/thang.type/#{heroConfig.thangType}/version?project=name,components,original"
@@ -107,7 +107,8 @@ module.exports = class LevelLoader extends CocoClass
     systemVersions = []
     articleVersions = []
 
-    for thang in @level.get('thangs') or []
+    flagThang = thangType: '53fa25f25bc220000052c2be', id: 'Placeholder Flag', components: []
+    for thang in (@level.get('thangs') or []).concat [flagThang]
       thangIDs.push thang.thangType
       @loadItemThangsEquippedByLevelThang(thang)
       for comp in thang.components or []
diff --git a/app/lib/scripts/ScriptManager.coffee b/app/lib/scripts/ScriptManager.coffee
index cea2b14ee..6983ac199 100644
--- a/app/lib/scripts/ScriptManager.coffee
+++ b/app/lib/scripts/ScriptManager.coffee
@@ -269,6 +269,7 @@ module.exports = ScriptManager = class ScriptManager extends CocoClass
     @publishNote(note)
 
   publishNote: (note) ->
+    Backbone.Mediator.publish 'playback:real-time-playback-ended', {}
     Backbone.Mediator.publish(note.channel, note.event)
 
   # ENDING NOTES
diff --git a/app/lib/surface/SpriteBoss.coffee b/app/lib/surface/SpriteBoss.coffee
index 06830036f..a5480740f 100644
--- a/app/lib/surface/SpriteBoss.coffee
+++ b/app/lib/surface/SpriteBoss.coffee
@@ -113,7 +113,7 @@ module.exports = class SpriteBoss extends CocoClass
     sprite = @createWizardSprite thangID: opponent.id, name: opponent.name, codeLanguage: opponent.codeLanguage
     if not opponent.levelSlug or opponent.levelSlug is 'brawlwood'
       sprite.targetPos = if opponent.team is 'ogres' then {x: 52, y: 52} else {x: 28, y: 28}
-    else if opponent.levelSlug is 'dungeon-arena'
+    else if opponent.levelSlug in ['dungeon-arena', 'sky-span']
       sprite.targetPos = if opponent.team is 'ogres' then {x: 72, y: 39} else {x: 9, y: 39}
     else if opponent.levelSlug is 'criss-cross'
       sprite.targetPos = if opponent.team is 'ogres' then {x: 50, y: 12} else {x: 0, y: 40}
diff --git a/app/lib/world/thang.coffee b/app/lib/world/thang.coffee
index d3379db9f..da69fdf0e 100644
--- a/app/lib/world/thang.coffee
+++ b/app/lib/world/thang.coffee
@@ -168,7 +168,16 @@ module.exports = class Thang
 
   getSpriteOptions: ->
     colorConfigs = @world?.getTeamColors() or {}
-    options = {}
-    if @team and colorConfigs[@team]
-      options.colorConfig = {team: colorConfigs[@team]}
+    options = {colorConfig: {}}
+    if @team and teamColor = colorConfigs[@team]
+      options.colorConfig.team = teamColor
+    if @color and color = @grabColorConfig @color
+      options.colorConfig.color = color
     options
+
+  grabColorConfig: (color) ->
+    {
+      green: {hue: 0.33, saturation: 0.5, lightness: 0.5}
+      black: {hue: 0, saturation: 0, lightness: 0.25}
+      violet: {hue: 0.83, saturation: 0.5, lightness: 0.5}
+    }[color]
diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index 6105e6424..621e82363 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -25,8 +25,8 @@ body.is-playing
     #gold-view
       right: 1%
     #control-bar-view .title
-      left: 0
-      width: 100%
+      left: 20%
+      width: 60%
       text-align: center
 
     .level-content
diff --git a/app/styles/play/spectate.sass b/app/styles/play/spectate.sass
index 24d18f42f..746a6589f 100644
--- a/app/styles/play/spectate.sass
+++ b/app/styles/play/spectate.sass
@@ -21,8 +21,8 @@
       display: inline-block
       color: #BEBEBE
       line-height: 15px
-      left: 0
-      width: 100%
+      left: 20%
+      width: 60%
       text-align: center
 
   max-width: 1920px
diff --git a/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee
index 1e41c8ead..7d5fa8b0f 100644
--- a/app/views/kinds/SearchView.coffee
+++ b/app/views/kinds/SearchView.coffee
@@ -83,5 +83,5 @@ module.exports = class SearchView extends RootView
 
   newModel: (e) ->
     modal = new NewModelModal model: @model, modelLabel: @modelLabel
-    modal.once 'success', @onNewModelSaved
+    modal.once 'model-created', @onNewModelSaved
     @openModalView modal
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index 87f07a885..528806c07 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -25,6 +25,7 @@ module.exports = class LevelPlaybackView extends CocoView
     'god:streaming-world-updated': 'onNewWorld'  # Maybe?
     'level-set-letterbox': 'onSetLetterbox'
     'tome:cast-spells': 'onTomeCast'
+    'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
 
   events:
     'click #debug-toggle': 'onToggleDebug'
@@ -288,20 +289,25 @@ module.exports = class LevelPlaybackView extends CocoView
   updateProgress: (progress, world) ->
     if world.frames.length isnt @lastLoadedFrameCount
       @updateBarWidth world.frames.length, world.maxTotalFrames, world.dt
+    wasLoaded = @worldCompletelyLoaded
     @worldCompletelyLoaded = world.frames.length is world.totalFrames
+    if @realTime and @worldCompletelyLoaded and not wasLoaded
+      Backbone.Mediator.publish 'playback:real-time-playback-ended', {}
     $('.scrubber .progress-bar', @$el).css('width', "#{progress * 100}%")
 
   updatePlayButton: (progress) ->
     if @worldCompletelyLoaded and progress >= 0.99 and @lastProgress < 0.99
       $('#play-button').removeClass('playing').removeClass('paused').addClass('ended')
-      if @realTime
-        @realTime = false
-        @togglePlaybackControls true
-        Backbone.Mediator.publish 'playback:real-time-playback-ended', {}
+      Backbone.Mediator.publish 'playback:real-time-playback-ended', {} if @realTime
     if progress < 0.99 and @lastProgress >= 0.99
       b = $('#play-button').removeClass('ended')
       if @playing then b.addClass('playing') else b.addClass('paused')
 
+  onRealTimePlaybackEnded: (e) ->
+    return unless @realTime
+    @realTime = false
+    @togglePlaybackControls true
+
   onSetDebug: (e) ->
     flag = $('#debug-toggle i.icon-ok')
     flag.toggleClass 'invisible', not e.debug
diff --git a/app/views/play/level/ThangAvatarView.coffee b/app/views/play/level/ThangAvatarView.coffee
index 90ea72be1..3c65bc848 100644
--- a/app/views/play/level/ThangAvatarView.coffee
+++ b/app/views/play/level/ThangAvatarView.coffee
@@ -70,7 +70,7 @@ module.exports = class ThangAvatarView extends CocoView
     @setProblems myProblems.length, worstLevel
 
   onNewWorld: (e) ->
-    @options.thang = @thang = e.world.thangMap[@thang.id] if @thang
+    @options.thang = @thang = e.world.thangMap[@thang.id] if @thang and e.world.thangMap[@thang.id]
 
   destroy: ->
     super()

From 3125c2b1290b11102aae035a1ec81f883d99121a Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sun, 24 Aug 2014 16:09:06 -0700
Subject: [PATCH 18/21] Add flag cursor and pending flags.

---
 app/lib/surface/CocoSprite.coffee          |  2 ++
 app/lib/surface/FlagSprite.coffee          | 34 +++++++++++++++++++
 app/lib/surface/IndieSprite.coffee         | 11 +++---
 app/lib/surface/SpriteBoss.coffee          | 39 ++++++++++++++++++++++
 app/lib/surface/Surface.coffee             |  8 +++--
 app/lib/surface/WizardSprite.coffee        |  5 ++-
 app/schemas/subscriptions/play.coffee      | 12 +++++--
 app/schemas/subscriptions/surface.coffee   | 12 +++++++
 app/styles/play/level.sass                 |  2 +-
 app/views/play/level/LevelFlagsView.coffee | 16 ++++++---
 10 files changed, 121 insertions(+), 20 deletions(-)
 create mode 100644 app/lib/surface/FlagSprite.coffee

diff --git a/app/lib/surface/CocoSprite.coffee b/app/lib/surface/CocoSprite.coffee
index 7f4db7220..ff03d2388 100644
--- a/app/lib/surface/CocoSprite.coffee
+++ b/app/lib/surface/CocoSprite.coffee
@@ -320,6 +320,8 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
     [@imageObject.x, @imageObject.y] = [sup.x, sup.y]
     @lastPos = p1.copy?() or _.clone(p1)
     @hasMoved = true
+    if @thangType.get('name') is 'Flag' and not @notOfThisWorld
+      _.defer => Backbone.Mediator.publish 'surface:flag-appeared', sprite: @
 
   updateBaseScale: ->
     scale = 1
diff --git a/app/lib/surface/FlagSprite.coffee b/app/lib/surface/FlagSprite.coffee
new file mode 100644
index 000000000..0c20b7ebf
--- /dev/null
+++ b/app/lib/surface/FlagSprite.coffee
@@ -0,0 +1,34 @@
+IndieSprite = require 'lib/surface/IndieSprite'
+{me} = require 'lib/auth'
+
+module.exports = class FlagSprite extends IndieSprite
+  subscriptions:
+    'surface:mouse-moved': 'onMouseMoved'
+
+  #shortcuts:
+
+  defaultPos: -> x: 20, y: 20, z: 1
+
+  constructor: (thangType, options) ->
+    super thangType, options
+    @toggleCursor options.isCursor
+
+  makeIndieThang: (thangType, options) ->
+    thang = super thangType, options
+    thang.width = thang.height = thang.depth = 2
+    thang.pos.z = 1
+    thang.isSelectable = false
+    thang.color = options.color
+    thang.team = options.team
+    thang
+
+  onMouseMoved: (e) ->
+    return unless @options.isCursor
+    wop = @options.camera.screenToWorld x: e.x, y: e.y
+    @thang.pos.x = wop.x
+    @thang.pos.y = wop.y
+
+  toggleCursor: (to) ->
+    @options.isCursor = to
+    @thang.alpha = if to then 0.33 else 0.67  # 1.0 is for flags that have been placed
+    @updateAlpha()
diff --git a/app/lib/surface/IndieSprite.coffee b/app/lib/surface/IndieSprite.coffee
index e66b90e16..a297b7508 100644
--- a/app/lib/surface/IndieSprite.coffee
+++ b/app/lib/surface/IndieSprite.coffee
@@ -1,8 +1,5 @@
-{me} = require('lib/auth')
 Thang = require 'lib/world/thang'
-Vector = require 'lib/world/vector'
 CocoSprite = require 'lib/surface/CocoSprite'
-Camera = require './Camera'
 
 module.exports = IndieSprite = class IndieSprite extends CocoSprite
   notOfThisWorld: true
@@ -11,16 +8,16 @@ module.exports = IndieSprite = class IndieSprite extends CocoSprite
     'note-group-ended': 'onNoteGroupEnded'
 
   constructor: (thangType, options) ->
-    options.thang = @makeIndieThang thangType, options.thangID, options.pos
+    options.thang = @makeIndieThang thangType, options
     super thangType, options
     @shadow = @thang
 
-  makeIndieThang: (thangType, thangID, pos) ->
-    @thang = thang = new Thang null, thangType.get('name'), thangID
+  makeIndieThang: (thangType, options) ->
+    @thang = thang = new Thang null, thangType.get('name'), options.thangID
     # Build needed results of what used to be Exists, Physical, Acts, and Selectable Components
     thang.exists = true
     thang.width = thang.height = thang.depth = 4
-    thang.pos = pos ? @defaultPos()
+    thang.pos = options.pos ? @defaultPos()
     thang.pos.z = thang.depth / 2
     thang.shape = 'ellipsoid'
     thang.rotation = 0
diff --git a/app/lib/surface/SpriteBoss.coffee b/app/lib/surface/SpriteBoss.coffee
index a5480740f..7771462be 100644
--- a/app/lib/surface/SpriteBoss.coffee
+++ b/app/lib/surface/SpriteBoss.coffee
@@ -3,6 +3,7 @@ CocoClass = require 'lib/CocoClass'
 Layer = require './Layer'
 IndieSprite = require 'lib/surface/IndieSprite'
 WizardSprite = require 'lib/surface/WizardSprite'
+FlagSprite = require 'lib/surface/FlagSprite'
 CocoSprite = require 'lib/surface/CocoSprite'
 Mark = require './Mark'
 Grid = require 'lib/world/Grid'
@@ -22,6 +23,10 @@ module.exports = class SpriteBoss extends CocoClass
     'god:streaming-world-updated': 'onNewWorld'
     'camera:dragged': 'onCameraDragged'
     'sprite:loaded': -> @update(true)
+    'level:flag-color-selected': 'onFlagColorSelected'
+    'level:flag-updated': 'onFlagUpdated'
+    'surface:flag-appeared': 'onFlagAppeared'
+    'surface:remove-selected-flag': 'onRemoveSelectedFlag'
 
   constructor: (@options) ->
     super()
@@ -37,6 +42,7 @@ module.exports = class SpriteBoss extends CocoClass
     @selfWizardSprite = null
     @createLayers()
     @spriteSheetCache = {}
+    @pendingFlags = []
 
   destroy: ->
     @removeSprite sprite for thangID, sprite of @sprites
@@ -316,6 +322,39 @@ module.exports = class SpriteBoss extends CocoClass
         instance.addEventListener 'complete', ->
           Backbone.Mediator.publish 'thang-finished-talking', thang: sprite?.thang
 
+  onFlagColorSelected: (e) ->
+    @removeSprite @flagCursorSprite if @flagCursorSprite
+    @flagCursorSprite = null
+    return unless e.color
+    @flagCursorSprite = new FlagSprite @thangTypeFor('Flag'), @createSpriteOptions(thangID: 'Flag Cursor', color: e.color, team: me.team, isCursor: true, pos: e.pos)
+    @addSprite @flagCursorSprite, @flagCursorSprite.thang.id, @spriteLayers['Floating']
+
+  onFlagUpdated: (e) ->
+    return unless e.active
+    pendingFlag = new FlagSprite @thangTypeFor('Flag'), @createSpriteOptions(thangID: 'Pending Flag ' + Math.random(), color: e.color, team: me.team, isCursor: false, pos: e.pos)
+    @addSprite pendingFlag, pendingFlag.thang.id, @spriteLayers['Floating']
+    @pendingFlags.push pendingFlag
+
+  onFlagAppeared: (e) ->
+    # Remove the pending flag that matches this one's color/team/position, and any color/team matches placed earlier.
+    t1 = e.sprite.thang
+    pending = (@pendingFlags ? []).slice()
+    foundExactMatch = false
+    for i in [pending.length - 1 .. 0] by -1
+      pendingFlag = pending[i]
+      t2 = pendingFlag.thang
+      matchedType = t1.color is t2.color and t1.team is t2.team
+      matched = matchedType and (foundExactMatch or Math.abs(t1.pos.x - t2.pos.x) < 0.00001 and Math.abs(t1.pos.y - t2.pos.y) < 0.00001)
+      if matched
+        foundExactMatch = true
+        @pendingFlags.splice(i, 1)
+        @removeSprite pendingFlag
+    null
+
+  onRemoveSelectedFlag: (e) ->
+    return unless @selectedSprite and @selectedSprite.thangType.get('name') is 'Flag' and @selectedSprite.thang.team is me.team
+    Backbone.Mediator.publish 'surface:remove-flag', color: @selectedSprite.thang.color
+
   # Marks
 
   updateSelection: ->
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 553cede03..92715bd5a 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -69,6 +69,7 @@ module.exports = Surface = class Surface extends CocoClass
     'camera:zoom-updated': 'onZoomUpdated'
     'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
     'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
+    'level:flag-color-selected': 'onFlagColorSelected'
     #'god:world-load-progress-changed': -> console.log 'it is actually', @world.age
 
   shortcuts:
@@ -634,10 +635,11 @@ module.exports = Surface = class Surface extends CocoClass
     @realTime = false
     @onResize()
     @spriteBoss.selfWizardSprite?.toggle true
-    @canvas.removeClass 'flag-selected'
+    @canvas.removeClass 'flag-color-selected'
 
-  onFlagSelected: (e) ->
-    @canvas.toggleClass 'flag-selected', Boolean(e.color)
+  onFlagColorSelected: (e) ->
+    @canvas.toggleClass 'flag-color-selected', Boolean(e.color)
+    e.pos = @camera.screenToWorld @mouseScreenPos if @mouseScreenPos
 
   # paths - TODO: move to SpriteBoss? but only update on frame drawing instead of on every frame update?
 
diff --git a/app/lib/surface/WizardSprite.coffee b/app/lib/surface/WizardSprite.coffee
index c562abd1c..8a7390234 100644
--- a/app/lib/surface/WizardSprite.coffee
+++ b/app/lib/surface/WizardSprite.coffee
@@ -1,5 +1,4 @@
 IndieSprite = require 'lib/surface/IndieSprite'
-Camera = require './Camera'
 {me} = require 'lib/auth'
 
 module.exports = class WizardSprite extends IndieSprite
@@ -38,8 +37,8 @@ module.exports = class WizardSprite extends IndieSprite
     else if options.name
       @setNameLabel options.name
 
-  makeIndieThang: (thangType, thangID, pos) ->
-    thang = super thangType, thangID, pos
+  makeIndieThang: (thangType, options) ->
+    thang = super thangType, options
     thang.isSelectable = false
     thang.bobHeight = 0.75
     thang.bobTime = 2
diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee
index b80467250..d55b6b46b 100644
--- a/app/schemas/subscriptions/play.coffee
+++ b/app/schemas/subscriptions/play.coffee
@@ -57,13 +57,21 @@ module.exports =
   'level:victory-hidden':
     {} # TODO schema
 
-  'level:flag-selected':
+  'level:flag-color-selected':
     type: 'object'
     additionalProperties: false
     properties:
-      color:  # omitted if we've deselected
+      color:
         type: 'string'
         enum: ['green', 'black', 'violet']
+        description: 'The flag color to place next, or omitted/null if deselected.'
+      pos:
+        type: 'object'
+        additionalProperties: false
+        required: ['x', 'y']
+        properties:
+          x: {type: 'number'}
+          y: {type: 'number'}
 
   'level:flag-updated':
     type: 'object'
diff --git a/app/schemas/subscriptions/surface.coffee b/app/schemas/subscriptions/surface.coffee
index 0c7413248..ee3b98458 100644
--- a/app/schemas/subscriptions/surface.coffee
+++ b/app/schemas/subscriptions/surface.coffee
@@ -94,3 +94,15 @@ module.exports =  # /app/lib/surface
 
   'echo-all-wizard-sprites':
     {} # TODO schema
+
+  'surface:flag-appeared':
+    type: 'object'
+    additionalProperties: false
+    required: ['sprite']
+    properties:
+      sprite:
+        type: 'object'
+
+  'surface:remove-selected-flag':
+    type: 'object'
+    additionalProperties: false
diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index 621e82363..c87d229bb 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -47,7 +47,7 @@ body.is-playing
     z-index: 1
     @include transition(0.5s ease-out)
 
-    &.flag-selected
+    &.flag-color-selected
       cursor: crosshair
 
   min-width: 1024px
diff --git a/app/views/play/level/LevelFlagsView.coffee b/app/views/play/level/LevelFlagsView.coffee
index c9d68c960..b06cc3487 100644
--- a/app/views/play/level/LevelFlagsView.coffee
+++ b/app/views/play/level/LevelFlagsView.coffee
@@ -12,6 +12,7 @@ module.exports = class LevelFlagsView extends CocoView
     'surface:stage-mouse-down': 'onStageMouseDown'
     'god:new-world-created': 'onNewWorld'
     'god:streaming-world-updated': 'onNewWorld'
+    'surface:remove-flag': 'onRemoveFlag'
 
   events:
     'click .green-flag': -> @onFlagSelected color: 'green', source: 'button'
@@ -23,6 +24,7 @@ module.exports = class LevelFlagsView extends CocoView
     'b': -> @onFlagSelected color: 'black', source: 'shortcut'
     'v': -> @onFlagSelected color: 'violet', source: 'shortcut'
     'esc': -> @onFlagSelected color: null, source: 'shortcut'
+    'delete, del, backspace': 'onDeletePressed'
 
   constructor: (options) ->
     super options
@@ -39,9 +41,10 @@ module.exports = class LevelFlagsView extends CocoView
     @$el.hide()
 
   onFlagSelected: (e) ->
+    return if @flagColor is e.color
     color = if e.source is 'button' and e.color is @flagColor then null else e.color
     @flagColor = color
-    Backbone.Mediator.publish 'level:flag-selected', color: color
+    Backbone.Mediator.publish 'level:flag-color-selected', color: color
     @$el.find('.flag-button').removeClass('active')
     @$el.find(".#{color}-flag").addClass('active') if color
 
@@ -52,14 +55,19 @@ module.exports = class LevelFlagsView extends CocoView
     @flags[@flagColor] = flag
     @flagHistory.push flag
     Backbone.Mediator.publish 'level:flag-updated', flag
-    console.log 'trying to place flag at', @world.age, 'and think it will happen by', flag.time
+    #console.log 'trying to place flag at', @world.age, 'and think it will happen by', flag.time
 
-  removeFlag: (e) ->
+  onDeletePressed: (e) ->
+    return unless @realTime
+    Backbone.Mediator.publish 'surface:remove-selected-flag', {}
+    @onFlagSelected color: null, source: 'shortcut'
+
+  onRemoveFlag: (e) ->
     delete @flags[e.color]
-    console.log e.color, 'deleted'
     flag = player: me.id, team: me.team, color: e.color, time: @world.dt * @world.frames.length, active: false
     @flagHistory.push flag
     Backbone.Mediator.publish 'level:flag-updated', flag
+    #console.log e.color, 'deleted at time', flag.time
 
   onNewWorld: (event) ->
     return unless event.world.name is @world.name

From 1a7e4554f00dbad506dea2de44ddb6da9e069c1d Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sun, 24 Aug 2014 16:48:59 -0700
Subject: [PATCH 19/21] Fixed a few minor bugs.

---
 app/lib/LevelLoader.coffee                 | 4 ++++
 app/lib/surface/CocoSprite.coffee          | 3 ++-
 app/views/play/level/LevelFlagsView.coffee | 1 +
 app/views/play/level/tome/TomeView.coffee  | 1 +
 4 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index 861dd0b22..cc9a6dd12 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -78,6 +78,10 @@ module.exports = class LevelLoader extends CocoClass
       @listenToOnce @opponentSession, 'sync', @loadDependenciesForSession
 
   loadDependenciesForSession: (session) ->
+    return if @levelID is 'sky-span'  # TODO
+    # TODO: I think this runs afoul of https://github.com/codecombat/codecombat/issues/1108
+    # TODO: this shouldn't happen when it's not a hero level, but we don't have level loaded yet,
+    # and the sessions are being created with default hero config regardless of whether it's a hero level.
     if heroConfig = session.get('heroConfig')
       url = "/db/thang.type/#{heroConfig.thangType}/version?project=name,components,original"
       @worldNecessities.push @maybeLoadURL(url, ThangType, 'thang')
diff --git a/app/lib/surface/CocoSprite.coffee b/app/lib/surface/CocoSprite.coffee
index ff03d2388..a3dbc4130 100644
--- a/app/lib/surface/CocoSprite.coffee
+++ b/app/lib/surface/CocoSprite.coffee
@@ -321,6 +321,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
     @lastPos = p1.copy?() or _.clone(p1)
     @hasMoved = true
     if @thangType.get('name') is 'Flag' and not @notOfThisWorld
+      # Let the pending flags know we're here (but not this call stack, they need to delete themselves, and we may be iterating sprites).
       _.defer => Backbone.Mediator.publish 'surface:flag-appeared', sprite: @
 
   updateBaseScale: ->
@@ -521,7 +522,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
     @letterboxOn = e.on
 
   onMouseEvent: (e, ourEventName) ->
-    return if @letterboxOn
+    return if @letterboxOn or not @imageObject
     p = @imageObject
     p = p.parent while p.parent
     newEvent = sprite: @, thang: @thang, originalEvent: e, canvas:p.canvas
diff --git a/app/views/play/level/LevelFlagsView.coffee b/app/views/play/level/LevelFlagsView.coffee
index b06cc3487..a98b6a09f 100644
--- a/app/views/play/level/LevelFlagsView.coffee
+++ b/app/views/play/level/LevelFlagsView.coffee
@@ -38,6 +38,7 @@ module.exports = class LevelFlagsView extends CocoView
 
   onRealTimePlaybackEnded: (e) ->
     @realTime = false
+    @onFlagSelected color: null
     @$el.hide()
 
   onFlagSelected: (e) ->
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index 92e634dd9..1d340dfee 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -209,6 +209,7 @@ module.exports = class TomeView extends CocoView
 
   spellFor: (thang, spellName) ->
     return null unless thang?.isProgrammable
+    return unless @thangSpells[thang.id]  # Probably in streaming mode, where we don't update until it's done.
     selectedThangSpells = (@spells[spellKey] for spellKey in @thangSpells[thang.id])
     if spellName
       spell = _.find selectedThangSpells, {name: spellName}

From 718d586f0768ece013eeff224e407b34b0343472 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sun, 24 Aug 2014 21:39:34 -0700
Subject: [PATCH 20/21] A ton of misc bug fixes and performance improvements,
 getting ready to merge world streaming and flags back in.

---
 app/initialize.coffee                         |  3 +-
 app/lib/God.coffee                            |  2 +-
 app/lib/simulator/Simulator.coffee            |  2 +-
 app/lib/surface/WizardSprite.coffee           |  3 +-
 app/lib/world/thang_state.coffee              |  2 -
 app/lib/world/world.coffee                    | 92 +++++++++----------
 app/locale/en.coffee                          |  2 +
 app/models/ThangType.coffee                   |  3 +-
 app/schemas/subscriptions/play.coffee         |  7 +-
 app/schemas/subscriptions/tome.coffee         |  9 +-
 app/styles/play/level/level-flags-view.sass   |  2 +-
 .../play/level/modal/keyboard_shortcuts.jade  |  4 +
 .../play/level/tome/cast_button.jade          |  4 +-
 app/views/play/ladder/LadderTabView.coffee    |  5 +-
 app/views/play/level/LevelFlagsView.coffee    |  7 +-
 app/views/play/level/LevelPlaybackView.coffee |  2 +-
 app/views/play/level/PlayLevelView.coffee     |  7 +-
 .../play/level/tome/CastButtonView.coffee     | 12 ++-
 app/views/play/level/tome/Spell.coffee        | 11 ++-
 app/views/play/level/tome/SpellView.coffee    |  2 +-
 .../play/level/tome/ThangListView.coffee      |  8 ++
 21 files changed, 110 insertions(+), 79 deletions(-)

diff --git a/app/initialize.coffee b/app/initialize.coffee
index f2ef617ce..a27c1f7a2 100644
--- a/app/initialize.coffee
+++ b/app/initialize.coffee
@@ -27,8 +27,7 @@ init = ->
   # Set up Backbone.Mediator schemas
   setUpDefinitions()
   setUpChannels()
-  #Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
-  Backbone.Mediator.setValidationEnabled false  # STREAM: Should change back
+  Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
   app.initialize()
   Backbone.history.start({ pushState: true })
   handleNormalUrls()
diff --git a/app/lib/God.coffee b/app/lib/God.coffee
index 754db721f..de1f6b685 100644
--- a/app/lib/God.coffee
+++ b/app/lib/God.coffee
@@ -91,7 +91,7 @@ module.exports = class God extends CocoClass
     userCodeMap = {}
     for spellKey, spell of spells
       for thangID, spellThang of spell.thangs
-        continue if spellThang.thang.programmableMethods[spell.name].cloneOf
+        continue if spellThang.thang?.programmableMethods[spell.name].cloneOf
         (userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
     userCodeMap
 
diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee
index 184d7c7dc..0e5a6631e 100644
--- a/app/lib/simulator/Simulator.coffee
+++ b/app/lib/simulator/Simulator.coffee
@@ -184,7 +184,7 @@ module.exports = class Simulator extends CocoClass
     try
       @commenceSimulationAndSetupCallback()
     catch err
-      console.error 'There was an error in simulation:', err, "-- trying again in #{@retryDelayInSeconds} seconds"
+      console.error 'There was an error in simulation:', err, err.stack, "-- trying again in #{@retryDelayInSeconds} seconds"
       @simulateAnotherTaskAfterDelay()
 
   assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->
diff --git a/app/lib/surface/WizardSprite.coffee b/app/lib/surface/WizardSprite.coffee
index 8a7390234..fc59f010d 100644
--- a/app/lib/surface/WizardSprite.coffee
+++ b/app/lib/surface/WizardSprite.coffee
@@ -59,7 +59,8 @@ module.exports = class WizardSprite extends IndieSprite
 
   toggle: (to) ->
     @imageObject?.visible = to
-    @labels.name?[if to then 'show' else 'hide']()
+    label[if to then 'show' else 'hide']() for name, label of @labels
+    mark.mark?.visible = to for name, mark of @marks
 
   onPlayerStatesChanged: (e) ->
     for playerID, state of e.states
diff --git a/app/lib/world/thang_state.coffee b/app/lib/world/thang_state.coffee
index 86caa1a64..117de1a97 100644
--- a/app/lib/world/thang_state.coffee
+++ b/app/lib/world/thang_state.coffee
@@ -47,7 +47,6 @@ module.exports = class ThangState
       value = @specialKeysToValues[specialKey]
     else if type is 'Thang'
       specialKey = storage[@frameIndex]
-      console.error "couldn't find world from thang", @thang, "for", @specialKeysToValues[specialKey] unless @thang.world
       value = @thang.world.getThangByID @specialKeysToValues[specialKey]
     else if type is 'array'
       specialKey = storage[@frameIndex]
@@ -174,7 +173,6 @@ module.exports = class ThangState
     # Optimize like no tomorrow--most performance-sensitive part of the whole app, called once per WorldFrame per Thang per trackedProperty, blocking the UI
     ts = new ThangState
     ts.thang = thang
-    console.error "couldn't find thang!", @ unless thang
     ts.frameIndex = frameIndex
     ts.trackedPropertyKeys = trackedPropertyKeys
     ts.trackedPropertyTypes = trackedPropertyTypes
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index 8fa6fc99f..e76ab2707 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -71,9 +71,11 @@ module.exports = class World
         @thangs[i] = thang
     @thangMap[thang.id] = thang
 
-  thangDialogueSounds: ->
+  thangDialogueSounds: (startFrame=0) ->
+    return [] unless startFrame < @frames.length
     [sounds, seen] = [[], {}]
-    for frame in @frames
+    for frameIndex in [startFrame ... @frames.length]
+      frame = @frames[frameIndex]
       for thangID, state of frame.thangStateMap
         continue unless state.thang.say and sayMessage = state.getStateForProp 'sayMessage'
         soundKey = state.thang.spriteName + ':' + sayMessage
@@ -90,64 +92,30 @@ module.exports = class World
 
   loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) ->
     return if @aborted
-    unless @thangs.length
-      console.log 'Warning: loadFrames called on empty World (no thangs).'
+    console.log 'Warning: loadFrames called on empty World (no thangs).' unless @thangs.length
     t1 = now()
     @t0 ?= t1
     @worldLoadStartTime ?= t1
     @lastRealTimeUpdate ?= 0
-    if loadUntilFrame
-      frameToLoadUntil = loadUntilFrame + 1
-    else
-      frameToLoadUntil = @totalFrames
+    continueLaterFn = =>
+      @loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading, loadUntilFrame) unless @destroyed
+    frameToLoadUntil = if loadUntilFrame then loadUntilFrame + 1 else @totalFrames  # Might stop early if debugging.
     i = @frames.length
     while i < frameToLoadUntil and i < @totalFrames
-      t2 = now()
-      if @realTime
-        shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
-        shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
-      else
-        shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL
-        shouldDelayRealTimeSimulation = false
-      if shouldUpdateProgress or shouldDelayRealTimeSimulation
-        if shouldUpdateProgress
-          @lastRealTimeUpdate = i * @dt if @realTime
-          #console.log 'we think it is now', (t2 - @worldLoadStartTime) / 1000, 'so delivering', @lastRealTimeUpdate
-          loadProgressCallback? i / @totalFrames unless @preloading
-        t1 = t2
-        if t2 - @t0 > 1000
-          console.log '  Loaded', i, 'of', @totalFrames, '(+' + (t2 - @t0).toFixed(0) + 'ms)' unless @realTime
-          @t0 = t2
-        continueFn = =>
-          return if @destroyed
-          if loadUntilFrame
-            @loadFrames(loadedCallback,errorCallback,loadProgressCallback, skipDeferredLoading, loadUntilFrame)
-          else
-            @loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
-        if skipDeferredLoading
-          continueFn()
-        else
-          delay = if shouldDelayRealTimeSimulation then REAL_TIME_BUFFERED_WAIT_INTERVAL else 0
-          setTimeout(continueFn, delay)
-        return
-
-      if @debugging
-        for thang in @thangs when thang.isProgrammable
-          userCode = @userCodeMap[thang.id] ? {}
-          for methodName, aether of userCode
-            framesToLoadFlowBefore = if methodName is 'plan' or methodName is 'makeBid' then 200 else 1  # Adjust if plan() is taking even longer
-            aether._shouldSkipFlow = i < loadUntilFrame - framesToLoadFlowBefore
+      return unless @shouldContinueLoading t1, loadProgressCallback, skipDeferredLoading, continueLaterFn
+      @adjustFlowSettings loadUntilFrame if @debugging
       try
         @getFrame(i)
-        ++i  # increment this after we have succeeded in getting the frame, otherwise we'll have to do that frame again
+        ++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
+        @addError error  # Not an Aether.errors.UserCodeError; maybe we can't recover
       unless @preloading or @debugging
         for error in (@unhandledRuntimeErrors ? [])
           return unless errorCallback error  # errorCallback tells us whether the error is recoverable
         @unhandledRuntimeErrors = []
+    @finishLoadingFrames loadProgressCallback, loadedCallback
 
+  finishLoadingFrames: (loadProgressCallback, loadedCallback) ->
     unless @debugging
       @ended = true
       system.finish @thangs for system in @systems
@@ -170,6 +138,38 @@ module.exports = class World
     remainingBuffer = @lastRealTimeUpdate * 1000 - timeSinceStart
     remainingBuffer < REAL_TIME_BUFFER_MIN
 
+  shouldContinueLoading: (t1, loadProgressCallback, skipDeferredLoading, continueLaterFn) ->
+    t2 = now()
+    if @realTime
+      shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
+      shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
+    else
+      shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL
+      shouldDelayRealTimeSimulation = false
+    return true unless shouldUpdateProgress or shouldDelayRealTimeSimulation
+    # Stop loading frames for now; continue in a moment.
+    if shouldUpdateProgress
+      @lastRealTimeUpdate = @frames.length * @dt if @realTime
+      #console.log 'we think it is now', (t2 - @worldLoadStartTime) / 1000, 'so delivering', @lastRealTimeUpdate
+      loadProgressCallback? @frames.length / @totalFrames unless @preloading
+    t1 = t2
+    if t2 - @t0 > 1000
+      console.log '  Loaded', @frames.length, 'of', @totalFrames, '(+' + (t2 - @t0).toFixed(0) + 'ms)' unless @realTime
+      @t0 = t2
+    if skipDeferredLoading
+      continueLaterFn()
+    else
+      delay = if shouldDelayRealTimeSimulation then REAL_TIME_BUFFERED_WAIT_INTERVAL else 0
+      setTimeout continueLaterFn, delay
+    false
+
+  adjustFlowSettings: (loadUntilFrame) ->
+    for thang in @thangs when thang.isProgrammable
+      userCode = @userCodeMap[thang.id] ? {}
+      for methodName, aether of userCode
+        framesToLoadFlowBefore = if methodName is 'plan' or methodName is 'makeBid' then 200 else 1  # Adjust if plan() is taking even longer
+        aether._shouldSkipFlow = @frames.length < loadUntilFrame - framesToLoadFlowBefore
+
   finalizePreload: (loadedCallback) ->
     @preloading = false
     loadedCallback() if @ended
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index e649a439b..27c340771 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -498,7 +498,9 @@
     space: "Space"
     enter: "Enter"
     escape: "Escape"
+    shift: "Shift"
     cast_spell: "Cast current spell."
+    run_real_time: "Run in real time."
     continue_script: "Continue past current script."
     skip_scripts: "Skip past all skippable scripts."
     toggle_playback: "Toggle play/pause."
diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee
index 8dbaae541..8a8d990ea 100644
--- a/app/models/ThangType.coffee
+++ b/app/models/ThangType.coffee
@@ -63,6 +63,7 @@ module.exports = class ThangType extends CocoModel
     options = _.clone options
     options.resolutionFactor ?= SPRITE_RESOLUTION_FACTOR
     options.async ?= false
+    options.thang = null  # Don't hold onto any bad Thang references.
     options
 
   buildSpriteSheet: (options) ->
@@ -188,7 +189,7 @@ module.exports = class ThangType extends CocoModel
     portrait = if portrait then '(Portrait)' else ''
     name = _.string.rpad @get('name'), 20
     time = _.string.lpad '' + new Date().getTime() - startTime, 6
-    #console.debug "Built sheet:  #{name} #{time}ms  #{kind}  #{portrait}"  # STREAM: uncomment
+    console.debug "Built sheet:  #{name} #{time}ms  #{kind}  #{portrait}"
 
   spriteSheetKey: (options) ->
     colorConfigs = []
diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee
index d55b6b46b..f9dce2648 100644
--- a/app/schemas/subscriptions/play.coffee
+++ b/app/schemas/subscriptions/play.coffee
@@ -62,9 +62,10 @@ module.exports =
     additionalProperties: false
     properties:
       color:
-        type: 'string'
-        enum: ['green', 'black', 'violet']
-        description: 'The flag color to place next, or omitted/null if deselected.'
+        oneOf: [
+          {type: 'null'}
+          {type: 'string', enum: ['green', 'black', 'violet'], description: 'The flag color to place next, or omitted/null if deselected.'}
+        ]
       pos:
         type: 'object'
         additionalProperties: false
diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee
index 42af94472..a582156e1 100644
--- a/app/schemas/subscriptions/tome.coffee
+++ b/app/schemas/subscriptions/tome.coffee
@@ -11,10 +11,11 @@ module.exports =
         type: "object"
       preload:
         type: "boolean"
+      realTime:
+        type: "boolean"
     required: []
     additionalProperties: false
 
-  # TODO do we really need both 'cast-spell' and 'cast-spells'?
   "tome:cast-spells":
     title: "Cast Spells"
     $schema: "http://json-schema.org/draft-04/schema#"
@@ -25,6 +26,8 @@ module.exports =
         type: "object"
       preload:
         type: "boolean"
+      realTime:
+        type: "boolean"
     required: []
     additionalProperties: false
 
@@ -33,7 +36,9 @@ module.exports =
     $schema: "http://json-schema.org/draft-04/schema#"
     description: "Published when you wish to manually recast all spells"
     type: "object"
-    properties: {}
+    properties:
+      realTime:
+        type: "boolean"
     required: []
     additionalProperties: false
 
diff --git a/app/styles/play/level/level-flags-view.sass b/app/styles/play/level/level-flags-view.sass
index c5204dbb1..167f9227c 100644
--- a/app/styles/play/level/level-flags-view.sass
+++ b/app/styles/play/level/level-flags-view.sass
@@ -26,4 +26,4 @@
       color: black
     &.violet-flag
       color: violet
- 
+
diff --git a/app/templates/play/level/modal/keyboard_shortcuts.jade b/app/templates/play/level/modal/keyboard_shortcuts.jade
index a920a6f73..41b67c8f8 100644
--- a/app/templates/play/level/modal/keyboard_shortcuts.jade
+++ b/app/templates/play/level/modal/keyboard_shortcuts.jade
@@ -8,6 +8,10 @@ block modal-body-content
     dt(title="Shift+" + enter)
       code ⇧+#{enter}
     dd(data-i18n="keyboard_shortcuts.cast_spell") Cast current spell.
+  dl.dl-horizontal
+    dt(title=ctrlName + "+Shift+" + enter)
+      code #{ctrl}+⇧+#{enter}
+    dd(data-i18n="keyboard_shortcuts.run_real_time") Run in real time.
   dl.dl-horizontal
     dt(title="Shift+" + space)
       code ⇧+#{space}
diff --git a/app/templates/play/level/tome/cast_button.jade b/app/templates/play/level/tome/cast_button.jade
index a94b388ac..3f3d6669f 100644
--- a/app/templates/play/level/tome/cast_button.jade
+++ b/app/templates/play/level/tome/cast_button.jade
@@ -1,5 +1,5 @@
 div.btn-group.btn-group-lg.cast-button-group
   .button-progress-overlay
-  button.btn.btn-inverse.banner.cast-button(title=castShortcutVerbose + ": Cast current spell", data-i18n="play_level.tome_cast_button_cast") Spell Cast
-  button.btn.btn-inverse.banner.cast-real-time-button(title=castRealTimeShortcutVerbose + ": Cast in real time") 
+  button.btn.btn-inverse.banner.cast-button(title=castVerbose, data-i18n="play_level.tome_cast_button_cast") Spell Cast
+  button.btn.btn-inverse.banner.cast-real-time-button(title=castRealTimeVerbose)
     i.glyphicon.glyphicon-play
\ No newline at end of file
diff --git a/app/views/play/ladder/LadderTabView.coffee b/app/views/play/ladder/LadderTabView.coffee
index 1780fa057..27a43c4b6 100644
--- a/app/views/play/ladder/LadderTabView.coffee
+++ b/app/views/play/ladder/LadderTabView.coffee
@@ -197,8 +197,9 @@ module.exports = class LadderTabView extends CocoView
 
     formatCount = d3.format(',.0')
 
-    x = d3.scale.linear().domain([-3000, 6000]).range([0, width])
-
+    minX = Math.floor(Math.min(histogramData...) / 1000) * 1000
+    maxX = Math.ceil(Math.max(histogramData...) / 1000) * 1000
+    x = d3.scale.linear().domain([minX, maxX]).range([0, width])
     data = d3.layout.histogram().bins(x.ticks(20))(histogramData)
     y = d3.scale.linear().domain([0, d3.max(data, (d) -> d.y)]).range([height, 10])
 
diff --git a/app/views/play/level/LevelFlagsView.coffee b/app/views/play/level/LevelFlagsView.coffee
index a98b6a09f..76d55a056 100644
--- a/app/views/play/level/LevelFlagsView.coffee
+++ b/app/views/play/level/LevelFlagsView.coffee
@@ -37,12 +37,13 @@ module.exports = class LevelFlagsView extends CocoView
     @flagHistory = []
 
   onRealTimePlaybackEnded: (e) ->
-    @realTime = false
     @onFlagSelected color: null
+    @realTime = false
     @$el.hide()
 
   onFlagSelected: (e) ->
-    return if @flagColor is e.color
+    return unless @realTime
+    return if @flagColor is e.color and e.source is 'shortcut'
     color = if e.source is 'button' and e.color is @flagColor then null else e.color
     @flagColor = color
     Backbone.Mediator.publish 'level:flag-color-selected', color: color
@@ -72,4 +73,4 @@ module.exports = class LevelFlagsView extends CocoView
 
   onNewWorld: (event) ->
     return unless event.world.name is @world.name
-    @world = event.world
+    @world = @options.world = event.world
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index 528806c07..63fdc9379 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -22,7 +22,7 @@ module.exports = class LevelPlaybackView extends CocoView
     'level-toggle-grid': 'onToggleGrid'
     'surface:frame-changed': 'onFrameChanged'
     'god:new-world-created': 'onNewWorld'
-    'god:streaming-world-updated': 'onNewWorld'  # Maybe?
+    'god:streaming-world-updated': 'onNewWorld'
     'level-set-letterbox': 'onSetLetterbox'
     'tome:cast-spells': 'onTomeCast'
     'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index ec479b504..dde56b11e 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -512,7 +512,12 @@ module.exports = class PlayLevelView extends RootView
     @world = e.world
     @world.scripts = scripts
     thangTypes = @supermodel.getModels(ThangType)
-    for [spriteName, message] in @world.thangDialogueSounds()
+    startFrame = @lastWorldFramesLoaded ? 0
+    if @world.frames.length is @world.totalFrames  # Finished loading
+      @lastWorldFramesLoaded = 0
+    else
+      @lastWorldFramesLoaded = @world.frames.length
+    for [spriteName, message] in @world.thangDialogueSounds startFrame
       continue unless thangType = _.find thangTypes, (m) -> m.get('name') is spriteName
       continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers')
       AudioPlayer.preloadSoundReference sound
diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee
index 3b310f2f4..f28500fdf 100644
--- a/app/views/play/level/tome/CastButtonView.coffee
+++ b/app/views/play/level/tome/CastButtonView.coffee
@@ -21,13 +21,15 @@ module.exports = class CastButtonView extends CocoView
     @spells = options.spells
     @levelID = options.levelID
     @castShortcut = '⇧↵'
-    @castShortcutVerbose = 'Shift+Enter'
-    @castRealTimeShortcutVerbose = 'Ctrl+Shift+Enter'
 
   getRenderData: (context={}) ->
     context = super context
-    context.castShortcutVerbose = @castShortcutVerbose
-    context.castRealTimeShortcutVerbose = @castRealTimeShortcutVerbose
+    shift = $.i18n.t 'keyboard_shortcuts.shift'
+    enter = $.i18n.t 'keyboard_shortcuts.enter'
+    castShortcutVerbose = "#{shift}+#{enter}"
+    castRealTimeShortcutVerbose = (if @isMac() then 'Cmd' else 'Ctrl') + '+' + castShortcutVerbose
+    context.castVerbose = castShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.cast_spell')
+    context.castRealTimeVerbose = castRealTimeShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_real_time')
     context
 
   afterRender: ->
@@ -101,6 +103,6 @@ module.exports = class CastButtonView extends CocoView
     @autocastDelay = delay = parseInt delay
     me.set('autocastDelay', delay)
     me.patch()
-    spell.view.setAutocastDelay delay for spellKey, spell of @spells
+    spell.view?.setAutocastDelay delay for spellKey, spell of @spells
     @castOptions.find('a').each ->
       $(@).toggleClass('selected', parseInt($(@).attr('data-delay')) is delay)
diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee
index af6628eac..1df67f589 100644
--- a/app/views/play/level/tome/Spell.coffee
+++ b/app/views/play/level/tome/Spell.coffee
@@ -40,10 +40,13 @@ module.exports = class Spell
     if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey)
       @source = sessionSource
     @thangs = {}
-    @view = new SpellView {spell: @, session: @session, worker: @worker}
-    @view.render()  # Get it ready and code loaded in advance
-    @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, language: @language
-    @tabView.render()
+    if true  # @canRead()  # Surely we could avoid creating these if we'll never use them? TODO
+      @view = new SpellView {spell: @, session: @session, worker: @worker}
+      @view.render()  # Get it ready and code loaded in advance
+      @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, language: @language
+      @tabView.render()
+    #else
+    #  @spell.loaded = true
     @team = @permissions.readwrite[0] ? 'common'
     Backbone.Mediator.publish 'tome:spell-created', spell: @
 
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index deb831a5e..80c86296f 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -517,7 +517,7 @@ module.exports = class SpellView extends CocoView
       spellThang.castAether = aether
       spellThang.aether = @spell.createAether thang
     #console.log thangID, @spell.spellKey, 'ran', aether.metrics.callsExecuted, 'times over', aether.metrics.statementsExecuted, 'statements, with max recursion depth', aether.metrics.maxDepth, 'and full flow/metrics', aether.metrics, aether.flow
-    @spell.transpile()
+    @spell.transpile()  # TODO: is there any way we can avoid doing this if it hasn't changed? Causes a slight hang.
     @updateAether false, false
 
   # --------------------------------------------------------------------------------------------------
diff --git a/app/views/play/level/tome/ThangListView.coffee b/app/views/play/level/tome/ThangListView.coffee
index 03ab371c1..2b09f44e8 100644
--- a/app/views/play/level/tome/ThangListView.coffee
+++ b/app/views/play/level/tome/ThangListView.coffee
@@ -29,6 +29,8 @@ module.exports = class ThangListView extends CocoView
       false
     ), @sortScoreForThang
     @muggleThangs = _.sortBy _.without(@thangs, @readwriteThangs..., @readThangs...), @sortScoreForThang
+    if @muggleThangs.length > 15
+      @muggleThangs = []  # Don't render a zillion of these. Slow, too long, maybe not useful.
 
   sortScoreForThang: (t) =>
     # Sort by my team, then most spells and fewest shared Thangs per spell,
@@ -73,6 +75,12 @@ module.exports = class ThangListView extends CocoView
     null
 
   adjustThangs: (spells, thangs) ->
+    # TODO: it would be nice to not have to do this any more, like if we migrate to the hero levels.
+    # Recreating all the ThangListEntryViews and their ThangAvatarViews is pretty slow.
+    # So they aren't even kept up-to-date during world streaming.
+    # Updating the existing subviews? Would be kind of complicated to get all the new thangs and spells propagated.
+    # I would do it, if I didn't think we were perhaps soon to not do the ThangList any more.
+    # Will temporary reduce the number of muggle thangs we're willing to draw.
     @spells = @options.spells = spells
     for entry in @entries
       entry.$el.remove()

From 08f2ee248b18af967c8e382fb4dddbe41b037337 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Sun, 24 Aug 2014 21:52:33 -0700
Subject: [PATCH 21/21] No longer constructing SpellViews for non-readable
 methods.

---
 app/models/Level.coffee                   |  2 +-
 app/views/play/level/tome/Spell.coffee    | 10 ++++------
 app/views/play/level/tome/TomeView.coffee |  3 +++
 3 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/app/models/Level.coffee b/app/models/Level.coffee
index da61d9d65..ad3458692 100644
--- a/app/models/Level.coffee
+++ b/app/models/Level.coffee
@@ -111,7 +111,7 @@ module.exports = class Level extends CocoModel
       visit = (c) ->
         return if c in sorted
         lc = _.find levelComponents, {original: c.original}
-        #console.error thang.id or thang.name, 'couldn\'t find lc for', c, 'of', levelComponents unless lc  # STREAM: uncomment
+        console.error thang.id or thang.name, 'couldn\'t find lc for', c, 'of', levelComponents unless lc
         return unless lc
         if lc.name is 'Programmable'
           # Programmable always comes last
diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee
index 1df67f589..ff2dc39e6 100644
--- a/app/views/play/level/tome/Spell.coffee
+++ b/app/views/play/level/tome/Spell.coffee
@@ -40,19 +40,17 @@ module.exports = class Spell
     if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey)
       @source = sessionSource
     @thangs = {}
-    if true  # @canRead()  # Surely we could avoid creating these if we'll never use them? TODO
+    if @canRead()  # We can avoid creating these views if we'll never use them.
       @view = new SpellView {spell: @, session: @session, worker: @worker}
       @view.render()  # Get it ready and code loaded in advance
       @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, language: @language
       @tabView.render()
-    #else
-    #  @spell.loaded = true
     @team = @permissions.readwrite[0] ? 'common'
     Backbone.Mediator.publish 'tome:spell-created', spell: @
 
   destroy: ->
-    @view.destroy()
-    @tabView.destroy()
+    @view?.destroy()
+    @tabView?.destroy()
     @thangs = null
     @worker = null
 
@@ -75,7 +73,7 @@ module.exports = class Spell
     (team ? me.team) in @permissions.readwrite
 
   getSource: ->
-    @view.getSource()
+    @view?.getSource() ? @source
 
   transpile: (source) ->
     if source
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index 1d340dfee..246fae8d3 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -142,6 +142,9 @@ module.exports = class TomeView extends CocoView
       else
         delete @thangSpells[thangID]
         spell.removeThangID thangID for spell in @spells
+    for spellKey, spell of @spells when not spell.canRead()  # Make sure these get transpiled (they have no views).
+      spell.transpile()
+      spell.loaded = true
     null
 
   onSpellLoaded: (e) ->