avocado-old/packages/entity/index.js

380 lines
10 KiB
JavaScript
Raw Normal View History

2019-03-17 23:45:48 -05:00
import * as I from 'immutable';
2019-04-16 16:40:20 -05:00
import D from 'debug';
import without from 'lodash.without';
2019-03-17 23:45:48 -05:00
2019-05-02 21:26:32 -05:00
import {compose, EventEmitter, fastApply} from '@avocado/core';
2019-03-17 23:45:48 -05:00
import {Resource} from '@avocado/resource';
2019-04-16 17:52:56 -05:00
import {Synchronized} from '@avocado/state';
2019-04-16 16:40:20 -05:00
2019-04-25 23:11:05 -05:00
import {hasTrait, lookupTrait} from './trait/registry';
2019-04-16 16:40:20 -05:00
2019-04-20 14:13:04 -05:00
const debug = D('@avocado:entity:traits');
2019-04-16 16:40:20 -05:00
const blacklistedAccessorKeys = [
'state',
];
// This really seems like a whole lot of complicated nonsense, but it's an
// unfortunate consequence of V8 (and maybe others) not optimizing mutable
// accessors in fast hidden classes.
const traitAccessorForPropertyMap = {};
function traitAccessorForProperty(type, property) {
if (!(type in traitAccessorForPropertyMap)) {
traitAccessorForPropertyMap[type] = {};
}
if (!(property in traitAccessorForPropertyMap[type])) {
traitAccessorForPropertyMap[type][property] = {
get: function() {
return this._traits[type][property];
},
set: function(value) {
this._traits[type][property] = value;
}
};
}
return traitAccessorForPropertyMap[type][property];
}
function defineTraitAccessors(from, to, instance) {
const type = instance.constructor.type();
2019-04-16 16:40:20 -05:00
do {
Object.getOwnPropertyNames(from).forEach((accessorKey) => {
if (-1 !== blacklistedAccessorKeys.indexOf(accessorKey)) {
return;
2019-04-16 16:40:20 -05:00
}
let descriptor = Object.getOwnPropertyDescriptor(from, accessorKey);
// Make sure it's actually an accessor.
2019-04-16 16:40:20 -05:00
if (!descriptor.get && !descriptor.set) {
return;
}
const accessor = traitAccessorForProperty(type, accessorKey);
2019-04-16 16:40:20 -05:00
if (descriptor.get) {
descriptor.get = accessor.get;
2019-04-16 16:40:20 -05:00
}
if (descriptor.set) {
descriptor.set = accessor.set;
2019-04-16 16:40:20 -05:00
}
Object.defineProperty(to, accessorKey, descriptor);
2019-04-16 16:40:20 -05:00
});
} while (Object.prototype !== (from = Object.getPrototypeOf(from)));
2019-04-12 14:11:26 -05:00
}
function enumerateTraitAccessorKeys(prototype) {
2019-04-16 16:40:20 -05:00
const keys = [];
do {
Object.getOwnPropertyNames(prototype).forEach((accessorKey) => {
if (-1 !== blacklistedAccessorKeys.indexOf(accessorKey)) {
return;
}
let descriptor = Object.getOwnPropertyDescriptor(prototype, accessorKey);
// Make sure it's actually an accessor.
2019-04-18 20:44:33 -05:00
if (!descriptor.get && !descriptor.set) {
return;
}
keys.push(accessorKey);
2019-04-16 16:40:20 -05:00
});
} while (Object.prototype !== (prototype = Object.getPrototypeOf(prototype)));
return keys;
2019-03-17 23:45:48 -05:00
}
const decorate = compose(
EventEmitter,
2019-04-16 17:52:56 -05:00
Synchronized,
2019-03-17 23:45:48 -05:00
);
2019-04-16 16:40:20 -05:00
export class Entity extends decorate(Resource) {
2019-03-17 23:45:48 -05:00
2019-05-04 11:39:23 -05:00
constructor(json) {
2019-03-17 23:45:48 -05:00
super();
2019-04-16 16:40:20 -05:00
this._hooks = {};
2019-04-19 00:58:36 -05:00
this.isDirty = true;
2019-04-16 16:40:20 -05:00
this._traits = {};
this._traitsFlat = [];
2019-05-05 20:04:59 -05:00
this._traitTickers = [];
this._traitRenderTickers = [];
this._traitsAcceptingPackets = [];
2019-04-16 16:40:20 -05:00
this.once('destroyed', () => {
this.removeAllTraits();
});
this.initializeSynchronizedChildren();
2019-05-04 11:39:23 -05:00
if ('undefined' !== typeof json) {
this.fromJSON(json);
}
2019-05-05 17:11:46 -05:00
// Bind to prevent lookup overhead.
this.tick = this.tick.bind(this);
2019-03-17 23:45:48 -05:00
}
2019-04-28 22:33:41 -05:00
acceptPacket(packet) {
for (let i = 0; i < this._traitsAcceptingPackets.length; i++) {
const instance = this._traitsAcceptingPackets[i];
2019-04-28 22:33:41 -05:00
instance.acceptPacket(packet);
}
}
2019-04-16 16:40:20 -05:00
addTrait(type, json = {}) {
if (this.is(type)) {
debug(`Tried to add trait "${type}" when it already exists!`);
return;
}
if (!hasTrait(type)) {
debug(`Tried to add trait "${type}" which isn't registered!`);
return;
}
const Trait = lookupTrait(type);
// Ensure dependencies.
const dependencies = Trait.dependencies();
const allTypes = this.allTraitTypes();
const lacking = without(dependencies, ...allTypes);
if (lacking.length > 0) {
debug(
`Tried to add trait "${type}" but lack one or more dependents: "${
lacking.join('", "')
}"!`
);
return;
}
// Instantiate.
const {params, state} = json;
const instance = new Trait(this, params, state);
// Proxy properties.
defineTraitAccessors(Trait.prototype, this, instance);
2019-04-16 16:40:20 -05:00
// Attach listeners.
2019-04-30 17:11:41 -05:00
const listeners = instance.memoizedListeners();
2019-04-16 16:40:20 -05:00
for (const eventName in listeners) {
this.on(eventName, listeners[eventName]);
}
// Proxy methods.
const methods = instance.methods();
for (const key in methods) {
this[key] = methods[key];
}
// Register hook listeners.
const hooks = instance.hooks();
for (const key in hooks) {
this._hooks[key] = this._hooks[key] || [];
this._hooks[key].push({
fn: hooks[key],
type: Trait.type(),
});
}
// Add state.
this.state = this._setInstanceState(this.state, type, instance);
// Track trait.
this._traits[type] = instance;
this._traitsFlat.push(instance);
if ('tick' in instance) {
2019-05-05 20:04:59 -05:00
this._traitTickers.push(instance.tick);
}
if ('renderTick' in instance) {
2019-05-05 20:04:59 -05:00
this._traitRenderTickers.push(instance.renderTick);
}
if ('acceptPacket' in instance) {
this._traitsAcceptingPackets.push(instance);
}
2019-04-16 16:40:20 -05:00
this.emit('traitAdded', type, instance);
2019-03-17 23:45:48 -05:00
}
addTraits(traits) {
for (const type in traits) {
2019-04-16 16:40:20 -05:00
this.addTrait(type, traits[type]);
2019-03-17 23:45:48 -05:00
}
}
allTraitInstances() {
2019-04-16 16:40:20 -05:00
return this._traits;
2019-03-17 23:45:48 -05:00
}
allTraitTypes() {
2019-04-16 16:40:20 -05:00
return Object.keys(this._traits);
2019-03-17 23:45:48 -05:00
}
fromJSON(json) {
super.fromJSON(json);
2019-04-16 16:40:20 -05:00
this.addTraits(json.traits);
2019-03-17 23:45:48 -05:00
return this;
}
hydrate() {
2019-04-16 16:40:20 -05:00
const promises = [];
for (let i = 0; i < this._traitsFlat.length; i++) {
const instance = this._traitsFlat[i];
2019-04-16 16:40:20 -05:00
promises.push(instance.hydrate());
}
return Promise.all(promises);
2019-03-17 23:45:48 -05:00
}
invokeHook(hook, ...args) {
2019-04-16 16:40:20 -05:00
const results = {};
if (!(hook in this._hooks)) {
return results;
}
for (const {fn, type} of this._hooks[hook]) {
2019-05-02 21:26:32 -05:00
results[type] = fastApply(null, fn, args);
2019-04-16 16:40:20 -05:00
}
return results;
2019-03-17 23:45:48 -05:00
}
invokeHookFlat(hook, ...args) {
2019-04-16 16:40:20 -05:00
const invokeResults = this.invokeHook(hook, ...args);
const results = [];
for (const type in invokeResults) {
results.push(invokeResults[type]);
}
return results;
2019-03-17 23:45:48 -05:00
}
2019-04-16 16:40:20 -05:00
is(type) {
return type in this._traits;
}
patchStateStep(type, step) {
let instance = this._traits[type];
// New trait requested?
if (!this._traits[type]) {
// Doesn't exist?
if (!hasTrait(type)) {
return;
}
2019-05-04 14:06:47 -05:00
if ('params' in step.value) {
step.value.params = step.value.params.toJS();
}
2019-04-16 16:40:20 -05:00
this.addTrait(type, step.value);
instance = this._traits[type];
}
else {
// Accept state.
instance.patchState([step]);
}
this.state = this._setInstanceState(this.state, type, instance);
2019-04-05 15:16:55 -05:00
}
2019-04-23 16:56:47 -05:00
renderTick(elapsed) {
2019-05-05 20:04:59 -05:00
for (let i = 0; i < this._traitRenderTickers.length; i++) {
this._traitRenderTickers[i](elapsed);
2019-04-23 16:56:47 -05:00
}
}
2019-03-17 23:45:48 -05:00
removeAllTraits() {
2019-04-16 16:40:20 -05:00
const types = this.allTraitTypes();
2019-03-17 23:45:48 -05:00
this.removeTraits(types);
}
removeTrait(type) {
2019-04-16 16:40:20 -05:00
if (!this.is(type)) {
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
return;
}
// Destroy instance.
const instance = this._traits[type];
instance.destroy();
// Remove methods, hooks, and properties.
const methods = instance.methods();
for (const key in methods) {
delete this[key];
}
const hooks = instance.hooks();
for (const key in hooks) {
const implementation = this._hooks[key].find(({type: hookType}) => {
return hookType === type;
});
this._hooks[key].splice(this._hooks[key].indexOf(implementation), 1);
2019-04-16 16:40:20 -05:00
}
const Trait = lookupTrait(type);
const properties = enumerateTraitAccessorKeys(Trait.prototype);
2019-04-18 20:44:33 -05:00
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
delete this[property];
2019-04-16 16:40:20 -05:00
}
// Remove all event listeners.
2019-04-30 17:11:41 -05:00
const listeners = instance.memoizedListeners();
2019-04-16 16:40:20 -05:00
for (const eventName in listeners) {
this.off(eventName, listeners[eventName]);
}
2019-05-03 01:22:26 -05:00
instance._memoizedListeners = {};
2019-04-16 16:40:20 -05:00
// Remove state.
this.state = this.state.delete(type);
// Remove instance.
delete this._traits[type];
this._traitsFlat.splice(this._traitsFlat.indexOf(instance), 1);
2019-05-05 20:04:59 -05:00
if ('tick' in instance) {
this._traitTickers.splice(
this._traitTickers.indexOf(instance.tick),
1
);
}
2019-05-05 20:04:59 -05:00
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);
}
2019-04-30 17:11:41 -05:00
// Unloop.
instance.entity = undefined;
2019-03-17 23:45:48 -05:00
}
removeTraits(types) {
types.forEach((type) => this.removeTrait(type));
}
2019-04-16 16:40:20 -05:00
_setInstanceState(state, type, instance) {
return state.setIn(
[type, 'state'], instance.state
).setIn(
[type, 'params'], instance.params
);
2019-03-17 23:45:48 -05:00
}
tick(elapsed) {
2019-05-05 20:04:59 -05:00
for (let i = 0; i < this._traitTickers.length; i++) {
this._traitTickers[i](elapsed);
}
2019-04-23 15:25:03 -05:00
if (AVOCADO_SERVER) {
this.tickMutateState();
2019-04-23 15:25:03 -05:00
}
}
tickMutateState() {
this.state = this.state.withMutations((state) => {
for (let i = 0; i < this._traitsFlat.length; i++) {
const instance = this._traitsFlat[i];
if (!instance.isDirty) {
continue;
}
instance.isDirty = false;
if (this.is('listed')) {
this.list && this.list.markEntityDirty(this);
}
this._setInstanceState(state, instance.constructor.type(), instance);
}
});
}
2019-03-17 23:45:48 -05:00
toJSON() {
2019-04-16 16:40:20 -05:00
const json = {};
for (const type in this._traits) {
json[type] = this._traits[type].toJSON();
}
2019-03-17 23:45:48 -05:00
return {
...super.toJSON(),
2019-04-16 16:40:20 -05:00
traits: json,
};
2019-03-17 23:45:48 -05:00
}
}
2019-04-28 22:33:41 -05:00
export {EntityPacket} from './entity.packet';
2019-04-13 20:53:02 -05:00
export {EntityList, EntityListView} from './list';
2019-03-17 23:45:48 -05:00
2019-04-28 22:33:41 -05:00
export {EntityPacketSynchronizer} from './packet-synchronizer';
2019-03-17 23:45:48 -05:00
export {
hasTrait,
lookupTrait,
registerTrait,
2019-04-25 23:11:05 -05:00
} from './trait/registry';
2019-03-17 23:45:48 -05:00
2019-03-23 23:24:18 -05:00
export {StateProperty, Trait} from './trait';