refactor: synchronization

This commit is contained in:
cha0s 2021-03-14 03:08:13 -05:00
parent 8afa52fc2e
commit 6bdbe70fec
47 changed files with 1432 additions and 1688 deletions

View File

@ -118,6 +118,7 @@ export default (latus) => class Behaved extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
this.#context.destroy(); this.#context.destroy();
this.#currentRoutine = undefined; this.#currentRoutine = undefined;
this.#routines = undefined; this.#routines = undefined;

View File

@ -85,8 +85,8 @@ export default (Trait, latus) => class DialogInitiator extends Trait {
}; };
} }
packets(informed) { packetsFor(informed) {
return super.packets(informed).concat( return super.packetsFor(informed).concat(
informed === this.entity informed === this.entity
? this.#dialogs.map((dialog) => ['OpenDialog', dialog]) ? this.#dialogs.map((dialog) => ['OpenDialog', dialog])
: [], : [],

View File

@ -1,4 +1,5 @@
const blacklistedAccessorKeys = [ const blacklistedAccessorKeys = [
's13nId',
'state', 'state',
'uri', 'uri',
'instanceUuid', 'instanceUuid',

View File

@ -1,32 +0,0 @@
import {Packet} from '@latus/socket';
export default (latus) => class EntityListUpdateEntityPacket extends Packet {
static pack(data) {
const {Bundle} = latus.get('%packets');
for (let i = 0; i < data.length; i++) {
// eslint-disable-next-line no-param-reassign
data[i].packets = Bundle.encode(data[i].packets);
}
return data;
}
static get data() {
return [
{
uuid: 'uint32',
packets: 'buffer',
},
];
}
static unpack(data) {
const {Bundle} = latus.get('%packets');
for (let i = 0; i < data.length; i++) {
// eslint-disable-next-line no-param-reassign
data[i].packets = Bundle.decode(data[i].packets);
}
return data;
}
};

View File

@ -1,44 +0,0 @@
import {SynchronizedUpdatePacket} from '@avocado/s13n';
export default (latus) => class EntityUpdateTraitPacket extends SynchronizedUpdatePacket {
static pack(data) {
const {Bundle} = latus.get('%packets');
const Traits = latus.get('%traits');
for (let i = 0; i < data.traits.length; i++) {
const {[data.traits[i].type]: Trait} = Traits;
// eslint-disable-next-line no-param-reassign
data.traits[i] = {
type: Trait.id,
packets: Bundle.encode(data.traits[i].packets),
};
}
return data;
}
static get s13nSchema() {
return {
traits: [
{
type: 'uint8',
packets: 'buffer',
},
],
};
}
static unpack(data) {
const {Bundle} = latus.get('%packets');
const Traits = latus.get('%traits');
for (let i = 0; i < data.traits.length; i++) {
const {[data.traits[i].type]: Trait} = Traits;
// eslint-disable-next-line no-param-reassign
data.traits[i] = {
type: Trait.type,
packets: Bundle.decode(data.traits[i].packets),
};
}
return data;
}
};

View File

@ -2,300 +2,272 @@ import {compile, Context} from '@avocado/behavior';
import {compose, EventEmitter} from '@latus/core'; import {compose, EventEmitter} from '@latus/core';
import {QuadTree, Rectangle} from '@avocado/math'; import {QuadTree, Rectangle} from '@avocado/math';
import {JsonResource} from '@avocado/resource'; import {JsonResource} from '@avocado/resource';
import {Serializer} from '@avocado/s13n'; import {Synchronized} from '@avocado/s13n';
const decorate = compose( export default (latus) => {
EventEmitter, const decorate = compose(
); EventEmitter,
Synchronized(latus),
);
return class EntityList extends decorate(JsonResource) {
export default (latus) => class EntityList extends decorate(JsonResource) { #afterDestructionTickers = [];
#afterDestructionTickers = []; #entities = {};
#entities = {}; #entityTickers = [];
#entityTickers = []; #flatEntities = [];
#flatEntities = []; #informedEntities = new Map();
#informedEntities = new Map(); #quadTree = new QuadTree();
#quadTree = new QuadTree(); async acceptPacket(packet) {
await super.acceptPacket(packet);
#serializer = new Serializer(); const {s13nType} = packet;
if ('create' === s13nType) {
async acceptPacket(packet) {
if ('EntityListUpdateEntity' === packet.constructor.type) {
for (let i = 0; i < packet.data.length; i++) {
const {uuid, packets} = packet.data[i];
for (let j = 0; j < packets.length; j++) {
if (this.#entities[uuid]) {
this.#entities[uuid].acceptPacket(packets[j]);
}
else {
this.#serializer.later(uuid, (entity) => {
if (entity) {
entity.acceptPacket(packets[j]);
}
});
}
}
}
}
const {constructor: {s13nType}} = packet;
switch (s13nType) {
case 'create': {
const uuid = packet.data.synchronized.id;
const {Entity} = latus.get('%resources'); const {Entity} = latus.get('%resources');
this.#serializer.create(uuid, Entity.load(packet.data.spec)); const {id} = packet.data.synchronized;
this.#serializer.later(uuid, (entity) => { this.addEntity(this.synchronized(Entity.resourceId, id));
this.addEntity(entity);
});
break;
}
case 'destroy': {
const uuid = packet.data.synchronized.id;
this.#serializer.cancelIfPending(uuid);
if (this.#entities[uuid]) {
this.#entities[uuid].destroy();
}
break;
}
default:
}
}
async addEntity(entity) {
const uuid = entity.instanceUuid;
// Already exists?
if (this.#entities[uuid]) {
return;
}
this.#entities[uuid] = entity;
this.#flatEntities.push(entity);
this.#entityTickers.push(entity.tick);
if ('client' !== process.env.SIDE) {
this.#informedEntities.set(entity, []);
}
// eslint-disable-next-line no-param-reassign
entity.list = this;
entity.emit('addedToList');
entity.once('destroying', () => this.onEntityDestroying(entity));
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();
}
}
get entities() {
return this.#entities;
}
findEntity(uuid) {
return uuid in this.#entities
? this.#entities[uuid]
: undefined;
}
async load(json = []) {
await super.load(json);
const {Entity} = latus.get('%resources');
const entityInstances = await Promise.all(json.map((entity) => Entity.load(entity)));
for (let i = 0; i < entityInstances.length; i++) {
this.addEntity(entityInstances[i]);
}
}
onEntityDestroying(entity) {
this.removeEntity(entity);
// In the process of destroying, allow entities to specify tickers that
// must live on past destruction.
const tickers = entity.invokeHookFlat('afterDestructionTickers');
for (let i = 0; i < tickers.length; i++) {
const ticker = tickers[i];
this.#afterDestructionTickers.push(ticker);
}
}
packets(informed) {
const packets = [];
// Visible entities.
const {areaToInform} = informed;
const previousVisibleEntities = this.#informedEntities.get(informed);
const visibleEntities = this.visibleEntities(areaToInform);
const updates = [];
for (let i = 0; i < visibleEntities.length; i++) {
const entity = visibleEntities[i];
// Newly visible entity.
const index = previousVisibleEntities.indexOf(entity);
if (-1 === index) {
packets.push(entity.createPacket(informed));
}
else {
previousVisibleEntities.splice(index, 1);
}
const entityPackets = entity.packets(informed);
if (entityPackets.length > 0) {
updates.push({
uuid: entity.instanceUuid,
packets: entityPackets,
});
} }
} }
// Send updates.
this.#informedEntities.set(informed, visibleEntities); async addEntity(entity) {
if (updates.length > 0) { const uuid = entity.instanceUuid;
packets.push([ // Already exists?
'EntityListUpdateEntity', if (this.#entities[uuid]) {
updates, return;
]);
}
// Send destroys.
for (let i = 0; i < previousVisibleEntities.length; i++) {
const entity = previousVisibleEntities[i];
// Newly removed entity.
if (-1 === visibleEntities.indexOf(entity)) {
packets.push(entity.destroyPacket(informed));
} }
} this.#entities[uuid] = entity;
return packets; this.#flatEntities.push(entity);
} this.#entityTickers.push(entity.tick);
get quadTree() {
return this.#quadTree;
}
queryEntities(query, condition, context) {
if (!context) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
context = new Context({}, latus); entity.list = this;
entity.emit('addedToList');
entity.once('destroying', () => this.onEntityDestroying(entity));
this.emit('entityAdded', entity);
if ('client' !== process.env.SIDE) {
this.#informedEntities.set(entity, []);
}
this.startSynchronizing(entity);
} }
const check = compile(condition, latus);
const candidates = this.visibleEntities(query, true); cleanPackets() {
const fails = []; super.cleanPackets();
for (let i = 0; i < candidates.length; ++i) { for (let i = 0; i < this.#flatEntities.length; i++) {
const entity = candidates[i]; this.#flatEntities[i].cleanPackets();
context.add('query', entity);
if (!check(context)) {
fails.push(entity);
} }
} }
for (let i = 0; i < fails.length; ++i) {
candidates.splice(candidates.indexOf(fails[i]), 1);
}
return candidates;
}
queryPoint(query, condition, context) { destroy() {
return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context); for (let i = 0; i < this.#flatEntities.length; i++) {
} this.#flatEntities[i].destroy();
removeEntity(entity) {
const uuid = entity.instanceUuid;
if (!(uuid in this.#entities)) {
return;
}
if ('client' !== process.env.SIDE) {
this.#informedEntities.delete(entity);
}
// eslint-disable-next-line no-param-reassign
entity.list = null;
entity.emit('removedFromList', this);
delete this.#entities[uuid];
this.#flatEntities.splice(this.#flatEntities.indexOf(entity), 1);
this.#entityTickers.splice(this.#entityTickers.indexOf(entity.tick), 1);
this.emit('entityRemoved', entity);
}
tick(elapsed) {
// Run after destruction tickers.
if (this.#afterDestructionTickers.length > 0) {
this.tickAfterDestructionTickers(elapsed);
}
// Run normal tickers.
this.tickEntities(elapsed);
}
tickAfterDestructionTickers(elapsed) {
const finishedTickers = [];
for (let i = 0; i < this.#afterDestructionTickers.length; ++i) {
const ticker = this.#afterDestructionTickers[i];
if (ticker(elapsed)) {
finishedTickers.push(ticker);
} }
} }
for (let i = 0; i < finishedTickers.length; ++i) {
const ticker = finishedTickers[i]; get entities() {
const index = this.#afterDestructionTickers.indexOf(ticker); return this.#entities;
this.#afterDestructionTickers.splice(index, 1);
} }
}
tickEntities(elapsed) { findEntity(uuid) {
for (let i = 0; i < this.#entityTickers.length; i++) { return uuid in this.#entities
this.#entityTickers[i](elapsed); ? this.#entities[uuid]
: undefined;
} }
}
toJSON() { async load(json = []) {
const json = []; await super.load(json);
for (let i = 0; i < this.#flatEntities.length; i++) { const {Entity} = latus.get('%resources');
const entity = this.#flatEntities[i]; const entityInstances = await Promise.all(json.map((entity) => Entity.load(entity)));
json.push(entity.mergeDiff(entity.toJSON())); for (let i = 0; i < entityInstances.length; i++) {
} this.addEntity(entityInstances[i]);
return json;
}
toNetwork(informed) {
const {areaToInform} = informed;
const visibleEntities = this.visibleEntities(areaToInform);
// Mark as notified.
this.#informedEntities.set(informed, visibleEntities);
return visibleEntities.map((entity) => entity.toNetwork(informed));
}
visibleEntities(query) {
const entities = [];
const quadTree = this.#quadTree;
const entries = Array.from(quadTree.query(query));
for (let i = 0; i < entries.length; i++) {
const [entity] = entries[i];
const {visibleAabb} = entity;
if (entity.isVisible && Rectangle.intersects(query, visibleAabb)) {
entities.push(entity);
} }
} }
return entities;
}
visibleEntitiesWithUri(query, uri) { onEntityDestroying(entity) {
return this.visibleEntities(query).filter((entity) => entity.uri === uri); this.removeEntity(entity);
} // In the process of destroying, allow entities to specify tickers that
// must live on past destruction.
async waitForEntity(uuid) { const tickers = entity.invokeHookFlat('afterDestructionTickers');
const entity = this.findEntity(uuid); for (let i = 0; i < tickers.length; i++) {
if (entity) { const ticker = tickers[i];
return entity; this.#afterDestructionTickers.push(ticker);
}
} }
return new Promise((resolve) => {
const find = () => { packetsFor(informed) {
const entity = this.findEntity(uuid); const packets = [];
if (entity) { const {Entity} = latus.get('%resources');
this.off('entityAdded', find); // Visible entities.
resolve(entity); const {areaToInform} = informed;
const previousVisibleEntities = this.#informedEntities.get(informed);
const visibleEntities = this.visibleEntities(areaToInform);
for (let i = 0; i < visibleEntities.length; i++) {
const entity = visibleEntities[i];
// Newly visible entity.
const index = previousVisibleEntities.indexOf(entity);
if (-1 === index) {
packets.push(entity.createPacket(informed));
} }
}; else {
this.on('entityAdded', find); previousVisibleEntities.splice(index, 1);
}); }
} const entityPackets = entity.packetsFor(informed);
if (entityPackets.length > 0) {
packets.push([
'SynchronizedUpdate',
{
packets: entityPackets,
synchronized: {
id: entity.s13nId,
type: Entity.resourceId,
},
},
]);
}
}
// Send updates.
this.#informedEntities.set(informed, visibleEntities);
// Send destroys.
for (let i = 0; i < previousVisibleEntities.length; i++) {
const entity = previousVisibleEntities[i];
// Newly removed entity.
if (-1 === visibleEntities.indexOf(entity)) {
packets.push(entity.destroyPacket(informed));
}
}
return packets;
}
get quadTree() {
return this.#quadTree;
}
queryEntities(query, condition, context) {
if (!context) {
// eslint-disable-next-line no-param-reassign
context = new Context({}, latus);
}
const check = compile(condition, latus);
const candidates = this.visibleEntities(query, true);
const fails = [];
for (let i = 0; i < candidates.length; ++i) {
const entity = candidates[i];
context.add('query', entity);
if (!check(context)) {
fails.push(entity);
}
}
for (let i = 0; i < fails.length; ++i) {
candidates.splice(candidates.indexOf(fails[i]), 1);
}
return candidates;
}
queryPoint(query, condition, context) {
return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context);
}
removeEntity(entity) {
const uuid = entity.instanceUuid;
if (!(uuid in this.#entities)) {
return;
}
this.stopSynchronizing(entity);
if ('client' !== process.env.SIDE) {
this.#informedEntities.delete(entity);
}
// eslint-disable-next-line no-param-reassign
entity.list = null;
entity.emit('removedFromList', this);
delete this.#entities[uuid];
this.#flatEntities.splice(this.#flatEntities.indexOf(entity), 1);
this.#entityTickers.splice(this.#entityTickers.indexOf(entity.tick), 1);
this.emit('entityRemoved', entity);
}
tick(elapsed) {
// Run after destruction tickers.
if (this.#afterDestructionTickers.length > 0) {
this.tickAfterDestructionTickers(elapsed);
}
// Run normal tickers.
this.tickEntities(elapsed);
}
tickAfterDestructionTickers(elapsed) {
const finishedTickers = [];
for (let i = 0; i < this.#afterDestructionTickers.length; ++i) {
const ticker = this.#afterDestructionTickers[i];
if (ticker(elapsed)) {
finishedTickers.push(ticker);
}
}
for (let i = 0; i < finishedTickers.length; ++i) {
const ticker = finishedTickers[i];
const index = this.#afterDestructionTickers.indexOf(ticker);
this.#afterDestructionTickers.splice(index, 1);
}
}
tickEntities(elapsed) {
for (let i = 0; i < this.#entityTickers.length; i++) {
this.#entityTickers[i](elapsed);
}
}
toJSON() {
const json = [];
for (let i = 0; i < this.#flatEntities.length; i++) {
const entity = this.#flatEntities[i];
json.push(entity.mergeDiff(entity.toJSON()));
}
return json;
}
toNetwork(informed) {
const {areaToInform} = informed;
const visibleEntities = this.visibleEntities(areaToInform);
// Mark as notified.
this.#informedEntities.set(informed, visibleEntities);
return visibleEntities.map((entity) => entity.toNetwork(informed));
}
visibleEntities(query) {
const entities = [];
const quadTree = this.#quadTree;
const entries = Array.from(quadTree.query(query));
for (let i = 0; i < entries.length; i++) {
const [entity] = entries[i];
const {visibleAabb} = entity;
if (entity.isVisible && Rectangle.intersects(query, visibleAabb)) {
entities.push(entity);
}
}
return entities;
}
visibleEntitiesWithUri(query, uri) {
return this.visibleEntities(query).filter((entity) => entity.uri === uri);
}
async waitForEntity(uuid) {
const entity = this.findEntity(uuid);
if (entity) {
return entity;
}
return new Promise((resolve) => {
const find = () => {
const entity = this.findEntity(uuid);
if (entity) {
this.off('entityAdded', find);
resolve(entity);
}
};
this.on('entityAdded', find);
});
}
};
}; };

