From 057e5bc682b6a13870e580d232da15c3c88f6c44 Mon Sep 17 00:00:00 2001 From: cha0s Date: Mon, 13 May 2019 21:07:57 -0500 Subject: [PATCH] refactor: immutablen't --- TODO.md | 2 + client/app.js | 61 ++--- client/ui/chat/index.js | 19 ++ common/combat/alive.trait.js | 25 ++ common/combat/damage.packet.js | 13 +- common/combat/trait-alive.packet.js | 12 + common/combat/vulnerable.trait.js | 41 +-- common/packets/self-entity.packet.js | 12 + common/world-time.js | 43 ++- common/world-time.packet.js | 12 + server/create-server-room.js | 49 ++-- server/game.js | 83 +----- server/traits/informed.trait.js | 391 ++++++++++----------------- 13 files changed, 341 insertions(+), 422 deletions(-) create mode 100644 client/ui/chat/index.js create mode 100644 common/combat/trait-alive.packet.js create mode 100644 common/packets/self-entity.packet.js create mode 100644 common/world-time.packet.js diff --git a/TODO.md b/TODO.md index 0f8e070..80b342e 100644 --- a/TODO.md +++ b/TODO.md @@ -6,3 +6,5 @@ - ✔ Forget remembered entities after a while - ❌ Optimize informed and Packer::pack: don't use .map on immutables - ✔ Optimize Damaging::tick +- ✔ Manual state sync + - ✔ Informed tracks seen entities (etc?), responsible for upgrading updates diff --git a/client/app.js b/client/app.js index a0f9d92..239f5be 100644 --- a/client/app.js +++ b/client/app.js @@ -3,7 +3,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; // 2nd party. import {compose, EventEmitter, Property} from '@avocado/core'; -import {EntityPacketSynchronizer} from '@avocado/entity'; import {Stage} from '@avocado/graphics'; import {ActionRegistry, InputNormalizer, InputPacket} from '@avocado/input'; import {Vector} from '@avocado/math'; @@ -20,6 +19,7 @@ import {World} from '@avocado/physics/matter/world'; import {Room, RoomView} from '@avocado/topdown'; // 1st party. import {augmentParserWithThroughput} from '../common/parser-throughput'; +import {SelfEntityPacket} from '../common/packets/self-entity.packet'; import {WorldTime} from '../common/world-time'; import {CycleTracker} from './cycle-tracker'; import {showMessage} from './overlay'; @@ -98,7 +98,6 @@ export class App extends decorate(class {}) { this.pointerMovementHandle = undefined; // Net. this.AugmentedParser = augmentParserWithThroughput(SocketIoParser); - this.entityPacketSynchronizer = new EntityPacketSynchronizer(); this.hasReceivedState = false; this.isConnected = false; this.socket = undefined; @@ -110,10 +109,10 @@ export class App extends decorate(class {}) { this.room.world.stepTime = config.simulationFrequency; // State synchronization. this.state = undefined; - this.synchronizer = new Synchronizer({ - room: this.room, - worldTime: this.worldTime, - }); + this.synchronizer = new Synchronizer([ + this.room, + this.worldTime, + ]); this.unpacker = new Unpacker(); } @@ -251,30 +250,17 @@ export class App extends decorate(class {}) { } onPacket(packet) { - this.entityPacketSynchronizer.acceptPacket(packet); - if (packet instanceof StateKeysPacket) { - this.unpacker.registerKeys(packet.data); + if (!this.hasReceivedState) { + this.renderIntoDom(document.querySelector('.app')); + this.startProcessingInput(); + this.startSimulation(); + this.startRendering(); + this.hasReceivedState = true; } - if (packet instanceof StatePacket) { - const patch = this.unpacker.unpack(packet.data); - // Yank out self entity. - if (!this.selfEntityUuid) { - for (const step of patch) { - const {op, path, value} = step; - if ('add' === op && '/selfEntity' === path) { - this.selfEntityUuid = value; - } - } - } - this.synchronizer.patchState(patch); - if (!this.hasReceivedState) { - this.renderIntoDom(document.querySelector('.app')); - this.startProcessingInput(); - this.startSimulation(); - this.startRendering(); - this.hasReceivedState = true; - } + if (packet instanceof SelfEntityPacket) { + this.selfEntityUuid = packet.data; } + this.synchronizer.acceptPacket(packet); } onPointerDown(event) { @@ -299,8 +285,6 @@ export class App extends decorate(class {}) { } onRoomEntityAdded(entity) { - // Packets. - this.entityPacketSynchronizer.trackEntity(entity); // Traits that shouldn't be on client. const noClientTraits = [ 'behaved', @@ -531,20 +515,9 @@ export class App extends decorate(class {}) { if (this.selfEntity) { this.selfEntity.inputState = this.actionState; } - // Tick synchronized. - this.synchronizer.tick(elapsed); - this.state = this.synchronizer.state; - // Emit packets. - const flushed = this.entityPacketSynchronizer.flushPackets(); - const it = flushed.values(); - for (let value = it.next(); !value.done; value = it.next()) { - const packets = value.value; - const it2 = packets.values(); - for (let value2 = it2.next(); !value2.done; value2 = it2.next()) { - const packet = value2.value; - this.socket.send(packet); - } - } + // Tick. + this.worldTime.tick(elapsed); + this.room.tick(elapsed); // Sample. this.tps.sample(elapsed); }, 1000 * config.simulationFrequency); diff --git a/client/ui/chat/index.js b/client/ui/chat/index.js new file mode 100644 index 0000000..83f0a10 --- /dev/null +++ b/client/ui/chat/index.js @@ -0,0 +1,19 @@ +// 3rd party. +import classnames from 'classnames'; +import React from 'react'; +// 2nd party. +import {compose} from '@avocado/core'; +import contempo from 'contempo'; + +const decorate = compose( + contempo(` + `), +); + +const ChatComponent = () => { + return
+
+
; +} + +export default decorate(ChatComponent); diff --git a/common/combat/alive.trait.js b/common/combat/alive.trait.js index 796e3fe..820add3 100644 --- a/common/combat/alive.trait.js +++ b/common/combat/alive.trait.js @@ -8,6 +8,8 @@ import { import {compose} from '@avocado/core'; import {StateProperty, Trait} from '@avocado/entity'; +import {TraitAlivePacket} from './trait-alive.packet'; + const decorate = compose( StateProperty('life', { track: true, @@ -75,10 +77,33 @@ export class Alive extends decorate(Trait) { this._context.clear(); } + acceptPacket(packet) { + if (packet instanceof TraitAlivePacket) { + this.entity.life = packet.data.life; + this.entity.maxLife = packet.data.maxLife; + } + } + get deathSound() { return this._deathSound; } + packetsForUpdate() { + const packets = []; + if ( + this.previousState.life !== this.state.life + || this.previousState.maxLife !== this.state.maxLife + ) { + packets.push(new TraitAlivePacket({ + life: this.state.life, + maxLife: this.state.maxLife, + }, this.entity)); + this.previousState.life = this.state.life; + this.previousState.maxLife = this.state.maxLife; + } + return packets; + } + listeners() { return { diff --git a/common/combat/damage.packet.js b/common/combat/damage.packet.js index c184527..a67165a 100644 --- a/common/combat/damage.packet.js +++ b/common/combat/damage.packet.js @@ -3,8 +3,8 @@ import {EntityPacket} from '@avocado/entity'; export class DamagePacket extends EntityPacket { static get schema() { - const superSchema = super.schema; - superSchema.data[0].damages = [ + const schema = super.schema; + schema.data.damages = [ { amount: 'varuint', damageSpec: { @@ -14,14 +14,7 @@ export class DamagePacket extends EntityPacket { isDamage: 'bool', }, ]; - return superSchema; - } - - mergeWith(other) { - for (let i = 0; i < other.data[0].damages.length; i++) { - const damage = other.data[0].damages[i]; - this.data[0].damages.push(damage); - } + return schema; } } diff --git a/common/combat/trait-alive.packet.js b/common/combat/trait-alive.packet.js new file mode 100644 index 0000000..1b94f84 --- /dev/null +++ b/common/combat/trait-alive.packet.js @@ -0,0 +1,12 @@ +import {EntityPacket} from '@avocado/entity'; + +export class TraitAlivePacket extends EntityPacket { + + static get schema() { + const schema = super.schema; + schema.data.life = 'uint16'; + schema.data.maxLife = 'uint16'; + return schema; + } + +} diff --git a/common/combat/vulnerable.trait.js b/common/combat/vulnerable.trait.js index 35281e5..a98c919 100644 --- a/common/combat/vulnerable.trait.js +++ b/common/combat/vulnerable.trait.js @@ -41,6 +41,7 @@ export class Vulnerable extends Trait { constructor(entity, params, state) { super(entity, params, state); this.damageId = 0; + this.damages = []; this._hasAddedEmitter = false; this._hasAddedEmitterRenderer = false; this._isHydrating = false; @@ -80,18 +81,17 @@ export class Vulnerable extends Trait { acceptPacket(packet) { if (packet instanceof DamagePacket) { - packet.forEachData(({damages}) => { - for (let i = 0; i < damages.length; ++i) { - const damage = damages[i]; - if (this.entity.is('listed') && this.entity.list) { - damage.from = this.entity.list.findEntity(damage.from); - } - else { - damage.from = undefined; - } - this.acceptDamage(damage); + const damages = packet.data.damages; + for (let i = 0; i < damages.length; ++i) { + const damage = damages[i]; + if (this.entity.is('listed') && this.entity.list) { + damage.from = this.entity.list.findEntity(damage.from); } - }); + else { + damage.from = undefined; + } + this.acceptDamage(damage); + } } } @@ -135,6 +135,17 @@ export class Vulnerable extends Trait { this._isInvulnerable = isInvulnerable; } + packetsForUpdate() { + const packets = []; + if (this.damages.length > 0) { + packets.push(new DamagePacket({ + damages: this.damages, + }, this.entity)); + this.damages = []; + } + return packets; + } + listeners() { return { @@ -148,13 +159,7 @@ export class Vulnerable extends Trait { tookDamage: (damage) => { if (AVOCADO_SERVER) { - this.entity.emit( - 'sendPacket', - new DamagePacket([{ - uuid: this.entity.instanceUuid, - damages: [damage], - }]) - ); + this.damages.push(damage); } }, diff --git a/common/packets/self-entity.packet.js b/common/packets/self-entity.packet.js new file mode 100644 index 0000000..4408080 --- /dev/null +++ b/common/packets/self-entity.packet.js @@ -0,0 +1,12 @@ +import {Packet} from '@avocado/net'; + +export class SelfEntityPacket extends Packet { + + static get schema() { + return { + ...super.schema, + data: 'string', + }; + } + +} diff --git a/common/world-time.js b/common/world-time.js index 5b81abf..f3fab37 100644 --- a/common/world-time.js +++ b/common/world-time.js @@ -1,18 +1,34 @@ +import {compose} from '@avocado/core'; +import {Synchronized} from '@avocado/state'; import {Ticker} from '@avocado/timing'; +import {WorldTimePacket} from './world-time.packet'; + const MAGIC_TO_FIT_HOUR_INTO_USHORT = 2730; -export class WorldTime { +const decorate = compose( + Synchronized, +); + +export class WorldTime extends decorate(class {}) { constructor() { + super(); this._hour = 0; + this._isUpdateReady = true; + this._lastHour = 0; this.ticker = new Ticker(0.25); - this._state = 0; this.ticker.on('tick', () => { - this._state = (this._hour * MAGIC_TO_FIT_HOUR_INTO_USHORT) >> 0; + this._isUpdateReady = true; }); } + acceptPacket(packet) { + if (packet instanceof WorldTimePacket) { + this._hour = packet.data / MAGIC_TO_FIT_HOUR_INTO_USHORT; + } + } + static format(time) { const rawHour = (time + 12) % 12; const hour = Math.floor(0 === Math.floor(rawHour) ? 12 : rawHour); @@ -31,26 +47,25 @@ export class WorldTime { set hour(hour) { this._hour = hour; - this._state = (this._hour * MAGIC_TO_FIT_HOUR_INTO_USHORT) >> 0; } - patchState(steps) { - for (const step of steps) { - const {path, value} = step; - if ('/' === path) { - this._hour = value / MAGIC_TO_FIT_HOUR_INTO_USHORT; - } + packetsForUpdate(force) { + const updates = []; + if (force || this._isUpdateReady) { + updates.push(new WorldTimePacket( + (this._hour * MAGIC_TO_FIT_HOUR_INTO_USHORT) >> 0 + )); } + if (!force) { + this._isUpdateReady = false; + } + return updates; } secondsPerHour() { return 60; } - get state() { - return this._state; - } - tick(elapsed) { this._hour += (elapsed / this.secondsPerHour()); this._hour = this._hour % 24; diff --git a/common/world-time.packet.js b/common/world-time.packet.js new file mode 100644 index 0000000..fa17555 --- /dev/null +++ b/common/world-time.packet.js @@ -0,0 +1,12 @@ +import {Packet} from '@avocado/net'; + +export class WorldTimePacket extends Packet { + + static get schema() { + return { + ...super.schema, + data: 'uint16', + }; + } + +} diff --git a/server/create-server-room.js b/server/create-server-room.js index a332c38..5726a84 100644 --- a/server/create-server-room.js +++ b/server/create-server-room.js @@ -356,8 +356,8 @@ const roomTileSize = [24, 24]; const roomSize = Vector.mul([16, 16], roomTileSize); const roomJSON = { size: roomSize, - layers: { - everything: { + layers: [ + { entities: [], tiles: { size: roomTileSize, @@ -396,7 +396,7 @@ const roomJSON = { }, tilesetUri: '/tileset.json', }, - }, + ], }; function mamaKittySpawnerJSON() { @@ -475,28 +475,29 @@ function mamaKittySpawnerJSON() { }; } -// for (let i = 0; i < 50; ++i) { -// const x = Math.floor(Math.random() * (roomSize[0] - 100)) + 50; -// const y = Math.floor(Math.random() * (roomSize[1] - 100)) + 50; -// roomJSON.layers.everything.entities.push(flowerBarrelJSON([x * 4, y * 4])); -// } -// for (let i = 0; i < 5; ++i) { -// const x = Math.floor(Math.random() * (roomSize[0] - 100)) + 50; -// const y = Math.floor(Math.random() * (roomSize[1] - 100)) + 50; -// roomJSON.layers.everything.entities.push(mamaKittySpawnerJSON()); -// } -// for (let i = 0; i < 60; ++i) { -// const x = Math.floor(Math.random() * (roomSize[0] - 100)) + 50; -// const y = Math.floor(Math.random() * (roomSize[1] - 100)) + 50; -// roomJSON.layers.everything.entities.push(fireJSON([x * 4, y * 4])); -// } -// for (let i = 0; i < 5; ++i) { -// const x = Math.floor(Math.random() * (roomSize[0] - 100)) + 50; -// const y = Math.floor(Math.random() * (roomSize[1] - 100)) + 50; -// roomJSON.layers.everything.entities.push(blueFireJSON([x * 4, y * 4])); -// } +for (let i = 0; i < 50; ++i) { + const x = Math.floor(Math.random() * (roomSize[0] - 100)) + 50; + const y = Math.floor(Math.random() * (roomSize[1] - 100)) + 50; + roomJSON.layers[0].entities.push(flowerBarrelJSON([x * 4, y * 4])); +} +for (let i = 0; i < 5; ++i) { + const x = Math.floor(Math.random() * (roomSize[0] - 100)) + 50; + const y = Math.floor(Math.random() * (roomSize[1] - 100)) + 50; + roomJSON.layers[0].entities.push(mamaKittySpawnerJSON()); +} +for (let i = 0; i < 60; ++i) { + const x = Math.floor(Math.random() * (roomSize[0] - 100)) + 50; + const y = Math.floor(Math.random() * (roomSize[1] - 100)) + 50; + roomJSON.layers[0].entities.push(fireJSON([x * 4, y * 4])); +} +for (let i = 0; i < 5; ++i) { + const x = Math.floor(Math.random() * (roomSize[0] - 100)) + 50; + const y = Math.floor(Math.random() * (roomSize[1] - 100)) + 50; + roomJSON.layers[0].entities.push(blueFireJSON([x * 4, y * 4])); +} export function createRoom() { - const room = (new Room()).fromJSON(roomJSON); + const room = new Room(); + room.fromJSON(roomJSON); room.world = new World(); return room; } diff --git a/server/game.js b/server/game.js index ac937c5..6ba23d1 100644 --- a/server/game.js +++ b/server/game.js @@ -3,11 +3,11 @@ import msgpack from 'msgpack-lite'; import {performance} from 'perf_hooks'; // 3rd party. // 2nd party. -import {EntityPacketSynchronizer} from '@avocado/entity'; import {InputPacket} from '@avocado/input'; import {Synchronizer} from '@avocado/state'; import {Ticker} from '@avocado/timing'; // 1st party. +import {SelfEntityPacket} from '../common/packets/self-entity.packet'; import {WorldTime} from '../common/world-time'; import {createEntityForConnection} from './create-entity-for-connection'; import {createRoom} from './create-server-room'; @@ -16,25 +16,19 @@ export default class Game { constructor() { const config = this.readConfig(); - // Packets. - this.entityPacketSynchronizer = new EntityPacketSynchronizer(); // Room. this.room = createRoom(); this.room.world.stepTime = config.simulationInterval; - for (const entity of this.room.allEntities()) { - this.onEntityAddedToRoom(entity); - } - this.room.on('entityAdded', this.onEntityAddedToRoom, this); // World time. Start at 10 am for testing. this.worldTime = new WorldTime(); this.worldTime.hour = 10; // Entity tracking. this.informables = []; // State synchronization. - this.synchronizer = new Synchronizer({ - room: this.room, - worldTime: this.worldTime, - }); + this.synchronizer = new Synchronizer([ + this.room, + this.worldTime, + ]); this.informTicker = new Ticker(config.informInterval); this.informTicker.on('tick', this.inform, this); // Simulation. @@ -50,41 +44,6 @@ export default class Game { fn(); } - bundleEntityPackets() { - const bundledEntityPackets = new Map(); - const flushed = this.entityPacketSynchronizer.flushPackets(); - for (let i = 0; i < this.informables.length; ++i) { - const entity = this.informables[i]; - // Get a list of all visible entities for this informable. - const areaToInform = entity.areaToInform; - const room = entity.room; - const visibleEntities = room.visibleEntities(areaToInform); - const it = flushed.entries(); - for (let value = it.next(); !value.done; value = it.next()) { - const packetEntity = value.value[0]; - if (-1 === visibleEntities.indexOf(packetEntity)) { - continue; - } - if (!bundledEntityPackets.has(entity)) { - bundledEntityPackets.set(entity, new Map()); - } - const packets = value.value[1]; - const it2 = packets.values(); - for (let value2 = it2.next(); !value2.done; value2 = it2.next()) { - const packet = value2.value; - const Packet = packet.constructor; - if (!bundledEntityPackets.get(entity).has(Packet)) { - bundledEntityPackets.get(entity).set(Packet, packet); - } - else { - bundledEntityPackets.get(entity).get(Packet).bundleWith(packet); - } - } - } - } - return bundledEntityPackets; - } - acceptConnection(socket) { // Create and track a new entity for the connection. const entity = createEntityForConnection(socket); @@ -97,9 +56,11 @@ export default class Game { } }); // Add entity to room. - this.room.addEntityToLayer(entity, 'everything'); + this.room.addEntityToLayer(entity, 0); // Initial information. - entity.inform(this.synchronizer.state); + const packets = this.synchronizer.packetsForUpdate(true); + packets.unshift(new SelfEntityPacket(entity.instanceUuid)); + entity.inform(packets); // Listen for events. socket.on('packet', this.createPacketListener(socket)); socket.on('disconnect', this.createDisconnectionListener(socket)); @@ -121,7 +82,8 @@ export default class Game { const elapsed = (now - lastTime) / 1000; lastTime = now; // Tick synchronized. - this.synchronizer.tick(elapsed); + this.room.tick(elapsed); + this.worldTime.tick(elapsed); // Tick informer. this.informTicker.tick(elapsed); } @@ -130,38 +92,19 @@ export default class Game { createPacketListener(socket) { const {entity} = socket; return (packet) => { - this.entityPacketSynchronizer.acceptPacket(packet); if (packet instanceof InputPacket) { entity.inputState = packet.toState(); } }; } - flushEntityPackets() { - const bundledEntityPackets = this.bundleEntityPackets(); - const entities = Array.from(bundledEntityPackets.keys()); - for (let i = 0; i < entities.length; i++) { - const entity = entities[i]; - const packets = Array.from(bundledEntityPackets.get(entity).values()); - for (let j = 0; j < packets.length; j++) { - const packet = packets[j]; - entity.socket.send(packet); - } - } - } - inform() { + const packets = this.synchronizer.packetsForUpdate(); // Inform entities of the new state. for (let i = 0; i < this.informables.length; ++i) { const entity = this.informables[i]; - entity.inform(this.synchronizer.state); + entity.inform(packets); } - // Flush entity packets. - this.flushEntityPackets(); - } - - onEntityAddedToRoom(entity) { - this.entityPacketSynchronizer.trackEntity(entity); } readConfig() { diff --git a/server/traits/informed.trait.js b/server/traits/informed.trait.js index 0f69ee6..034c817 100644 --- a/server/traits/informed.trait.js +++ b/server/traits/informed.trait.js @@ -4,14 +4,9 @@ import * as I from 'immutable'; import immutablediff from 'immutablediff'; import {compose} from '@avocado/core'; -import {Trait} from '@avocado/entity'; +import {EntityCreatePacket, EntityPacket, EntityRemovePacket, Trait} from '@avocado/entity'; import {Rectangle, Vector} from '@avocado/math'; -import { - Packer, - StateKeysPacket, - StatePacket, - Synchronizer, -} from '@avocado/state'; +import {Synchronizer} from '@avocado/state'; const decorate = compose( ); @@ -24,12 +19,8 @@ export class Informed extends decorate(Trait) { constructor(entity, params, state) { super(entity, params, state); - this._hasSetSelfEntity = false; - this._packer = new Packer(); - this._rememberedEntities = []; - this._rememberedUuids = []; + this.seenEntities = []; this._socket = undefined; - this._state = I.Map(); } destroy() { @@ -37,9 +28,6 @@ export class Informed extends decorate(Trait) { delete this._socket.entity; delete this._socket; } - this._rememberedEntities = []; - this._rememberedUuids = []; - this._state = this._state.clear(); } get areaToInform() { @@ -54,197 +42,6 @@ export class Informed extends decorate(Trait) { ); } - 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; } @@ -257,30 +54,156 @@ export class Informed extends decorate(Trait) { 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; + inform: (packets) => { + if (0 === packets.length) { 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)); + // Filter invisible entities. + packets = 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'); + }); + // Build entity map. + 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); } } - const packed = this._packer.pack(steps); - if (this._socket) { - this._socket.send(new StatePacket(packed)); + // Locate visible entities. + const areaToInform = this.areaToInform; + const visibleEntities = this.entity.room.visibleEntities(areaToInform); + // Document out of range entities. + 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)) { + outOfRangeEntities.push(entity); + } + } + + // TODO? Upgrade seen entity packets that are out of range to entity + // remembers. + + // Filter all the rest of out of range entity updates and remove them + // from 'packet entities' map. + packets = 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 + && -1 !== this.seenEntities.indexOf(entity) + ) { + return true; + } + return -1 === outOfRangeEntities.indexOf(packet.entity); + }); + for (let i = 0; i < outOfRangeEntities.length; i++) { + packetEntitiesMap.delete(outOfRangeEntities[i]); + } + // Filter known creates. TODO: downgrade to trait updates. + packets = packets.filter((packet) => { + if (!packet.entity) { + return true; + } + if (!(packet instanceof EntityCreatePacket)) { + return true; + } + return -1 === this.seenEntities.indexOf(packet.entity); + }); + // Inject creates for any visible-but-not-yet-seen entities. + const currentEntities = Array.from(packetEntitiesMap.keys()); + for (let i = 0; i < visibleEntities.length; i++) { + const entity = visibleEntities[i]; + if ( + // Haven't seen? + -1 === this.seenEntities.indexOf(entity) + // Isn't already addressed by some present update? + && -1 === currentEntities.indexOf(entity) + ) { + packets.push(new EntityCreatePacket(entity.toJSON(), entity)); + this.seenEntities.push(entity); + } + } + // Upgrade unknown entity updates to entity creates. + // TODO would be nice to JIT inject a create right before the update. + const unknownUpdatedEntities = []; + // First filter out the bad updates. + packets = packets.filter((packet) => { + // Only care about entity packets. + if (!(packet instanceof EntityPacket)) { + return true; + } + // But not creates. + if (packet instanceof EntityCreatePacket) { + return true; + } + // Nor removes. + if (packet instanceof EntityRemovePacket) { + return true; + } + // Known? Nevermind. + if (-1 !== this.seenEntities.indexOf(packet.entity)) { + return true; + } + // Side-effect. + if (-1 === unknownUpdatedEntities.indexOf(packet.entity)) { + unknownUpdatedEntities.push(packet.entity); + } + return false; + }); + // Then, inject creates. + for (let i = 0; i < unknownUpdatedEntities.length; i++) { + const entity = unknownUpdatedEntities[i]; + packets.push(new EntityCreatePacket(entity.toJSON(), entity)); + this.seenEntities.push(entity); + } + // Inject removes for any previously seen entity that isn't visible + // anymore. + for (let i = 0; i < this.seenEntities.length; i++) { + const entity = this.seenEntities[i]; + if (-1 === visibleEntities.indexOf(entity)) { + packets.push(new EntityRemovePacket({}, entity)); + } + } + // Document any new entities. + it = packetEntitiesMap.keys(); + for (let value = it.next(); !value.done; value = it.next()) { + const entity = value.value; + if (-1 === this.seenEntities.indexOf(entity)) { + this.seenEntities.push(entity); + } + } + // Unsee any removed entities. + 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); + } + } + // Ship it! + for (let i = 0; i < packets.length; i++) { + const packet = packets[i]; + this._socket.send(packet); } }, @@ -294,20 +217,4 @@ export class Informed extends decorate(Trait) { }; } - 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); - } - } - }