@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
320 lines (231 loc) • 7.93 kB
JavaScript
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;
}
}
}
}