View File

@ -14,428 +14,412 @@ import BaseEntity from '../base-entity';
const debug = D('@avocado/entity'); const debug = D('@avocado/entity');
const decorate = compose(
EventEmitter,
Synchronized,
);
let numericUid = 'client' !== process.env.SIDE ? 1 : 1000000000; let numericUid = 'client' !== process.env.SIDE ? 1 : 1000000000;
export default (latus) => class Entity extends decorate(BaseEntity) { export default (latus) => {
const decorate = compose(
EventEmitter,
Synchronized(latus),
);
return class Entity extends decorate(BaseEntity) {
#hooks = {}; #hooks = {};
#isDestroying = false; #isDestroying = false;
#markedAsDirty = true; #markedAsDirty = true;
#originalJson; #originalJson;
#tickingPromisesTickers = []; #tickingPromisesTickers = [];
#traits = {}; #traits = {};
#traitsFlat = []; #traitsFlat = [];
#traitTickers = []; #traitTickers = [];
#traitRenderTickers = []; #traitRenderTickers = [];
#traitsAcceptingPackets = []; #traitsAcceptingPackets = [];
constructor() { constructor() {
super(); super();
this.once('destroyed', () => { this.once('destroyed', () => {
this.removeAllTraits(); this.removeAllTraits();
}); });
// Bind to prevent lookup overhead. // Bind to prevent lookup overhead.
this.tick = this.tick.bind(this); this.tick = this.tick.bind(this);
// Fast props. // Fast props.
this.elapsed = 0; this.elapsed = 0;
this.list = null; this.list = null;
this.numericUid = numericUid++; this.numericUid = numericUid++;
this.position = [0, 0]; this.position = [0, 0];
this.room = null; this.room = null;
this.uptime = 0; this.uptime = 0;
this.visibleAabb = [0, 0, 0, 0]; this.visibleAabb = [0, 0, 0, 0];
} }
acceptPacket(packet) { addTickingPromise(tickingPromise) {
if ('EntityUpdateTrait' === packet.constructor.type) { const ticker = tickingPromise.tick.bind(tickingPromise);
const {traits} = packet.data; this.#tickingPromisesTickers.push(ticker);
for (let i = 0; i < traits.length; i++) { return tickingPromise.then(() => {
const {type, packets} = traits[i]; const index = this.#tickingPromisesTickers.indexOf(ticker);
for (let j = 0; j < packets.length; j++) { if (-1 !== index) {
this.#traits[type].acceptPacket(packets[j]); this.#tickingPromisesTickers.splice(index, 1);
} }
} });
} }
}
addTickingPromise(tickingPromise) { async _addTrait(type, json = {}) {
const ticker = tickingPromise.tick.bind(tickingPromise); const {[type]: Trait} = latus.get('%traits');
this.#tickingPromisesTickers.push(ticker); if (!Trait) {
return tickingPromise.then(() => { // eslint-disable-next-line no-console
const index = this.#tickingPromisesTickers.indexOf(ticker); console.error(`Tried to add trait "${type}" which isn't registered!`);
if (-1 !== index) { return undefined;
this.#tickingPromisesTickers.splice(index, 1);
} }
}); if (this.is(type)) {
} await this.#traits[type].load({
entity: this,
async _addTrait(type, json = {}) { ...json,
const {[type]: Trait} = latus.get('%traits'); });
if (!Trait) { this.emit('traitAdded', type, this.#traits[type]);
// eslint-disable-next-line no-console return undefined;
console.error(`Tried to add trait "${type}" which isn't registered!`); }
return undefined; // Ensure dependencies.
} const dependencies = Trait.dependencies();
if (this.is(type)) { const allTypes = this.traitTypes();
await this.#traits[type].load({ const lacking = without(dependencies, ...allTypes);
if (lacking.length > 0) {
// eslint-disable-next-line no-console
console.error(
`Tried to add trait "${type}" but lack one or more dependents: "${lacking.join('", "')}"!`,
);
return undefined;
}
// Instantiate.
const trait = await Trait.load({
entity: this, entity: this,
...json, ...json,
}); });
this.emit('traitAdded', type, this.#traits[type]); // Proxy properties.
return undefined; defineTraitAccessors(Trait.prototype, this, trait);
} // Attach listeners.
// Ensure dependencies. const listeners = Object.entries(trait.memoizedListeners());
const dependencies = Trait.dependencies(); for (let i = 0; i < listeners.length; i++) {
const allTypes = this.traitTypes(); const [event, listener] = listeners[i];
const lacking = without(dependencies, ...allTypes); this.on(event, listener);
if (lacking.length > 0) { }
// eslint-disable-next-line no-console // Proxy methods.
console.error( const methods = Object.entries(trait.methods());
`Tried to add trait "${type}" but lack one or more dependents: "${lacking.join('", "')}"!`, for (let i = 0; i < methods.length; i++) {
); const [key, method] = methods[i];
return undefined; this[key] = method;
} }
// Instantiate. // Register hook listeners.
const trait = await Trait.load({ const hooks = Object.entries(trait.hooks());
entity: this, for (let i = 0; i < hooks.length; i++) {
...json, const [key, fn] = hooks[i];
}); this.#hooks[key] = this.#hooks[key] || [];
// Proxy properties. this.#hooks[key].push({
defineTraitAccessors(Trait.prototype, this, trait); fn,
// Attach listeners. type,
const listeners = Object.entries(trait.memoizedListeners());
for (let i = 0; i < listeners.length; i++) {
const [event, listener] = listeners[i];
this.on(event, listener);
}
// Proxy methods.
const methods = Object.entries(trait.methods());
for (let i = 0; i < methods.length; i++) {
const [key, method] = methods[i];
this[key] = method;
}
// Register hook listeners.
const hooks = Object.entries(trait.hooks());
for (let i = 0; i < hooks.length; i++) {
const [key, fn] = hooks[i];
this.#hooks[key] = this.#hooks[key] || [];
this.#hooks[key].push({
fn,
type,
});
}
// Track trait.
this.#traits[type] = trait;
this.#traitsFlat.push(trait);
if ('tick' in trait) {
this.#traitTickers.push(trait.tick);
}
if ('renderTick' in trait) {
this.#traitRenderTickers.push(trait.renderTick);
}
if ('acceptPacket' in trait) {
this.#traitsAcceptingPackets.push(trait);
}
this.emit('traitAdded', type, trait);
return trait;
}
async addTrait(type, json) {
return this.addTraits({[type]: json});
}
async addTraits(traits) {
const Traits = latus.get('%traits');
const reorganized = {};
const add = (type) => {
const deps = Traits[type]?.dependencies() || [];
if (deps.length > 0) {
deps.forEach((type) => {
if (!this.is(type)) {
add(type);
}
}); });
} }
if (!reorganized[type]) { // Track trait.
reorganized[type] = traits[type] || {}; this.#traits[type] = trait;
this.#traitsFlat.push(trait);
if ('tick' in trait) {
this.#traitTickers.push(trait.tick);
} }
}; if ('renderTick' in trait) {
Object.keys(traits).forEach(add); this.#traitRenderTickers.push(trait.renderTick);
const entries = Object.entries(reorganized); }
const instances = {}; if ('acceptPacket' in trait) {
for (let i = 0; i < entries.length; i++) { this.#traitsAcceptingPackets.push(trait);
const [type, json] = entries[i]; }
// eslint-disable-next-line no-await-in-loop this.startSynchronizing(trait);
instances[type] = await this._addTrait(type, json); this.emit('traitAdded', type, trait);
return trait;
} }
return instances;
}
cleanPackets() { async addTrait(type, json) {
if (!this.#markedAsDirty) { return this.addTraits({[type]: json});
return;
} }
for (let i = 0; i < this.#traitsFlat.length; i++) {
this.#traitsFlat[i].cleanPackets();
}
this.#markedAsDirty = false;
}
async destroy() { async addTraits(traits) {
if (this.#isDestroying) { const Traits = latus.get('%traits');
return; const reorganized = {};
const add = (type) => {
const deps = Traits[type]?.dependencies() || [];
if (deps.length > 0) {
deps.forEach((type) => {
if (!this.is(type)) {
add(type);
}
});
}
if (!reorganized[type]) {
reorganized[type] = traits[type] || {};
}
};
Object.keys(traits).forEach(add);
const entries = Object.entries(reorganized);
const instances = {};
for (let i = 0; i < entries.length; i++) {
const [type, json] = entries[i];
// eslint-disable-next-line no-await-in-loop
instances[type] = await this._addTrait(type, json);
}
return instances;
} }
this.#isDestroying = true;
const destroyers = this.invokeHookFlat('destroy');
if (destroyers.length > 0) {
await this.addTickingPromise(new TickingPromise(
() => {},
(elapsed, resolve) => {
if (destroyers.every((destroy) => destroy(elapsed), (r) => !!r)) {
resolve();
}
},
));
}
this.emit('destroying');
this.emit('destroyed');
}
invokeHook(hook, ...args) { cleanPackets() {
const results = {}; super.cleanPackets();
if (!(hook in this.#hooks)) { if (!this.#markedAsDirty) {
return;
}
for (let i = 0; i < this.#traitsFlat.length; i++) {
this.#traitsFlat[i].cleanPackets();
}
this.#markedAsDirty = false;
}
async destroy() {
if (this.#isDestroying) {
return;
}
this.#isDestroying = true;
const destroyers = this.invokeHookFlat('destroy');
if (destroyers.length > 0) {
await this.addTickingPromise(new TickingPromise(
() => {},
(elapsed, resolve) => {
if (destroyers.every((destroy) => destroy(elapsed), (r) => !!r)) {
resolve();
}
},
));
}
this.emit('destroying');
this.emit('destroyed');
}
invokeHook(hook, ...args) {
const results = {};
if (!(hook in this.#hooks)) {
return results;
}
const values = Object.values(this.#hooks[hook]);
for (let i = 0; i < values.length; i++) {
const {fn, type} = values[i];
results[type] = fastApply(null, fn, args);
}
return results; return results;
} }
const values = Object.values(this.#hooks[hook]);
for (let i = 0; i < values.length; i++) { invokeHookFlat(hook, ...args) {
const {fn, type} = values[i]; return Object.values(this.invokeHook(hook, ...args));
results[type] = fastApply(null, fn, args);
} }
return results;
}
invokeHookFlat(hook, ...args) { invokeHookReduced(hook, ...args) {
return Object.values(this.invokeHook(hook, ...args)); return this.invokeHookFlat(hook, ...args)
} .reduce((r, result) => ({...r, ...result}), {});
invokeHookReduced(hook, ...args) {
return this.invokeHookFlat(hook, ...args)
.reduce((r, result) => ({...r, ...result}), {});
}
is(type) {
return type in this.#traits;
}
async load(json = {}) {
await super.load(json);
const {instanceUuid, traits = {}} = json;
if (!this.#originalJson) {
this.#originalJson = json;
} }
this.instanceUuid = instanceUuid || this.numericUid;
await this.addTraits(traits);
}
markAsDirty() { is(type) {
this.#markedAsDirty = true; return type in this.#traits;
} }
packets(informed) { async load(json = {}) {
const packets = []; await super.load(json);
const updates = []; const {instanceUuid, traits = {}} = json;
const traits = Object.entries(this.#traits); if (!this.#originalJson) {
for (let i = 0; i < traits.length; i++) { this.#originalJson = json;
const [type, trait] = traits[i]; }
const traitPackets = trait.packets(informed); this.instanceUuid = instanceUuid || this.numericUid;
if (traitPackets.length > 0) { await this.addTraits(traits);
updates.push({ }
type,
packets: traitPackets, markAsDirty() {
}); this.#markedAsDirty = true;
}
packetsFor(informed) {
const packets = [];
const traits = Object.values(this.#traits);
for (let i = 0; i < traits.length; i++) {
const trait = traits[i];
const traitPackets = trait.packetsFor(informed);
if (traitPackets.length > 0) {
packets.push([
'SynchronizedUpdate',
{
packets: traitPackets,
synchronized: {
id: trait.s13nId,
type: trait.constructor.resourceId,
},
},
]);
}
}
return packets;
}
renderTick(elapsed) {
for (let i = 0; i < this.#traitRenderTickers.length; i++) {
this.#traitRenderTickers[i](elapsed);
} }
} }
if (updates.length > 0) {
packets.push([
'EntityUpdateTrait',
{
synchronized: {
id: 0,
type: 0,
},
traits: updates,
},
]);
}
return packets;
}
renderTick(elapsed) { removeAllTraits() {
for (let i = 0; i < this.#traitRenderTickers.length; i++) { const types = this.traitTypes();
this.#traitRenderTickers[i](elapsed); this.removeTraits(types);
} }
}
removeAllTraits() { removeTrait(type) {
const types = this.traitTypes(); if (!this.is(type)) {
this.removeTraits(types); debug(`Tried to remove trait "${type}" when it doesn't exist!`);
} return;
}
removeTrait(type) { // Destroy instance.
if (!this.is(type)) { const instance = this.#traits[type];
debug(`Tried to remove trait "${type}" when it doesn't exist!`); this.stopSynchronizing(instance);
return; // Remove methods, hooks, and properties.
const methods = instance.methods();
const keys = Object.keys(methods);
for (let i = 0; i < keys.length; i++) {
delete this[keys[i]];
}
const hooks = Object.keys(instance.hooks());
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
const implementation = this.#hooks[hook].find(({type: hookType}) => hookType === type);
this.#hooks[hook].splice(this.#hooks[hook].indexOf(implementation), 1);
}
const {[type]: Trait} = latus.get('%traits');
const properties = enumerateTraitAccessorKeys(Trait.prototype);
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
delete this[property];
}
// Remove all event listeners.
const listeners = Object.entries(instance.memoizedListeners());
for (let i = 0; i < listeners.length; i++) {
const [event, listener] = listeners[i];
this.off(event, listener);
}
instance.destroy();
// Remove instance.
delete this.#traits[type];
this.#traitsFlat.splice(this.#traitsFlat.indexOf(instance), 1);
if ('tick' in instance) {
this.#traitTickers.splice(this.#traitTickers.indexOf(instance.tick), 1);
}
if ('renderTick' in instance) {
this.#traitRenderTickers.splice(this.#traitRenderTickers.indexOf(instance.renderTick), 1);
}
const acceptPacketIndex = this.#traitsAcceptingPackets.indexOf(instance);
if (-1 !== acceptPacketIndex) {
this.#traitsAcceptingPackets.splice(acceptPacketIndex, 1);
}
// Unloop.
instance.entity = undefined;
} }
// Destroy instance.
const instance = this.#traits[type];
instance.destroy();
// Remove methods, hooks, and properties.
const methods = instance.methods();
const keys = Object.keys(methods);
for (let i = 0; i < keys.length; i++) {
delete this[keys[i]];
}
const hooks = Object.keys(instance.hooks());
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
const implementation = this.#hooks[hook].find(({type: hookType}) => hookType === type);
this.#hooks[hook].splice(this.#hooks[hook].indexOf(implementation), 1);
}
const {[type]: Trait} = latus.get('%traits');
const properties = enumerateTraitAccessorKeys(Trait.prototype);
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
delete this[property];
}
// Remove all event listeners.
const listeners = Object.entries(instance.memoizedListeners());
for (let i = 0; i < listeners.length; i++) {
const [event, listener] = listeners[i];
this.off(event, listener);
}
instance._memoizedListeners = {};
// Remove instance.
delete this.#traits[type];
this.#traitsFlat.splice(this.#traitsFlat.indexOf(instance), 1);
if ('tick' in instance) {
this.#traitTickers.splice(this.#traitTickers.indexOf(instance.tick), 1);
}
if ('renderTick' in instance) {
this.#traitRenderTickers.splice(this.#traitRenderTickers.indexOf(instance.renderTick), 1);
}
const acceptPacketIndex = this.#traitsAcceptingPackets.indexOf(instance);
if (-1 !== acceptPacketIndex) {
this.#traitsAcceptingPackets.splice(acceptPacketIndex, 1);
}
// Unloop.
instance.entity = undefined;
}
removeTraits(types) { removeTraits(types) {
types.forEach((type) => this.removeTrait(type)); types.forEach((type) => this.removeTrait(type));
}
s13nId() {
return this.instanceUuid;
}
tick(elapsed) {
this.elapsed = elapsed;
this.uptime += elapsed;
for (let i = 0; i < this.#traitTickers.length; i++) {
this.#traitTickers[i](elapsed);
} }
for (let i = 0; i < this.#tickingPromisesTickers.length; i++) {
this.#tickingPromisesTickers[i](elapsed); get s13nId() {
return this.instanceUuid;
} }
}
toJSON() { tick(elapsed) {
const json = {}; this.elapsed = elapsed;
const traits = Object.entries(this.#traits); this.uptime += elapsed;
for (let i = 0; i < traits.length; i++) { for (let i = 0; i < this.#traitTickers.length; i++) {
const [type, trait] = traits[i]; this.#traitTickers[i](elapsed);
json[type] = trait.toJSON(); }
for (let i = 0; i < this.#tickingPromisesTickers.length; i++) {
this.#tickingPromisesTickers[i](elapsed);
}
} }
return {
traits: json,
};
}
toNetwork(informed) { toJSON() {
const pristine = {traits: {}}; const json = {};
const json = { const traits = Object.entries(this.#traits);
traits: {}, for (let i = 0; i < traits.length; i++) {
}; const [type, trait] = traits[i];
const traits = Object.entries(this.#traits); json[type] = trait.toJSON();
for (let i = 0; i < traits.length; i++) { }
const [type, trait] = traits[i]; return {
pristine.traits[type] = trait.constructor.withDefaults( traits: json,
this.#originalJson ? this.#originalJson.traits[type] : {}, };
);
json.traits[type] = trait.toNetwork(informed);
} }
const merged = mergeDiff(pristine, json) || {};
return {
instanceUuid: this.instanceUuid,
...(this.uri ? {extends: this.uri} : {}),
...merged,
};
}
trait(type) { toNetwork(informed) {
return this.#traits[type]; const pristine = {traits: {}};
} const json = {
traits: {},
};
const traits = Object.entries(this.#traits);
for (let i = 0; i < traits.length; i++) {
const [type, trait] = traits[i];
pristine.traits[type] = trait.constructor.withDefaults(
this.#originalJson ? this.#originalJson.traits[type] : {},
);
json.traits[type] = trait.toNetwork(informed);
}
const merged = mergeDiff(pristine, json) || {};
return {
instanceUuid: this.instanceUuid,
...(this.uri ? {extends: this.uri} : {}),
...merged,
};
}
get traits() { trait(type) {
return this.#traits; return this.#traits[type];
} }
traitTypes() { get traits() {
return Object.keys(this.#traits); return this.#traits;
} }
static withDefaults(json = {}) { traitTypes() {
const Traits = latus.get('%traits'); return Object.keys(this.#traits);
return { }
...json,
traits: Object.entries(json.traits)
.reduce((r, [type, traitJson]) => ({
...r,
[type]: Traits[type]
? Traits[type].withDefaults(traitJson)
: traitJson,
}), {}),
};
}
static withoutDefaults(json) { static withDefaults(json = {}) {
const Traits = latus.get('%traits'); const Traits = latus.get('%traits');
const without = { return {
...json, ...json,
traits: Object.entries(json.traits) traits: Object.entries(json.traits)
.reduce((r, [type, traitJson]) => ({ .reduce((r, [type, traitJson]) => ({
...r, ...r,
[type]: Traits[type] [type]: Traits[type]
? Traits[type].withoutDefaults(traitJson) ? Traits[type].withDefaults(traitJson)
: traitJson, : traitJson,
}), {}), }), {}),
}; };
return without; }
}
static withoutDefaults(json) {
const Traits = latus.get('%traits');
const without = {
...json,
traits: Object.entries(json.traits)
.reduce((r, [type, traitJson]) => ({
...r,
[type]: Traits[type]
? Traits[type].withoutDefaults(traitJson)
: traitJson,
}), {}),
};
return without;
}
};
}; };

View File

@ -112,6 +112,7 @@ export default (latus) => class Alive extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
this.#context.destroy(); this.#context.destroy();
} }
@ -193,7 +194,7 @@ export default (latus) => class Alive extends decorate(Trait) {
}; };
} }
packets() { packetsFor() {
const packets = this.#packets.concat(); const packets = this.#packets.concat();
const {life, maxLife} = this.stateDifferences(); const {life, maxLife} = this.stateDifferences();
if (life || maxLife) { if (life || maxLife) {

View File

@ -90,7 +90,7 @@ export default () => class Directional extends decorate(Trait) {
}; };
} }
packets() { packetsFor() {
const {direction} = this.stateDifferences(); const {direction} = this.stateDifferences();
if (direction) { if (direction) {
return [[ return [[

View File

@ -37,6 +37,7 @@ export default () => class DomNode extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
super.parentNode?.removeChild(this.entity.node); super.parentNode?.removeChild(this.entity.node);
} }

View File

@ -224,7 +224,7 @@ export default () => class Mobile extends decorate(Trait) {
}; };
} }
packets() { packetsFor() {
const {isMobile, speed} = this.stateDifferences(); const {isMobile, speed} = this.stateDifferences();
if (isMobile || speed) { if (isMobile || speed) {
return [[ return [[

View File

@ -75,6 +75,7 @@ export default () => class Positioned extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
this.off('trackedPositionChanged', this.ontrackedPositionChanged); this.off('trackedPositionChanged', this.ontrackedPositionChanged);
if ('client' === process.env.SIDE) { if ('client' === process.env.SIDE) {
this.off('serverPositionChanged', this.onServerPositionChanged); this.off('serverPositionChanged', this.onServerPositionChanged);
@ -107,7 +108,7 @@ export default () => class Positioned extends decorate(Trait) {
this.serverPositionDirty = true; this.serverPositionDirty = true;
} }
packets() { packetsFor() {
const {x, y} = this.stateDifferences(); const {x, y} = this.stateDifferences();
if (x || y) { if (x || y) {
return [[ return [[

View File

@ -143,6 +143,7 @@ export default (latus) => class Spawner extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
while (this.#children.length > 0) { while (this.#children.length > 0) {
const child = this.#children.pop(); const child = this.#children.pop();
if (child) { if (child) {

View File

@ -112,7 +112,7 @@ describe('Alive', () => {
it('generates and accepts life packets', async () => { it('generates and accepts life packets', async () => {
entity.life = 80; entity.life = 80;
entity.maxLife = 90; entity.maxLife = 90;
const packets = entity.trait('Alive').packets(); const packets = entity.trait('Alive').packetsFor();
expect(packets).to.have.lengthOf(1); expect(packets).to.have.lengthOf(1);
expect(packets[0][0]).to.equal('TraitUpdateAlive'); expect(packets[0][0]).to.equal('TraitUpdateAlive');
expect(packets[0][1]).to.deep.equal({life: 80, maxLife: 90}); expect(packets[0][1]).to.deep.equal({life: 80, maxLife: 90});
@ -124,7 +124,7 @@ describe('Alive', () => {
it('generates and accepts death packets', async () => { it('generates and accepts death packets', async () => {
entity.life = 0; entity.life = 0;
entity.tick(); entity.tick();
const packets = entity.trait('Alive').packets(); const packets = entity.trait('Alive').packetsFor();
expect(packets).to.have.lengthOf(2); expect(packets).to.have.lengthOf(2);
expect(packets[0][0]).to.equal('Died'); expect(packets[0][0]).to.equal('Died');
expect(packets[1][0]).to.equal('TraitUpdateAlive'); expect(packets[1][0]).to.equal('TraitUpdateAlive');

View File

@ -42,7 +42,7 @@ describe('Directional', () => {
}); });
it('generates and accepts direction packets', async () => { it('generates and accepts direction packets', async () => {
entity.direction = 2; entity.direction = 2;
const packets = entity.trait('Directional').packets(); const packets = entity.trait('Directional').packetsFor();
expect(packets).to.have.lengthOf(1); expect(packets).to.have.lengthOf(1);
expect(packets[0][0]).to.equal('TraitUpdateDirectionalDirection'); expect(packets[0][0]).to.equal('TraitUpdateDirectionalDirection');
expect(packets[0][1]).to.equal(2); expect(packets[0][1]).to.equal(2);

View File

@ -30,7 +30,7 @@ describe('Positioned', () => {
if ('client' !== process.env.SIDE) { if ('client' !== process.env.SIDE) {
it('generates and accepts movement packets', async () => { it('generates and accepts movement packets', async () => {
entity.setPosition([1, 1]); entity.setPosition([1, 1]);
const packets = entity.trait('Positioned').packets(); const packets = entity.trait('Positioned').packetsFor();
expect(packets).to.have.lengthOf(1); expect(packets).to.have.lengthOf(1);
expect(packets[0][0]).to.equal('TraitUpdatePositionedPosition'); expect(packets[0][0]).to.equal('TraitUpdatePositionedPosition');
expect(packets[0][1]).to.deep.equal([1, 1]); expect(packets[0][1]).to.deep.equal([1, 1]);

View File

@ -62,6 +62,7 @@ export default (latus) => class Pictured extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
if (this.#sprites) { if (this.#sprites) {
const sprites = Object.entries(this.#sprites); const sprites = Object.entries(this.#sprites);
for (let i = 0; i < sprites.length; i++) { for (let i = 0; i < sprites.length; i++) {

View File

@ -35,6 +35,7 @@ export default () => class Rastered extends Trait {
} }
destroy() { destroy() {
super.destroy();
if (this.#container) { if (this.#container) {
this.#container.destroy(); this.#container.destroy();
} }

View File

@ -98,6 +98,7 @@ export default () => class Visible extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
this.removeFromQuadTree(this.entity.list); this.removeFromQuadTree(this.entity.list);
} }
@ -167,7 +168,7 @@ export default () => class Visible extends decorate(Trait) {
}; };
} }
packets() { packetsFor() {
const {isVisible, opacity, rotation} = this.stateDifferences(); const {isVisible, opacity, rotation} = this.stateDifferences();
if (isVisible || opacity || rotation) { if (isVisible || opacity || rotation) {
return [[ return [[

View File

@ -26,6 +26,7 @@ export default () => class Controllable extends Trait {
} }
destroy() { destroy() {
super.destroy();
this.#actionRegistry.stopListening(); this.#actionRegistry.stopListening();
} }

View File

@ -55,7 +55,7 @@ export default (latus) => class Interactive extends decorate(Trait) {
}; };
} }
packets() { packetsFor() {
const {isInteractive} = this.stateDifferences(); const {isInteractive} = this.stateDifferences();
if (isInteractive) { if (isInteractive) {
return [[ return [[

View File

@ -126,6 +126,7 @@ export default (latus) => class Collider extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
this.releaseAllCollisions(); this.releaseAllCollisions();
} }

View File

@ -322,7 +322,7 @@ export default (latus) => class Emitter extends decorate(Trait) {
/* eslint-enable no-param-reassign */ /* eslint-enable no-param-reassign */
} }
packets() { packetsFor() {
return this.#emitting.length > 0 return this.#emitting.length > 0
? [['EmitParticles', this.#emitting]] ? [['EmitParticles', this.#emitting]]
: []; : [];

View File

@ -93,6 +93,7 @@ export default () => class Physical extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
this.world = undefined; this.world = undefined;
} }

View File

@ -34,6 +34,7 @@ export default () => class Shaped extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
this.#shape.destroy(); this.#shape.destroy();
if (this.#shapeView) { if (this.#shapeView) {
this.#shapeView.destroy(); this.#shapeView.destroy();

View File

@ -4,19 +4,14 @@ import {
SynchronizedUpdatePacket, SynchronizedUpdatePacket,
} from './packets'; } from './packets';
export * from './packets'; export {default as Synchronized} from './synchronized';
export {default as ReceiverSynchronizer} from './receiver-synchronizer';
export {default as SenderSynchronizer} from './sender-synchronizer';
export {default as Serializer} from './serializer';
export {default as Synchronized, synchronized} from './synchronized';
export default { export default {
hooks: { hooks: {
'@latus/socket/packets': () => ({ '@latus/socket/packets': (latus) => ({
SynchronizedCreate: SynchronizedCreatePacket, SynchronizedCreate: SynchronizedCreatePacket,
SynchronizedDestroy: SynchronizedDestroyPacket, SynchronizedDestroy: SynchronizedDestroyPacket,
SynchronizedUpdate: SynchronizedUpdatePacket, SynchronizedUpdate: SynchronizedUpdatePacket(latus),
}), }),
}, },
}; };

View File

@ -1,9 +1,29 @@
import SynchronizedPacket from './synchronized'; import SynchronizedPacket from './synchronized';
export default class SynchronizedUpdatePacket extends SynchronizedPacket { export default (latus) => class SynchronizedUpdatePacket extends SynchronizedPacket {
static pack(data) {
const {Bundle} = latus.get('%packets');
// eslint-disable-next-line no-param-reassign
data.packets = Bundle.encode(data.packets);
return data;
}
static get s13nSchema() {
return {
packets: 'buffer',
};
}
static get s13nType() { static get s13nType() {
return 'update'; return 'update';
} }
} static unpack(data) {
const {Bundle} = latus.get('%packets');
// eslint-disable-next-line no-param-reassign
data.packets = Bundle.decode(data.packets);
return data;
}
};

View File

@ -16,6 +16,10 @@ export default class SynchronizedPacket extends Packet {
return {}; return {};
} }
get s13nType() {
return this.constructor.s13nType;
}
static get s13nType() { static get s13nType() {
return 'none'; return 'none';
} }

View File

@ -1,95 +0,0 @@
import {Class, compose, EventEmitter} from '@latus/core';
import Serializer from './serializer';
const decorate = compose(
EventEmitter,
);
export default class ReceiverSynchronizer extends decorate(Class) {
#serializer = new Serializer();
#synchronized = {};
constructor(latus) {
super();
this.latus = latus;
}
async acceptPacket(packet) {
const {constructor: {s13nType}} = packet;
if (!s13nType) {
return;
}
const {id, type} = packet.data.synchronized;
switch (s13nType) {
case 'create': {
this.createSynchronized(type, id, packet.data.spec);
break;
}
case 'destroy': {
this.deleteSynchronized(type, id);
break;
}
case 'update': {
this.#serializer.later(`${type}:${id}`, (resource) => {
resource.acceptPacket(packet);
});
break;
}
default:
}
}
addSynchronized(synchronized) {
const {resourceId: type} = synchronized.constructor;
if (!(type in this.#synchronized)) {
this.#synchronized[type] = {};
}
const id = synchronized.s13nId();
this.#synchronized[type][id] = synchronized;
}
async createSynchronized(type, id, json) {
const {[type]: Resource} = this.latus.get('%resources');
if (!(type in this.#synchronized)) {
this.#synchronized[type] = {};
}
if (this.#synchronized[type][id]) {
await this.#synchronized[type][id].load(json);
}
else {
this.#serializer.create(`${type}:${id}`, Resource.load(json));
await this.#serializer.later(`${type}:${id}`, (resource) => {
this.#synchronized[type][id] = resource;
});
}
this.emit('created', type, this.#synchronized[type][id], id);
}
async deleteSynchronized(type, id) {
if (!this.hasSynchronized(type, id)) {
return;
}
this.#serializer.cancelIfPending(`${type}:${id}`);
const resource = await this.#synchronized[type][id];
if (resource) {
await resource.destroy();
delete this.#synchronized[type][id];
}
}
hasSynchronized(type, id) {
return !!this.#synchronized[type][id];
}
synchronized(type, id) {
return this.synchronizedOfType(type)[id];
}
synchronizedOfType(type) {
return this.#synchronized[type] || {};
}
}

View File

@ -1,96 +0,0 @@
export default class SenderSynchronizer {
#added = [];
#nextSyncPackets = [];
#queuedPackets = [];
#removed = [];
#synchronized = {};
#synchronizedFlat = [];
constructor(latus) {
this.latus = latus;
}
addSynchronized(synchronized) {
if (this.hasSynchronized(synchronized)) {
return;
}
this.#added.push(synchronized);
const {resourceId: type} = synchronized.constructor;
if (!(type in this.#synchronized)) {
this.#synchronized[type] = {};
}
this.#synchronizedFlat.push(synchronized);
const id = synchronized.s13nId();
this.#synchronized[type][id] = synchronized;
}
destroy() {
this.#queuedPackets = [];
this.#synchronized = {};
}
hasSynchronized(synchronized) {
return -1 !== this.#synchronizedFlat.indexOf(synchronized);
}
packetsFor(informed) {
const payload = [];
for (let i = 0; i < this.#synchronizedFlat.length; i++) {
const synchronized = this.#synchronizedFlat[i];
if (-1 !== this.#added.indexOf(synchronized)) {
payload.push(synchronized.createPacket(informed));
}
else if (-1 !== this.#removed.indexOf(synchronized)) {
payload.push(synchronized.destroyPacket(informed));
}
else {
const packets = synchronized.packetsFor(this.latus, informed);
for (let j = 0; j < packets.length; j++) {
payload.push(packets[j]);
}
}
}
this.#added = [];
this.#removed = [];
return payload;
}
removeSynchronized(synchronized) {
if (!this.hasSynchronized(synchronized)) {
return;
}
this.#removed.push(synchronized);
const index = this.#synchronizedFlat.indexOf(synchronized);
this.#synchronizedFlat.splice(index, 1);
const {resourceId: type} = synchronized.constructor;
const id = synchronized.s13nId();
delete this.#synchronized[type][id];
}
async send(socket, informed) {
const synchronizerPackets = this.packetsFor(informed);
for (let i = 0; i < synchronizerPackets.length; i++) {
this.#nextSyncPackets.push(synchronizerPackets[i]);
}
for (let i = 0; i < this.#queuedPackets.length; i++) {
this.#nextSyncPackets.push(this.#queuedPackets[i]);
}
this.#queuedPackets = [];
if (socket && this.#nextSyncPackets.length > 0) {
const nextSyncPackets = this.#nextSyncPackets;
this.#nextSyncPackets = [];
await socket.send(['Bundle', nextSyncPackets]);
}
}
queuePacket(packet) {
this.#queuedPackets.push(packet);
}
}

View File

@ -7,13 +7,6 @@ export default class Serializer {
#promises = new Map(); #promises = new Map();
cancelIfPending(id) {
const pending = this.#pending.get(id);
if (pending) {
pending.cancel();
}
}
create(id, creator) { create(id, creator) {
const promise = creator.then(async (resource) => { const promise = creator.then(async (resource) => {
if (!this.#pending.has(id)) { if (!this.#pending.has(id)) {
@ -27,13 +20,18 @@ export default class Serializer {
throw error; throw error;
} }
}); });
this.#pending.set(id, { this.#pending.set(id, () => this.#pending.delete(id));
cancel: () => this.#pending.delete(id),
});
this.#promises.set(id, promise); this.#promises.set(id, promise);
return promise; return promise;
} }
destroy(id) {
// Cancel...
this.#pending.get(id)?.();
this.#pending.delete(id);
this.#promises.delete(id);
}
later(id, fn) { later(id, fn) {
const promise = this.#promises.get(id); const promise = this.#promises.get(id);
if (!promise) { if (!promise) {

View File

@ -1,92 +0,0 @@
export const synchronized = ({config}) => config['%synchronized'];
export default function SynchronizedMixin(Superclass) {
return class Synchronized extends Superclass {
constructor(...args) {
super(...args);
this._idempotentPackets = [];
}
cleanPackets() {
this._idempotentPackets = [];
}
createPacket(informed) {
const id = this.s13nId();
return [
'SynchronizedCreate',
{
synchronized: {
id,
type: this.constructor.resourceId,
},
spec: this.toNetwork(informed),
},
];
}
// eslint-disable-next-line class-methods-use-this
destroy() {}
destroyPacket() {
const id = this.s13nId();
return [
'SynchronizedDestroy',
{
synchronized: {
id,
type: this.constructor.resourceId,
},
},
];
}
fromNetwork() {
throw new ReferenceError(
`${this.constructor.resourceType || this.constructor.name}::fromNetwork is undefined`,
);
}
// eslint-disable-next-line class-methods-use-this
packets() {
return [];
}
// eslint-disable-next-line class-methods-use-this
packetsAreIdempotent() {
return true;
}
packetsFor(latus, informed) {
if (this._idempotentPackets.length > 0) {
return this._idempotentPackets;
}
const packets = this.packets(informed);
// Embed synchronization info.
const id = this.s13nId();
for (let i = 0; i < packets.length; i++) {
packets[i][1].synchronized = {
id,
type: this.constructor.resourceId,
};
}
if (this.packetsAreIdempotent()) {
this._idempotentPackets = packets;
}
return packets;
}
// eslint-disable-next-line class-methods-use-this
s13nId() {
return 0;
}
toNetwork() {
return this.toJSON();
}
};
}

View File

@ -0,0 +1,107 @@
import Serializer from '../serializer';
export default (latus) => (Resource) => (
class SynchronizedResource extends Resource {
constructor() {
super();
this._serializer = new Serializer();
this._synchronized = {};
}
async acceptPacket(packet) {
const {s13nType} = packet;
// eslint-disable-next-line max-len
if (!s13nType) {
return;
}
const {id, type} = packet.data.synchronized;
switch (s13nType) {
case 'create':
await this.createSynchronized(type, id, packet.data.spec);
break;
case 'destroy':
await this.destroySynchronized(type, id);
break;
case 'update':
if (this._synchronized[type]?.[id]) {
const promises = [];
for (let i = 0; i < packet.data.packets.length; i++) {
promises.push(this._synchronized[type][id].acceptPacket(packet.data.packets[i]));
}
await Promise.all(promises);
}
else {
await this._serializer.later(
`${type}:${id}`,
(resource) => resource.acceptPacket(packet),
);
}
break;
default:
}
}
async createSynchronized(type, id, json) {
const {[type]: Resource} = latus.get('%resources');
if (!(type in this._synchronized)) {
this._synchronized[type] = {};
}
if (this._synchronized[type][id]) {
await this._synchronized[type][id].load(json);
}
else {
this._serializer.create(`${type}:${id}`, Resource.load(json));
await this._serializer.later(`${type}:${id}`, (resource) => {
this._synchronized[type][id] = resource;
});
}
}
// eslint-disable-next-line class-methods-use-this
destroy() {}
async destroySynchronized(type, id) {
if (!this._synchronized[type]?.[id]) {
return;
}
this._serializer.destroy(`${type}:${id}`);
const resource = await this._synchronized[type][id];
if (resource) {
await resource.destroy();
delete this._synchronized[type][id];
}
}
// eslint-disable-next-line class-methods-use-this
get s13nId() {
return 0;
}
startSynchronizing(resource) {
const id = resource.s13nId;
const type = resource.constructor.resourceId;
if (this._synchronized[type]?.[id]) {
return;
}
if (!(type in this._synchronized)) {
this._synchronized[type] = {};
}
this._synchronized[type][id] = resource;
}
stopSynchronizing(resource) {
const id = resource.s13nId;
const type = resource.constructor.resourceId;
if (!this._synchronized[type]?.[id]) {
return;
}
delete this._synchronized[type][id];
}
synchronized(type, id) {
return this._synchronized[type]?.[id];
}
}
);

View File

@ -0,0 +1,4 @@
import Client from './client';
import Server from './server';
export default process.env.SIDE === 'client' ? Client : Server;

View File

@ -0,0 +1,123 @@
export default () => (Resource) => (
class SynchronizedResource extends Resource {
constructor() {
super();
this._added = [];
this._removed = [];
this._synchronized = [];
}
cleanPackets() {
this._added = [];
this._removed = [];
for (let i = 0; i < this._synchronized.length; i++) {
this._synchronized[i].cleanPackets();
}
}
createPacket(informed) {
return [
'SynchronizedCreate',
{
synchronized: {
id: this.s13nId,
type: this.constructor.resourceId,
},
spec: this.toNetwork(informed),
},
];
}
destroy() {
super.destroy();
this._added = [];
this._removed = [];
this._synchronized = [];
}
destroyPacket() {
return [
'SynchronizedDestroy',
{
synchronized: {
id: this.s13nId,
type: this.constructor.resourceId,
},
},
];
}
isSynchronizing(resource) {
return -1 !== this._synchronized.indexOf(resource);
}
packetsFor(informed) {
const packets = [];
for (let i = 0; i < this._synchronized.length; i++) {
const synchronized = this._synchronized[i];
if (-1 !== this._added.indexOf(synchronized)) {
packets.push(synchronized.createPacket(informed));
}
else if (-1 !== this._removed.indexOf(synchronized)) {
packets.push(synchronized.destroyPacket(informed));
}
else {
const updatePacket = synchronized.updatePacket(informed);
if (updatePacket) {
packets.push(updatePacket);
}
}
}
return packets;
}
// eslint-disable-next-line class-methods-use-this
get s13nChildren() {
return [];
}
// eslint-disable-next-line class-methods-use-this
get s13nId() {
return 0;
}
startSynchronizing(synchronized) {
if (this.isSynchronizing(synchronized)) {
return;
}
this._added.push(synchronized);
this._synchronized.push(synchronized);
}
stopSynchronizing(synchronized) {
if (!this.isSynchronizing(synchronized)) {
return;
}
this._removed.push(synchronized);
this._synchronized.splice(this._synchronized.indexOf(synchronized), 1);
}
toNetwork() {
return this.toJSON();
}
updatePacket(informed) {
const packets = this.packetsFor(informed);
if (0 === packets.length) {
return undefined;
}
return [
'SynchronizedUpdate',
{
packets,
synchronized: {
id: this.s13nId,
type: this.constructor.resourceId,
},
},
];
}
}
);

View File

@ -68,6 +68,7 @@ export default (latus) => class Audible extends Trait {
} }
destroy() { destroy() {
super.destroy();
Object.values(this.#sounds).forEach((sound) => { Object.values(this.#sounds).forEach((sound) => {
Promise.resolve(sound).then((sound) => { Promise.resolve(sound).then((sound) => {
sound.destroy(); sound.destroy();
@ -122,7 +123,7 @@ export default (latus) => class Audible extends Trait {
})); }));
} }
packets() { packetsFor() {
return Object.keys(this.#playing).map((key) => ['PlaySound', {sound: key}]); return Object.keys(this.#playing).map((key) => ['PlaySound', {sound: key}]);
} }

View File

@ -89,6 +89,7 @@ export default (latus) => class Animated extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
if (this.#animationViews) { if (this.#animationViews) {
const animationViews = Object.entries(this.#animationViews); const animationViews = Object.entries(this.#animationViews);
for (let i = 0; i < animationViews.length; i++) { for (let i = 0; i < animationViews.length; i++) {
@ -246,7 +247,7 @@ export default (latus) => class Animated extends decorate(Trait) {
return this.params.animations[key].offset; return this.params.animations[key].offset;
} }
packets() { packetsFor() {
const {currentAnimation, isAnimating} = this.stateDifferences(); const {currentAnimation, isAnimating} = this.stateDifferences();
if (this.#forceUpdate || currentAnimation || isAnimating) { if (this.#forceUpdate || currentAnimation || isAnimating) {
this.#forceUpdate = false; this.#forceUpdate = false;

View File

@ -1,32 +0,0 @@
import {Packet} from '@latus/socket';
export default (latus) => class LayersUpdateLayerPacket extends Packet {
static pack(data) {
const {Bundle} = latus.get('%packets');
for (let i = 0; i < data.length; i++) {
// eslint-disable-next-line no-param-reassign
data[i].layerPackets = Bundle.encode(data[i].layerPackets);
}
return data;
}
static get data() {
return [
{
layerIndex: 'uint8',
layerPackets: 'buffer',
},
];
}
static unpack(data) {
const {Bundle} = latus.get('%packets');
for (let i = 0; i < data.length; i++) {
// eslint-disable-next-line no-param-reassign
data[i].layerPackets = Bundle.decode(data[i].layerPackets);
}
return data;
}
};

View File

@ -1,25 +0,0 @@
import {SynchronizedUpdatePacket} from '@avocado/s13n';
export default (latus) => class RoomUpdateLayers extends SynchronizedUpdatePacket {
static pack(data) {
const {Bundle} = latus.get('%packets');
// eslint-disable-next-line no-param-reassign
data.layersPackets = Bundle.encode(data.layersPackets);
return data;
}
static get s13nSchema() {
return {
layersPackets: 'buffer',
};
}
static unpack(data) {
const {Bundle} = latus.get('%packets');
// eslint-disable-next-line no-param-reassign
data.layersPackets = Bundle.decode(data.layersPackets);
return data;
}
};

View File

@ -1,230 +1,215 @@
import {Property} from '@avocado/core'; import {Property} from '@avocado/core';
import {JsonResource} from '@avocado/resource'; import {JsonResource} from '@avocado/resource';
import {Synchronized} from '@avocado/s13n';
import {compose, EventEmitter} from '@latus/core'; import {compose, EventEmitter} from '@latus/core';
const decorate = compose( export default (latus) => {
EventEmitter, const decorate = compose(
Property('tileset', { EventEmitter,
track: true, Property('tileset', {
}), track: true,
); }),
Synchronized(latus),
);
return class Layer extends decorate(JsonResource) {
export default (latus) => class Layer extends decorate(JsonResource) { #s13nId;
constructor() { constructor() {
super(); super();
this.tileEntities = {}; this.tileEntities = {};
this.tileGeometry = []; this.tileGeometry = [];
const {EntityList, Tiles} = latus.get('%resources'); const {EntityList, Tiles} = latus.get('%resources');
this.setEntityList(new EntityList()); this.setEntityList(new EntityList());
this.setTiles(new Tiles()); this.setTiles(new Tiles());
}
acceptPacket(packet) {
const {constructor: {s13nType}} = packet;
switch (s13nType) {
case 'create':
case 'destroy':
this.entityList.acceptPacket(packet);
break;
default:
} }
switch (packet.constructor.type) {
case 'EntityListUpdateEntity': addEntity(entity) {
this.entityList.acceptPacket(packet); this.entityList.addEntity(entity);
break;
case 'TilesUpdate':
this.tiles.acceptPacket(packet);
break;
default:
} }
}
addEntity(entity) { addTileEntity(entity, index) {
this.entityList.addEntity(entity); if (!this.tileEntities[index]) {
} this.tileEntities[index] = [];
}
addTileEntity(entity, index) { if (-1 !== this.tileEntities[index].indexOf(entity)) {
if (!this.tileEntities[index]) { return;
this.tileEntities[index] = []; }
this.tileEntities[index].push(entity);
} }
if (-1 !== this.tileEntities[index].indexOf(entity)) {
return; cleanPackets() {
super.cleanPackets();
this.entityList.cleanPackets();
this.tiles.cleanPackets();
} }
this.tileEntities[index].push(entity);
}
cleanPackets() { destroy() {
this.entityList.cleanPackets(); this.entityList.destroy();
this.tiles.cleanPackets();
}
destroy() {
this.entityList.destroy();
this.entityList.off('entityAdded', this.onEntityAddedToLayer);
this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer);
this.tiles.off('dataChanged', this.onTileDataChanged);
if (this.tileset) {
this.tileset.destroy();
}
}
get entities() {
return this.entityList.entities;
}
findEntity(uuid) {
return this.entityList.findEntity(uuid);
}
hasTileEntityWithUriAt(tilePosition, uri) {
const tileEntities = this.tileEntitiesAt(tilePosition);
if (0 === tileEntities.length) {
return false;
}
const entitiesWithUri = tileEntities.filter((entity) => entity.uri === uri);
return entitiesWithUri.length > 0;
}
indexAt(position) {
return this.tiles.indexAt(position);
}
async load(json = {}) {
await super.load(json);
const {
entities,
tiles,
tilesetUri,
world,
} = json;
const {EntityList, Tiles, Tileset} = latus.get('%resources');
this.setTiles(new Tiles(tiles));
this.tileset = tilesetUri
? await Tileset.load({extends: tilesetUri})
: new Tileset();
this.setEntityList(
entities
? await EntityList.load(entities)
: new EntityList(),
);
this.world = world;
}
async onEntityAddedToLayer(entity) {
await entity.addTrait('Layered');
// eslint-disable-next-line no-param-reassign
entity.layer = this;
entity.emit('addedToLayer');
this.emit('entityAdded', entity);
}
onEntityRemovedFromLayer(entity) {
// eslint-disable-next-line no-param-reassign
entity.layer = null;
entity.emit('removedFromLayer', this);
entity.removeTrait('Layered');
this.emit('entityRemoved', entity);
}
onTileDataChanged() {
this.emit('tileDataChanged');
}
packets(informed) {
const packets = [];
const entityListPackets = this.entityList.packets(informed);
for (let i = 0; i < entityListPackets.length; i++) {
packets.push(entityListPackets[i]);
}
const tilesPackets = this.tiles.packets(informed);
for (let i = 0; i < tilesPackets.length; i++) {
packets.push(tilesPackets[i]);
}
return packets;
}
removeEntity(entity) {
this.entityList.removeEntity(entity);
}
removeTileEntity(entity, index) {
if (!this.tileEntities[index]) {
return;
}
const entityIndex = this.tileEntities[index].indexOf(entity);
if (-1 === entityIndex) {
return;
}
this.tileEntities[index].splice(entityIndex, 1);
}
setEntityList(entityList) {
if (this.entityList) {
Object.values(this.entityList.entities).forEach((entity) => {
this.onEntityRemovedFromLayer(entity);
});
this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer);
this.entityList.off('entityAdded', this.onEntityAddedToLayer); this.entityList.off('entityAdded', this.onEntityAddedToLayer);
} this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer);
this.entityList = entityList;
this.entityList.on('entityAdded', this.onEntityAddedToLayer, this);
this.entityList.on('entityRemoved', this.onEntityRemovedFromLayer, this);
Object.values(this.entityList.entities).forEach((entity) => {
this.onEntityAddedToLayer(entity);
});
}
setTileAt(position, tile) {
this.tiles.setTileAt(position, tile);
}
setTiles(tiles) {
if (this.tiles) {
this.tiles.off('dataChanged', this.onTileDataChanged); this.tiles.off('dataChanged', this.onTileDataChanged);
if (this.tileset) {
this.tileset.destroy();
}
} }
this.tiles = tiles;
this.tiles.on('dataChanged', this.onTileDataChanged, this);
}
tick(elapsed) { get entities() {
this.entityList.tick(elapsed); return this.entityList.entities;
}
tileAt(position) {
return this.tiles.tileAt(position);
}
tileEntitiesAt(tilePosition) {
const index = this.indexAt(tilePosition);
if (!this.tileEntities[index]) {
return [];
} }
return this.tileEntities[index];
}
toJSON() { findEntity(uuid) {
return { return this.entityList.findEntity(uuid);
entities: this.entityList.toJSON(), }
tilesetUri: this.tileset.uri,
tiles: this.tiles.toJSON(),
};
}
toNetwork(informed) { hasTileEntityWithUriAt(tilePosition, uri) {
return { const tileEntities = this.tileEntitiesAt(tilePosition);
entities: this.entityList.toNetwork(informed), if (0 === tileEntities.length) {
tilesetUri: this.tileset.uri, return false;
tiles: this.tiles.toNetwork(informed), }
}; const entitiesWithUri = tileEntities.filter((entity) => entity.uri === uri);
} return entitiesWithUri.length > 0;
}
visibleEntities(query) { indexAt(position) {
return this.entityList.visibleEntities(query); return this.tiles.indexAt(position);
} }
// visibleEntitiesWithUri(query, uri) { async load(json = {}) {
// return this.entityList.visibleEntitiesWithUri(query, uri); await super.load(json);
// } const {
entities,
tiles,
tilesetUri,
world,
} = json;
const {EntityList, Tiles, Tileset} = latus.get('%resources');
this.setTiles(new Tiles(tiles));
this.tileset = tilesetUri
? await Tileset.load({extends: tilesetUri})
: new Tileset();
this.setEntityList(
entities
? await EntityList.load(entities)
: new EntityList(),
);
this.world = world;
}
async onEntityAddedToLayer(entity) {
await entity.addTrait('Layered');
// eslint-disable-next-line no-param-reassign
entity.layer = this;
entity.emit('addedToLayer');
this.emit('entityAdded', entity);
}
onEntityRemovedFromLayer(entity) {
// eslint-disable-next-line no-param-reassign
entity.layer = null;
entity.emit('removedFromLayer', this);
entity.removeTrait('Layered');
this.emit('entityRemoved', entity);
}
onTileDataChanged() {
this.emit('tileDataChanged');
}
removeEntity(entity) {
this.entityList.removeEntity(entity);
}
removeTileEntity(entity, index) {
if (!this.tileEntities[index]) {
return;
}
const entityIndex = this.tileEntities[index].indexOf(entity);
if (-1 === entityIndex) {
return;
}
this.tileEntities[index].splice(entityIndex, 1);
}
get s13nId() {
return this.#s13nId;
}
set s13nId(s13nId) {
this.#s13nId = s13nId;
}
setEntityList(entityList) {
if (this.entityList) {
this.stopSynchronizing(this.entityList);
Object.values(this.entityList.entities).forEach((entity) => {
this.onEntityRemovedFromLayer(entity);
});
this.entityList.off('entityRemoved', this.onEntityRemovedFromLayer);
this.entityList.off('entityAdded', this.onEntityAddedToLayer);
}
this.entityList = entityList;
this.entityList.on('entityAdded', this.onEntityAddedToLayer, this);
this.entityList.on('entityRemoved', this.onEntityRemovedFromLayer, this);
Object.values(this.entityList.entities).forEach((entity) => {
this.onEntityAddedToLayer(entity);
});
this.startSynchronizing(this.entityList);
}
setTileAt(position, tile) {
this.tiles.setTileAt(position, tile);
}
setTiles(tiles) {
if (this.tiles) {
this.stopSynchronizing(this.tiles);
this.tiles.off('dataChanged', this.onTileDataChanged);
}
this.tiles = tiles;
this.tiles.on('dataChanged', this.onTileDataChanged, this);
this.startSynchronizing(tiles);
}
tick(elapsed) {
this.entityList.tick(elapsed);
}
tileAt(position) {
return this.tiles.tileAt(position);
}
tileEntitiesAt(tilePosition) {
const index = this.indexAt(tilePosition);
if (!this.tileEntities[index]) {
return [];
}
return this.tileEntities[index];
}
toJSON() {
return {
entities: this.entityList.toJSON(),
tilesetUri: this.tileset.uri,
tiles: this.tiles.toJSON(),
};
}
toNetwork(informed) {
return {
entities: this.entityList.toNetwork(informed),
tilesetUri: this.tileset.uri,
tiles: this.tiles.toNetwork(informed),
};
}
visibleEntities(query) {
return this.entityList.visibleEntities(query);
}
// visibleEntitiesWithUri(query, uri) {
// return this.entityList.visibleEntitiesWithUri(query, uri);
// }
};
}; };

View File

@ -1,171 +1,147 @@
import {compose, EventEmitter} from '@latus/core'; import {compose, EventEmitter} from '@latus/core';
import {JsonResource} from '@avocado/resource'; import {JsonResource} from '@avocado/resource';
import {Synchronized} from '@avocado/s13n';
const decorate = compose( export default (latus) => {
EventEmitter, const decorate = compose(
); EventEmitter,
Synchronized(latus),
);
return class Layers extends decorate(JsonResource) {
export default (latus) => class Layers extends decorate(JsonResource) { constructor() {
super();
this.layers = [];
}
constructor() { addEntityToLayer(entity, layerIndex) {
super(); const layer = this.layers[layerIndex];
this.layers = []; if (!layer) {
} return;
}
layer.addEntity(entity);
}
acceptPacket(packet) { addLayer(layer) {
if ('LayersUpdateLayer' === packet.constructor.type) { // eslint-disable-next-line no-param-reassign
for (let i = 0; i < packet.data.length; i++) { layer.s13nId = this.layers.length;
const {layerIndex, layerPackets} = packet.data[i]; this.startSynchronizing(layer);
for (let j = 0; j < layerPackets.length; j++) { layer.on('entityAdded', this.onEntityAddedToLayers, this);
this.layers[layerIndex].acceptPacket(layerPackets[j]); layer.on('entityRemoved', this.onEntityRemovedFromLayers, this);
this.layers.push(layer);
this.emit('layerAdded', layer);
}
cleanPackets() {
super.cleanPackets();
for (let i = 0; i < this.layers.length; i++) {
this.layers[i].cleanPackets();
}
}
destroy() {
for (let i = 0; i < this.layers.length; i++) {
const layer = this.layers[i];
this.removeLayer(layer);
layer.destroy();
}
}
get entities() {
const entities = {};
for (let i = 0; i < this.layers.length; i++) {
const layerEntities = Object.entries(this.layers[i].entities);
for (let j = 0; j < layerEntities.length; j++) {
const [uuid, entity] = layerEntities[j];
entities[uuid] = entity;
} }
} }
return entities;
} }
}
addEntityToLayer(entity, layerIndex) { findEntity(uuid) {
const layer = this.layers[layerIndex]; for (let i = 0; i < this.layers.length; i++) {
if (!layer) { const foundEntity = this.layers[i].findEntity(uuid);
return; if (foundEntity) {
return foundEntity;
}
}
return undefined;
} }
layer.addEntity(entity);
}
addLayer(layer) { layer(index) {
layer.on('entityAdded', this.onEntityAddedToLayers, this); return this.layers[index];
layer.on('entityRemoved', this.onEntityRemovedFromLayers, this);
this.layers.push(layer);
this.emit('layerAdded', layer);
}
cleanPackets() {
for (let i = 0; i < this.layers.length; i++) {
this.layers[i].cleanPackets();
} }
}
destroy() { async load(json = []) {
for (let i = 0; i < this.layers.length; i++) { await super.load(json);
const layer = this.layers[i]; this.removeAllLayers();
this.removeLayer(layer); const {Layer} = latus.get('%resources');
layer.destroy(); const layers = await Promise.all(json.map((layer) => Layer.load(layer)));
} for (let i = 0; i < layers.length; i++) {
} this.addLayer(layers[i]);
get entities() {
const entities = {};
for (let i = 0; i < this.layers.length; i++) {
const layerEntities = Object.entries(this.layers[i].entities);
for (let j = 0; j < layerEntities.length; j++) {
const [uuid, entity] = layerEntities[j];
entities[uuid] = entity;
} }
} }
return entities;
}
findEntity(uuid) { onEntityAddedToLayers(entity) {
for (let i = 0; i < this.layers.length; i++) { this.emit('entityAdded', entity);
const foundEntity = this.layers[i].findEntity(uuid); }
if (foundEntity) {
return foundEntity; onEntityRemovedFromLayers(entity) {
this.emit('entityRemoved', entity);
}
removeAllLayers() {
for (let i = 0; i < this.layers.length; i++) {
this.removeLayer(this.layers[i]);
} }
} }
return undefined;
}
layer(index) { removeEntityFromLayer(entity, layerIndex) {
return this.layers[index]; const layer = this.layers[layerIndex];
} if (!layer) {
return;
async load(json = []) { }
await super.load(json); layer.removeEntity(entity);
this.removeAllLayers();
const {Layer} = latus.get('%resources');
const layers = await Promise.all(json.map((layer) => Layer.load(layer)));
for (let i = 0; i < layers.length; i++) {
this.addLayer(layers[i]);
} }
}
onEntityAddedToLayers(entity) { removeLayer(layer) {
this.emit('entityAdded', entity); const index = this.layers.indexOf(layer);
} if (-1 === index) {
return;
}
layer.off('entityAdded', this.onEntityAddedToLayers);
layer.off('entityRemoved', this.onEntityRemovedFromLayers);
this.layers.splice(index, 1);
this.emit('layerRemoved', layer);
this.stopSynchronizing(layer);
}
onEntityRemovedFromLayers(entity) { tick(elapsed) {
this.emit('entityRemoved', entity); for (let i = 0; i < this.layers.length; i++) {
} this.layers[i].tick(elapsed);
packets(informed) {
const packets = [];
const updates = [];
for (let i = 0; i < this.layers.length; i++) {
const layerPackets = this.layers[i].packets(informed);
if (layerPackets.length > 0) {
updates.push({
layerIndex: i,
layerPackets,
});
} }
} }
if (updates.length > 0) {
packets.push([ toJSON() {
'LayersUpdateLayer', return this.layers.map((layer) => layer.toJSON());
updates,
]);
} }
return packets;
}
removeAllLayers() { toNetwork(informed) {
for (let i = 0; i < this.layers.length; i++) { return this.layers.map((layer) => layer.toNetwork(informed));
this.removeLayer(this.layers[i]);
} }
}
removeEntityFromLayer(entity, layerIndex) { visibleEntities(query) {
const layer = this.layers[layerIndex]; const entities = [];
if (!layer) { for (let i = 0; i < this.layers.length; i++) {
return; const layerVisibleEntities = this.layers[i].visibleEntities(query);
} for (let j = 0; j < layerVisibleEntities.length; j++) {
layer.removeEntity(entity); const layerVisibleEntity = layerVisibleEntities[j];
} entities.push(layerVisibleEntity);
}
removeLayer(layer) {
const index = this.layers.indexOf(layer);
if (-1 === index) {
return;
}
layer.off('entityAdded', this.onEntityAddedToLayers);
layer.off('entityRemoved', this.onEntityRemovedFromLayers);
this.layers.splice(index, 1);
this.emit('layerRemoved', layer);
}
tick(elapsed) {
for (let i = 0; i < this.layers.length; i++) {
this.layers[i].tick(elapsed);
}
}
toJSON() {
return this.layers.map((layer) => layer.toJSON());
}
toNetwork(informed) {
return this.layers.map((layer) => layer.toNetwork(informed));
}
visibleEntities(query) {
const entities = [];
for (let i = 0; i < this.layers.length; i++) {
const layerVisibleEntities = this.layers[i].visibleEntities(query);
for (let j = 0; j < layerVisibleEntities.length; j++) {
const layerVisibleEntity = layerVisibleEntities[j];
entities.push(layerVisibleEntity);
} }
return entities;
} }
return entities;
}
};
}; };

View File

@ -3,157 +3,125 @@ import {JsonResource} from '@avocado/resource';
import {Synchronized} from '@avocado/s13n'; import {Synchronized} from '@avocado/s13n';
import {compose, EventEmitter} from '@latus/core'; import {compose, EventEmitter} from '@latus/core';
const decorate = compose(
EventEmitter,
Synchronized,
Vector.Mixin('size', 'width', 'height', {
default: [0, 0],
}),
);
let s13nId = 1; let s13nId = 1;
export default (latus) => class Room extends decorate(JsonResource) { export default (latus) => {
const decorate = compose(
EventEmitter,
Vector.Mixin('size', 'width', 'height', {
default: [0, 0],
}),
Synchronized(latus),
);
return class Room extends decorate(JsonResource) {
constructor() { constructor() {
super(); super();
this._s13nId = s13nId++; this._s13nId = s13nId++;
const {Layers} = latus.get('%resources'); const {Layers} = latus.get('%resources');
this.setLayers(new Layers()); this.setLayers(new Layers());
}
acceptPacket(packet) {
// Layer updates.
if ('RoomUpdateLayers' === packet.constructor.type) {
const {layersPackets} = packet.data;
for (let i = 0; i < layersPackets.length; ++i) {
this.layers.acceptPacket(layersPackets[i]);
}
} }
}
addEntityToLayer(entity, layerIndex = 0) { addEntityToLayer(entity, layerIndex = 0) {
this.layers.addEntityToLayer(entity, layerIndex); this.layers.addEntityToLayer(entity, layerIndex);
}
cleanPackets() {
super.cleanPackets();
this.layers.cleanPackets();
}
destroy() {
super.destroy();
this.layers.destroy();
this.layers.off('entityAdded', this.onEntityAddedToRoom);
this.layers.off('entityRemoved', this.onEntityRemovedFromRoom);
}
get entities() {
return this.layers.entities;
}
findEntity(uuid) {
return this.layers.findEntity(uuid);
}
layer(index) {
return this.layers.layer(index);
}
async load(json = {}) {
await super.load(json);
const {layers, size} = json;
if (size) {
this.size = size;
} }
const {Layers} = latus.get('%resources');
this.setLayers(
layers
? await Layers.load(layers)
: new Layers(),
);
}
onEntityAddedToRoom(entity) { destroy() {
// eslint-disable-next-line no-param-reassign super.destroy();
entity.room = this; this.layers.destroy();
entity.emit('addedToRoom');
this.emit('entityAdded', entity);
}
onEntityRemovedFromRoom(entity) {
// eslint-disable-next-line no-param-reassign
entity.room = null;
entity.emit('removedFromRoom', this);
this.emit('entityRemoved', entity);
}
packets(informed) {
const payload = [];
// Layer updates.
const layersPackets = this.layers.packets(informed);
if (layersPackets.length > 0) {
payload.push([
'RoomUpdateLayers',
{
layersPackets,
},
]);
}
return payload;
}
// eslint-disable-next-line class-methods-use-this
packetsAreIdempotent() {
return false;
}
removeEntityFromLayer(entity, layerIndex) {
this.layers.removeEntityFromLayer(entity, layerIndex);
}
setLayers(layers) {
if (this.layers) {
const entities = Object.values(this.layers.entities);
for (let i = 0; i < entities.length; i++) {
this.onEntityRemovedFromRoom(entities[i]);
}
this.layers.off('entityAdded', this.onEntityAddedToRoom); this.layers.off('entityAdded', this.onEntityAddedToRoom);
this.layers.off('entityRemoved', this.onEntityRemovedFromRoom); this.layers.off('entityRemoved', this.onEntityRemovedFromRoom);
} }
this.layers = layers;
this.layers.on('entityRemoved', this.onEntityRemovedFromRoom, this); get entities() {
this.layers.on('entityAdded', this.onEntityAddedToRoom, this); return this.layers.entities;
const entities = Object.values(this.layers.entities);
for (let i = 0; i < entities.length; i++) {
this.onEntityAddedToRoom(entities[i]);
} }
}
s13nId() { findEntity(uuid) {
return this._s13nId; return this.layers.findEntity(uuid);
} }
tick(elapsed) { layer(index) {
this.layers.tick(elapsed); return this.layers.layer(index);
} }
toJSON() { async load(json = {}) {
return { await super.load(json);
layer: this.layers.toJSON(), const {layers, size} = json;
size: this.size, if (size) {
}; this.size = size;
} }
const {Layers} = latus.get('%resources');
this.setLayers(
layers
? await Layers.load(layers)
: new Layers(),
);
}
toNetwork(informed) { onEntityAddedToRoom(entity) {
return { // eslint-disable-next-line no-param-reassign
layers: this.layers.toNetwork(informed), entity.room = this;
size: this.size, entity.emit('addedToRoom');
}; this.emit('entityAdded', entity);
} }
visibleEntities(query) { onEntityRemovedFromRoom(entity) {
return this.layers.visibleEntities(query); // eslint-disable-next-line no-param-reassign
} entity.room = null;
entity.emit('removedFromRoom', this);
this.emit('entityRemoved', entity);
}
removeEntityFromLayer(entity, layerIndex) {
this.layers.removeEntityFromLayer(entity, layerIndex);
}
setLayers(layers) {
if (this.layers) {
this.stopSynchronizing(this.layers);
const entities = Object.values(this.layers.entities);
for (let i = 0; i < entities.length; i++) {
this.onEntityRemovedFromRoom(entities[i]);
}
this.layers.off('entityAdded', this.onEntityAddedToRoom);
this.layers.off('entityRemoved', this.onEntityRemovedFromRoom);
}
this.layers = layers;
this.layers.on('entityRemoved', this.onEntityRemovedFromRoom, this);
this.layers.on('entityAdded', this.onEntityAddedToRoom, this);
const entities = Object.values(this.layers.entities);
for (let i = 0; i < entities.length; i++) {
this.onEntityAddedToRoom(entities[i]);
}
this.startSynchronizing(this.layers);
}
get s13nId() {
return this._s13nId;
}
tick(elapsed) {
this.layers.tick(elapsed);
}
toJSON() {
return {
layer: this.layers.toJSON(),
size: this.size,
};
}
toNetwork(informed) {
return {
layers: this.layers.toNetwork(informed),
size: this.size,
};
}
visibleEntities(query) {
return this.layers.visibleEntities(query);
}
};
}; };

View File

@ -1,122 +1,125 @@
import {Rectangle, Vector} from '@avocado/math'; import {Rectangle, Vector} from '@avocado/math';
import {Class, compose, EventEmitter} from '@latus/core'; import {Class, compose, EventEmitter} from '@latus/core';
import {Synchronized} from '@avocado/s13n';
const decorate = compose( export default (latus) => {
EventEmitter, const decorate = compose(
Vector.Mixin('size', 'width', 'height', { EventEmitter,
default: [0, 0], Vector.Mixin('size', 'width', 'height', {
}), default: [0, 0],
); }),
Synchronized(latus),
);
return class Tiles extends decorate(Class) {
export default () => class Tiles extends decorate(Class) { constructor({data, size} = {}) {
super();
constructor({data, size} = {}) { this._packets = [];
super(); if (size) {
this._packets = []; super.size = size;
if (size) {
super.size = size;
}
this.data = data && data.length > 0 ? data.concat() : Array(Vector.area(this.size)).fill(0);
}
acceptPacket(packet) {
if ('TilesUpdate' === packet.constructor.type) {
this.setTileAt(packet.data.position, packet.data.tile);
}
}
cleanPackets() {
this._packets = [];
}
forEachTile(fn) {
let [x, y] = [0, 0];
const [width, height] = this.size;
let i = 0;
for (let k = 0; k < height; ++k) {
for (let j = 0; j < width; ++j) {
fn(this.data[i], x, y, i);
++i;
++x;
} }
x = 0; this.data = data && data.length > 0 ? data.concat() : Array(Vector.area(this.size)).fill(0);
++y;
} }
}
indexAt(position) { acceptPacket(packet) {
return this.width * position[1] + position[0]; if ('TilesUpdate' === packet.constructor.type) {
} this.setTileAt(packet.data.position, packet.data.tile);
packets() {
return this._packets;
}
get rectangle() {
return Rectangle.compose([0, 0], this.size);
}
setTileAt(position, tile) {
const oldTile = this.tileAt(position);
if (oldTile === tile) {
return;
}
const index = this.indexAt(position);
if (index < 0 || index >= this.data.length) {
return;
}
this.data[index] = tile;
this._packets.push([
'TilesUpdate',
{
position,
tile,
},
]);
this.emit('dataChanged');
}
slice(rectangle) {
const tilesRectangle = this.rectangle;
// Get intersection.
if (!Rectangle.intersects(rectangle, tilesRectangle)) {
return [];
}
// eslint-disable-next-line prefer-const
let [x, y, sliceWidth, sliceHeight] = Rectangle.intersection(rectangle, tilesRectangle);
// No muls in the loop.
const ox = x;
let sliceRow = 0;
const dataWidth = this.width;
let dataRow = y * dataWidth;
// Copy slice.
const slice = new Array(sliceWidth * sliceHeight);
for (let j = 0; j < sliceHeight; ++j) {
for (let i = 0; i < sliceWidth; ++i) {
slice[sliceRow + (x - ox)] = this.data[dataRow + x];
x++;
} }
sliceRow += sliceWidth;
dataRow += dataWidth;
x -= sliceWidth;
} }
return slice;
}
tileAt(position) { cleanPackets() {
const index = this.indexAt(position); this._packets = [];
return index < 0 || index >= this.data.length ? undefined : this.data[index]; }
}
toNetwork() { forEachTile(fn) {
return this.toJSON(); let [x, y] = [0, 0];
} const [width, height] = this.size;
let i = 0;
for (let k = 0; k < height; ++k) {
for (let j = 0; j < width; ++j) {
fn(this.data[i], x, y, i);
++i;
++x;
}
x = 0;
++y;
}
}
toJSON() { indexAt(position) {
return { return this.width * position[1] + position[0];
size: this.size, }
data: this.data,
};
}
packetsFor() {
return this._packets;
}
get rectangle() {
return Rectangle.compose([0, 0], this.size);
}
setTileAt(position, tile) {
const oldTile = this.tileAt(position);
if (oldTile === tile) {
return;
}
const index = this.indexAt(position);
if (index < 0 || index >= this.data.length) {
return;
}
this.data[index] = tile;
this._packets.push([
'TilesUpdate',
{
position,
tile,
},
]);
this.emit('dataChanged');
}
slice(rectangle) {
const tilesRectangle = this.rectangle;
// Get intersection.
if (!Rectangle.intersects(rectangle, tilesRectangle)) {
return [];
}
// eslint-disable-next-line prefer-const
let [x, y, sliceWidth, sliceHeight] = Rectangle.intersection(rectangle, tilesRectangle);
// No muls in the loop.
const ox = x;
let sliceRow = 0;
const dataWidth = this.width;
let dataRow = y * dataWidth;
// Copy slice.
const slice = new Array(sliceWidth * sliceHeight);
for (let j = 0; j < sliceHeight; ++j) {
for (let i = 0; i < sliceWidth; ++i) {
slice[sliceRow + (x - ox)] = this.data[dataRow + x];
x++;
}
sliceRow += sliceWidth;
dataRow += dataWidth;
x -= sliceWidth;
}
return slice;
}
tileAt(position) {
const index = this.indexAt(position);
return index < 0 || index >= this.data.length ? undefined : this.data[index];
}
toNetwork() {
return this.toJSON();
}
toJSON() {
return {
size: this.size,
data: this.data,
};
}
};
}; };

View File

@ -28,6 +28,7 @@ export default () => class Followed extends Trait {
} }
destroy() { destroy() {
super.destroy();
const {room} = this.entity; const {room} = this.entity;
if (room) { if (room) {
room.off('sizeChanged', this.onRoomSizeChanged); room.off('sizeChanged', this.onRoomSizeChanged);

View File

@ -23,6 +23,7 @@ export default () => class Layered extends Trait {
} }
destroy() { destroy() {
super.destroy();
this.detachFromLayer(this.entity.layer); this.detachFromLayer(this.entity.layer);
} }

View File

@ -26,6 +26,7 @@ export default () => class TileEntity extends decorate(Trait) {
} }
destroy() { destroy() {
super.destroy();
this.removeTileEntity(this.entity.tileIndex); this.removeTileEntity(this.entity.tileIndex);
} }

View File

@ -1,12 +1,6 @@
import {JsonResource} from '@avocado/resource'; import {JsonResource} from '@avocado/resource';
import {Synchronized} from '@avocado/s13n';
import {compose} from '@latus/core';
const decorate = compose( export default class Trait extends JsonResource {
Synchronized,
);
export default class Trait extends decorate(JsonResource) {
#markedAsDirty = true; #markedAsDirty = true;
@ -69,8 +63,9 @@ export default class Trait extends decorate(JsonResource) {
return []; return [];
} }
// eslint-disable-next-line class-methods-use-this destroy() {
destroy() {} this.#memoizedListeners = undefined;
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
hooks() { hooks() {
@ -120,14 +115,18 @@ export default class Trait extends decorate(JsonResource) {
return {}; return {};
} }
// eslint-disable-next-line class-methods-use-this, no-unused-vars // eslint-disable-next-line class-methods-use-this
packets(informed) { packetsFor() {
return []; return [];
} }
static get resourceId() {
return this.id;
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
packetsAreIdempotent() { get s13nId() {
return false; return 0;
} }
stateDifferences() { stateDifferences() {
@ -145,6 +144,10 @@ export default class Trait extends decorate(JsonResource) {
return differences; return differences;
} }
toNetwork() {
return this.toJSON();
}
toJSON() { toJSON() {
return { return {
params: this.params, params: this.params,