humus-old/server/traits/informed.trait.js
2019-04-22 14:06:57 -05:00

243 lines
6.7 KiB
JavaScript

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;
}
}
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);
// 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);
const visibleArea = Rectangle.expand(
camera.rectangle,
Vector.scale(size, 0.5),
);
// Write over entity list for every layer.
for (const {index, layer} of room.layers) {
const visibleEntities = layer.visibleEntities(visibleArea);
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));
},
};
}
}