mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-02 11:58:10 -05:00
d77625bc77
* Tweak API doc behavior and styling * Instead of moving to the left during active dialogues, just move to the top * Allow pointer events * Adjust close button * Re-enable pinning API docs for game-dev and web-dev levels * Make sidebar in PlayGameDevLevelView stretch, better layout columns * Set up content of PlayGameDevLevelView sidebar to scroll * Add rest of PlayGameDevLevelView sidebar content, rework what loading looks like * Finish PlayGameDevLevelView * Add share area below * Cover the brown background, paint it gray * Tweak PlayGameDevLevelView * Have progress bar show everything * Fix Surface resize handling * Fix PlayGameDevLevelView resizing incorrectly when playing * Add GameDevVictoryModal to PlayGameDevLevelView * Don't show missing-doctype annotation in Ace * Hook up GameDevVictoryModal copy button * Fix onChangeAnnotation runtime error * Fix onLevelLoaded runtime error * Have CourseVictoryModal link to /courses when course is done * Trim, update CourseDetailsView * Remove last vestiges of teacherMode * Remove giant navigation buttons at top * Quick switch to flat style * Add analytics for game-dev * Update Analytics events for gamedev * Prefix event names with context * Send to Mixpanel * Include more properties * Mostly set up indefinite play and autocast for game-dev levels * Set up cast buttons and shortcut for game-dev * Add rudimentary instructions when students play game-dev levels * Couple tweaks * fix a bit of code that expects frames to always stick around * have PlayGameDevLevelView render a couple frames on load * API Docs use 'game' instead of 'hero' * Move tags to head without combining * Add HTML comment-start string Fixes missing entry point arrows * Fix some whitespace
656 lines
29 KiB
CoffeeScript
656 lines
29 KiB
CoffeeScript
Vector = require './vector'
|
|
Rectangle = require './rectangle'
|
|
Ellipse = require './ellipse'
|
|
LineSegment = require './line_segment'
|
|
WorldFrame = require './world_frame'
|
|
Thang = require './thang'
|
|
ThangState = require './thang_state'
|
|
Rand = require './rand'
|
|
WorldScriptNote = require './world_script_note'
|
|
{now, consolidateThangs, typedArraySupport} = require './world_utils'
|
|
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 = 3 * PROGRESS_UPDATE_INTERVAL
|
|
REAL_TIME_BUFFERED_WAIT_INTERVAL = 0.5 * PROGRESS_UPDATE_INTERVAL
|
|
REAL_TIME_COUNTDOWN_DELAY = 3000 # match CountdownScreen
|
|
ITEM_ORIGINAL = '53e12043b82921000051cdf9'
|
|
EXISTS_ORIGINAL = '524b4150ff92f1f4f8000024'
|
|
COUNTDOWN_LEVELS = ['sky-span']
|
|
|
|
module.exports = class World
|
|
@className: 'World'
|
|
age: 0
|
|
ended: false
|
|
preloading: false # Whether we are just preloading a world in case we soon cast it
|
|
debugging: false # Whether we are just rerunning to debug a world we've already cast
|
|
headless: false # Whether we are just simulating for goal states instead of all serialized results
|
|
framesSerializedSoFar: 0
|
|
framesClearedSoFar: 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}
|
|
Thang.resetThangIDs()
|
|
|
|
@userCodeMap ?= {}
|
|
@thangs = []
|
|
@thangMap = {}
|
|
@systems = []
|
|
@systemMap = {}
|
|
@scriptNotes = []
|
|
@rand = new Rand 0 # Existence System may change this seed
|
|
@frames = [new WorldFrame(@, 0)]
|
|
|
|
destroy: ->
|
|
@goalManager?.destroy()
|
|
thang.destroy() for thang in @thangs
|
|
@[key] = undefined for key of @
|
|
@destroyed = true
|
|
@destroy = ->
|
|
|
|
getFrame: (frameIndex) ->
|
|
# Optimize it a bit--assume we have all if @ended and are at the previous frame otherwise
|
|
frames = @frames
|
|
if @ended
|
|
frame = frames[frameIndex]
|
|
else if frameIndex
|
|
frame = frames[frameIndex - 1].getNextFrame()
|
|
frames.push frame
|
|
else
|
|
frame = frames[0]
|
|
@age = frameIndex * @dt
|
|
frame
|
|
|
|
getThangByID: (id) ->
|
|
@thangMap[id]
|
|
|
|
setThang: (thang) ->
|
|
thang.stateChanged = true
|
|
for old, i in @thangs
|
|
if old.id is thang.id
|
|
@thangs[i] = thang
|
|
break
|
|
@thangMap[thang.id] = thang
|
|
|
|
thangDialogueSounds: (startFrame=0) ->
|
|
return [] unless startFrame < @frames.length
|
|
[sounds, seen] = [[], {}]
|
|
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
|
|
unless seen[soundKey]
|
|
sounds.push [state.thang.spriteName, sayMessage]
|
|
seen[soundKey] = true
|
|
sounds
|
|
|
|
setGoalManager: (@goalManager) ->
|
|
|
|
addError: (error) ->
|
|
(@runtimeErrors ?= []).push error
|
|
(@unhandledRuntimeErrors ?= []).push error
|
|
|
|
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, preloadedCallback, skipDeferredLoading, loadUntilFrame) ->
|
|
return if @aborted
|
|
@totalFrames = 2 if @justBegin
|
|
console.log 'Warning: loadFrames called on empty World (no thangs).' unless @thangs.length
|
|
continueLaterFn = =>
|
|
@loadFrames(loadedCallback, errorCallback, loadProgressCallback, preloadedCallback, skipDeferredLoading, loadUntilFrame) unless @destroyed
|
|
if @realTime and not @countdownFinished
|
|
@realTimeSpeedFactor = 1
|
|
unless @showsCountdown
|
|
if @levelID in ['woodland-cleaver', 'village-guard', 'shield-rush']
|
|
@realTimeSpeedFactor = 2
|
|
else if @levelID in ['thornbush-farm', 'back-to-back', 'ogre-encampment', 'peasant-protection', 'munchkin-swarm', 'munchkin-harvest', 'swift-dagger', 'shrapnel', 'arcane-ally', 'touch-of-death', 'bonemender']
|
|
@realTimeSpeedFactor = 3
|
|
if @showsCountdown
|
|
return setTimeout @finishCountdown(continueLaterFn), REAL_TIME_COUNTDOWN_DELAY
|
|
else
|
|
@finishCountdown continueLaterFn
|
|
t1 = now()
|
|
@t0 ?= t1
|
|
@worldLoadStartTime ?= t1
|
|
@lastRealTimeUpdate ?= 0
|
|
frameToLoadUntil = if loadUntilFrame then loadUntilFrame + 1 else @totalFrames # Might stop early if debugging.
|
|
i = @frames.length
|
|
while true
|
|
if @indefiniteLength
|
|
break if not @realTime # realtime has been stopped
|
|
break if @victory? # game won or lost # TODO: give a couple seconds of buffer after victory is set instead of ending instantly
|
|
else
|
|
break if i >= frameToLoadUntil
|
|
break if i >= @totalFrames
|
|
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
|
|
catch 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, preloadedCallback
|
|
|
|
finishLoadingFrames: (loadProgressCallback, loadedCallback, preloadedCallback) ->
|
|
unless @debugging
|
|
@ended = true
|
|
system.finish @thangs for system in @systems
|
|
if @preloading
|
|
preloadedCallback()
|
|
else
|
|
loadProgressCallback? 1
|
|
loadedCallback()
|
|
|
|
finishCountdown: (continueLaterFn) -> =>
|
|
return if @destroyed
|
|
@countdownFinished = true
|
|
continueLaterFn()
|
|
|
|
shouldDelayRealTimeSimulation: (t) ->
|
|
return false unless @realTime
|
|
timeSinceStart = (t - @worldLoadStartTime) * @realTimeSpeedFactor
|
|
timeLoaded = @frames.length * @dt * 1000
|
|
timeBuffered = timeLoaded - timeSinceStart
|
|
timeBuffered > REAL_TIME_BUFFER_MAX * @realTimeSpeedFactor
|
|
|
|
shouldUpdateRealTimePlayback: (t) ->
|
|
return false unless @realTime
|
|
return false if @frames.length * @dt is @lastRealTimeUpdate
|
|
timeLoaded = @frames.length * @dt * 1000
|
|
timeSinceStart = (t - @worldLoadStartTime) * @realTimeSpeedFactor
|
|
remainingBuffer = @lastRealTimeUpdate * 1000 - timeSinceStart
|
|
remainingBuffer < REAL_TIME_BUFFER_MIN * @realTimeSpeedFactor
|
|
|
|
shouldContinueLoading: (t1, loadProgressCallback, skipDeferredLoading, continueLaterFn) ->
|
|
t2 = now()
|
|
chunkSize = @frames.length - @framesSerializedSoFar
|
|
simedTime = @frames.length / @frameRate
|
|
|
|
chunkTime = switch
|
|
when simedTime > 15 then 7
|
|
when simedTime > 10 then 5
|
|
when simedTime > 5 then 3
|
|
when simedTime > 2 then 1
|
|
else 0.5
|
|
|
|
bailoutTime = Math.max(2000*chunkTime, 10000)
|
|
|
|
dt = t2 - t1
|
|
|
|
if @realTime
|
|
shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
|
|
shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
|
|
else
|
|
shouldUpdateProgress = (dt > PROGRESS_UPDATE_INTERVAL and (chunkSize / @frameRate >= chunkTime) or dt > bailoutTime)
|
|
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
|
|
|
|
abort: ->
|
|
@aborted = true
|
|
|
|
addFlagEvent: (flagEvent) ->
|
|
@flagHistory.push flagEvent
|
|
|
|
addRealTimeInputEvent: (realTimeInputEvent) ->
|
|
@realTimeInputEvents.push realTimeInputEvent
|
|
|
|
loadFromLevel: (level, willSimulate=true) ->
|
|
@levelID = level.slug
|
|
@levelComponents = level.levelComponents
|
|
@thangTypes = level.thangTypes
|
|
@loadScriptsFromLevel level
|
|
@loadSystemsFromLevel level
|
|
@loadThangsFromLevel level, willSimulate
|
|
@showsCountdown = @levelID in COUNTDOWN_LEVELS or _.any(@thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag)
|
|
@picoCTFProblem = level.picoCTFProblem if level.picoCTFProblem
|
|
if @picoCTFProblem?.instances and not @picoCTFProblem.flag_sha1
|
|
@picoCTFProblem = _.merge @picoCTFProblem, @picoCTFProblem.instances[0]
|
|
system.start @thangs for system in @systems
|
|
|
|
loadSystemsFromLevel: (level) ->
|
|
# Remove old Systems
|
|
@systems = []
|
|
@systemMap = {}
|
|
|
|
# Load new Systems
|
|
for levelSystem in level.systems
|
|
systemModel = levelSystem.model
|
|
config = levelSystem.config
|
|
systemClass = @loadClassFromCode systemModel.js, systemModel.name, 'system'
|
|
#console.log "using db system class ---\n", systemClass, "\n--- from code ---n", systemModel.js, "\n---"
|
|
system = new systemClass @, config
|
|
@addSystems system
|
|
null
|
|
|
|
loadThangsFromLevel: (level, willSimulate) ->
|
|
# Remove old Thangs
|
|
@thangs = []
|
|
@thangMap = {}
|
|
|
|
# Load new Thangs
|
|
toAdd = (@loadThangFromLevel thangConfig, level.levelComponents, level.thangTypes for thangConfig in level.thangs ? [])
|
|
@extraneousThangs = consolidateThangs toAdd if willSimulate # Combine walls, for example; serialize the leftovers later
|
|
@addThang thang for thang in toAdd
|
|
null
|
|
|
|
loadThangFromLevel: (thangConfig, levelComponents, thangTypes, equipBy=null) ->
|
|
components = []
|
|
for component, componentIndex in thangConfig.components
|
|
componentModel = _.find levelComponents, (c) ->
|
|
c.original is component.original and c.version.major is (component.majorVersion ? 0)
|
|
componentClass = @loadClassFromCode componentModel.js, componentModel.name, 'component'
|
|
components.push [componentClass, component.config]
|
|
if component.original is ITEM_ORIGINAL
|
|
isItem = true
|
|
component.config.ownerID = equipBy if equipBy
|
|
else if component.original is EXISTS_ORIGINAL
|
|
existsConfigIndex = componentIndex
|
|
if isItem and existsConfigIndex?
|
|
# For memory usage performance, make sure these don't get any tracked properties assigned.
|
|
components[existsConfigIndex][1] = {exists: false, stateless: true}
|
|
thangTypeOriginal = thangConfig.thangType
|
|
thangTypeModel = _.find thangTypes, (t) -> t.original is thangTypeOriginal
|
|
return console.error thangConfig.id ? equipBy, 'could not find ThangType for', thangTypeOriginal unless thangTypeModel
|
|
thangTypeName = thangTypeModel.name
|
|
thang = new Thang @, thangTypeName, thangConfig.id
|
|
try
|
|
thang.addComponents components...
|
|
catch e
|
|
console.error 'couldn\'t load components for', thangTypeOriginal, thangConfig.id, 'because', e.toString(), e.stack
|
|
thang
|
|
|
|
addThang: (thang) ->
|
|
@thangs.unshift thang # Interactions happen in reverse order of specification/drawing
|
|
@setThang thang
|
|
@updateThangState thang
|
|
thang.updateRegistration()
|
|
thang
|
|
|
|
loadScriptsFromLevel: (level) ->
|
|
@scriptNotes = []
|
|
@scripts = []
|
|
@addScripts level.scripts...
|
|
|
|
loadClassFromCode: (js, name, kind='component') ->
|
|
# Cache them based on source code so we don't have to worry about extra compilations
|
|
@componentCodeClassMap ?= {}
|
|
@systemCodeClassMap ?= {}
|
|
map = if kind is 'component' then @componentCodeClassMap else @systemCodeClassMap
|
|
c = map[js]
|
|
return c if c
|
|
try
|
|
c = map[js] = eval js
|
|
catch err
|
|
console.error "Couldn't compile #{kind} code:", err, "\n", js
|
|
c = map[js] = {}
|
|
c.className = name
|
|
c
|
|
|
|
updateThangState: (thang) ->
|
|
@frames[@frames.length-1].thangStateMap[thang.id] = thang.getState()
|
|
|
|
size: ->
|
|
@calculateBounds() unless @width? and @height?
|
|
return [@width, @height] if @width? and @height?
|
|
|
|
getBounds: ->
|
|
@calculateBounds() unless @bounds?
|
|
return @bounds
|
|
|
|
calculateBounds: ->
|
|
bounds = {left: 0, top: 0, right: 0, bottom: 0}
|
|
hasLand = _.some @thangs, 'isLand'
|
|
for thang in @thangs when thang.isLand or (not hasLand and thang.rectangle) # Look at Lands only
|
|
rect = thang.rectangle().axisAlignedBoundingBox()
|
|
bounds.left = Math.min(bounds.left, rect.x - rect.width / 2)
|
|
bounds.right = Math.max(bounds.right, rect.x + rect.width / 2)
|
|
bounds.bottom = Math.min(bounds.bottom, rect.y - rect.height / 2)
|
|
bounds.top = Math.max(bounds.top, rect.y + rect.height / 2)
|
|
@width = bounds.right - bounds.left
|
|
@height = bounds.top - bounds.bottom
|
|
@bounds = bounds
|
|
[@width, @height]
|
|
|
|
publishNote: (channel, event) ->
|
|
event ?= {}
|
|
channel = 'world:' + channel
|
|
for script in @scripts ? []
|
|
continue if script.channel isnt channel
|
|
scriptNote = new WorldScriptNote script, event
|
|
continue if scriptNote.invalid
|
|
@scriptNotes.push scriptNote
|
|
return unless @goalManager
|
|
@goalManager.submitWorldGenerationEvent(channel, event, @frames.length)
|
|
|
|
getGoalState: (goalID) ->
|
|
@goalManager.getGoalState(goalID)
|
|
|
|
setGoalState: (goalID, status) ->
|
|
@goalManager.setGoalState(goalID, status)
|
|
|
|
endWorld: (victory=false, delay=3, tentative=false) ->
|
|
@totalFrames = Math.min(@totalFrames, @frames.length + Math.floor(delay / @dt)) # end a few seconds later
|
|
@victory = victory # TODO: should just make this signify the winning superteam
|
|
@victoryIsTentative = tentative
|
|
status = if @victory then 'won' else 'lost'
|
|
@publishNote status
|
|
console.log "The world ended in #{status} on frame #{@totalFrames}"
|
|
|
|
addSystems: (systems...) ->
|
|
@systems = @systems.concat systems
|
|
for system in systems
|
|
@systemMap[system.constructor.className] = system
|
|
getSystem: (systemClassName) ->
|
|
@systemMap?[systemClassName]
|
|
|
|
addScripts: (scripts...) ->
|
|
@scripts = (@scripts ? []).concat scripts
|
|
|
|
addTrackedProperties: (props...) ->
|
|
@trackedProperties = (@trackedProperties ? []).concat props
|
|
|
|
serialize: ->
|
|
# Code hotspot; optimize it
|
|
@freeMemoryBeforeFinalSerialization() if @ended
|
|
startFrame = @framesSerializedSoFar
|
|
endFrame = @frames.length
|
|
if @indefiniteLength
|
|
toClear = Math.max(@framesSerializedSoFar-10, 0)
|
|
for i in _.range(@framesClearedSoFar, toClear)
|
|
@frames[i] = null
|
|
@framesClearedSoFar = @framesSerializedSoFar
|
|
#console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames
|
|
[transferableObjects, nontransferableObjects] = [0, 0]
|
|
serializedFlagHistory = (_.omit(_.clone(flag), 'processed') for flag in @flagHistory)
|
|
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}, flagHistory: serializedFlagHistory, difficulty: @difficulty, scores: @getScores(), randomSeed: @randomSeed, picoCTFFlag: @picoCTFFlag}
|
|
o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
|
|
|
|
for thangID, methods of @userCodeMap
|
|
serializedMethods = o.userCodeMap[thangID] = {}
|
|
for methodName, method of methods
|
|
serializedMethods[methodName] = method.serialize?() ? method # serialize the method again if it has been deserialized
|
|
|
|
t0 = now()
|
|
o.trackedPropertiesThangIDs = []
|
|
o.trackedPropertiesPerThangIndices = []
|
|
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. So we could try to undo the workaround.
|
|
transferableStorageBytesNeeded = 0
|
|
nFrames = endFrame - startFrame
|
|
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 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
|
|
trackedPropertiesIndices.push propIndex
|
|
trackedPropertiesKeys.push thang.trackedPropertiesKeys[propIndex]
|
|
trackedPropertiesTypes.push thang.trackedPropertiesTypes[propIndex]
|
|
o.trackedPropertiesPerThangIndices.push trackedPropertiesIndices
|
|
o.trackedPropertiesPerThangKeys.push trackedPropertiesKeys
|
|
o.trackedPropertiesPerThangTypes.push trackedPropertiesTypes
|
|
trackedPropertiesPerThangValues.push []
|
|
o.trackedPropertiesPerThangValuesOffsets.push []
|
|
for type in trackedPropertiesTypes
|
|
transferableStorageBytesNeeded += ThangState.transferableBytesNeededForType(type, nFrames)
|
|
if typedArraySupport
|
|
o.storageBuffer = new ArrayBuffer(transferableStorageBytesNeeded)
|
|
else
|
|
o.storageBuffer = []
|
|
storageBufferOffset = 0
|
|
for trackedPropertiesValues, thangIndex in trackedPropertiesPerThangValues
|
|
trackedPropertiesValuesOffsets = o.trackedPropertiesPerThangValuesOffsets[thangIndex]
|
|
for type, propIndex in o.trackedPropertiesPerThangTypes[thangIndex]
|
|
[storage, bytesStored] = ThangState.createArrayForType type, nFrames, o.storageBuffer, storageBufferOffset
|
|
trackedPropertiesValues.push storage
|
|
trackedPropertiesValuesOffsets.push storageBufferOffset
|
|
++transferableObjects if bytesStored
|
|
++nontransferableObjects unless bytesStored
|
|
if typedArraySupport
|
|
storageBufferOffset += bytesStored
|
|
else
|
|
# Instead of one big array with each storage as a view into it, they're all separate, so let's keep 'em around for flattening.
|
|
storageBufferOffset += storage.length
|
|
o.storageBuffer.push storage
|
|
|
|
o.specialKeysToValues = [null, Infinity, NaN]
|
|
# Whatever is in specialKeysToValues index 0 will be default for anything missing, so let's make sure it's null.
|
|
# Don't think we can include undefined or it'll be treated as a sparse array; haven't tested performance.
|
|
o.specialValuesToKeys = {}
|
|
for specialValue, i in o.specialKeysToValues
|
|
o.specialValuesToKeys[specialValue] = i
|
|
|
|
t1 = now()
|
|
o.frameHashes = []
|
|
for frameIndex in [startFrame ... endFrame]
|
|
o.frameHashes.push @frames[frameIndex].serialize(frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesPerThangIndices, o.trackedPropertiesPerThangTypes, trackedPropertiesPerThangValues, o.specialValuesToKeys, o.specialKeysToValues)
|
|
t2 = now()
|
|
|
|
unless typedArraySupport
|
|
flattened = []
|
|
for storage in o.storageBuffer
|
|
for value in storage
|
|
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) / 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
|
|
@freeMemoryAfterEachSerialization() unless @ended
|
|
{serializedWorld: o, transferableObjects: [o.storageBuffer], startFrame: startFrame, endFrame: endFrame}
|
|
|
|
@deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback, startFrame, endFrame, level, 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()
|
|
nFrames = endFrame - startFrame
|
|
if streamingWorld
|
|
w = streamingWorld
|
|
# Make sure we get any Aether updates from the new frames into the already-deserialized streaming world Aethers.
|
|
for thangID, methods of o.userCodeMap
|
|
for methodName, serializedAether of methods
|
|
for aetherStateKey in ['flow', 'metrics', 'style', 'problems']
|
|
w.userCodeMap[thangID] ?= {}
|
|
w.userCodeMap[thangID][methodName] ?= {}
|
|
w.userCodeMap[thangID][methodName][aetherStateKey] = serializedAether[aetherStateKey]
|
|
else
|
|
w = new World o.userCodeMap, classMap
|
|
[w.totalFrames, w.maxTotalFrames, w.frameRate, w.dt, w.scriptNotes, w.victory, w.flagHistory, w.difficulty, w.scores, w.randomSeed, w.picoCTFFlag] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory, o.flagHistory, o.difficulty, o.scores, o.randomSeed, o.picoCTFFlag]
|
|
w[prop] = val for prop, val of o.trackedProperties
|
|
|
|
perf.t1 = now()
|
|
if w.thangs.length
|
|
for thangConfig in o.thangs
|
|
if thang = w.thangMap[thangConfig.id]
|
|
for prop, val of thangConfig.finalState
|
|
thang[prop] = val
|
|
else
|
|
w.thangs.push thang = Thang.deserialize(thangConfig, w, classMap, level.levelComponents)
|
|
w.setThang thang
|
|
else
|
|
w.thangs = (Thang.deserialize(thang, w, classMap, level.levelComponents) 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()
|
|
|
|
o.trackedPropertiesThangs = (w.getThangByID thangID for thangID in o.trackedPropertiesThangIDs)
|
|
o.trackedPropertiesPerThangValues = []
|
|
for trackedPropertyTypes, thangIndex in o.trackedPropertiesPerThangTypes
|
|
o.trackedPropertiesPerThangValues.push (trackedPropertiesValues = [])
|
|
trackedPropertiesValuesOffsets = o.trackedPropertiesPerThangValuesOffsets[thangIndex]
|
|
for type, propIndex in trackedPropertyTypes
|
|
storage = ThangState.createArrayForType(type, nFrames, o.storageBuffer, trackedPropertiesValuesOffsets[propIndex])[0]
|
|
unless typedArraySupport
|
|
# This could be more efficient
|
|
i = trackedPropertiesValuesOffsets[propIndex]
|
|
storage = o.storageBuffer.slice i, i + storage.length
|
|
trackedPropertiesValues.push storage
|
|
perf.t3 = now()
|
|
|
|
perf.batches = 0
|
|
perf.framesCPUTime = 0
|
|
w.frames = [] unless streamingWorld
|
|
clearTimeout @deserializationTimeout if @deserializationTimeout
|
|
|
|
if w.indefiniteLength
|
|
clearTo = Math.max(w.frames.length - 100, 0)
|
|
if clearTo > w.framesClearedSoFar
|
|
for i in _.range(w.framesClearedSoFar, clearTo)
|
|
w.frames[i] = null
|
|
w.framesClearedSoFar = clearTo
|
|
|
|
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
|
|
w # Return in-progress deserializing world
|
|
|
|
# Spread deserialization out across multiple calls so the interface stays responsive
|
|
@deserializeSomeFrames: (o, w, finishedWorldCallback, perf, startFrame, endFrame) =>
|
|
++perf.batches
|
|
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)
|
|
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()
|
|
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.'
|
|
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 wall clock time,', (perf.framesCPUTime).toFixed(2) + 'ms CPU time'
|
|
finishedWorldCallback w
|
|
|
|
findFirstChangedFrame: (oldWorld) ->
|
|
return 0 unless oldWorld
|
|
for newFrame, i in @frames
|
|
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.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
|
|
|
|
freeMemoryBeforeFinalSerialization: ->
|
|
@levelComponents = null
|
|
@thangTypes = null
|
|
|
|
freeMemoryAfterEachSerialization: ->
|
|
@frames[i] = null for frame, i in @frames when i < @frames.length - 1
|
|
|
|
pointsForThang: (thangID, camera=null) ->
|
|
# Optimized
|
|
@pointsForThangCache ?= {}
|
|
cacheKey = thangID
|
|
allPoints = @pointsForThangCache[cacheKey]
|
|
unless allPoints
|
|
allPoints = []
|
|
lastFrameIndex = @frames.length - 1
|
|
lastPos = x: null, y: null
|
|
for frameIndex in [lastFrameIndex .. 0] by -1
|
|
frame = @frames[frameIndex]
|
|
continue unless frame # may have been evicted for game dev levels
|
|
if pos = frame.thangStateMap[thangID]?.getStateForProp 'pos'
|
|
pos = camera.worldToSurface {x: pos.x, y: pos.y} if camera # without z
|
|
if not lastPos.x? or (Math.abs(lastPos.x - pos.x) + Math.abs(lastPos.y - pos.y)) > 1
|
|
lastPos = pos
|
|
allPoints.push lastPos.y, lastPos.x unless lastPos.y is 0 and lastPos.x is 0
|
|
allPoints.reverse()
|
|
@pointsForThangCache[cacheKey] = allPoints
|
|
|
|
return allPoints
|
|
|
|
actionsForThang: (thangID, keepIdle=false) ->
|
|
# Optimized
|
|
@actionsForThangCache ?= {}
|
|
cacheKey = thangID + '_' + Boolean(keepIdle)
|
|
cached = @actionsForThangCache[cacheKey]
|
|
return cached if cached
|
|
states = (frame.thangStateMap[thangID] for frame in @frames)
|
|
actions = []
|
|
lastAction = ''
|
|
for state, i in states
|
|
action = state?.getStateForProp 'action'
|
|
continue unless action and (action isnt lastAction or state.actionActivated)
|
|
continue unless state.action isnt 'idle' or keepIdle
|
|
actions.push {frame: i, pos: state.pos, name: action}
|
|
lastAction = action
|
|
@actionsForThangCache[cacheKey] = actions
|
|
return actions
|
|
|
|
getTeamColors: ->
|
|
teamConfigs = @teamConfigs or {}
|
|
colorConfigs = {}
|
|
colorConfigs[teamName] = config.color for teamName, config of teamConfigs
|
|
colorConfigs
|
|
|
|
teamForPlayer: (n) ->
|
|
playableTeams = @playableTeams ? ['humans']
|
|
playableTeams[n % playableTeams.length]
|
|
|
|
getScores: ->
|
|
time: @age
|
|
'damage-taken': @getSystem('Combat')?.damageTakenForTeam 'humans'
|
|
'damage-dealt': @getSystem('Combat')?.damageDealtForTeam 'humans'
|
|
'gold-collected': @getSystem('Inventory')?.teamGold.humans?.collected
|
|
'difficulty': @difficulty
|