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), ); } 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.is('visible'); }); } 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 ); // 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 ); } }; } }