From ae28821e8ebc19748aae590a61c45b203ec79259 Mon Sep 17 00:00:00 2001 From: Marco Alka Date: Sun, 21 May 2023 21:08:17 +0200 Subject: [PATCH] #9 prepare eventing and implement it for resource adding and PDA --- package.json | 6 +-- src/events/internal-events.ts | 36 +++++++++---- src/index.ts | 13 +++-- src/pda/pda.spec.ts | 15 +++++- src/pda/pda.ts | 4 +- src/pda/sim-ecs-pda.ts | 35 ++++++++++--- src/system/system-builder.ts | 20 ++++++- src/system/system.spec.ts | 22 +++++++- src/system/system_context.spec.ts | 12 +++++ src/system/system_context.ts | 35 +++++++++++++ src/world/runtime/runtime-world.ts | 55 ++++++++++++++------ src/world/runtime/runtime-world_events.ts | 11 ---- src/world/runtime/runtime-world_resources.ts | 14 ----- src/world/runtime/runtime-world_states.ts | 2 +- 14 files changed, 212 insertions(+), 68 deletions(-) create mode 100644 src/system/system_context.spec.ts create mode 100644 src/system/system_context.ts delete mode 100644 src/world/runtime/runtime-world_events.ts diff --git a/package.json b/package.json index 93d7f9d..bc85eed 100644 --- a/package.json +++ b/package.json @@ -53,20 +53,19 @@ "@types/chai": "~4.3.5", "@types/mocha": "~10.0.1", "chai": "~4.3.7", + "esbuild": "~0.17.18", "madge": "~6.0.0", "mocha": "~10.2.0", "nyc": "~15.1.0", "ts-node": "~10.9.1", "typedoc": "~0.24.7", - "typescript": "~5.0.4", - "esbuild": "~0.17.18" + "typescript": "^5.0.4" }, "files": [ "dist" ], "scripts": { "prebuild": "npm run loop-test-ts && npm run test", - "bench": "cd examples/bench && npm run bench", "build": "npm run esmbuild && npm run dtsbuild && npm run doc", "build-examples": "cd examples && tsc --project .", @@ -90,7 +89,6 @@ "prepare": "npm run build", "test": "echo 'Testing disabled until Mocha can handle TS ESM!'", "test-original": "mocha --experimental-specifier-resolution=node --loader=ts-node/esm src/**/*.test.ts", - "postbuild": "npm run loop-test-js" } } diff --git a/src/events/internal-events.ts b/src/events/internal-events.ts index 412c3fc..1e12bea 100644 --- a/src/events/internal-events.ts +++ b/src/events/internal-events.ts @@ -1,34 +1,52 @@ import type {ISystem} from "../system/system.spec.ts"; import type {TObjectProto} from "../_.spec.ts"; -import type {IIStateProto} from "../state/state.spec.ts"; +import type {IIStateProto, IState} from "../state/state.spec.ts"; -class SimECSPDAEvent { +export class SimECSEvent {} + +class SimECSPDAEvent extends SimECSEvent { + constructor( + public readonly state: Readonly | undefined, + ) { super() } +} + +export class SimECSPDAPopStateEvent extends SimECSPDAEvent { constructor( - public readonly state: Readonly - ) {} + public readonly oldState: Readonly | undefined, + public readonly newState: Readonly | undefined, + ) { + super(newState); + } } -export class SimECSPDAPushStateEvent extends SimECSPDAEvent {} +export class SimECSPDAPushStateEvent extends SimECSPDAEvent { + constructor( + public readonly oldState: Readonly | undefined, + public readonly newState: Readonly, + ) { + super(newState); + } +} -class SimECSResourceEvent { +class SimECSResourceEvent extends SimECSEvent { constructor( public resourceType: T, public resourceObject: InstanceType, - ) {} + ) { super() } } export class SimECSAddResourceEvent extends SimECSResourceEvent {} export class SimECSReplaceResourceEvent extends SimECSResourceEvent {} -class SimECSSystemResourceEvent { +class SimECSSystemResourceEvent extends SimECSEvent{ constructor( public readonly system: Readonly, public readonly paramName: string, public readonly resource: Readonly, - ) {} + ) { super() } } export class SimECSSystemAddResource extends SimECSSystemResourceEvent {} diff --git a/src/index.ts b/src/index.ts index 9e862fd..f9c5bb3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,13 @@ +// Polyfills + +// @ts-ignore +Symbol.dispose ??= Symbol('Symbol.dispose'); +// @ts-ignore +Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose'); + + +// Import all + export * from './ecs/ecs-entity.ts'; export * from './ecs/ecs-query.ts'; export * from './ecs/ecs-sync-point.ts'; @@ -7,13 +17,10 @@ export * from './entity/entity-builder.ts'; export * from './query/query.ts'; export * from './events/event-bus.ts'; export * from './pda/sim-ecs-pda.ts'; - -// Scheduler exports export * from './scheduler/scheduler.ts'; export * from './scheduler/pipeline/pipeline.ts'; export * from './scheduler/pipeline/stage.ts'; export * from './scheduler/pipeline/sync-point.ts'; - export * from './serde/serde.ts'; export * from './serde/serial-format.ts'; export * from './state/state.ts'; diff --git a/src/pda/pda.spec.ts b/src/pda/pda.spec.ts index ab5ad2b..5e97fb3 100644 --- a/src/pda/pda.spec.ts +++ b/src/pda/pda.spec.ts @@ -3,9 +3,22 @@ import type {TTypeProto} from "../_.spec.ts"; export interface IPushDownAutomaton { readonly state?: T + + /** + * Clear the PDA by GC-ing all entries + */ clear(): void + + /** + * Remove the current state from the stack and return it + */ pop(): T | undefined - push

