import * as I from 'immutable'; import immutablediff from 'immutablediff'; import {compose} from '@avocado/core'; import {Trait} from '@avocado/entity'; import {Rectangle, Vector} from '@avocado/math'; import { Packer, StateKeysPacket, StatePacket, Synchronizer, } from '@avocado/state'; const decorate = compose( ); export class Informed extends decorate(Trait) { initialize() { this._packer = new Packer(); this._rememberedEntities = {}; this._socket = undefined; this._state = I.Map(); } destroy() { if (this._socket) { delete this._socket.entity; delete this._socket; } this._rememberedEntities = {}; this._state = this._state.clear(); } get areaToInform() { // Reduce entity list to visible. const room = this.entity.room; const camera = this.entity.camera; // Blow up camera rectangle to compensate for camera desync. const size = Rectangle.size(camera.rectangle); return Rectangle.expand( camera.rectangle, Vector.scale(size, 0.5), ); } entityOverrides(path, fn) { const pathOverrides = [ ['physical', 'state', 'addedToPhysics'], ['visible', 'state', 'isVisible'], // Ticking change last. ['existent', 'state', 'isTicking'], ] return I.List().withMutations((steps) => { for (const pathOverride of pathOverrides) { steps.push(I.Map({ op: 'replace', path: path + '/' + pathOverride.join('/'), value: fn(pathOverride), })); } }); } entityStepFilter(op) { return (step) => { if (op !== step.get('op')) { return false; } const parts = step.get('parts'); if (!parts) { return false; } if (6 !== parts.length) { return false; } if ('room' !== parts[1]) { return false; } if ('layers' !== parts[2]) { return false; } if ('entityList' !== parts[4]) { return false; } return true; }; } rewriteEntityAdds(state, steps) { steps = steps.map((step) => { return step.set('parts', step.get('path').split('/')); }); const isAdd = this.entityStepFilter('add'); const adds = steps.filter(isAdd); steps = steps.withMutations((steps) => { const iterator = adds.values(); for (const add of iterator) { // Remember the entity. const parts = add.get('parts'); const layerId = parts[3]; const uuid = parts[5]; if (!(uuid in this._rememberedEntities)) { continue; } const rememberedEntity = this._rememberedEntities[uuid]; // Take a diff from what the client remembers to now. const currentState = state.getIn( ['room', 'layers', layerId, 'entityList', uuid] ); const addSteps = immutablediff(rememberedEntity, currentState); // Translate step paths to full state location. const fullAddSteps = addSteps.map((addStep) => { return addStep.set('path', add.get('path') + addStep.get('path')); }); for (let i = 0; i < fullAddSteps.size; ++i) { steps.push(fullAddSteps.get(i)); } // Add overrides. const overrides = this.entityOverrides( add.get('path'), (path) => rememberedEntity.getIn(path), ); for (let i = 0; i < overrides.size; ++i) { steps.push(overrides.get(i)); } } }); // Filter adds for remembered entities. steps = steps.filter((step) => { if (!isAdd(step)) { return true; } const parts = step.get('parts'); const uuid = parts[5]; return !(uuid in this._rememberedEntities); }); // Forget all remembered entities. adds.forEach((step) => { const parts = step.get('parts'); const uuid = parts[5]; delete this._rememberedEntities[uuid]; }); return steps; } rewriteEntityRemovals(state, steps) { steps = steps.map((step) => { return step.set('parts', step.get('path').split('/')); }); const isRemove = this.entityStepFilter('remove'); const removals = steps.filter(isRemove); steps = steps.withMutations((steps) => { for (const removal of removals.values()) { // Remember the entity. const parts = removal.get('parts'); const layerId = parts[3]; const uuid = parts[5]; const remembered = state.getIn( ['room', 'layers', layerId, 'entityList', uuid] ); // Actually destroyed? if (!remembered) { return; } this._rememberedEntities[uuid] = remembered; // Add overrides. const overrides = this.entityOverrides( removal.get('path'), () => false ); for (let i = 0; i < overrides.size; ++i) { steps.push(overrides.get(i)); } } }); // Remove all removes. return steps.filter((step) => { if (!isRemove(step)) { return true; } const parts = step.get('parts'); const uuid = parts[5]; return !(uuid in this._rememberedEntities); }); } reduceState(state) { // Set client's self entity. state = state.set('selfEntity', this.entity.instanceUuid); const areaToInform = this.entity.areaToInform; const room = this.entity.room; // Write over entity list for every layer. for (const {index, layer} of room.layers) { const visibleEntities = layer.visibleEntities(areaToInform); const reduceEntityListRaw = {}; for (let i = 0; i < visibleEntities.length; ++i) { const entity = visibleEntities[i]; reduceEntityListRaw[entity.instanceUuid] = entity.state; } let reducedEntityList = I.Map(reduceEntityListRaw); const entityListPath = ['room', 'layers', index, 'entityList']; state = state.setIn(entityListPath, reducedEntityList); } return state; } get socket() { return this._socket; } set socket(socket) { socket.entity = this.entity; this._socket = socket; } methods() { return { inform: (state) => { if (!this._socket) { return; } // Reduce state. const reducedState = this.reduceState(state); // Take a pure JS diff. let steps = immutablediff(this._state, reducedState); if (0 === steps.size) { this._state = reducedState; return; } // Rewrite entity adds/removals. steps = this.rewriteEntityAdds(state, steps); steps = this.rewriteEntityRemovals(state, steps); // Remember state. this._state = reducedState; // Emit! const keys = this._packer.computeNewKeys(steps); if (0 !== keys[0].length) { this._socket.send(new StateKeysPacket(keys)); } const packed = this._packer.pack(steps); this._socket.send(new StatePacket(packed)); }, seesEntity: (entity) => { const areaToInform = this.entity.areaToInform; const room = this.entity.room; const visibleEntities = room.visibleEntities(areaToInform); return -1 !== visibleEntities.indexOf(entity); } }; } }