/** Functions to provide system level functionalities. */ import * as bitECS from 'bitecs' import React from 'react' import multiLogger from '@xrengine/common/src/logger' import { ReactorProps, ReactorRoot, startReactor } from '@xrengine/hyperflux' import { nowMilliseconds } from '../../common/functions/nowMilliseconds' import { World } from '../classes/World' import { defineQuery, Query, QueryComponents, useQuery } from './ComponentFunctions' import { EntityReactorProps } from './EntityFunctions' import { SystemUpdateType } from './SystemUpdateType' const logger = multiLogger.child({ component: 'engine:ecs:SystemFunctions' }) export type CreateSystemSyncFunctionType = (world: World, props?: A) => SystemDefintion export type CreateSystemFunctionType = (world: World, props?: A) => Promise export type SystemModule = { default: CreateSystemFunctionType } export type SystemLoader = () => Promise> export interface SystemDefintion { execute: () => void cleanup: () => Promise subsystems?: Array> } /** Internal */ interface SystemInstanceData { execute: () => void cleanup: () => Promise subsystems: SystemInstanceData[] } export interface SystemInstance extends SystemInstanceData { name: string uuid: string type: SystemUpdateType before?: string after?: string sceneSystem: boolean enabled: boolean subsystems: SystemInstance[] } export type SystemSyncFunctionType = { systemFunction: CreateSystemSyncFunctionType uuid: string type: SystemUpdateType args?: A } export type SystemModuleType = { systemLoader: SystemLoader /** any string to uniquely identity this module - can be a uuidv4 or a string name */ uuid: string type: SystemUpdateType sceneSystem?: boolean /** @todo replace this with a more robust identifier */ before?: string after?: string args?: A } export type SystemFactoryType = { uuid: string systemModule: SystemModule type: SystemUpdateType before?: string after?: string sceneSystem?: boolean args?: A } const createExecute = (system: SystemDefintion, subsystems: SystemInstanceData[], name: string, uuid: string) => { let lastWarningTime = 0 const warningCooldownDuration = 1000 * 10 // 10 seconds return () => { const startTime = nowMilliseconds() try { system.execute() } catch (e) { logger.error(`Failed to execute system ${name}`) logger.error(e) } const endTime = nowMilliseconds() const systemDuration = endTime - startTime if (systemDuration > 50 && lastWarningTime < endTime - warningCooldownDuration) { lastWarningTime = endTime logger.warn(`Long system execution detected. System: ${name} \n Duration: ${systemDuration}`) } for (const sys of subsystems) { sys.execute() } } } const loadSubsystems = ( world: World, parentSystemFactory: SystemFactoryType, subsystems: Array> = [] ) => { return Promise.all( subsystems.map(async (subsystemInit, i) => { const subsystem = await subsystemInit() const name = subsystem.default.name const uuid = name const type = 'SUB_SYSTEM' as any return { uuid, name, type, sceneSystem: parentSystemFactory.sceneSystem, enabled: true, ...(await loadSystemInjection(world, { systemModule: subsystem, uuid, type })) } }) ) } const loadSystemInjection = async (world: World, s: SystemFactoryType, type?: SystemUpdateType, args?: any) => { const name = s.systemModule.default.name try { if (type) logger.info(`${name} initializing on ${type} pipeline`) else logger.info(`${name} initializing`) const system = await s.systemModule.default(world, args) const subsystems = await loadSubsystems(world, s, system.subsystems) logger.info(`${name} (${s.uuid}) ready`) return { execute: createExecute(system, subsystems, name, s.uuid), cleanup: system.cleanup, subsystems } as SystemInstanceData } catch (e) { console.error(e) logger.error(new Error(`System ${name} (${s.uuid}) failed to initialize!`, { cause: e.stack })) return null } } export const initSystems = async (world: World, systemModulesToLoad: SystemModuleType[]) => { const systemModule = await Promise.all( systemModulesToLoad.map(async (s) => { return { uuid: s.uuid, args: s.args, type: s.type, before: s.before, after: s.after, sceneSystem: s.sceneSystem, systemModule: await s.systemLoader() } as SystemFactoryType }) ) const systems = await Promise.all( systemModule.map(async (s) => { return { uuid: s.uuid, name: s.systemModule.default.name, type: s.type, before: s.before, after: s.after, sceneSystem: s.sceneSystem, enabled: true, ...(await loadSystemInjection(world, s)) } as SystemInstance }) ) systems.forEach((s) => { if (s) { world.systemsByUUID[s.uuid] = s if (s.before) { const index = world.pipelines[s.type].findIndex((system) => system.uuid === s.before) if (index === -1) throw new Error(`System with id ${s.before} could not be found in pipeline ${s.type}`) world.pipelines[s.type].splice(index, 0, s) } else if (s.after) { const index = world.pipelines[s.type].findIndex((system) => system.uuid === s.after) if (index === -1) throw new Error(`System with id ${s.after} could not be found in pipeline ${s.type}`) world.pipelines[s.type].splice(index + 1, 0, s) } else { world.pipelines[s.type].push(s) } } }) } export const initSystemSync = (world: World, systemArgs: SystemSyncFunctionType) => { const name = systemArgs.systemFunction.name logger.info(`${name} initializing on ${systemArgs.type} pipeline`) const system = systemArgs.systemFunction(world, systemArgs.args) logger.info(`${name} ready`) let lastWarningTime = 0 const warningCooldownDuration = 1000 * 10 // 10 seconds const systemData = { uuid: systemArgs.uuid, name, type: systemArgs.type, sceneSystem: false, enabled: true, execute: () => { const startTime = nowMilliseconds() try { system.execute() } catch (e) { logger.error(`Failed to execute system ${name} (${systemArgs.uuid})`) logger.error(e) } const endTime = nowMilliseconds() const systemDuration = endTime - startTime if (systemDuration > 50 && lastWarningTime < endTime - warningCooldownDuration) { lastWarningTime = endTime logger.warn( `Long system execution detected. System: ${name} (${systemArgs.uuid}) \n Duration: ${systemDuration}` ) } }, cleanup: system.cleanup, subsystems: [] } as SystemInstance world.systemsByUUID[systemData.uuid] = systemData world.pipelines[systemData.type].push(systemData) } export const unloadAllSystems = (world: World, sceneSystemsOnly = false) => { const promises = [] as Promise[] Object.entries(world.pipelines).forEach(([type, pipeline]) => { const systemsToRemove: any[] = [] pipeline.forEach((s) => { if (sceneSystemsOnly) { if (s.sceneSystem) systemsToRemove.push(s) } else { systemsToRemove.push(s) } }) systemsToRemove.forEach((s) => { const i = pipeline.indexOf(s) pipeline.splice(i, 1) delete world.systemsByUUID[s.uuid] promises.push(s.cleanup()) }) }) return promises } export const unloadSystems = (world: World, uuids: string[]) => { const systemsToUnload = uuids.map((uuid) => world.systemsByUUID[uuid]) const promises = [] as Promise[] for (const system of systemsToUnload) { const pipeline = world.pipelines[system.type] const i = pipeline.indexOf(system) pipeline.splice(i, 1) delete world.systemsByUUID[system.uuid] promises.push(system.cleanup()) } return promises } export const unloadSystem = (world: World, uuid: string) => { const systemToUnload = world.systemsByUUID[uuid] const pipeline = world.pipelines[systemToUnload.type] const i = pipeline.indexOf(systemToUnload) pipeline.splice(i, 1) delete world.systemsByUUID[systemToUnload.uuid] return systemToUnload.cleanup() } function QueryReactor(props: { root: ReactorRoot query: QueryComponents ChildEntityReactor: React.FC }) { const entities = useQuery(props.query) return ( <> {entities.map((entity) => ( ))} ) } export const startQueryReactor = (Components: QueryComponents, ChildEntityReactor: React.FC) => { if (!ChildEntityReactor.name) Object.defineProperty(ChildEntityReactor, 'name', { value: 'ChildEntityReactor' }) return startReactor(function HyperfluxQueryReactor({ root }: ReactorProps) { return }) }