Buckets:
ktongue/docker_container / simsite /frontend /node_modules /three /examples /jsm /libs /ecsy.module.js
| /** | |
| * Return the name of a component | |
| * @param {Component} Component | |
| * @private | |
| */ | |
| /** | |
| * Get a key from a list of components | |
| * @param {Array(Component)} Components Array of components to generate the key | |
| * @private | |
| */ | |
| function queryKey(Components) { | |
| var ids = []; | |
| for (var n = 0; n < Components.length; n++) { | |
| var T = Components[n]; | |
| if (!componentRegistered(T)) { | |
| throw new Error(`Tried to create a query with an unregistered component`); | |
| } | |
| if (typeof T === "object") { | |
| var operator = T.operator === "not" ? "!" : T.operator; | |
| ids.push(operator + T.Component._typeId); | |
| } else { | |
| ids.push(T._typeId); | |
| } | |
| } | |
| return ids.sort().join("-"); | |
| } | |
| // Detector for browser's "window" | |
| const hasWindow = typeof window !== "undefined"; | |
| // performance.now() "polyfill" | |
| const now = | |
| hasWindow && typeof window.performance !== "undefined" | |
| ? performance.now.bind(performance) | |
| : Date.now.bind(Date); | |
| function componentRegistered(T) { | |
| return ( | |
| (typeof T === "object" && T.Component._typeId !== undefined) || | |
| (T.isComponent && T._typeId !== undefined) | |
| ); | |
| } | |
| class SystemManager { | |
| constructor(world) { | |
| this._systems = []; | |
| this._executeSystems = []; // Systems that have `execute` method | |
| this.world = world; | |
| this.lastExecutedSystem = null; | |
| } | |
| registerSystem(SystemClass, attributes) { | |
| if (!SystemClass.isSystem) { | |
| throw new Error( | |
| `System '${SystemClass.name}' does not extend 'System' class` | |
| ); | |
| } | |
| if (this.getSystem(SystemClass) !== undefined) { | |
| console.warn(`System '${SystemClass.getName()}' already registered.`); | |
| return this; | |
| } | |
| var system = new SystemClass(this.world, attributes); | |
| if (system.init) system.init(attributes); | |
| system.order = this._systems.length; | |
| this._systems.push(system); | |
| if (system.execute) { | |
| this._executeSystems.push(system); | |
| this.sortSystems(); | |
| } | |
| return this; | |
| } | |
| unregisterSystem(SystemClass) { | |
| let system = this.getSystem(SystemClass); | |
| if (system === undefined) { | |
| console.warn( | |
| `Can unregister system '${SystemClass.getName()}'. It doesn't exist.` | |
| ); | |
| return this; | |
| } | |
| this._systems.splice(this._systems.indexOf(system), 1); | |
| if (system.execute) { | |
| this._executeSystems.splice(this._executeSystems.indexOf(system), 1); | |
| } | |
| // @todo Add system.unregister() call to free resources | |
| return this; | |
| } | |
| sortSystems() { | |
| this._executeSystems.sort((a, b) => { | |
| return a.priority - b.priority || a.order - b.order; | |
| }); | |
| } | |
| getSystem(SystemClass) { | |
| return this._systems.find((s) => s instanceof SystemClass); | |
| } | |
| getSystems() { | |
| return this._systems; | |
| } | |
| removeSystem(SystemClass) { | |
| var index = this._systems.indexOf(SystemClass); | |
| if (!~index) return; | |
| this._systems.splice(index, 1); | |
| } | |
| executeSystem(system, delta, time) { | |
| if (system.initialized) { | |
| if (system.canExecute()) { | |
| let startTime = now(); | |
| system.execute(delta, time); | |
| system.executeTime = now() - startTime; | |
| this.lastExecutedSystem = system; | |
| system.clearEvents(); | |
| } | |
| } | |
| } | |
| stop() { | |
| this._executeSystems.forEach((system) => system.stop()); | |
| } | |
| execute(delta, time, forcePlay) { | |
| this._executeSystems.forEach( | |
| (system) => | |
| (forcePlay || system.enabled) && this.executeSystem(system, delta, time) | |
| ); | |
| } | |
| stats() { | |
| var stats = { | |
| numSystems: this._systems.length, | |
| systems: {}, | |
| }; | |
| for (var i = 0; i < this._systems.length; i++) { | |
| var system = this._systems[i]; | |
| var systemStats = (stats.systems[system.getName()] = { | |
| queries: {}, | |
| executeTime: system.executeTime, | |
| }); | |
| for (var name in system.ctx) { | |
| systemStats.queries[name] = system.ctx[name].stats(); | |
| } | |
| } | |
| return stats; | |
| } | |
| } | |
| class ObjectPool { | |
| // @todo Add initial size | |
| constructor(T, initialSize) { | |
| this.freeList = []; | |
| this.count = 0; | |
| this.T = T; | |
| this.isObjectPool = true; | |
| if (typeof initialSize !== "undefined") { | |
| this.expand(initialSize); | |
| } | |
| } | |
| acquire() { | |
| // Grow the list by 20%ish if we're out | |
| if (this.freeList.length <= 0) { | |
| this.expand(Math.round(this.count * 0.2) + 1); | |
| } | |
| var item = this.freeList.pop(); | |
| return item; | |
| } | |
| release(item) { | |
| item.reset(); | |
| this.freeList.push(item); | |
| } | |
| expand(count) { | |
| for (var n = 0; n < count; n++) { | |
| var clone = new this.T(); | |
| clone._pool = this; | |
| this.freeList.push(clone); | |
| } | |
| this.count += count; | |
| } | |
| totalSize() { | |
| return this.count; | |
| } | |
| totalFree() { | |
| return this.freeList.length; | |
| } | |
| totalUsed() { | |
| return this.count - this.freeList.length; | |
| } | |
| } | |
| /** | |
| * @private | |
| * @class EventDispatcher | |
| */ | |
| class EventDispatcher { | |
| constructor() { | |
| this._listeners = {}; | |
| this.stats = { | |
| fired: 0, | |
| handled: 0, | |
| }; | |
| } | |
| /** | |
| * Add an event listener | |
| * @param {String} eventName Name of the event to listen | |
| * @param {Function} listener Callback to trigger when the event is fired | |
| */ | |
| addEventListener(eventName, listener) { | |
| let listeners = this._listeners; | |
| if (listeners[eventName] === undefined) { | |
| listeners[eventName] = []; | |
| } | |
| if (listeners[eventName].indexOf(listener) === -1) { | |
| listeners[eventName].push(listener); | |
| } | |
| } | |
| /** | |
| * Check if an event listener is already added to the list of listeners | |
| * @param {String} eventName Name of the event to check | |
| * @param {Function} listener Callback for the specified event | |
| */ | |
| hasEventListener(eventName, listener) { | |
| return ( | |
| this._listeners[eventName] !== undefined && | |
| this._listeners[eventName].indexOf(listener) !== -1 | |
| ); | |
| } | |
| /** | |
| * Remove an event listener | |
| * @param {String} eventName Name of the event to remove | |
| * @param {Function} listener Callback for the specified event | |
| */ | |
| removeEventListener(eventName, listener) { | |
| var listenerArray = this._listeners[eventName]; | |
| if (listenerArray !== undefined) { | |
| var index = listenerArray.indexOf(listener); | |
| if (index !== -1) { | |
| listenerArray.splice(index, 1); | |
| } | |
| } | |
| } | |
| /** | |
| * Dispatch an event | |
| * @param {String} eventName Name of the event to dispatch | |
| * @param {Entity} entity (Optional) Entity to emit | |
| * @param {Component} component | |
| */ | |
| dispatchEvent(eventName, entity, component) { | |
| this.stats.fired++; | |
| var listenerArray = this._listeners[eventName]; | |
| if (listenerArray !== undefined) { | |
| var array = listenerArray.slice(0); | |
| for (var i = 0; i < array.length; i++) { | |
| array[i].call(this, entity, component); | |
| } | |
| } | |
| } | |
| /** | |
| * Reset stats counters | |
| */ | |
| resetCounters() { | |
| this.stats.fired = this.stats.handled = 0; | |
| } | |
| } | |
| class Query { | |
| /** | |
| * @param {Array(Component)} Components List of types of components to query | |
| */ | |
| constructor(Components, manager) { | |
| this.Components = []; | |
| this.NotComponents = []; | |
| Components.forEach((component) => { | |
| if (typeof component === "object") { | |
| this.NotComponents.push(component.Component); | |
| } else { | |
| this.Components.push(component); | |
| } | |
| }); | |
| if (this.Components.length === 0) { | |
| throw new Error("Can't create a query without components"); | |
| } | |
| this.entities = []; | |
| this.eventDispatcher = new EventDispatcher(); | |
| // This query is being used by a reactive system | |
| this.reactive = false; | |
| this.key = queryKey(Components); | |
| // Fill the query with the existing entities | |
| for (var i = 0; i < manager._entities.length; i++) { | |
| var entity = manager._entities[i]; | |
| if (this.match(entity)) { | |
| // @todo ??? this.addEntity(entity); => preventing the event to be generated | |
| entity.queries.push(this); | |
| this.entities.push(entity); | |
| } | |
| } | |
| } | |
| /** | |
| * Add entity to this query | |
| * @param {Entity} entity | |
| */ | |
| addEntity(entity) { | |
| entity.queries.push(this); | |
| this.entities.push(entity); | |
| this.eventDispatcher.dispatchEvent(Query.prototype.ENTITY_ADDED, entity); | |
| } | |
| /** | |
| * Remove entity from this query | |
| * @param {Entity} entity | |
| */ | |
| removeEntity(entity) { | |
| let index = this.entities.indexOf(entity); | |
| if (~index) { | |
| this.entities.splice(index, 1); | |
| index = entity.queries.indexOf(this); | |
| entity.queries.splice(index, 1); | |
| this.eventDispatcher.dispatchEvent( | |
| Query.prototype.ENTITY_REMOVED, | |
| entity | |
| ); | |
| } | |
| } | |
| match(entity) { | |
| return ( | |
| entity.hasAllComponents(this.Components) && | |
| !entity.hasAnyComponents(this.NotComponents) | |
| ); | |
| } | |
| toJSON() { | |
| return { | |
| key: this.key, | |
| reactive: this.reactive, | |
| components: { | |
| included: this.Components.map((C) => C.name), | |
| not: this.NotComponents.map((C) => C.name), | |
| }, | |
| numEntities: this.entities.length, | |
| }; | |
| } | |
| /** | |
| * Return stats for this query | |
| */ | |
| stats() { | |
| return { | |
| numComponents: this.Components.length, | |
| numEntities: this.entities.length, | |
| }; | |
| } | |
| } | |
| Query.prototype.ENTITY_ADDED = "Query#ENTITY_ADDED"; | |
| Query.prototype.ENTITY_REMOVED = "Query#ENTITY_REMOVED"; | |
| Query.prototype.COMPONENT_CHANGED = "Query#COMPONENT_CHANGED"; | |
| /** | |
| * @private | |
| * @class QueryManager | |
| */ | |
| class QueryManager { | |
| constructor(world) { | |
| this._world = world; | |
| // Queries indexed by a unique identifier for the components it has | |
| this._queries = {}; | |
| } | |
| onEntityRemoved(entity) { | |
| for (var queryName in this._queries) { | |
| var query = this._queries[queryName]; | |
| if (entity.queries.indexOf(query) !== -1) { | |
| query.removeEntity(entity); | |
| } | |
| } | |
| } | |
| /** | |
| * Callback when a component is added to an entity | |
| * @param {Entity} entity Entity that just got the new component | |
| * @param {Component} Component Component added to the entity | |
| */ | |
| onEntityComponentAdded(entity, Component) { | |
| // @todo Use bitmask for checking components? | |
| // Check each indexed query to see if we need to add this entity to the list | |
| for (var queryName in this._queries) { | |
| var query = this._queries[queryName]; | |
| if ( | |
| !!~query.NotComponents.indexOf(Component) && | |
| ~query.entities.indexOf(entity) | |
| ) { | |
| query.removeEntity(entity); | |
| continue; | |
| } | |
| // Add the entity only if: | |
| // Component is in the query | |
| // and Entity has ALL the components of the query | |
| // and Entity is not already in the query | |
| if ( | |
| !~query.Components.indexOf(Component) || | |
| !query.match(entity) || | |
| ~query.entities.indexOf(entity) | |
| ) | |
| continue; | |
| query.addEntity(entity); | |
| } | |
| } | |
| /** | |
| * Callback when a component is removed from an entity | |
| * @param {Entity} entity Entity to remove the component from | |
| * @param {Component} Component Component to remove from the entity | |
| */ | |
| onEntityComponentRemoved(entity, Component) { | |
| for (var queryName in this._queries) { | |
| var query = this._queries[queryName]; | |
| if ( | |
| !!~query.NotComponents.indexOf(Component) && | |
| !~query.entities.indexOf(entity) && | |
| query.match(entity) | |
| ) { | |
| query.addEntity(entity); | |
| continue; | |
| } | |
| if ( | |
| !!~query.Components.indexOf(Component) && | |
| !!~query.entities.indexOf(entity) && | |
| !query.match(entity) | |
| ) { | |
| query.removeEntity(entity); | |
| continue; | |
| } | |
| } | |
| } | |
| /** | |
| * Get a query for the specified components | |
| * @param {Component} Components Components that the query should have | |
| */ | |
| getQuery(Components) { | |
| var key = queryKey(Components); | |
| var query = this._queries[key]; | |
| if (!query) { | |
| this._queries[key] = query = new Query(Components, this._world); | |
| } | |
| return query; | |
| } | |
| /** | |
| * Return some stats from this class | |
| */ | |
| stats() { | |
| var stats = {}; | |
| for (var queryName in this._queries) { | |
| stats[queryName] = this._queries[queryName].stats(); | |
| } | |
| return stats; | |
| } | |
| } | |
| class Component { | |
| constructor(props) { | |
| if (props !== false) { | |
| const schema = this.constructor.schema; | |
| for (const key in schema) { | |
| if (props && props.hasOwnProperty(key)) { | |
| this[key] = props[key]; | |
| } else { | |
| const schemaProp = schema[key]; | |
| if (schemaProp.hasOwnProperty("default")) { | |
| this[key] = schemaProp.type.clone(schemaProp.default); | |
| } else { | |
| const type = schemaProp.type; | |
| this[key] = type.clone(type.default); | |
| } | |
| } | |
| } | |
| if ( props !== undefined) { | |
| this.checkUndefinedAttributes(props); | |
| } | |
| } | |
| this._pool = null; | |
| } | |
| copy(source) { | |
| const schema = this.constructor.schema; | |
| for (const key in schema) { | |
| const prop = schema[key]; | |
| if (source.hasOwnProperty(key)) { | |
| this[key] = prop.type.copy(source[key], this[key]); | |
| } | |
| } | |
| // @DEBUG | |
| { | |
| this.checkUndefinedAttributes(source); | |
| } | |
| return this; | |
| } | |
| clone() { | |
| return new this.constructor().copy(this); | |
| } | |
| reset() { | |
| const schema = this.constructor.schema; | |
| for (const key in schema) { | |
| const schemaProp = schema[key]; | |
| if (schemaProp.hasOwnProperty("default")) { | |
| this[key] = schemaProp.type.copy(schemaProp.default, this[key]); | |
| } else { | |
| const type = schemaProp.type; | |
| this[key] = type.copy(type.default, this[key]); | |
| } | |
| } | |
| } | |
| dispose() { | |
| if (this._pool) { | |
| this._pool.release(this); | |
| } | |
| } | |
| getName() { | |
| return this.constructor.getName(); | |
| } | |
| checkUndefinedAttributes(src) { | |
| const schema = this.constructor.schema; | |
| // Check that the attributes defined in source are also defined in the schema | |
| Object.keys(src).forEach((srcKey) => { | |
| if (!schema.hasOwnProperty(srcKey)) { | |
| console.warn( | |
| `Trying to set attribute '${srcKey}' not defined in the '${this.constructor.name}' schema. Please fix the schema, the attribute value won't be set` | |
| ); | |
| } | |
| }); | |
| } | |
| } | |
| Component.schema = {}; | |
| Component.isComponent = true; | |
| Component.getName = function () { | |
| return this.displayName || this.name; | |
| }; | |
| class SystemStateComponent extends Component {} | |
| SystemStateComponent.isSystemStateComponent = true; | |
| class EntityPool extends ObjectPool { | |
| constructor(entityManager, entityClass, initialSize) { | |
| super(entityClass, undefined); | |
| this.entityManager = entityManager; | |
| if (typeof initialSize !== "undefined") { | |
| this.expand(initialSize); | |
| } | |
| } | |
| expand(count) { | |
| for (var n = 0; n < count; n++) { | |
| var clone = new this.T(this.entityManager); | |
| clone._pool = this; | |
| this.freeList.push(clone); | |
| } | |
| this.count += count; | |
| } | |
| } | |
| /** | |
| * @private | |
| * @class EntityManager | |
| */ | |
| class EntityManager { | |
| constructor(world) { | |
| this.world = world; | |
| this.componentsManager = world.componentsManager; | |
| // All the entities in this instance | |
| this._entities = []; | |
| this._nextEntityId = 0; | |
| this._entitiesByNames = {}; | |
| this._queryManager = new QueryManager(this); | |
| this.eventDispatcher = new EventDispatcher(); | |
| this._entityPool = new EntityPool( | |
| this, | |
| this.world.options.entityClass, | |
| this.world.options.entityPoolSize | |
| ); | |
| // Deferred deletion | |
| this.entitiesWithComponentsToRemove = []; | |
| this.entitiesToRemove = []; | |
| this.deferredRemovalEnabled = true; | |
| } | |
| getEntityByName(name) { | |
| return this._entitiesByNames[name]; | |
| } | |
| /** | |
| * Create a new entity | |
| */ | |
| createEntity(name) { | |
| var entity = this._entityPool.acquire(); | |
| entity.alive = true; | |
| entity.name = name || ""; | |
| if (name) { | |
| if (this._entitiesByNames[name]) { | |
| console.warn(`Entity name '${name}' already exist`); | |
| } else { | |
| this._entitiesByNames[name] = entity; | |
| } | |
| } | |
| this._entities.push(entity); | |
| this.eventDispatcher.dispatchEvent(ENTITY_CREATED, entity); | |
| return entity; | |
| } | |
| // COMPONENTS | |
| /** | |
| * Add a component to an entity | |
| * @param {Entity} entity Entity where the component will be added | |
| * @param {Component} Component Component to be added to the entity | |
| * @param {Object} values Optional values to replace the default attributes | |
| */ | |
| entityAddComponent(entity, Component, values) { | |
| // @todo Probably define Component._typeId with a default value and avoid using typeof | |
| if ( | |
| typeof Component._typeId === "undefined" && | |
| !this.world.componentsManager._ComponentsMap[Component._typeId] | |
| ) { | |
| throw new Error( | |
| `Attempted to add unregistered component "${Component.getName()}"` | |
| ); | |
| } | |
| if (~entity._ComponentTypes.indexOf(Component)) { | |
| { | |
| console.warn( | |
| "Component type already exists on entity.", | |
| entity, | |
| Component.getName() | |
| ); | |
| } | |
| return; | |
| } | |
| entity._ComponentTypes.push(Component); | |
| if (Component.__proto__ === SystemStateComponent) { | |
| entity.numStateComponents++; | |
| } | |
| var componentPool = this.world.componentsManager.getComponentsPool( | |
| Component | |
| ); | |
| var component = componentPool | |
| ? componentPool.acquire() | |
| : new Component(values); | |
| if (componentPool && values) { | |
| component.copy(values); | |
| } | |
| entity._components[Component._typeId] = component; | |
| this._queryManager.onEntityComponentAdded(entity, Component); | |
| this.world.componentsManager.componentAddedToEntity(Component); | |
| this.eventDispatcher.dispatchEvent(COMPONENT_ADDED, entity, Component); | |
| } | |
| /** | |
| * Remove a component from an entity | |
| * @param {Entity} entity Entity which will get removed the component | |
| * @param {*} Component Component to remove from the entity | |
| * @param {Bool} immediately If you want to remove the component immediately instead of deferred (Default is false) | |
| */ | |
| entityRemoveComponent(entity, Component, immediately) { | |
| var index = entity._ComponentTypes.indexOf(Component); | |
| if (!~index) return; | |
| this.eventDispatcher.dispatchEvent(COMPONENT_REMOVE, entity, Component); | |
| if (immediately) { | |
| this._entityRemoveComponentSync(entity, Component, index); | |
| } else { | |
| if (entity._ComponentTypesToRemove.length === 0) | |
| this.entitiesWithComponentsToRemove.push(entity); | |
| entity._ComponentTypes.splice(index, 1); | |
| entity._ComponentTypesToRemove.push(Component); | |
| entity._componentsToRemove[Component._typeId] = | |
| entity._components[Component._typeId]; | |
| delete entity._components[Component._typeId]; | |
| } | |
| // Check each indexed query to see if we need to remove it | |
| this._queryManager.onEntityComponentRemoved(entity, Component); | |
| if (Component.__proto__ === SystemStateComponent) { | |
| entity.numStateComponents--; | |
| // Check if the entity was a ghost waiting for the last system state component to be removed | |
| if (entity.numStateComponents === 0 && !entity.alive) { | |
| entity.remove(); | |
| } | |
| } | |
| } | |
| _entityRemoveComponentSync(entity, Component, index) { | |
| // Remove T listing on entity and property ref, then free the component. | |
| entity._ComponentTypes.splice(index, 1); | |
| var component = entity._components[Component._typeId]; | |
| delete entity._components[Component._typeId]; | |
| component.dispose(); | |
| this.world.componentsManager.componentRemovedFromEntity(Component); | |
| } | |
| /** | |
| * Remove all the components from an entity | |
| * @param {Entity} entity Entity from which the components will be removed | |
| */ | |
| entityRemoveAllComponents(entity, immediately) { | |
| let Components = entity._ComponentTypes; | |
| for (let j = Components.length - 1; j >= 0; j--) { | |
| if (Components[j].__proto__ !== SystemStateComponent) | |
| this.entityRemoveComponent(entity, Components[j], immediately); | |
| } | |
| } | |
| /** | |
| * Remove the entity from this manager. It will clear also its components | |
| * @param {Entity} entity Entity to remove from the manager | |
| * @param {Bool} immediately If you want to remove the component immediately instead of deferred (Default is false) | |
| */ | |
| removeEntity(entity, immediately) { | |
| var index = this._entities.indexOf(entity); | |
| if (!~index) throw new Error("Tried to remove entity not in list"); | |
| entity.alive = false; | |
| this.entityRemoveAllComponents(entity, immediately); | |
| if (entity.numStateComponents === 0) { | |
| // Remove from entity list | |
| this.eventDispatcher.dispatchEvent(ENTITY_REMOVED, entity); | |
| this._queryManager.onEntityRemoved(entity); | |
| if (immediately === true) { | |
| this._releaseEntity(entity, index); | |
| } else { | |
| this.entitiesToRemove.push(entity); | |
| } | |
| } | |
| } | |
| _releaseEntity(entity, index) { | |
| this._entities.splice(index, 1); | |
| if (this._entitiesByNames[entity.name]) { | |
| delete this._entitiesByNames[entity.name]; | |
| } | |
| entity._pool.release(entity); | |
| } | |
| /** | |
| * Remove all entities from this manager | |
| */ | |
| removeAllEntities() { | |
| for (var i = this._entities.length - 1; i >= 0; i--) { | |
| this.removeEntity(this._entities[i]); | |
| } | |
| } | |
| processDeferredRemoval() { | |
| if (!this.deferredRemovalEnabled) { | |
| return; | |
| } | |
| for (let i = 0; i < this.entitiesToRemove.length; i++) { | |
| let entity = this.entitiesToRemove[i]; | |
| let index = this._entities.indexOf(entity); | |
| this._releaseEntity(entity, index); | |
| } | |
| this.entitiesToRemove.length = 0; | |
| for (let i = 0; i < this.entitiesWithComponentsToRemove.length; i++) { | |
| let entity = this.entitiesWithComponentsToRemove[i]; | |
| while (entity._ComponentTypesToRemove.length > 0) { | |
| let Component = entity._ComponentTypesToRemove.pop(); | |
| var component = entity._componentsToRemove[Component._typeId]; | |
| delete entity._componentsToRemove[Component._typeId]; | |
| component.dispose(); | |
| this.world.componentsManager.componentRemovedFromEntity(Component); | |
| //this._entityRemoveComponentSync(entity, Component, index); | |
| } | |
| } | |
| this.entitiesWithComponentsToRemove.length = 0; | |
| } | |
| /** | |
| * Get a query based on a list of components | |
| * @param {Array(Component)} Components List of components that will form the query | |
| */ | |
| queryComponents(Components) { | |
| return this._queryManager.getQuery(Components); | |
| } | |
| // EXTRAS | |
| /** | |
| * Return number of entities | |
| */ | |
| count() { | |
| return this._entities.length; | |
| } | |
| /** | |
| * Return some stats | |
| */ | |
| stats() { | |
| var stats = { | |
| numEntities: this._entities.length, | |
| numQueries: Object.keys(this._queryManager._queries).length, | |
| queries: this._queryManager.stats(), | |
| numComponentPool: Object.keys(this.componentsManager._componentPool) | |
| .length, | |
| componentPool: {}, | |
| eventDispatcher: this.eventDispatcher.stats, | |
| }; | |
| for (var ecsyComponentId in this.componentsManager._componentPool) { | |
| var pool = this.componentsManager._componentPool[ecsyComponentId]; | |
| stats.componentPool[pool.T.getName()] = { | |
| used: pool.totalUsed(), | |
| size: pool.count, | |
| }; | |
| } | |
| return stats; | |
| } | |
| } | |
| const ENTITY_CREATED = "EntityManager#ENTITY_CREATE"; | |
| const ENTITY_REMOVED = "EntityManager#ENTITY_REMOVED"; | |
| const COMPONENT_ADDED = "EntityManager#COMPONENT_ADDED"; | |
| const COMPONENT_REMOVE = "EntityManager#COMPONENT_REMOVE"; | |
| class ComponentManager { | |
| constructor() { | |
| this.Components = []; | |
| this._ComponentsMap = {}; | |
| this._componentPool = {}; | |
| this.numComponents = {}; | |
| this.nextComponentId = 0; | |
| } | |
| hasComponent(Component) { | |
| return this.Components.indexOf(Component) !== -1; | |
| } | |
| registerComponent(Component, objectPool) { | |
| if (this.Components.indexOf(Component) !== -1) { | |
| console.warn( | |
| `Component type: '${Component.getName()}' already registered.` | |
| ); | |
| return; | |
| } | |
| const schema = Component.schema; | |
| if (!schema) { | |
| throw new Error( | |
| `Component "${Component.getName()}" has no schema property.` | |
| ); | |
| } | |
| for (const propName in schema) { | |
| const prop = schema[propName]; | |
| if (!prop.type) { | |
| throw new Error( | |
| `Invalid schema for component "${Component.getName()}". Missing type for "${propName}" property.` | |
| ); | |
| } | |
| } | |
| Component._typeId = this.nextComponentId++; | |
| this.Components.push(Component); | |
| this._ComponentsMap[Component._typeId] = Component; | |
| this.numComponents[Component._typeId] = 0; | |
| if (objectPool === undefined) { | |
| objectPool = new ObjectPool(Component); | |
| } else if (objectPool === false) { | |
| objectPool = undefined; | |
| } | |
| this._componentPool[Component._typeId] = objectPool; | |
| } | |
| componentAddedToEntity(Component) { | |
| this.numComponents[Component._typeId]++; | |
| } | |
| componentRemovedFromEntity(Component) { | |
| this.numComponents[Component._typeId]--; | |
| } | |
| getComponentsPool(Component) { | |
| return this._componentPool[Component._typeId]; | |
| } | |
| } | |
| const Version = "0.3.1"; | |
| const proxyMap = new WeakMap(); | |
| const proxyHandler = { | |
| set(target, prop) { | |
| throw new Error( | |
| `Tried to write to "${target.constructor.getName()}#${String( | |
| prop | |
| )}" on immutable component. Use .getMutableComponent() to modify a component.` | |
| ); | |
| }, | |
| }; | |
| function wrapImmutableComponent(T, component) { | |
| if (component === undefined) { | |
| return undefined; | |
| } | |
| let wrappedComponent = proxyMap.get(component); | |
| if (!wrappedComponent) { | |
| wrappedComponent = new Proxy(component, proxyHandler); | |
| proxyMap.set(component, wrappedComponent); | |
| } | |
| return wrappedComponent; | |
| } | |
| class Entity { | |
| constructor(entityManager) { | |
| this._entityManager = entityManager || null; | |
| // Unique ID for this entity | |
| this.id = entityManager._nextEntityId++; | |
| // List of components types the entity has | |
| this._ComponentTypes = []; | |
| // Instance of the components | |
| this._components = {}; | |
| this._componentsToRemove = {}; | |
| // Queries where the entity is added | |
| this.queries = []; | |
| // Used for deferred removal | |
| this._ComponentTypesToRemove = []; | |
| this.alive = false; | |
| //if there are state components on a entity, it can't be removed completely | |
| this.numStateComponents = 0; | |
| } | |
| // COMPONENTS | |
| getComponent(Component, includeRemoved) { | |
| var component = this._components[Component._typeId]; | |
| if (!component && includeRemoved === true) { | |
| component = this._componentsToRemove[Component._typeId]; | |
| } | |
| return wrapImmutableComponent(Component, component) | |
| ; | |
| } | |
| getRemovedComponent(Component) { | |
| const component = this._componentsToRemove[Component._typeId]; | |
| return wrapImmutableComponent(Component, component) | |
| ; | |
| } | |
| getComponents() { | |
| return this._components; | |
| } | |
| getComponentsToRemove() { | |
| return this._componentsToRemove; | |
| } | |
| getComponentTypes() { | |
| return this._ComponentTypes; | |
| } | |
| getMutableComponent(Component) { | |
| var component = this._components[Component._typeId]; | |
| if (!component) { | |
| return; | |
| } | |
| for (var i = 0; i < this.queries.length; i++) { | |
| var query = this.queries[i]; | |
| // @todo accelerate this check. Maybe having query._Components as an object | |
| // @todo add Not components | |
| if (query.reactive && query.Components.indexOf(Component) !== -1) { | |
| query.eventDispatcher.dispatchEvent( | |
| Query.prototype.COMPONENT_CHANGED, | |
| this, | |
| component | |
| ); | |
| } | |
| } | |
| return component; | |
| } | |
| addComponent(Component, values) { | |
| this._entityManager.entityAddComponent(this, Component, values); | |
| return this; | |
| } | |
| removeComponent(Component, forceImmediate) { | |
| this._entityManager.entityRemoveComponent(this, Component, forceImmediate); | |
| return this; | |
| } | |
| hasComponent(Component, includeRemoved) { | |
| return ( | |
| !!~this._ComponentTypes.indexOf(Component) || | |
| (includeRemoved === true && this.hasRemovedComponent(Component)) | |
| ); | |
| } | |
| hasRemovedComponent(Component) { | |
| return !!~this._ComponentTypesToRemove.indexOf(Component); | |
| } | |
| hasAllComponents(Components) { | |
| for (var i = 0; i < Components.length; i++) { | |
| if (!this.hasComponent(Components[i])) return false; | |
| } | |
| return true; | |
| } | |
| hasAnyComponents(Components) { | |
| for (var i = 0; i < Components.length; i++) { | |
| if (this.hasComponent(Components[i])) return true; | |
| } | |
| return false; | |
| } | |
| removeAllComponents(forceImmediate) { | |
| return this._entityManager.entityRemoveAllComponents(this, forceImmediate); | |
| } | |
| copy(src) { | |
| // TODO: This can definitely be optimized | |
| for (var ecsyComponentId in src._components) { | |
| var srcComponent = src._components[ecsyComponentId]; | |
| this.addComponent(srcComponent.constructor); | |
| var component = this.getComponent(srcComponent.constructor); | |
| component.copy(srcComponent); | |
| } | |
| return this; | |
| } | |
| clone() { | |
| return new Entity(this._entityManager).copy(this); | |
| } | |
| reset() { | |
| this.id = this._entityManager._nextEntityId++; | |
| this._ComponentTypes.length = 0; | |
| this.queries.length = 0; | |
| for (var ecsyComponentId in this._components) { | |
| delete this._components[ecsyComponentId]; | |
| } | |
| } | |
| remove(forceImmediate) { | |
| return this._entityManager.removeEntity(this, forceImmediate); | |
| } | |
| } | |
| const DEFAULT_OPTIONS = { | |
| entityPoolSize: 0, | |
| entityClass: Entity, | |
| }; | |
| class World { | |
| constructor(options = {}) { | |
| this.options = Object.assign({}, DEFAULT_OPTIONS, options); | |
| this.componentsManager = new ComponentManager(this); | |
| this.entityManager = new EntityManager(this); | |
| this.systemManager = new SystemManager(this); | |
| this.enabled = true; | |
| this.eventQueues = {}; | |
| if (hasWindow && typeof CustomEvent !== "undefined") { | |
| var event = new CustomEvent("ecsy-world-created", { | |
| detail: { world: this, version: Version }, | |
| }); | |
| window.dispatchEvent(event); | |
| } | |
| this.lastTime = now() / 1000; | |
| } | |
| registerComponent(Component, objectPool) { | |
| this.componentsManager.registerComponent(Component, objectPool); | |
| return this; | |
| } | |
| registerSystem(System, attributes) { | |
| this.systemManager.registerSystem(System, attributes); | |
| return this; | |
| } | |
| hasRegisteredComponent(Component) { | |
| return this.componentsManager.hasComponent(Component); | |
| } | |
| unregisterSystem(System) { | |
| this.systemManager.unregisterSystem(System); | |
| return this; | |
| } | |
| getSystem(SystemClass) { | |
| return this.systemManager.getSystem(SystemClass); | |
| } | |
| getSystems() { | |
| return this.systemManager.getSystems(); | |
| } | |
| execute(delta, time) { | |
| if (!delta) { | |
| time = now() / 1000; | |
| delta = time - this.lastTime; | |
| this.lastTime = time; | |
| } | |
| if (this.enabled) { | |
| this.systemManager.execute(delta, time); | |
| this.entityManager.processDeferredRemoval(); | |
| } | |
| } | |
| stop() { | |
| this.enabled = false; | |
| } | |
| play() { | |
| this.enabled = true; | |
| } | |
| createEntity(name) { | |
| return this.entityManager.createEntity(name); | |
| } | |
| stats() { | |
| var stats = { | |
| entities: this.entityManager.stats(), | |
| system: this.systemManager.stats(), | |
| }; | |
| return stats; | |
| } | |
| } | |
| class System { | |
| canExecute() { | |
| if (this._mandatoryQueries.length === 0) return true; | |
| for (let i = 0; i < this._mandatoryQueries.length; i++) { | |
| var query = this._mandatoryQueries[i]; | |
| if (query.entities.length === 0) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| getName() { | |
| return this.constructor.getName(); | |
| } | |
| constructor(world, attributes) { | |
| this.world = world; | |
| this.enabled = true; | |
| // @todo Better naming :) | |
| this._queries = {}; | |
| this.queries = {}; | |
| this.priority = 0; | |
| // Used for stats | |
| this.executeTime = 0; | |
| if (attributes && attributes.priority) { | |
| this.priority = attributes.priority; | |
| } | |
| this._mandatoryQueries = []; | |
| this.initialized = true; | |
| if (this.constructor.queries) { | |
| for (var queryName in this.constructor.queries) { | |
| var queryConfig = this.constructor.queries[queryName]; | |
| var Components = queryConfig.components; | |
| if (!Components || Components.length === 0) { | |
| throw new Error("'components' attribute can't be empty in a query"); | |
| } | |
| // Detect if the components have already been registered | |
| let unregisteredComponents = Components.filter( | |
| (Component) => !componentRegistered(Component) | |
| ); | |
| if (unregisteredComponents.length > 0) { | |
| throw new Error( | |
| `Tried to create a query '${ | |
| this.constructor.name | |
| }.${queryName}' with unregistered components: [${unregisteredComponents | |
| .map((c) => c.getName()) | |
| .join(", ")}]` | |
| ); | |
| } | |
| var query = this.world.entityManager.queryComponents(Components); | |
| this._queries[queryName] = query; | |
| if (queryConfig.mandatory === true) { | |
| this._mandatoryQueries.push(query); | |
| } | |
| this.queries[queryName] = { | |
| results: query.entities, | |
| }; | |
| // Reactive configuration added/removed/changed | |
| var validEvents = ["added", "removed", "changed"]; | |
| const eventMapping = { | |
| added: Query.prototype.ENTITY_ADDED, | |
| removed: Query.prototype.ENTITY_REMOVED, | |
| changed: Query.prototype.COMPONENT_CHANGED, // Query.prototype.ENTITY_CHANGED | |
| }; | |
| if (queryConfig.listen) { | |
| validEvents.forEach((eventName) => { | |
| if (!this.execute) { | |
| console.warn( | |
| `System '${this.getName()}' has defined listen events (${validEvents.join( | |
| ", " | |
| )}) for query '${queryName}' but it does not implement the 'execute' method.` | |
| ); | |
| } | |
| // Is the event enabled on this system's query? | |
| if (queryConfig.listen[eventName]) { | |
| let event = queryConfig.listen[eventName]; | |
| if (eventName === "changed") { | |
| query.reactive = true; | |
| if (event === true) { | |
| // Any change on the entity from the components in the query | |
| let eventList = (this.queries[queryName][eventName] = []); | |
| query.eventDispatcher.addEventListener( | |
| Query.prototype.COMPONENT_CHANGED, | |
| (entity) => { | |
| // Avoid duplicates | |
| if (eventList.indexOf(entity) === -1) { | |
| eventList.push(entity); | |
| } | |
| } | |
| ); | |
| } else if (Array.isArray(event)) { | |
| let eventList = (this.queries[queryName][eventName] = []); | |
| query.eventDispatcher.addEventListener( | |
| Query.prototype.COMPONENT_CHANGED, | |
| (entity, changedComponent) => { | |
| // Avoid duplicates | |
| if ( | |
| event.indexOf(changedComponent.constructor) !== -1 && | |
| eventList.indexOf(entity) === -1 | |
| ) { | |
| eventList.push(entity); | |
| } | |
| } | |
| ); | |
| } | |
| } else { | |
| let eventList = (this.queries[queryName][eventName] = []); | |
| query.eventDispatcher.addEventListener( | |
| eventMapping[eventName], | |
| (entity) => { | |
| // @fixme overhead? | |
| if (eventList.indexOf(entity) === -1) | |
| eventList.push(entity); | |
| } | |
| ); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| stop() { | |
| this.executeTime = 0; | |
| this.enabled = false; | |
| } | |
| play() { | |
| this.enabled = true; | |
| } | |
| // @question rename to clear queues? | |
| clearEvents() { | |
| for (let queryName in this.queries) { | |
| var query = this.queries[queryName]; | |
| if (query.added) { | |
| query.added.length = 0; | |
| } | |
| if (query.removed) { | |
| query.removed.length = 0; | |
| } | |
| if (query.changed) { | |
| if (Array.isArray(query.changed)) { | |
| query.changed.length = 0; | |
| } else { | |
| for (let name in query.changed) { | |
| query.changed[name].length = 0; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| toJSON() { | |
| var json = { | |
| name: this.getName(), | |
| enabled: this.enabled, | |
| executeTime: this.executeTime, | |
| priority: this.priority, | |
| queries: {}, | |
| }; | |
| if (this.constructor.queries) { | |
| var queries = this.constructor.queries; | |
| for (let queryName in queries) { | |
| let query = this.queries[queryName]; | |
| let queryDefinition = queries[queryName]; | |
| let jsonQuery = (json.queries[queryName] = { | |
| key: this._queries[queryName].key, | |
| }); | |
| jsonQuery.mandatory = queryDefinition.mandatory === true; | |
| jsonQuery.reactive = | |
| queryDefinition.listen && | |
| (queryDefinition.listen.added === true || | |
| queryDefinition.listen.removed === true || | |
| queryDefinition.listen.changed === true || | |
| Array.isArray(queryDefinition.listen.changed)); | |
| if (jsonQuery.reactive) { | |
| jsonQuery.listen = {}; | |
| const methods = ["added", "removed", "changed"]; | |
| methods.forEach((method) => { | |
| if (query[method]) { | |
| jsonQuery.listen[method] = { | |
| entities: query[method].length, | |
| }; | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| return json; | |
| } | |
| } | |
| System.isSystem = true; | |
| System.getName = function () { | |
| return this.displayName || this.name; | |
| }; | |
| function Not(Component) { | |
| return { | |
| operator: "not", | |
| Component: Component, | |
| }; | |
| } | |
| class TagComponent extends Component { | |
| constructor() { | |
| super(false); | |
| } | |
| } | |
| TagComponent.isTagComponent = true; | |
| const copyValue = (src) => src; | |
| const cloneValue = (src) => src; | |
| const copyArray = (src, dest) => { | |
| if (!src) { | |
| return src; | |
| } | |
| if (!dest) { | |
| return src.slice(); | |
| } | |
| dest.length = 0; | |
| for (let i = 0; i < src.length; i++) { | |
| dest.push(src[i]); | |
| } | |
| return dest; | |
| }; | |
| const cloneArray = (src) => src && src.slice(); | |
| const copyJSON = (src) => JSON.parse(JSON.stringify(src)); | |
| const cloneJSON = (src) => JSON.parse(JSON.stringify(src)); | |
| const copyCopyable = (src, dest) => { | |
| if (!src) { | |
| return src; | |
| } | |
| if (!dest) { | |
| return src.clone(); | |
| } | |
| return dest.copy(src); | |
| }; | |
| const cloneClonable = (src) => src && src.clone(); | |
| function createType(typeDefinition) { | |
| var mandatoryProperties = ["name", "default", "copy", "clone"]; | |
| var undefinedProperties = mandatoryProperties.filter((p) => { | |
| return !typeDefinition.hasOwnProperty(p); | |
| }); | |
| if (undefinedProperties.length > 0) { | |
| throw new Error( | |
| `createType expects a type definition with the following properties: ${undefinedProperties.join( | |
| ", " | |
| )}` | |
| ); | |
| } | |
| typeDefinition.isType = true; | |
| return typeDefinition; | |
| } | |
| /** | |
| * Standard types | |
| */ | |
| const Types = { | |
| Number: createType({ | |
| name: "Number", | |
| default: 0, | |
| copy: copyValue, | |
| clone: cloneValue, | |
| }), | |
| Boolean: createType({ | |
| name: "Boolean", | |
| default: false, | |
| copy: copyValue, | |
| clone: cloneValue, | |
| }), | |
| String: createType({ | |
| name: "String", | |
| default: "", | |
| copy: copyValue, | |
| clone: cloneValue, | |
| }), | |
| Array: createType({ | |
| name: "Array", | |
| default: [], | |
| copy: copyArray, | |
| clone: cloneArray, | |
| }), | |
| Ref: createType({ | |
| name: "Ref", | |
| default: undefined, | |
| copy: copyValue, | |
| clone: cloneValue, | |
| }), | |
| JSON: createType({ | |
| name: "JSON", | |
| default: null, | |
| copy: copyJSON, | |
| clone: cloneJSON, | |
| }), | |
| }; | |
| function generateId(length) { | |
| var result = ""; | |
| var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; | |
| var charactersLength = characters.length; | |
| for (var i = 0; i < length; i++) { | |
| result += characters.charAt(Math.floor(Math.random() * charactersLength)); | |
| } | |
| return result; | |
| } | |
| function injectScript(src, onLoad) { | |
| var script = document.createElement("script"); | |
| // @todo Use link to the ecsy-devtools repo? | |
| script.src = src; | |
| script.onload = onLoad; | |
| (document.head || document.documentElement).appendChild(script); | |
| } | |
| /* global Peer */ | |
| function hookConsoleAndErrors(connection) { | |
| var wrapFunctions = ["error", "warning", "log"]; | |
| wrapFunctions.forEach((key) => { | |
| if (typeof console[key] === "function") { | |
| var fn = console[key].bind(console); | |
| console[key] = (...args) => { | |
| connection.send({ | |
| method: "console", | |
| type: key, | |
| args: JSON.stringify(args), | |
| }); | |
| return fn.apply(null, args); | |
| }; | |
| } | |
| }); | |
| window.addEventListener("error", (error) => { | |
| connection.send({ | |
| method: "error", | |
| error: JSON.stringify({ | |
| message: error.error.message, | |
| stack: error.error.stack, | |
| }), | |
| }); | |
| }); | |
| } | |
| function includeRemoteIdHTML(remoteId) { | |
| let infoDiv = document.createElement("div"); | |
| infoDiv.style.cssText = ` | |
| align-items: center; | |
| background-color: #333; | |
| color: #aaa; | |
| display:flex; | |
| font-family: Arial; | |
| font-size: 1.1em; | |
| height: 40px; | |
| justify-content: center; | |
| left: 0; | |
| opacity: 0.9; | |
| position: absolute; | |
| right: 0; | |
| text-align: center; | |
| top: 0; | |
| `; | |
| infoDiv.innerHTML = `Open ECSY devtools to connect to this page using the code: <b style="color: #fff">${remoteId}</b> <button onClick="generateNewCode()">Generate new code</button>`; | |
| document.body.appendChild(infoDiv); | |
| return infoDiv; | |
| } | |
| function enableRemoteDevtools(remoteId) { | |
| if (!hasWindow) { | |
| console.warn("Remote devtools not available outside the browser"); | |
| return; | |
| } | |
| window.generateNewCode = () => { | |
| window.localStorage.clear(); | |
| remoteId = generateId(6); | |
| window.localStorage.setItem("ecsyRemoteId", remoteId); | |
| window.location.reload(false); | |
| }; | |
| remoteId = remoteId || window.localStorage.getItem("ecsyRemoteId"); | |
| if (!remoteId) { | |
| remoteId = generateId(6); | |
| window.localStorage.setItem("ecsyRemoteId", remoteId); | |
| } | |
| let infoDiv = includeRemoteIdHTML(remoteId); | |
| window.__ECSY_REMOTE_DEVTOOLS_INJECTED = true; | |
| window.__ECSY_REMOTE_DEVTOOLS = {}; | |
| let Version = ""; | |
| // This is used to collect the worlds created before the communication is being established | |
| let worldsBeforeLoading = []; | |
| let onWorldCreated = (e) => { | |
| var world = e.detail.world; | |
| Version = e.detail.version; | |
| worldsBeforeLoading.push(world); | |
| }; | |
| window.addEventListener("ecsy-world-created", onWorldCreated); | |
| let onLoaded = () => { | |
| // var peer = new Peer(remoteId); | |
| var peer = new Peer(remoteId, { | |
| host: "peerjs.ecsy.io", | |
| secure: true, | |
| port: 443, | |
| config: { | |
| iceServers: [ | |
| { url: "stun:stun.l.google.com:19302" }, | |
| { url: "stun:stun1.l.google.com:19302" }, | |
| { url: "stun:stun2.l.google.com:19302" }, | |
| { url: "stun:stun3.l.google.com:19302" }, | |
| { url: "stun:stun4.l.google.com:19302" }, | |
| ], | |
| }, | |
| debug: 3, | |
| }); | |
| peer.on("open", (/* id */) => { | |
| peer.on("connection", (connection) => { | |
| window.__ECSY_REMOTE_DEVTOOLS.connection = connection; | |
| connection.on("open", function () { | |
| // infoDiv.style.visibility = "hidden"; | |
| infoDiv.innerHTML = "Connected"; | |
| // Receive messages | |
| connection.on("data", function (data) { | |
| if (data.type === "init") { | |
| var script = document.createElement("script"); | |
| script.setAttribute("type", "text/javascript"); | |
| script.onload = () => { | |
| script.parentNode.removeChild(script); | |
| // Once the script is injected we don't need to listen | |
| window.removeEventListener( | |
| "ecsy-world-created", | |
| onWorldCreated | |
| ); | |
| worldsBeforeLoading.forEach((world) => { | |
| var event = new CustomEvent("ecsy-world-created", { | |
| detail: { world: world, version: Version }, | |
| }); | |
| window.dispatchEvent(event); | |
| }); | |
| }; | |
| script.innerHTML = data.script; | |
| (document.head || document.documentElement).appendChild(script); | |
| script.onload(); | |
| hookConsoleAndErrors(connection); | |
| } else if (data.type === "executeScript") { | |
| let value = eval(data.script); | |
| if (data.returnEval) { | |
| connection.send({ | |
| method: "evalReturn", | |
| value: value, | |
| }); | |
| } | |
| } | |
| }); | |
| }); | |
| }); | |
| }); | |
| }; | |
| // Inject PeerJS script | |
| injectScript( | |
| "https://cdn.jsdelivr.net/npm/peerjs@0.3.20/dist/peer.min.js", | |
| onLoaded | |
| ); | |
| } | |
| if (hasWindow) { | |
| const urlParams = new URLSearchParams(window.location.search); | |
| // @todo Provide a way to disable it if needed | |
| if (urlParams.has("enable-remote-devtools")) { | |
| enableRemoteDevtools(); | |
| } | |
| } | |
| export { Component, Not, ObjectPool, System, SystemStateComponent, TagComponent, Types, Version, World, Entity as _Entity, cloneArray, cloneClonable, cloneJSON, cloneValue, copyArray, copyCopyable, copyJSON, copyValue, createType, enableRemoteDevtools }; | |
Xet Storage Details
- Size:
- 45.5 kB
- Xet hash:
- af0e7006e21bc0bf44563c139997a1f38b715813dd5aea2583d5be74d5db1814
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.