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()