import * as I from 'immutable'; import immutablediff from 'immutablediff'; import isPlainObject from 'is-plain-object'; import {compose} from '@avocado/core'; import {Trait} from '@avocado/entity'; import {Rectangle, Vector} from '@avocado/math'; import {Packer, Synchronizer} from '@avocado/state'; import {KeysPacket} from '../../common/packet/keys.packet'; import {StatePacket} from '../../common/packet/state.packet'; 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; } } reduceState(state) { // Set client's self entity. state = state.set('selfEntity', this.entity.instanceUuid); // 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 position = Rectangle.position(camera.rectangle); const size = Rectangle.size(camera.rectangle); const visibleArea = Rectangle.compose( Vector.sub( position, Vector.scale(size, 0.25), ), Vector.scale(size, 1.5), ); // Write over entity list for every layer. for (const {index, layer} of room.layers) { const visibleEntities = layer.visibleEntities(visibleArea); let reducedEntityList = I.Map(); for (const entity of visibleEntities) { reducedEntityList = reducedEntityList.set( entity.instanceUuid, entity.state, ); } 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) => { // Reduce state. const reducedState = this.reduceState(state); // Take a pure JS diff. const steps = immutablediff(this._state, reducedState).toJS(); // Rewrite entity removals. const entityRemovals = steps.filter((step) => { return 'remove' === step.op && step.path.match( /\/room\/layers\/.*\/entityList\/[a-z0-9-]+/ ); }); for (const entityRemoval of entityRemovals) { // Remember the entity. const parts = entityRemoval.path.split('/'); parts.shift(); const uuid = parts[4]; this._rememberedEntities[uuid] = state.getIn(parts); // Swap out the removal op for replaces to hide the entity. const index = steps.indexOf(entityRemoval); steps.splice(index, 1); const pathOverrides = [ ['existent', 'state', 'isTicking'], ['physical', 'state', 'addedToPhysics'], ['visible', 'state', 'isVisible'], ] for (const pathOverride of pathOverrides) { steps.push({ op: 'replace', path: entityRemoval.path + '/' + pathOverride.join('/'), value: false, }); } } // Rewrite entity adds. const entityAdds = steps.filter((step) => { return 'add' === step.op && step.path.match( /\/room\/layers\/.*\/entityList\/[a-z0-9-]+/ ); }); for (const entityAdd of entityAdds) { // Remembered entity? const parts = entityAdd.path.split('/'); parts.shift(); const uuid = parts[4]; 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(parts); const addSteps = immutablediff( rememberedEntity, currentState, ).toJS(); // Translate step paths to full state location. const fullAddSteps = addSteps.map((addStep) => { return { op: addStep.op, path: entityAdd.path + addStep.path, value: addStep.value, }; }); // Swap out the add op for replaces to show the entity. const index = steps.indexOf(entityAdd); steps.splice(index, 1); steps.push(...fullAddSteps); const pathOverrides = [ ['existent', 'state', 'isTicking'], ['physical', 'state', 'addedToPhysics'], ['visible', 'state', 'isVisible'], ] for (const pathOverride of pathOverrides) { steps.push({ op: 'replace', path: entityAdd.path + '/' + pathOverride.join('/'), value: rememberedEntity.getIn(pathOverride), }); } delete this._rememberedEntities[uuid]; } // Remember state. this._state = reducedState; if (0 === steps.length) { return; } // Emit! const keys = this._packer.computeNewKeys(steps); if (0 !== keys[0].length) { this._socket.send(new KeysPacket(keys)); } const packed = this._packer.pack(steps); this._socket.send(new StatePacket(packed)); }, }; } }