humus-old/server/traits/informed.trait.js
2019-06-05 20:11:21 -05:00

319 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 {EntityCreatePacket, EntityPacket, EntityRemovePacket, Trait} from '@avocado/entity';
import {Rectangle, Vector} from '@avocado/math';
import {BundlePacket} from '@avocado/net';
import {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.seenEntities = [];
this._socket = undefined;
}
destroy() {
if (this._socket) {
delete this._socket.entity;
delete this._socket;
}
}
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),
);
}
deduplicateEntityCreatePackets(packets) {
const created = new Map();
return packets.filter((packet) => {
const entity = packet.entity;
if (!entity) {
return true;
}
// Only care about creates.
if (!(packet instanceof EntityCreatePacket)) {
return true;
}
if (created.has(entity)) {
return false;
}
created.set(entity, true);
return true;
});
}
filterInvisibleEntityPackets(packets) {
return packets.filter((packet) => {
const entity = packet.entity;
if (!entity) {
return true;
}
// Removes could be on destroyed entities, so pass them.
if (packet instanceof EntityRemovePacket) {
return true;
}
return entity.isVisible;
});
}
filterKnownEntityCreatePackets(packets) {
return packets.filter((packet) => {
if (!packet.entity) {
return true;
}
if (!(packet instanceof EntityCreatePacket)) {
return true;
}
return !this.hasSeenEntity(packet.entity);
});
}
filterOutOfRangeEntityPackets(packets, outOfRangeEntities) {
return packets.filter((packet) => {
const entity = packet.entity;
if (!entity) {
return true;
}
// Send removes even if they're out of range, if client knows about
// them.
if (
packet instanceof EntityRemovePacket
&& this.hasSeenEntity(entity)
) {
return true;
}
return -1 === outOfRangeEntities.indexOf(packet.entity);
});
}
hasSeenEntity(entity) {
return -1 !== this.seenEntities.indexOf(entity);
}
injectEntityCreatePackets(packets, visibleEntities) {
// Get a list of all visible but not yet seen entities.
const visibleButNotYetSeen = [];
for (let i = 0; i < visibleEntities.length; i++) {
const entity = visibleEntities[i];
if (!this.hasSeenEntity(entity)) {
visibleButNotYetSeen.push(entity);
}
}
// Get a list of all existing created entities.
const allExistingCreatedEntities = packets.filter((packet) => {
return packet instanceof EntityCreatePacket;
}).map((packet) => {
return packet.entity;
});
// JIT inject creates before any unknown updates.
for (let i = 0; i < packets.length; i++) {
const packet = packets[i];
// Only care about entity packets.
if (!(packet instanceof EntityPacket)) {
continue;
}
// Only unknown.
const entity = packet.entity;
if (this.hasSeenEntity(entity)) {
continue;
}
if (-1 !== allExistingCreatedEntities.indexOf(entity)) {
continue;
}
// Not creates nor removes.
if (
packet instanceof EntityCreatePacket
|| packet instanceof EntityRemovePacket
) {
// This does count as seen.
const index = visibleButNotYetSeen.indexOf(entity);
if (-1 !== index) {
visibleButNotYetSeen.splice(index, 1);
}
continue;
}
// Inject.
packets.splice(
i,
0,
new EntityCreatePacket(entity.mergeDiff(), entity)
);
i += 1;
// We've seen it.
const index = visibleButNotYetSeen.indexOf(entity);
if (-1 !== index) {
visibleButNotYetSeen.splice(index, 1);
}
allExistingCreatedEntities.push(entity);
}
// Append creates for any visible-but-not-yet-seen entities.
for (let i = 0; i < visibleButNotYetSeen.length; i++) {
const entity = visibleButNotYetSeen[i];
// Skip any existing creates.
if (-1 === allExistingCreatedEntities.indexOf(entity)) {
packets.push(new EntityCreatePacket(entity.mergeDiff(), entity));
}
}
return packets;
}
injectEntityRemovePackets(packets, visibleEntities) {
const alreadyRemovedEntities = packets.filter((packet) => {
return packet instanceof EntityRemovePacket;
}).map((packet) => {
return packet.entity;
});
for (let i = 0; i < this.seenEntities.length; i++) {
const entity = this.seenEntities[i];
if (-1 === visibleEntities.indexOf(entity)) {
if (-1 === alreadyRemovedEntities.indexOf(entity)) {
packets.push(new EntityRemovePacket({}, entity));
}
}
}
return packets;
}
markEntitiesSeen(visibleEntities) {
for (let i = 0; i < visibleEntities.length; i++) {
const entity = visibleEntities[i];
if (-1 === this.seenEntities.indexOf(entity)) {
this.seenEntities.push(entity);
}
}
}
markEntitiesUnseen(packets) {
const removedEntities = packets.filter((packet) => {
return packet instanceof EntityRemovePacket;
}).map((packet) => {
return packet.entity;
});
for (let i = 0; i < removedEntities.length; i++) {
const entity = removedEntities[i];
const index = this.seenEntities.indexOf(entity)
if (-1 !== index) {
this.seenEntities.splice(index, 1);
}
}
}
reducePacketEntitiesByRange(packets) {
// Unique packet entities.
const packetEntitiesMap = new Map();
for (let i = 0; i < packets.length; i++) {
const entity = packets[i].entity;
if (entity && !packetEntitiesMap.has(entity)) {
packetEntitiesMap.set(entity, true);
}
}
// Locate visible entities.
const areaToInform = this.areaToInform;
const visibleEntities = this.entity.room.visibleEntities(areaToInform);
// Document out of range entities.
const inRangeEntities = [];
const outOfRangeEntities = [];
let it = packetEntitiesMap.keys();
for (let value = it.next(); !value.done; value = it.next()) {
const entity = value.value;
if (-1 === visibleEntities.indexOf(entity)) {
if (-1 === outOfRangeEntities.indexOf(entity)) {
outOfRangeEntities.push(entity);
}
}
else {
if (-1 === inRangeEntities.indexOf(entity)) {
inRangeEntities.push(entity);
}
}
}
return [inRangeEntities, outOfRangeEntities, visibleEntities];
}
get socket() {
return this._socket;
}
set socket(socket) {
socket.entity = this.entity;
this._socket = socket;
}
methods() {
return {
inform: (packets) => {
if (0 === packets.length) {
return;
}
// TODO: filter packets that are only delivered to self entity.
// Filter invisible entities.
packets = this.filterInvisibleEntityPackets(packets);
// Reduce entities by range.
const [
inRangeEntities,
outOfRangeEntities,
visibleEntities,
] = this.reducePacketEntitiesByRange(packets);
// TODO? Upgrade seen entity packets that are out of range to entity
// remembers.
// Filter the out of range entity updates.
packets = this.filterOutOfRangeEntityPackets(
packets,
outOfRangeEntities
);
// Deduplicate entity creates.
packets = this.deduplicateEntityCreatePackets(packets);
// Filter known creates.
packets = this.filterKnownEntityCreatePackets(packets);
// Inject create packets.
packets = this.injectEntityCreatePackets(packets, visibleEntities);
// Inject removes for any previously seen entity that isn't visible
// anymore.
packets = this.injectEntityRemovePackets(packets, visibleEntities);
// "See" entities.
this.markEntitiesSeen(visibleEntities);
// Unsee any removed entities.
this.markEntitiesUnseen(packets);
// Ship it!
if (this._socket) {
this._socket.send(new BundlePacket(packets));
}
},
seesEntity: (entity) => {
return Rectangle.isTouching(
this.entity.areaToInform,
entity.visibleAabb
);
}
};
}
}