import {performance} from 'perf_hooks'; 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) { static type() { return 'informed'; } constructor(entity, params, state) { super(entity, params, state); this._hasSetSelfEntity = false; this._packer = new Packer(); this._rememberedEntities = []; this._rememberedUuids = []; this._socket = undefined; this._state = I.Map(); } destroy() { if (this._socket) { delete this._socket.entity; delete this._socket; } this._rememberedEntities = []; this._rememberedUuids = []; 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) { // Entity we remember?. const parts = add.get('parts'); const layerId = parts[3]; const uuid = parts[5]; const index = this._rememberedUuids.indexOf(uuid); if (-1 === index) { continue; } const rememberedEntity = this._rememberedEntities[index].entity; // Reset remembrance timeout. this._rememberedEntities[index].rememberFor = 60; // 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]; const index = this._rememberedUuids.indexOf(uuid); return -1 === index; }); // Forget all remembered entities. adds.forEach((step) => { const parts = step.get('parts'); const uuid = parts[5]; const index = this._rememberedUuids.indexOf(uuid); this._rememberedEntities.splice(index, 1); this._rememberedUuids.splice(index, 1); }); 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._rememberedUuids.push(uuid); this._rememberedEntities.push({ entity: remembered, // Remember entities for one minute. rememeberFor: 60, }); // 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]; const index = this._rememberedUuids.indexOf(uuid); return -1 === index; }); } reduceState(state) { // Set client's self entity. const areaToInform = this.entity.areaToInform; const room = this.entity.room; // Write over entity list for every layer. const layers = room.layers.layers; const reducedEntityList = {}; for (const index in layers) { const layer = layers[index]; const visibleEntities = layer.visibleEntities(areaToInform); if (0 === visibleEntities.length) { continue; } reducedEntityList[index] = {}; for (let i = 0; i < visibleEntities.length; ++i) { const entity = visibleEntities[i]; reducedEntityList[index][entity.instanceUuid] = entity.state; } } return this.reduceStateMutations(state, reducedEntityList); } reduceStateMutations(state, reducedEntityList) { if ( this._hasSetSelfEntity && 0 === Object.keys(reducedEntityList).length ) { return state; } return state.withMutations((state) => { state.set('selfEntity', this.entity.instanceUuid); for (const index in reducedEntityList) { const entityListPath = ['room', 'layers', index, 'entityList']; state.setIn(entityListPath, I.Map(reducedEntityList[index])); } }); } 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. 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) { if (this._socket) { this._socket.send(new StateKeysPacket(keys)); } } const packed = this._packer.pack(steps); if (this._socket) { this._socket.send(new StatePacket(packed)); } }, seesEntity: (entity) => { return Rectangle.isTouching( this.entity.areaToInform, entity.visibleAabb ); } }; } tick(elapsed) { const removeUuids = []; for (let i = 0; i < this._rememberedEntities.length; i++) { this._rememberedEntities[i].rememberFor -= elapsed; if (this._rememberedEntities[i].rememberFor <= 0) { removeUuids.push(this._rememberedUuids[i]); } } for (let i = 0; i < removeUuids.length; i++) { const uuid = removeUuids[i]; const index = this._rememberedUuids.indexOf(uuid); this._rememberedEntities.splice(index, 1); this._rememberedUuids.splice(index, 1); } } }