>(state: P): void + + /** + * Put a new state on the stack and return a ref to the created instance + * @param state + */ + push

>(state: P): T } export type TPushDownAutomatonProto = { new(): IPushDownAutomaton }; diff --git a/src/pda/pda.ts b/src/pda/pda.ts index c7becc3..6c3aa04 100644 --- a/src/pda/pda.ts +++ b/src/pda/pda.ts @@ -32,11 +32,13 @@ export class PushDownAutomaton implements IPushDownAutomaton { return oldTail.state; } - push

>(State: P): void { + push

>(State: P): T { this.currentState = new State(); this.statesTail = { prevNode: this.statesTail, state: this.currentState, }; + + return this.currentState; } } diff --git a/src/pda/sim-ecs-pda.ts b/src/pda/sim-ecs-pda.ts index 4194ffb..9ce6eba 100644 --- a/src/pda/sim-ecs-pda.ts +++ b/src/pda/sim-ecs-pda.ts @@ -1,20 +1,41 @@ import {PushDownAutomaton} from "./pda.ts"; import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; import type {TTypeProto} from "../_.spec.ts"; -import {SimECSPDAPushStateEvent} from "../events/internal-events.ts"; +import {SimECSPDAPopStateEvent, SimECSPDAPushStateEvent} from "../events/internal-events.ts"; import type {IState} from "../state/state.spec.ts"; +import type {ITransitionActions} from "../world/actions.spec.ts"; export * from "./pda.ts"; -export class SimECSPushDownAutomaton extends PushDownAutomaton { +export class SimECSPushDownAutomaton { + #pda = new PushDownAutomaton(); + constructor( protected world: IRuntimeWorld - ) { - super(); + ) {} + + get state(): T | undefined { + return this.#pda.state; + } + + clear(actions: Readonly): void { + while (this.#pda.state !== undefined) { + this.#pda.pop()!.destroy(actions); + } + + this.#pda.clear(); + } + + async pop(): Promise { + const oldState = this.#pda.pop(); + await this.world.eventBus.publish(new SimECSPDAPopStateEvent(oldState, this.#pda.state)) + return oldState as T | undefined; } - push

>(State: P): Promise { - super.push(State); - return this.world.eventBus.publish(new SimECSPDAPushStateEvent(State)); + async push

>(State: P): Promise { + const oldState = this.state; + const newState = this.#pda.push(State); + await this.world.eventBus.publish(new SimECSPDAPushStateEvent(oldState, newState)); + return newState; } } diff --git a/src/system/system-builder.ts b/src/system/system-builder.ts index fa83043..3c5f928 100644 --- a/src/system/system-builder.ts +++ b/src/system/system-builder.ts @@ -1,5 +1,9 @@ import type {ISystemBuilder} from "./system-builder.spec.ts"; import type {ISystem, TSystemFunction, TSystemParameterDesc} from "./system.spec.ts"; +import {setRuntimeContext, unsetRuntimeContext} from "./system_context.ts"; +import {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; +import {RuntimeWorld} from "../world/runtime/runtime-world.ts"; +import {ISystemContext} from "./system_context.spec.ts"; export * from "./system-builder.spec.ts"; @@ -16,11 +20,25 @@ export class SystemBuilder implements IS build(): Readonly>> { const self = this; - const System = class implements Readonly>> { + const System = class implements Readonly>>, ISystemContext { + /** @internal */ + _context = undefined; + /** @internal */ + _handlers = new Map(); + readonly name = self.systemName; readonly parameterDesc = self.parameterDesc; readonly runFunction = self.runFunction; readonly setupFunction = self.setupFunction; + + get runtimeContext(): IRuntimeWorld | undefined { + return this._context; + } + + /** @internal */ + setRuntimeContext = setRuntimeContext; + /** @internal */ + unsetRuntimeContext = unsetRuntimeContext; }; Object.defineProperty(System, 'name', { diff --git a/src/system/system.spec.ts b/src/system/system.spec.ts index c3ea7c2..9c6796a 100644 --- a/src/system/system.spec.ts +++ b/src/system/system.spec.ts @@ -4,6 +4,8 @@ import type {ISystemActions} from "../world/actions.spec.ts"; import {systemEventReaderSym, systemEventWriterSym, systemResourceTypeSym, systemRunParamSym} from "./_.ts"; import type {IEventReader} from "../events/event-reader.spec.ts"; import type {IEventWriter} from "../events/event-writer.spec.ts"; +import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; +import {type RuntimeWorld} from "../world/runtime/runtime-world.ts"; export type TSystemParameter = IEntitiesQuery @@ -18,10 +20,28 @@ export type TSystemFunction = (params: Reado export interface ISystem { /** @internal */ [systemRunParamSym]?: Readonly + readonly name: string - readonly parameterDesc: Readonly + readonly parameterDesc: PDESC readonly runFunction: TSystemFunction> + readonly runtimeContext: IRuntimeWorld | undefined readonly setupFunction: TSystemFunction> + + /** + * Set world in which the System will be executed. + * This will be used to register event readers for speedy cache-syncs + * + * @internal + * @param world + */ + setRuntimeContext(world: IRuntimeWorld): void + + /** + * Unset the world in which the system was executed. + * + * @internal + */ + unsetRuntimeContext(world: IRuntimeWorld): void } export interface ISystemResource { diff --git a/src/system/system_context.spec.ts b/src/system/system_context.spec.ts new file mode 100644 index 0000000..fc61187 --- /dev/null +++ b/src/system/system_context.spec.ts @@ -0,0 +1,12 @@ +import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec"; +import type {SimECSEvent} from "../events/internal-events.ts"; +import type {TTypeProto} from "../_.spec.ts"; +import type {TSubscriber} from "../events/_.ts"; + +/** @internal */ +export interface ISystemContext { + /** @internal */ + _context: IRuntimeWorld | undefined + /** @internal */ + _handlers: Map, TSubscriber>> +} diff --git a/src/system/system_context.ts b/src/system/system_context.ts new file mode 100644 index 0000000..aa871b8 --- /dev/null +++ b/src/system/system_context.ts @@ -0,0 +1,35 @@ +import {ISystem} from "./system.spec"; +import { + SimECSReplaceResourceEvent, +} from "../events/internal-events.ts"; +import {RuntimeWorld} from "../world/runtime/runtime-world.ts"; +import {TObjectProto, TTypeProto} from "../_.spec.ts"; +import {ISystemContext} from "./system_context.spec.ts"; +import {TSubscriber} from "../events/_.ts"; + + +export function setRuntimeContext(this: ISystem & ISystemContext, context: RuntimeWorld): void { + { + const handler: TSubscriber>> = handleSimECSReplaceResourceEvent.bind(this); + this._handlers.set(SimECSReplaceResourceEvent, handler); + context.eventBus.subscribe(SimECSReplaceResourceEvent, handler); + } + + this._context = context; +} + +export function unsetRuntimeContext(this: ISystem & ISystemContext, context: RuntimeWorld): void { + let handler; + for (handler of this._handlers) { + context.eventBus.unsubscribe(handler[0], handler[1]); + } + + this._context = undefined; +} + + +function handleSimECSReplaceResourceEvent(this: ISystem, event: Readonly>) { + Object.entries(this.parameterDesc) + .filter(param => param[1] instanceof event.resourceType) + .forEach(param => this.parameterDesc[param[0]] = event.resourceObject); +} diff --git a/src/world/runtime/runtime-world.ts b/src/world/runtime/runtime-world.ts index 3a613fc..f233aae 100644 --- a/src/world/runtime/runtime-world.ts +++ b/src/world/runtime/runtime-world.ts @@ -35,7 +35,7 @@ import type {IState} from "../../state/state.spec.ts"; import {popState, pushState} from "./runtime-world_states.ts"; import {EventBus} from "../../events/event-bus.ts"; import type {IScheduler} from "../../scheduler/scheduler.spec.ts"; -import type {TExecutor, TObjectProto} from "../../_.spec.ts"; +import type {TExecutor} from "../../_.spec.ts"; import type {IMutableWorld} from "../world.spec.ts"; import {Commands} from "./commands/commands.ts"; import type {ISystemActions, ITransitionActions} from "../actions.spec.ts"; @@ -44,7 +44,6 @@ import type {ISystem} from "../../system/system.spec.ts"; import {setEntitiesSym} from "../../query/_.ts"; import type {IRuntimeWorldData} from "./runtime-world.spec.ts"; import {Query} from "../../query/query.ts"; -import {registerSystemAddResourceEvent} from "./runtime-world_events.ts"; import {SimECSPDAPushStateEvent} from "../../events/internal-events.ts"; import type {ISyncPoint} from "../../scheduler/pipeline/sync-point.spec.ts"; import {systemRunParamSym} from "../../system/_.ts"; @@ -67,7 +66,7 @@ export class RuntimeWorld implements IRuntimeWorld, IMutableWorld { protected readonly pda = new SimECSPushDownAutomaton(this); protected queries = new Set>>(); protected shouldRunSystems = false; - protected systemResourceMap = new Map, Readonly<{ paramName: string, resourceType: Readonly }>>(); + protected systems: Set; protected systemWorld: ISystemActions; protected transitionWorld: ITransitionActions; @@ -118,8 +117,29 @@ export class RuntimeWorld implements IRuntimeWorld, IMutableWorld { }, this.systemWorld) } - this.registerSystemAddResourceEvent(); - //this.registerSystemReplaceResourceEvent(); + { // Get all systems and cache the result + this.systems = new Set(this.config.defaultScheduler.getSystems()); + let scheduler; + let system; + + for (scheduler of this.config.stateSchedulers.values()) { + for (system of scheduler.getSystems()) { + this.systems.add(system); + } + } + } + + { // set the context for all systems + let system; + + for (system of this.systems) { + if (system.runtimeContext !== undefined) { + throw new Error('Cannot use a system in two runtimes at the same time!'); + } + + system.setRuntimeContext(this); + } + } } get awaiter(): Promise | undefined { @@ -171,13 +191,14 @@ export class RuntimeWorld implements IRuntimeWorld, IMutableWorld { public async prepare(): Promise { await this.config.defaultScheduler.prepare(this); - this.pda.clear(); + this.pda.clear(this.transitionActions); { let scheduler; let query, system; for (system of this.config.defaultScheduler.getSystems()) { + system.setRuntimeContext(this); for (query of getQueriesFromSystem(system)) { this.queries.add(query); } @@ -231,7 +252,7 @@ export class RuntimeWorld implements IRuntimeWorld, IMutableWorld { } this.eventBus.subscribe(SimECSPDAPushStateEvent, event => { - const stateScheduler = this.config.stateSchedulers.get(event.state) ?? this.config.defaultScheduler; + const stateScheduler = this.config.stateSchedulers.get(event.newState.constructor) ?? this.config.defaultScheduler; let syncPoint; for (syncPoint of stateScheduler.pipeline!.getGroups()) { @@ -245,7 +266,7 @@ export class RuntimeWorld implements IRuntimeWorld, IMutableWorld { { const execFn = this.executionFunction; const cleanUp = () => { - this.pda.clear(); + this.pda.clear(this.transitionActions); { let syncPoint; @@ -304,6 +325,17 @@ export class RuntimeWorld implements IRuntimeWorld, IMutableWorld { this.shouldRunSystems = false; } + // @ts-ignore + [Symbol.dispose]() { + { // unset the context for all systems + let system; + + for (system of this.systems) { + system.unsetRuntimeContext(this); + } + } + } + /// **************************************************************************************************************** /// Entities @@ -318,13 +350,6 @@ export class RuntimeWorld implements IRuntimeWorld, IMutableWorld { public removeEntity = removeEntity; - /// **************************************************************************************************************** - /// Events - /// **************************************************************************************************************** - - protected registerSystemAddResourceEvent = registerSystemAddResourceEvent; - - /// **************************************************************************************************************** /// Groups /// **************************************************************************************************************** diff --git a/src/world/runtime/runtime-world_events.ts b/src/world/runtime/runtime-world_events.ts deleted file mode 100644 index b37fe41..0000000 --- a/src/world/runtime/runtime-world_events.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {RuntimeWorld} from "./runtime-world.ts"; -import {SimECSSystemAddResource} from "../../events/internal-events.ts"; - -export function registerSystemAddResourceEvent(this: RuntimeWorld) { - this.eventBus.subscribe(SimECSSystemAddResource, event => { - this.systemResourceMap.set(event.system, { - paramName: event.paramName, - resourceType: event.resource, - }); - }); -} diff --git a/src/world/runtime/runtime-world_resources.ts b/src/world/runtime/runtime-world_resources.ts index 48571e4..a3102e5 100644 --- a/src/world/runtime/runtime-world_resources.ts +++ b/src/world/runtime/runtime-world_resources.ts @@ -61,18 +61,4 @@ export async function replaceResource( type, resourceObj, )); - - { // Also replace the resources for all systems - let system, resourceDesc; - for ([system, resourceDesc] of this.systemResourceMap) { - if (resourceDesc.resourceType.name == type.name) { - (system[systemRunParamSym] as TSystemParameterDesc)[resourceDesc.paramName] = resourceObj; - await this.eventBus.publish(new SimECSSystemReplaceResource( - system, - resourceDesc.paramName, - resourceDesc.resourceType, - )); - } - } - } } diff --git a/src/world/runtime/runtime-world_states.ts b/src/world/runtime/runtime-world_states.ts index 3492bde..634b04f 100644 --- a/src/world/runtime/runtime-world_states.ts +++ b/src/world/runtime/runtime-world_states.ts @@ -7,7 +7,7 @@ import {EventBus} from "../../events/event-bus.ts"; export async function popState(this: RuntimeWorld): Promise { unsubscribeEventsOfSchedulerSystems(this.eventBus, this.currentScheduler!); - await this.pda.pop()?.deactivate(this.transitionWorld); + await (await this.pda.pop())?.deactivate(this.transitionWorld); const newState = this.pda.state; if (!newState) {