import { Entity, IEngine, NetworkEntity as _NetworkEntity, INetowrkEntity, NetworkParent as _NetworkParent, Transform as _Transform, SyncComponents as _SyncComponents, INetowrkParent, TransformComponent, ISyncComponents } from '@dcl/ecs' import { IProfile } from './message-bus-sync' import { getDesyncedComponents } from './state' export type SyncEntity = (entityId: Entity, componentIds: number[], entityEnumId?: number) => void export function entityUtils(engine: IEngine, profile: IProfile) { const NetworkEntity = engine.getComponent(_NetworkEntity.componentId) as INetowrkEntity const NetworkParent = engine.getComponent(_NetworkParent.componentId) as INetowrkParent const Transform = engine.getComponent(_Transform.componentId) as TransformComponent const SyncComponents = engine.getComponent(_SyncComponents.componentId) as ISyncComponents /** * Create a network entity (sync) through comms, and sync the received components */ function syncEntity(entityId: Entity, componentIds: number[], entityEnumId?: number) { let componentsIdsMutable = [...componentIds] // Profile not initialized if (!profile?.networkId) { throw new Error('Profile not initialized. Call syncEntity inside the main() function.') } // We use the networkId generated by the user address to identify this entity through the network const networkValue = { entityId, networkId: profile.networkId } // If there is an entityEnumId, it means is the same entity for all the clients created on the main funciton. // So the networkId should be the same in all the clients to avoid re-creating this entity. // For this case we use networkId = 0. if (entityEnumId !== undefined) { networkValue.networkId = 0 networkValue.entityId = entityEnumId as Entity // Check if this enum is already used for (const [_, network] of engine.getEntitiesWith(NetworkEntity)) { if (network.networkId === networkValue.networkId && network.entityId === networkValue.entityId) { throw new Error('syncEntity failed because the id provided is already in use') } } } for (const component of getDesyncedComponents(engine)) { if (componentsIdsMutable.includes(component.componentId)) { console.log(`⚠️ ${component.componentName} can't be sync through the network!`) componentsIdsMutable = componentsIdsMutable.filter(($) => $ !== component.componentId) } } // If is not defined, then is a entity created in runtime (what we called dynamic/runtime entities). NetworkEntity.createOrReplace(entityId, networkValue) SyncComponents.createOrReplace(entityId, { componentIds: componentsIdsMutable }) } /** * Returns an iterable of all the childrens of the given entity. * for (const children of getChildren(parent)) { console.log(children) } * or just => const childrens: Entity[] = Array.from(getChildren(parent)) */ function* getChildren(parent: Entity): Iterable { const network = NetworkEntity.getOrNull(parent) if (network) { for (const [entity, parent] of engine.getEntitiesWith(NetworkParent)) { if (parent.entityId === network.entityId && parent.networkId === network.networkId) { yield entity } } } } function getFirstChild(entity: Entity) { return Array.from(getChildren(entity))[0] } /** * Returns the parent entity of the given entity. */ function getParent(child: Entity): Entity | undefined { const parent = NetworkParent.getOrNull(child) if (!parent) return undefined for (const [entity, network] of engine.getEntitiesWith(NetworkEntity)) { if (parent.networkId === network.networkId && parent.entityId === network.entityId) { return entity } } return undefined } /** * Adds the network parenting to sync entities. * Equivalent to Transform.parent for local entities */ function parentEntity(entity: Entity, parent: Entity) { const network = NetworkEntity.getOrNull(parent) if (!network) { throw new Error('Entity is not sync. Call syncEntity on the parent.') } // Create network parent component NetworkParent.createOrReplace(entity, network) // If we dont have a transform for this entity, create an empty one to send it to the renderer if (!Transform.getOrNull(entity)) { Transform.create(entity) } else { // Force a tick update of the transform so the renderer receives the NEW parent. // createOrReplace bypasses the CRDT unchanged-data suppression optimization, // ensuring the renderer transport can inject the network parent into the message. const t = Transform.get(entity) Transform.createOrReplace(entity, { position: { x: t.position.x, y: t.position.y, z: t.position.z }, rotation: { x: t.rotation.x, y: t.rotation.y, z: t.rotation.z, w: t.rotation.w }, scale: { x: t.scale.x, y: t.scale.y, z: t.scale.z }, parent: t.parent }) } } /** * Removes the network parenting from an entity */ function removeParent(entity: Entity) { const network = NetworkEntity.getOrNull(entity) if (!network) { throw new Error('Entity is not sync') } NetworkParent.deleteFrom(entity) } return { syncEntity, getChildren, getParent, parentEntity, removeParent, getFirstChild } }