mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 08:41:46 -05:00
223 lines
10 KiB
CoffeeScript
223 lines
10 KiB
CoffeeScript
{clone, typedArraySupport} = require './world_utils'
|
|
Vector = require './vector'
|
|
|
|
if typedArraySupport
|
|
FloatArrayType = Float32Array # Better performance than Float64Array
|
|
bytesPerFloat = FloatArrayType.BYTES_PER_ELEMENT ? FloatArrayType.prototype.BYTES_PER_ELEMENT
|
|
else
|
|
bytesPerFloat = 4
|
|
|
|
module.exports = class ThangState
|
|
@className: 'ThangState'
|
|
@trackedPropertyTypes: [
|
|
'boolean'
|
|
'number'
|
|
'string'
|
|
'array' # will turn everything into strings
|
|
'object' # grrr
|
|
'Vector'
|
|
'Thang' # serialized as ids, like strings
|
|
]
|
|
|
|
hasRestored: false
|
|
constructor: (thang) ->
|
|
@props = [] # parallel array to @thang's trackedPropertiesKeys/Types
|
|
return unless thang
|
|
@thang = thang
|
|
for prop, propIndex in thang.trackedPropertiesKeys
|
|
type = thang.trackedPropertiesTypes[propIndex]
|
|
value = thang[prop]
|
|
if type is 'Vector'
|
|
@props.push value?.copy() # could try storing [x, y, z] or {x, y, z} here instead if this is expensive
|
|
else if type is 'object' or type is 'array'
|
|
@props.push clone(value, true)
|
|
else
|
|
@props.push value
|
|
|
|
# Either pass storage and type, or don't pass either of them
|
|
getStoredProp: (propIndex, type, storage) ->
|
|
# Optimize it
|
|
unless type
|
|
type = @trackedPropertyTypes[propIndex]
|
|
storage = @trackedPropertyValues[propIndex]
|
|
if type is 'Vector'
|
|
value = new Vector storage[3 * @frameIndex], storage[3 * @frameIndex + 1], storage[3 * @frameIndex + 2]
|
|
else if type is 'string'
|
|
specialKey = storage[@frameIndex]
|
|
value = @specialKeysToValues[specialKey]
|
|
else if type is 'Thang'
|
|
specialKey = storage[@frameIndex]
|
|
value = @thang.world.getThangByID @specialKeysToValues[specialKey]
|
|
else if type is 'array'
|
|
specialKey = storage[@frameIndex]
|
|
valueString = @specialKeysToValues[specialKey]
|
|
if valueString and valueString.length > 1
|
|
# Trim leading Group Separator and trailing Record Separator, split by Record Separators, restore string array.
|
|
value = valueString.substring(1, valueString.length - 1).split '\x1E'
|
|
else
|
|
value = []
|
|
else
|
|
value = storage[@frameIndex]
|
|
value
|
|
|
|
getStateForProp: (prop) ->
|
|
# 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
|
|
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
|
|
|
|
restore: ->
|
|
# 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
|
|
#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
|
|
@thang.stateChanged = true
|
|
@
|
|
|
|
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
|
|
value = @props[propIndex]
|
|
else
|
|
type = @trackedPropertyTypes[propIndex]
|
|
storage = @trackedPropertyValues[propIndex]
|
|
value = @getStoredProp propIndex, type, storage
|
|
if prop is 'pos'
|
|
if @thang.teleport and @thang.pos.distanceSquared(value) > 900
|
|
# Don't interpolate; it was probably a teleport. https://github.com/codecombat/codecombat/issues/738
|
|
@thang.pos = value
|
|
else
|
|
@thang.pos = @thang.pos.copy()
|
|
@thang.pos.x = inverse * @thang.pos.x + ratio * value.x
|
|
@thang.pos.y = inverse * @thang.pos.y + ratio * value.y
|
|
@thang.pos.z = inverse * @thang.pos.z + ratio * value.z
|
|
else if prop is 'rotation'
|
|
@thang.rotation = inverse * @thang.rotation + ratio * value
|
|
@thang.partialState = true
|
|
@thang.stateChanged = true
|
|
@
|
|
|
|
serialize: (frameIndex, trackedPropertyIndices, trackedPropertyTypes, trackedPropertyValues, specialValuesToKeys, specialKeysToValues) ->
|
|
# Performance hotspot--called once per tracked property per Thang per frame. Optimize the crap out of it.
|
|
for type, newPropIndex in trackedPropertyTypes
|
|
originalPropIndex = trackedPropertyIndices[newPropIndex]
|
|
storage = trackedPropertyValues[newPropIndex]
|
|
value = @props[originalPropIndex]
|
|
if value
|
|
# undefined, null, false, 0 won't trigger in this serialization code scheme anyway, so we can't differentiate between them when deserializing
|
|
if type is 'Vector'
|
|
storage[3 * frameIndex] = value.x
|
|
storage[3 * frameIndex + 1] = value.y
|
|
storage[3 * frameIndex + 2] = value.z
|
|
else if type is 'string'
|
|
specialKey = specialValuesToKeys[value]
|
|
unless specialKey
|
|
specialKey = specialKeysToValues.length
|
|
specialValuesToKeys[value] = specialKey
|
|
specialKeysToValues.push value
|
|
storage[frameIndex] = specialKey
|
|
storage[frameIndex] = specialKey
|
|
else if type is 'Thang'
|
|
value = value.id
|
|
specialKey = specialValuesToKeys[value]
|
|
unless specialKey
|
|
specialKey = specialKeysToValues.length
|
|
specialValuesToKeys[value] = specialKey
|
|
specialKeysToValues.push value
|
|
storage[frameIndex] = specialKey
|
|
storage[frameIndex] = specialKey
|
|
else if type is 'array'
|
|
# We make sure the array keys won't collide with any string keys by using some unprintable characters.
|
|
stringPieces = ['\x1D'] # Group Separator
|
|
for element in value
|
|
if element and element.id # Was checking element.isThang, but we can't store non-strings anyway
|
|
element = element.id
|
|
stringPieces.push element, '\x1E' # Record Separator(s)
|
|
value = stringPieces.join('')
|
|
specialKey = specialValuesToKeys[value]
|
|
unless specialKey
|
|
specialKey = specialKeysToValues.length
|
|
specialValuesToKeys[value] = specialKey
|
|
specialKeysToValues.push value
|
|
storage[frameIndex] = specialKey
|
|
storage[frameIndex] = specialKey
|
|
else
|
|
storage[frameIndex] = value
|
|
#console.log @thang.id, 'assigned prop', originalPropIndex, newPropIndex, value, type, 'at', frameIndex, 'to', storage[frameIndex]
|
|
null
|
|
|
|
@deserialize: (world, frameIndex, thang, trackedPropertyKeys, trackedPropertyTypes, trackedPropertyValues, specialKeysToValues) ->
|
|
# 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
|
|
ts.frameIndex = frameIndex
|
|
ts.trackedPropertyKeys = trackedPropertyKeys
|
|
ts.trackedPropertyTypes = trackedPropertyTypes
|
|
ts.trackedPropertyValues = trackedPropertyValues
|
|
ts.specialKeysToValues = specialKeysToValues
|
|
ts
|
|
|
|
@transferableBytesNeededForType: (type, nFrames) ->
|
|
bytes = switch type
|
|
when 'boolean' then 1
|
|
when 'number' then bytesPerFloat
|
|
when 'Vector' then bytesPerFloat * 3
|
|
when 'string' then 4
|
|
when 'Thang' then 4 # turn them into strings of their ids
|
|
when 'array' then 4 # turn them into strings and hope it doesn't explode?
|
|
else 0
|
|
# We need to be a multiple of bytesPerFloat otherwise bigger-byte array (Float64Array, etc.) offsets won't work
|
|
# http://www.kirupa.com/forum/showthread.php?378737-Typed-Arrays-Y-U-No-offset-at-values-other-than-multiples-of-element-size
|
|
bytesPerFloat * Math.ceil(nFrames * bytes / bytesPerFloat)
|
|
|
|
@createArrayForType: (type, nFrames, buffer, offset) ->
|
|
bytes = @transferableBytesNeededForType type, nFrames
|
|
storage = switch type
|
|
when 'boolean'
|
|
new Uint8Array(buffer, offset, nFrames)
|
|
when 'number'
|
|
new FloatArrayType(buffer, offset, nFrames)
|
|
when 'Vector'
|
|
new FloatArrayType(buffer, offset, nFrames * 3)
|
|
when 'string'
|
|
new Uint32Array(buffer, offset, nFrames)
|
|
when 'Thang'
|
|
new Uint32Array(buffer, offset, nFrames)
|
|
when 'array'
|
|
new Uint32Array(buffer, offset, nFrames)
|
|
else
|
|
[]
|
|
[storage, bytes]
|
|
|
|
unless typedArraySupport
|
|
# Fall back to normal arrays in IE 9
|
|
ThangState.createArrayForType = (type, nFrames, buffer, offset) ->
|
|
bytes = @transferableBytesNeededForType type, nFrames
|
|
elementsPerFrame = if type is 'Vector' then 3 else 1
|
|
storage = (0 for i in [0 ... nFrames * elementsPerFrame])
|
|
[storage, bytes]
|