diff --git a/TODO.md b/TODO.md index 33d92c8..a3b84b0 100644 --- a/TODO.md +++ b/TODO.md @@ -38,3 +38,4 @@ - ❌ Target trait updates beyond just visible; e.g. others don't need to know about inventory change - ❌ afterDestructionTickers shouldn't exist... destruction should be deferred +- ❌ Remove position pack check from trait and add to Vector diff --git a/packages/entity/entity.synchronized.js b/packages/entity/entity.synchronized.js index 3d459b1..66d7b50 100644 --- a/packages/entity/entity.synchronized.js +++ b/packages/entity/entity.synchronized.js @@ -13,6 +13,7 @@ import {Resource} from '@avocado/resource'; import {EntityCreatePacket} from './packets/entity-create.packet'; import {hasTrait, lookupTrait} from './trait/registry'; +import {EntityUpdateTraitPacket} from './packets/entity-update-trait.packet'; const debug = D('@avocado:entity:traits'); @@ -107,6 +108,7 @@ export class Entity extends decorate(Resource) { this.tick = this.tick.bind(this); // Fast props. this.numericUid = numericUid++; + this.instanceUuid = this.numericUid; this.position = [0, 0]; this.room = null; this.visibleAabb = [0, 0, 0, 0]; @@ -117,6 +119,15 @@ export class Entity extends decorate(Resource) { } acceptPacket(packet) { + if (packet instanceof EntityUpdateTraitPacket) { + const {traits} = packet.data; + for (let i = 0; i < traits.length; i++) { + const {type, packets} = traits[i]; + for (let j = 0; j < packets.length; j++) { + this._traits[type].acceptPacket(packets[j]); + } + } + } } addTrait(type, json = {}) { @@ -194,6 +205,12 @@ export class Entity extends decorate(Resource) { return Object.keys(this._traits); } + cleanPackets() { + for (let i = 0; i < this._traitsFlat.length; i++) { + this._traitsFlat[i].cleanPackets(); + } + } + fromJSON(json) { super.fromJSON(json); if (json.instanceUuid) { @@ -262,6 +279,25 @@ export class Entity extends decorate(Resource) { packets(informed) { const packets = []; + const updates = []; + for (const type in this._traits) { + const traitPackets = this._traits[type].packets(informed); + if (traitPackets.length > 0) { + updates.push({ + type, + packets: traitPackets, + }); + } + } + if (updates.length > 0) { + packets.push(new EntityUpdateTraitPacket({ + synchronized: { + id: 0, + type: 0, + }, + traits: updates, + })); + } return packets; } @@ -335,6 +371,10 @@ export class Entity extends decorate(Resource) { types.forEach((type) => this.removeTrait(type)); } + synchronizationId() { + return this.instanceUuid; + } + tick(elapsed) { for (let i = 0; i < this._traitTickers.length; i++) { this._traitTickers[i](elapsed); @@ -348,7 +388,7 @@ export class Entity extends decorate(Resource) { } return { ...super.toJSON(), - instanceUuid: this.numericUid, + instanceUuid: this.instanceUuid, traits: json, }; } diff --git a/packages/entity/index.js b/packages/entity/index.js index fd9855e..e58c534 100644 --- a/packages/entity/index.js +++ b/packages/entity/index.js @@ -4,6 +4,10 @@ export {EntityCreatePacket} from './packets/entity-create.packet'; export {EntityRemovePacket} from './packets/entity-remove.packet'; export {EntityPacket} from './packets/entity.packet'; +export { + EntityListUpdateEntityPacket, +} from './packets/entity-list-update-entity.packet'; + export {EntityList, EntityListView} from './list'; export { diff --git a/packages/entity/list/index.js b/packages/entity/list/index.js index 7d284ab..000f163 100644 --- a/packages/entity/list/index.js +++ b/packages/entity/list/index.js @@ -1,8 +1,7 @@ import {compose, EventEmitter} from '@avocado/core'; import {QuadTree, Rectangle, Vector} from '@avocado/math'; -import {EntityCreatePacket} from '../packets/entity-create.packet'; -import {EntityRemovePacket} from '../packets/entity-remove.packet'; +import {EntityListUpdateEntityPacket} from '../packets/entity-list-update-entity.packet'; import {Entity} from '../entity.synchronized'; const decorate = compose( @@ -30,6 +29,18 @@ export class EntityList extends decorate(class {}) { } acceptPacket(packet) { + if (packet instanceof EntityListUpdateEntityPacket) { + for (let i = 0; i < packet.data.length; i++) { + const {uuid, packets} = packet.data[i]; + // WHY ? + if (!this._entities[uuid]) { + continue; + } + for (let j = 0; j < packets.length; j++) { + this._entities[uuid].acceptPacket(packets[j]); + } + } + } } addEntity(entity) { @@ -58,6 +69,12 @@ export class EntityList extends decorate(class {}) { this.emit('entityAdded', entity); } + cleanPackets() { + for (let i = 0; i < this._flatEntities.length; i++) { + this._flatEntities[i].cleanPackets(); + } + } + destroy() { for (let i = 0; i < this._flatEntities.length; i++) { this._flatEntities[i].destroy(); @@ -86,6 +103,20 @@ export class EntityList extends decorate(class {}) { packets(informed) { const packets = []; + const updates = []; + for (const uuid in this._entities) { + const entity = this._entities[uuid]; + const entityPackets = entity.packets(informed); + if (entityPackets.length > 0) { + updates.push({ + uuid: entity.instanceUuid, + packets: entityPackets, + }); + } + } + if (updates.length > 0) { + packets.push(new EntityListUpdateEntityPacket(updates)); + } return packets; } diff --git a/packages/entity/packets/entity-list-update-entity.packet.js b/packages/entity/packets/entity-list-update-entity.packet.js new file mode 100644 index 0000000..3f5a271 --- /dev/null +++ b/packages/entity/packets/entity-list-update-entity.packet.js @@ -0,0 +1,35 @@ +import {BundlePacket, Packet} from '@avocado/net'; + +export class EntityListUpdateEntityPacket extends Packet { + + static pack(packet) { + const data = packet.data[1]; + for (let i = 0; i < data.length; i++) { + data[i].packets = BundlePacket.packPacket( + new BundlePacket(data[i].packets) + ); + } + return super.pack(packet); + } + + static get schema() { + return { + ...super.schema, + data: [ + { + uuid: 'uint32', + packets: 'buffer', + }, + ], + }; + } + + static unpack(packet) { + const data = super.unpack(packet); + for (let i = 0; i < data.length; i++) { + data[i].packets = BundlePacket.unpack(data[i].packets); + } + return data; + } + +} diff --git a/packages/entity/packets/entity-update-trait.packet.js b/packages/entity/packets/entity-update-trait.packet.js new file mode 100644 index 0000000..d2a3dec --- /dev/null +++ b/packages/entity/packets/entity-update-trait.packet.js @@ -0,0 +1,34 @@ +import {BundlePacket, SynchronizedUpdatePacket} from '@avocado/net'; + +export class EntityUpdateTraitPacket extends SynchronizedUpdatePacket { + + static pack(packet) { + const data = packet.data[1]; + for (let i = 0; i < data.traits.length; i++) { + data.traits[i].packets = BundlePacket.packPacket( + new BundlePacket(data.traits[i].packets) + ); + } + return super.pack(packet); + } + + static get synchronizationSchema() { + return { + traits: [ + { + type: 'string', + packets: 'buffer', + }, + ], + }; + } + + static unpack(packet) { + const data = super.unpack(packet); + for (let i = 0; i < data.traits.length; i++) { + data.traits[i].packets = BundlePacket.unpack(data.traits[i].packets); + } + return data; + } + +} diff --git a/packages/entity/packets/trait-positioned.packet.js b/packages/entity/packets/trait-positioned.packet.js deleted file mode 100644 index cc06c7d..0000000 --- a/packages/entity/packets/trait-positioned.packet.js +++ /dev/null @@ -1,11 +0,0 @@ -import {EntityPacket} from './entity.packet'; - -export class TraitPositionedPacket extends EntityPacket { - - static get schema() { - const schema = super.schema; - schema.data.position = 'uint32'; - return schema; - } - -} diff --git a/packages/entity/packets/trait-update-positioned-position.packet.js b/packages/entity/packets/trait-update-positioned-position.packet.js new file mode 100644 index 0000000..68d240f --- /dev/null +++ b/packages/entity/packets/trait-update-positioned-position.packet.js @@ -0,0 +1,27 @@ +import {Vector} from '@avocado/math'; +import {Packet} from '@avocado/net'; + +export class TraitUpdatePositionedPositionPacket extends Packet { + + static pack(packet) { + const data = packet.data[1]; + data.position = Vector.packToUint32(data.position); + return super.pack(packet); + } + + static get schema() { + return { + ...super.schema, + data: { + position: 'uint32', + }, + }; + } + + static unpack(packet) { + const data = super.unpack(packet); + data.position = Vector.unpackFromUint32(data.position); + return data; + } + +} diff --git a/packages/entity/trait/index.js b/packages/entity/trait/index.js index 441b5dd..76b1483 100644 --- a/packages/entity/trait/index.js +++ b/packages/entity/trait/index.js @@ -1,7 +1,9 @@ import {compose, merge, mergeDiff, Property} from '@avocado/core'; import {Vector} from '@avocado/math'; +import {SynchronizedMixin} from '@avocado/net'; const decorate = compose( + SynchronizedMixin, ); export class Trait extends decorate(class {}) { @@ -25,6 +27,9 @@ export class Trait extends decorate(class {}) { acceptPacket(packet) { } + cleanPackets() { + } + static contextType() { return {}; } @@ -106,6 +111,10 @@ export class Trait extends decorate(class {}) { return packets; } + packetsAreIdempotent() { + return false; + } + toJSON() { return { params: this.params, diff --git a/packages/entity/traits/positioned.trait.js b/packages/entity/traits/positioned.trait.js index 4befea1..f65cb58 100644 --- a/packages/entity/traits/positioned.trait.js +++ b/packages/entity/traits/positioned.trait.js @@ -2,7 +2,7 @@ import {compose, EventEmitter} from '@avocado/core'; import {Vector} from '@avocado/math'; import {Trait} from '../trait'; -import {TraitPositionedPacket} from '../packets/trait-positioned.packet'; +import {TraitUpdatePositionedPositionPacket} from '../packets/trait-update-positioned-position.packet'; const decorate = compose( EventEmitter, @@ -31,9 +31,10 @@ export class Positioned extends decorate(Trait) { constructor(entity, params, state) { super(entity, params, state); this.on('_positionChanged', this.on_positionChanged, this); - const x = this.state.x >> 2; - const y = this.state.y >> 2; + const x = this.state.x; + const y = this.state.y; this._position = [x, y]; + this._positionChanged = false; this.entity.position[0] = x; this.entity.position[1] = y; if (AVOCADO_CLIENT) { @@ -44,6 +45,10 @@ export class Positioned extends decorate(Trait) { } } + cleanPackets() { + this._positionChanged = false; + } + destroy() { this.off('_positionChanged', this.on_positionChanged); if (AVOCADO_CLIENT) { @@ -52,9 +57,8 @@ export class Positioned extends decorate(Trait) { } acceptPacket(packet) { - if (packet instanceof TraitPositionedPacket) { - this.serverX = (packet.data.position & 0xFFFF) >> 2; - this.serverY = (packet.data.position >> 16) >> 2; + if (packet instanceof TraitUpdatePositionedPositionPacket) { + [this.serverX, this.serverY] = packet.data.position; } } @@ -62,8 +66,9 @@ export class Positioned extends decorate(Trait) { this.entity.position[0] = newPosition[0]; this.entity.position[1] = newPosition[1]; if (AVOCADO_SERVER) { - this.state.x = newPosition[0] << 2; - this.state.y = newPosition[1] << 2; + this.state.x = newPosition[0]; + this.state.y = newPosition[1]; + this._positionChanged = true; } this.entity.emit('positionChanged', oldPosition, newPosition); } @@ -72,19 +77,17 @@ export class Positioned extends decorate(Trait) { this.serverPositionDirty = true; } - packetsForUpdate() { + packets(informed) { const packets = []; - if (this.isDirty) { + if (this._positionChanged) { // Physics slop can end us up with negatives. Don't allow them in the // packed representation, even though it means a slight loss in accuracy. // The world bounds will (should!) keep things *eventually* correct. const x = Math.max(0, this.state.x); const y = Math.max(0, this.state.y); - const packed = (y << 16) | (x << 0); - packets.push(new TraitPositionedPacket({ - position: packed, + packets.push(new TraitUpdatePositionedPositionPacket({ + position: [x, y], }, this.entity)); - this.makeClean(); } return packets; } diff --git a/packages/entity/traits/spawner.trait.js b/packages/entity/traits/spawner.trait.js index 256e95e..f5bdbb0 100644 --- a/packages/entity/traits/spawner.trait.js +++ b/packages/entity/traits/spawner.trait.js @@ -115,8 +115,8 @@ export class Spawner extends decorate(Trait) { if (!json.traits.positioned.state) { json.traits.positioned.state = {}; } - json.traits.positioned.state.x = position[0] << 2; - json.traits.positioned.state.y = position[1] << 2; + json.traits.positioned.state.x = position[0]; + json.traits.positioned.state.y = position[1]; return this.entity.spawn(key, json); }, diff --git a/packages/net/packet/bundle.packet.js b/packages/net/packet/bundle.packet.js index a471c8c..6bab772 100644 --- a/packages/net/packet/bundle.packet.js +++ b/packages/net/packet/bundle.packet.js @@ -32,10 +32,8 @@ export class BundlePacket extends Packet { packedPacket.copy(buffer, caret, 0); caret += packedPacket.length; } - return this.builder.encode({ - _id: packet.data[0], - data: buffer, - }); + packet.data[1] = buffer; + return super.pack(packet); } static get schema() { @@ -46,16 +44,19 @@ export class BundlePacket extends Packet { } static unpack(packet) { - const {data} = this.builder.decode(packet); + const data = super.unpack(packet) const packets = []; let caret = 0; while (caret < data.length) { // Read packed length. const length = data.readUInt32LE(caret); caret += 4; - // Read packed data. - const packedPacket = Buffer.from(data.buffer, caret, length); - caret += length; + // Read packed data. TODO: manual blitting sucks... + const packedPacket = Buffer.allocUnsafe(length); + let i = 0; + while (i < length) { + packedPacket.writeUInt8(data.readUInt8(caret++), i++); + } // Lookup packet. const packetId = packedPacket.readUInt8(0); const Packet = packetFromId(packetId); diff --git a/packages/net/s13n/synchronized.js b/packages/net/s13n/synchronized.js index 1e3e258..804ca8a 100644 --- a/packages/net/s13n/synchronized.js +++ b/packages/net/s13n/synchronized.js @@ -17,7 +17,9 @@ export function SynchronizedMixin(Superclass) { this.fromJSON(json); } - packets(informed) {} + packets(informed) { + return []; + } packetsAreIdempotent() { return true; diff --git a/packages/topdown/layer.js b/packages/topdown/layer.js index 03400ea..edd392d 100644 --- a/packages/topdown/layer.js +++ b/packages/topdown/layer.js @@ -1,5 +1,10 @@ import {compose, EventEmitter, Property} from '@avocado/core'; -import {Entity, EntityCreatePacket, EntityList} from '@avocado/entity'; +import { + Entity, + EntityCreatePacket, + EntityList, + EntityListUpdateEntityPacket, +} from '@avocado/entity'; import {Vector} from '@avocado/math'; import {ShapeList} from '@avocado/physics'; @@ -50,6 +55,9 @@ export class Layer extends decorate(class {}) { if (packet instanceof LayerUpdateTilesetUriPacket) { this.tilesetUri = packet.data; } + if (packet instanceof EntityListUpdateEntityPacket) { + this.entityList.acceptPacket(packet); + } } addEntity(entity) { @@ -100,6 +108,7 @@ export class Layer extends decorate(class {}) { cleanPackets() { this._tilesetUriChanged = false; + this.entityList.cleanPackets(); } destroy() { @@ -202,6 +211,10 @@ export class Layer extends decorate(class {}) { if (this._tilesetUriChanged) { packets.push(new LayerUpdateTilesetUriPacket(this.tilesetUri)); } + const entityListPackets = this.entityList.packets(informed); + for (let i = 0; i < entityListPackets.length; i++) { + packets.push(entityListPackets[i]); + } return packets; } diff --git a/packages/topdown/packets/layers-update-layer.packet.js b/packages/topdown/packets/layers-update-layer.packet.js index 23b2ecf..29cba04 100644 --- a/packages/topdown/packets/layers-update-layer.packet.js +++ b/packages/topdown/packets/layers-update-layer.packet.js @@ -14,6 +14,7 @@ export class LayersUpdateLayerPacket extends Packet { static get schema() { return { + ...super.schema, data: [ { layerIndex: 'uint8',