@dcl/ecs
Version:
Decentraland ECS
268 lines (267 loc) • 11.7 kB
JavaScript
import * as components from '../components';
import { componentNumberFromName } from '../components/component-number';
import { checkNotThenable } from '../runtime/invariant';
import { Schemas } from '../schemas';
import { crdtSceneSystem } from '../systems/crdt';
import { createComponentDefinitionFromSchema } from './lww-element-set-component-definition';
import { createEntityContainer } from './entity';
import { SystemContainer, SYSTEMS_REGULAR_PRIORITY } from './systems';
import { createValueSetComponentDefinitionFromSchema } from './grow-only-value-set-component-definition';
import { removeEntityWithChildren as removeEntityWithChildrenEngine } from '../runtime/helpers/tree';
import { CrdtMessageType } from '../serialization/crdt';
export * from './input';
export * from './readonly';
export * from './types';
function preEngine(options) {
const entityContainer = options?.entityContainer ?? createEntityContainer();
const componentsDefinition = new Map();
const systems = SystemContainer();
let sealed = false;
function addSystem(fn, priority = SYSTEMS_REGULAR_PRIORITY, name) {
systems.add(fn, priority, name);
}
function removeSystem(selector) {
return systems.remove(selector);
}
function addEntity() {
const entity = entityContainer.generateEntity();
return entity;
}
function removeEntity(entity) {
for (const [, component] of componentsDefinition) {
// TODO: hack for the moment.
// We still need the NetworkEntity to forward this message to the SyncTransport.
// If we remove it then we can't notify the other users which entity was deleted.
if (component.componentName === 'core-schema::Network-Entity')
continue;
component.entityDeleted(entity, true);
}
return entityContainer.removeEntity(entity);
}
function removeEntityWithChildren(entity) {
return removeEntityWithChildrenEngine({ removeEntity, defineComponentFromSchema, getEntitiesWith, defineComponent }, entity);
}
function registerComponentDefinition(componentName, component) {
/* istanbul ignore next */
if (sealed)
throw new Error('Engine is already sealed. No components can be added at this stage');
const componentId = componentNumberFromName(componentName);
const prev = componentsDefinition.get(componentId);
if (prev) {
throw new Error(`Component number ${componentId} was already registered.`);
}
/* istanbul ignore next */
if (component.componentName !== componentName) {
throw new Error(`Component name doesn't match componentDefinition.componentName ${componentName} != ${component.componentName}`);
}
/* istanbul ignore next */
if (component.componentId !== componentId) {
throw new Error(`Component number doesn't match componentDefinition.componentId ${componentId} != ${component.componentId}`);
}
componentsDefinition.set(componentId, component);
return component;
}
function defineComponentFromSchema(componentName, schema) {
const componentId = componentNumberFromName(componentName);
const prev = componentsDefinition.get(componentId);
if (prev) {
// TODO: assert spec === prev.spec
return prev;
}
/* istanbul ignore next */
if (sealed)
throw new Error('Engine is already sealed. No components can be added at this stage');
const newComponent = createComponentDefinitionFromSchema(componentName, componentId, schema);
componentsDefinition.set(componentId, newComponent);
return newComponent;
}
function defineValueSetComponentFromSchema(componentName, schema, options) {
const componentId = componentNumberFromName(componentName);
const prev = componentsDefinition.get(componentId);
if (prev) {
// TODO: assert spec === prev.spec
return prev;
}
/* istanbul ignore next */
if (sealed)
throw new Error('Engine is already sealed. No components can be added at this stage');
const newComponent = createValueSetComponentDefinitionFromSchema(componentName, componentId, schema, options);
componentsDefinition.set(componentId, newComponent);
return newComponent;
}
function defineComponent(componentName, mapSpec, constructorDefault) {
const componentId = componentNumberFromName(componentName);
const prev = componentsDefinition.get(componentId);
if (prev) {
// TODO: assert spec === prev.spec
return prev;
}
if (sealed)
throw new Error('Engine is already sealed. No components can be added at this stage');
const schemaSpec = Schemas.Map(mapSpec, constructorDefault);
const def = createComponentDefinitionFromSchema(componentName, componentId, schemaSpec);
const newComponent = {
...def,
create(entity, val) {
return def.create(entity, val);
},
createOrReplace(entity, val) {
return def.createOrReplace(entity, val);
}
};
componentsDefinition.set(componentId, newComponent);
return newComponent;
}
function getComponent(componentIdOrName) {
const componentId = typeof componentIdOrName === 'number' ? componentIdOrName : componentNumberFromName(componentIdOrName);
const component = componentsDefinition.get(componentId);
if (!component) {
throw new Error(`Component ${componentIdOrName} not found. You need to declare the components at the beginnig of the engine declaration`);
}
return component;
}
function getComponentOrNull(componentIdOrName) {
const componentId = typeof componentIdOrName === 'number' ? componentIdOrName : componentNumberFromName(componentIdOrName);
return (componentsDefinition.get(componentId) ??
/* istanbul ignore next */
null);
}
function* getEntitiesWith(...components) {
for (const [entity, ...groupComp] of getComponentDefGroup(...components)) {
yield [entity, ...groupComp.map((c) => c.get(entity))];
}
}
function getEntityOrNullByName(value) {
const NameComponent = components.Name({ defineComponent });
for (const [entity, name] of getEntitiesWith(NameComponent)) {
if (name.value === value)
return entity;
}
return null;
}
function getEntityByName(value) {
const entity = getEntityOrNullByName(value);
return entity;
}
function* getEntitiesByTag(tagName) {
const TagComponent = components.Tags({ defineComponent });
for (const [entity, component] of getEntitiesWith(TagComponent)) {
if (entity !== 0 && component.tags?.some((tag) => tag === tagName)) {
yield entity;
}
}
}
function* getComponentDefGroup(...args) {
const [firstComponentDef, ...componentDefinitions] = args;
for (const [entity] of firstComponentDef.iterator()) {
let matches = true;
for (const componentDef of componentDefinitions) {
if (!componentDef.has(entity)) {
matches = false;
break;
}
}
if (matches) {
yield [entity, ...args];
}
}
}
function getSystems() {
return systems.getSystems();
}
function componentsIter() {
return componentsDefinition.values();
}
function removeComponentDefinition(componentIdOrName) {
if (sealed)
throw new Error('Engine is already sealed. No components can be removed at this stage');
const componentId = typeof componentIdOrName === 'number' ? componentIdOrName : componentNumberFromName(componentIdOrName);
componentsDefinition.delete(componentId);
}
components.Transform({ defineComponentFromSchema });
function seal() {
if (!sealed) {
sealed = true;
}
}
return {
addEntity,
removeEntity,
removeEntityWithChildren,
addSystem,
getSystems,
removeSystem,
defineComponent,
defineComponentFromSchema,
defineValueSetComponentFromSchema,
getEntitiesWith,
getComponent,
getComponentOrNull: getComponentOrNull,
getEntityOrNullByName,
getEntityByName,
getEntitiesByTag,
removeComponentDefinition,
registerComponentDefinition,
entityContainer,
componentsIter,
seal
};
}
/**
* Internal constructor of new engines, this is an internal API
* @public
* @deprecated Prevent manual usage prefer "engine" for scene development
*/
export function Engine(options) {
const partialEngine = preEngine(options);
const onChangeFunction = (entity, operation, component, componentValue) => {
if (operation === CrdtMessageType.DELETE_ENTITY) {
for (const component of partialEngine.componentsIter()) {
component?.__onChangeCallbacks(entity, undefined);
}
}
else {
component?.__onChangeCallbacks(entity, componentValue);
}
return options?.onChangeFunction(entity, operation, component, componentValue);
};
const crdtSystem = crdtSceneSystem(partialEngine, onChangeFunction);
async function update(dt) {
await crdtSystem.receiveMessages();
for (const system of partialEngine.getSystems()) {
const ret = system.fn(dt);
checkNotThenable(ret, `A system (${system.name || 'anonymous'}) returned a thenable. Systems cannot be async functions. Documentation: https://dcl.gg/sdk/sync-systems`);
}
// get the deleted entities to send the DeleteEntity CRDT commands
const deletedEntites = partialEngine.entityContainer.releaseRemovedEntities();
await crdtSystem.sendMessages(deletedEntites);
}
return {
_id: Date.now(),
addEntity: partialEngine.addEntity,
removeEntity: partialEngine.removeEntity,
removeEntityWithChildren: partialEngine.removeEntityWithChildren,
addSystem: partialEngine.addSystem,
removeSystem: partialEngine.removeSystem,
defineComponent: partialEngine.defineComponent,
defineComponentFromSchema: partialEngine.defineComponentFromSchema,
defineValueSetComponentFromSchema: partialEngine.defineValueSetComponentFromSchema,
registerComponentDefinition: partialEngine.registerComponentDefinition,
getEntitiesWith: partialEngine.getEntitiesWith,
getComponent: partialEngine.getComponent,
getComponentOrNull: partialEngine.getComponentOrNull,
removeComponentDefinition: partialEngine.removeComponentDefinition,
componentsIter: partialEngine.componentsIter,
seal: partialEngine.seal,
getEntityOrNullByName: partialEngine.getEntityOrNullByName,
getEntityByName: partialEngine.getEntityByName,
getEntitiesByTag: partialEngine.getEntitiesByTag,
update,
RootEntity: 0,
PlayerEntity: 1,
CameraEntity: 2,
getEntityState: partialEngine.entityContainer.getEntityState,
addTransport: crdtSystem.addTransport,
entityContainer: partialEngine.entityContainer
};
}