315 lines
8.8 KiB
JavaScript
315 lines
8.8 KiB
JavaScript
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.
|
|
// state = state.set('selfEntity', this.entity.instanceUuid);
|
|
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);
|
|
}
|
|
}
|
|
|
|
}
|