refactor: immutablen't
This commit is contained in:
parent
55eaf48f0c
commit
057e5bc682
2
TODO.md
2
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
|
||||
|
|
|
@ -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);
|
||||
|
|
19
client/ui/chat/index.js
Normal file
19
client/ui/chat/index.js
Normal file
|
@ -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 <div className="chat">
|
||||
<div className="item-slot-inner"></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default decorate(ChatComponent);
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
12
common/combat/trait-alive.packet.js
Normal file
12
common/combat/trait-alive.packet.js
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
12
common/packets/self-entity.packet.js
Normal file
12
common/packets/self-entity.packet.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {Packet} from '@avocado/net';
|
||||
|
||||
export class SelfEntityPacket extends Packet {
|
||||
|
||||
static get schema() {
|
||||
return {
|
||||
...super.schema,
|
||||
data: 'string',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
12
common/world-time.packet.js
Normal file
12
common/world-time.packet.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {Packet} from '@avocado/net';
|
||||
|
||||
export class WorldTimePacket extends Packet {
|
||||
|
||||
static get schema() {
|
||||
return {
|
||||
...super.schema,
|
||||
data: 'uint16',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user