diff --git a/packages/entity/list.js b/packages/entity/list.js index a646166..230ee2b 100644 --- a/packages/entity/list.js +++ b/packages/entity/list.js @@ -4,6 +4,7 @@ import mapValues from 'lodash.mapvalues'; import {arrayUnique, compose} from '@avocado/core'; import {QuadTree, Rectangle, Vector} from '@avocado/math'; import {EventEmitter} from '@avocado/mixins'; +import {StateSynchronizer} from '@avocado/state'; import {create} from './index'; @@ -56,26 +57,60 @@ export class EntityList extends decorate(class {}) { } patchState(patch) { - for (const uuid in patch) { - const localUuid = this.uuidMap_PRIVATE[uuid]; - const entity = this.entities_PRIVATE[localUuid]; - if (entity) { - if (false === patch[uuid]) { - // Entity removed. - this.removeEntity(entity); - } - else { - entity.patchState(patch[uuid]); - this.state_PRIVATE = this.state_PRIVATE.set(localUuid, entity.state); + for (const step of patch) { + const {op, path, value} = step; + if ('/' === path) { + for (const uuid in value) { + const localUuid = this.uuidMap_PRIVATE[uuid]; + const entity = this.entities_PRIVATE[localUuid]; + if (entity) { + if (false === value[uuid]) { + // Entity removed. + this.removeEntity(entity); + } + else { + entity.patchState([ + { + op: 'replace', + path: '/', + value: value[uuid], + } + ]); + this.state_PRIVATE = this.state_PRIVATE.set(localUuid, entity.state); + } + } + else { + // New entity. Create with patch as traits. + const newEntity = create().fromJSON({ + traits: value[uuid], + }); + this.uuidMap_PRIVATE[uuid] = newEntity.instanceUuid; + this.addEntity(newEntity); + } } } else { - // New entity. Create with patch as traits. - const newEntity = create().fromJSON({ - traits: patch[uuid], - }); - this.uuidMap_PRIVATE[uuid] = newEntity.instanceUuid; - this.addEntity(newEntity); + const [uuid, substep] = StateSynchronizer.forwardStep(step); + const localUuid = this.uuidMap_PRIVATE[uuid]; + const entity = this.entities_PRIVATE[localUuid]; + if (entity) { + if (false === substep.value[uuid]) { + // Entity removed. + this.removeEntity(entity); + } + else { + entity.patchState([substep]); + this.state_PRIVATE = this.state_PRIVATE.set(localUuid, entity.state); + } + } + else { + // New entity. Create with patch as traits. + const newEntity = create().fromJSON({ + traits: substep.value[uuid], + }); + this.uuidMap_PRIVATE[uuid] = newEntity.instanceUuid; + this.addEntity(newEntity); + } } } } diff --git a/packages/entity/trait.js b/packages/entity/trait.js index 4e38697..41c66e8 100644 --- a/packages/entity/trait.js +++ b/packages/entity/trait.js @@ -3,6 +3,7 @@ import * as I from 'immutable'; import {Vector} from '@avocado/math'; import {Property} from '@avocado/mixins'; import {Resource} from '@avocado/resource'; +import {StateSynchronizer} from '@avocado/state'; export class Trait { @@ -38,20 +39,39 @@ export class Trait { } patchState(patch) { - if (!patch.state) { - return; - } - const undefinedProperties = {}; - for (const key in patch.state) { - const value = patch.state[key]; - if (key in this.entity) { - this.entity[key] = value; + for (const step of patch) { + const {op, path, value} = step; + if ('/' === path) { + if (!value.state) { + continue; + } + const undefinedProperties = {}; + for (const key in value.state) { + const value = value.state[key]; + if (key in this.entity) { + this.entity[key] = value; + } + else { + undefinedProperties[key] = value; + } + } + this.state = this.state.merge(undefinedProperties); } else { - undefinedProperties[key] = value; + const [stepKey, substep] = StateSynchronizer.forwardStep(step); + if ('state' === stepKey) { + const key = substep.path.substr(1); + const undefinedProperties = {}; + if (key in this.entity) { + this.entity[key] = substep.value; + } + else { + undefinedProperties[key] = substep.value; + } + this.state = this.state.merge(undefinedProperties); + } } } - this.state = this.state.merge(undefinedProperties); } toJSON() { diff --git a/packages/entity/traits.js b/packages/entity/traits.js index 6b9b702..7a0259a 100644 --- a/packages/entity/traits.js +++ b/packages/entity/traits.js @@ -3,6 +3,7 @@ import without from 'lodash.without'; import * as I from 'immutable'; import {Resource} from '@avocado/resource'; +import {StateSynchronizer} from '@avocado/state'; import {hasTrait, lookupTrait, registerTrait} from './trait-registry'; @@ -184,22 +185,51 @@ export class Traits { } patchState(patch) { - for (const type in patch) { - let instance = this.traits_PRIVATE[type]; - // New trait requested? - if (!this.traits_PRIVATE[type]) { - // Doesn't exist? - if (!hasTrait(type)) { - continue; + for (const step of patch) { + const {op, path, value} = step; + if ('/' === path) { + for (const type in value) { + let instance = this.traits_PRIVATE[type]; + // New trait requested? + if (!this.traits_PRIVATE[type]) { + // Doesn't exist? + if (!hasTrait(type)) { + continue; + } + this.addTrait(type, value[type]); + instance = this.traits_PRIVATE[type]; + } + else { + // Accept state. + instance.patchState([ + { + op: 'replace', + path: '/', + value: patch[type], + } + ]); + } + this._setInstanceState(type, instance); } - this.addTrait(type, patch[type]); - instance = this.traits_PRIVATE[type]; } else { - // Accept state. - instance.patchState(patch[type]); + const [type, substep] = StateSynchronizer.forwardStep(step); + let instance = this.traits_PRIVATE[type]; + // New trait requested? + if (!this.traits_PRIVATE[type]) { + // Doesn't exist? + if (!hasTrait(type)) { + continue; + } + this.addTrait(type, substep.value[type]); + instance = this.traits_PRIVATE[type]; + } + else { + // Accept state. + instance.patchState([substep]); + } + this._setInstanceState(type, instance); } - this._setInstanceState(type, instance); } } diff --git a/packages/state/synchronizer.js b/packages/state/synchronizer.js index 6b64cc6..a8a8e9b 100644 --- a/packages/state/synchronizer.js +++ b/packages/state/synchronizer.js @@ -12,33 +12,19 @@ export class StateSynchronizer { diff(previousState) { // Take a pure JS diff. const steps = immutablediff(previousState, this._state).toJS(); - const updateSteps = steps.filter(StateSynchronizer.isStepUpdate); - return StateSynchronizer.hydratePathValues(updateSteps); + return steps; } - static hydratePathValues(pathValues) { - let accumulated = {}; - for (const {path, value} of pathValues) { - if ('/' === path) { - accumulated = value; - } - else { - const parts = path.split('/'); - parts.shift(); - let walk = accumulated; - for (let i = 0; i < parts.length; ++i) { - const part = parts[i]; - walk[part] = walk[part] || {}; - if (i === parts.length - 1) { - walk[part] = value; - } - else { - walk = walk[part]; - } - } - } - } - return accumulated; + static forwardStep(step) { + const {path, op, value} = step; + const parts = path.split('/'); + const [key] = parts.splice(1, 1); + const subpath = parts.join('/'); + return [key, { + op, + path: subpath ? subpath : '/', + value, + }]; } static isStepUpdate(step) { @@ -46,12 +32,26 @@ export class StateSynchronizer { } patchState(patch) { - for (const key in patch) { + const stepMap = {}; + for (const {op, path, value} of patch) { + const parts = path.split('/'); + const [key] = parts.splice(1, 1); + if (!stepMap[key]) { + stepMap[key] = []; + } + const subpath = parts.join('/'); + stepMap[key].push({ + op, + path: subpath ? subpath : '/', + value, + }); + } + for (const key in stepMap) { const stateful = this._statefuls[key]; if (!stateful) { continue; } - stateful.patchState(patch[key]); + stateful.patchState(stepMap[key]); } } diff --git a/packages/topdown/layer.js b/packages/topdown/layer.js index 22c6ae9..17fcf46 100644 --- a/packages/topdown/layer.js +++ b/packages/topdown/layer.js @@ -3,6 +3,7 @@ import * as I from 'immutable'; import {compose} from '@avocado/core'; import {create as createEntity, EntityList} from '@avocado/entity'; import {EventEmitter, Property} from '@avocado/mixins'; +import {StateSynchronizer} from '@avocado/state'; import {Tiles} from './tiles'; @@ -81,14 +82,40 @@ export class Layer extends decorate(class {}) { } patchState(patch) { - if (patch.entityList) { - this.entityList.patchState(patch.entityList); - } - if (patch.tilesetUri) { - this.tilesetUri = patch.tilesetUri; - } - if (patch.tiles) { - this.tiles.patchState(patch.tiles); + for (const step of patch) { + const {op, path, value} = step; + if ('/' === path) { + if (value.entityList) { + this.entityList.patchState([ + { + op: 'replace', + path: '/', + value: value.entityList + } + ]); + } + if (value.tilesetUri) { + this.tilesetUri = value.tilesetUri; + } + if (value.tiles) { + this.tiles.patchState([ + { + op: 'replace', + path: '/', + value: value.tiles + } + ]); + } + } + else { + const [key, substep] = StateSynchronizer.forwardStep(step); + if ('entityList' === key) { + this.entityList.patchState([substep]); + } + if ('tiles' === key) { + this.tiles.patchState([substep]); + } + } } } diff --git a/packages/topdown/layers.js b/packages/topdown/layers.js index b8081ef..3b1669b 100644 --- a/packages/topdown/layers.js +++ b/packages/topdown/layers.js @@ -2,6 +2,7 @@ import * as I from 'immutable'; import {compose} from '@avocado/core'; import {EventEmitter} from '@avocado/mixins'; +import {StateSynchronizer} from '@avocado/state'; import {Layer} from './layer'; @@ -84,12 +85,31 @@ export class Layers extends decorate(class {}) { } patchState(patch) { - for (const index in patch) { - const layer = this.layers[index] ? this.layers[index] : new Layer(); - if (!this.layers[index]) { - this.addLayer(index, layer); + for (const step of patch) { + const {op, path, value} = step; + if ('/' === path) { + for (const index in value) { + const layer = this.layers[index] ? this.layers[index] : new Layer(); + if (!this.layers[index]) { + this.addLayer(index, layer); + } + layer.patchState([ + { + op: 'replace', + path: '/', + value: value[index], + } + ]); + } + } + else { + const [index, substep] = StateSynchronizer.forwardStep(step); + const layer = this.layers[index] ? this.layers[index] : new Layer(); + if (!this.layers[index]) { + this.addLayer(index, layer); + } + layer.patchState([substep]); } - layer.patchState(patch[index]); } } diff --git a/packages/topdown/room.js b/packages/topdown/room.js index 78ca065..178ca2b 100644 --- a/packages/topdown/room.js +++ b/packages/topdown/room.js @@ -4,6 +4,7 @@ import {compose} from '@avocado/core'; import {Vector} from '@avocado/math'; import {EventEmitter, Property} from '@avocado/mixins'; import {RectangleShape} from '@avocado/physics'; +import {StateSynchronizer} from '@avocado/state'; import {Layers} from './layers'; @@ -96,14 +97,31 @@ export class Room extends decorate(class {}) { } patchState(patch) { - if (patch.width) { - this.width = patch.width; - } - if (patch.height) { - this.height = patch.height; - } - if (patch.layers) { - this.layers.patchState(patch.layers); + for (const step of patch) { + const {op, path, value} = step; + if ('/' === path) { + if (value.width) { + this.width = value.width; + } + if (value.height) { + this.height = value.height; + } + if (value.layers) { + this.layers.patchState([ + { + op: 'replace', + path: '/', + value: value.layers, + } + ]) + } + } + else { + const [key, substep] = StateSynchronizer.forwardStep(step); + if ('layers' === key) { + this.layers.patchState([substep]); + } + } } } diff --git a/packages/topdown/tiles.js b/packages/topdown/tiles.js index 50c3dc5..3b23da2 100644 --- a/packages/topdown/tiles.js +++ b/packages/topdown/tiles.js @@ -20,20 +20,24 @@ export class Tiles extends decorate(class {}) { } patchState(patch) { - if (patch.width) { - this.width = patch.width; - } - if (patch.height) { - this.height = patch.height; - } - if (patch.data) { - const oldData = this.data; - for (const i in patch.data) { - const index = parseInt(i); - this.data = this.data.set(index, patch.data[i]); - } - if (oldData !== this.data) { - this.emit('dataChanged'); + for (const {op, path, value} of patch) { + if ('/' === path) { + if (value.width) { + this.width = value.width; + } + if (value.height) { + this.height = value.height; + } + if (value.data) { + const oldData = this.data; + for (const i in value.data) { + const index = parseInt(i); + this.data = this.data.set(index, value.data[i]); + } + if (oldData !== this.data) { + this.emit('dataChanged'); + } + } } } }