UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

320 lines (231 loc) • 7.93 kB
import { BaseProcess } from "../../core/process/BaseProcess.js"; import { ProcessState } from "../../core/process/ProcessState.js"; import { EnginePlugin } from "./EnginePlugin.js"; import { PluginReferenceContext } from "./PluginReferenceContext.js"; import { isSubclassOf } from "./isSubclassOf.js"; import { assert } from "../../core/assert.js"; export class EnginePluginManager extends BaseProcess { /** * @private * @type {Map<Class, PluginReferenceContext>} */ __plugins = new Map(); /** * * @type {Engine} */ engine = null; /** * * @type {number} * @private */ __version = 0; /** * * @returns {EnginePlugin[]} * @private */ __getActivePluginInstances() { const result = []; for (const [PluginClass, ctx] of this.__plugins) { if (ctx.referenceCount <= 0) { continue; } result.push(ctx.instance); } return result; } /** * * @returns {PluginReferenceContext[]} * @private */ __getActivePluginContexts() { const result = []; for (const [PluginClass, ctx] of this.__plugins) { if (ctx.referenceCount <= 0) { continue; } result.push(ctx); } return result; } /** * * @param {PluginReferenceContext} context * @param {ProcessState} state * @returns {Promise<void>} * @private */ async __transition_plugin_to_state(context, state) { const plugin = context.instance; if (state === ProcessState.Initialized || state === ProcessState.Running) { const dependencies = plugin.dependencies; // resolve dependencies for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; const dependency_context = this.__plugins.get(dependency); const dependency_state = dependency_context.instance.getState().getValue(); if (dependency_state !== ProcessState.Running) { if (state === ProcessState.Running) { await dependency_context.transition(state, this.engine); } else if (dependency_state !== ProcessState.Initialized) { // initialized state await dependency_context.transition(state, this.engine); } } } } await context.transition(state, this.engine); } /** * * @param {ProcessState} state * @returns {Promise<void>} * @private */ async __transition_active_plugins_to(state) { let v0; do { v0 = this.__version; /** * * @type {PluginReferenceContext[]} */ const contexts = this.__getActivePluginContexts(); const context_count = contexts.length; const promises = []; for (let i = 0; i < context_count; i++) { const context = contexts[i]; const promise = this.__transition_plugin_to_state(context, state); promises.push(promise); } await Promise.all(promises); } while (v0 !== this.__version); } /** * * @param {PluginReferenceContext} ctx * @private */ __handle_last_plugin_reference_release(ctx) { this.__version++; // remove plugin reference const PluginClass = ctx.getKlass(); const removed = this.__plugins.delete(PluginClass); if (!removed) { return; } ctx.dispose(); } /** * * @param {Engine} engine */ initialize(engine) { this.engine = engine; super.initialize(); const enginePlugins = this.__getActivePluginInstances(); const n = enginePlugins.length; for (let i = 0; i < n; i++) { const plugin = enginePlugins[i]; plugin.initialize(engine); } } finalize() { super.finalize() const enginePlugins = this.__getActivePluginInstances(); const n = enginePlugins.length; for (let i = 0; i < n; i++) { const plugin = enginePlugins[i]; plugin.finalize(); } } async startup() { super.startup(); return this.__transition_active_plugins_to(ProcessState.Running); } async shutdown() { super.shutdown(); return this.__transition_active_plugins_to(ProcessState.Finalized); } /** * @template T * @param {Class<T>} PluginClass * @returns {Promise<Reference<T>>} */ async acquire(PluginClass) { // check plugin class hierarchy if (!isSubclassOf(PluginClass, EnginePlugin)) { throw new Error(`PluginClass is not a subclass of EnginePlugin`); } let ctx = this.__plugins.get(PluginClass); let reference; if (ctx === undefined) { this.__version++; // context not found, create it const instance = new PluginClass(); ctx = new PluginReferenceContext(instance, PluginClass); // monitor the context ctx.on.lastReleased.add(this.__handle_last_plugin_reference_release, this); // all dependencies acquired, register the plugin this.__plugins.set(PluginClass, ctx); // get reference to make sure that the context doesn't come up as having no references reference = ctx.getReference(); // TODO address cyclic dependencies, these can cause a deadlock // acquire dependencies const dependency_refs = await this.acquireMany(instance.dependencies); ctx.dependency_references.addAll(dependency_refs); const manager_state_object = this.getState(); const manager_state_value = manager_state_object.getValue(); // bring the plugin into proper state const engine = this.engine; await ctx.transition(manager_state_value, engine); } else { reference = ctx.getReference(); await ctx.synchronize(); } return reference; } /** * * @param {Class[]} PluginClasses * @returns {Promise<Reference[]>} */ async acquireMany(PluginClasses) { assert.isArray(PluginClasses, 'PluginClasses'); const promises = PluginClasses.map(this.acquire, this); return await Promise.all(promises); } /** * @template T * @param {Class<T>} PluginClass * @returns {T|undefined} */ getPlugin(PluginClass) { const plugins = this.__getActivePluginInstances(); const n = plugins.length; for (let i = 0; i < n; i++) { const enginePlugin = plugins[i]; if (enginePlugin.constructor === PluginClass) { return enginePlugin; } } } /** * @template T * @param {string} id * @returns {EnginePlugin|T|undefined} */ getById(id) { const plugins = this.__getActivePluginInstances(); const n = plugins.length; for (let i = 0; i < n; i++) { const enginePlugin = plugins[i]; if (enginePlugin.id === id) { return enginePlugin; } } } }