Platform

Plugins & options

How to load and manage engine plugins at runtime, and how to define and persist hierarchical reactive options with auto-generated UI.

Meep extends the core engine through two parallel systems: plugins, which add services to a running engine and declare their own dependencies, and options, a hierarchical reactive settings tree that can persist to storage and render into a built-in UI panel.

Plugins

What a plugin is

An EnginePlugin is a lifecycle object managed by engine.plugins (EnginePluginManager). Each plugin has three steady states — initialized, running, and finalized — and an async transition between each:

MethodCalled when
initialize(engine)plugin first acquired; receives the Engine reference
startup()engine starts (or immediately, if the engine is already running)
shutdown()last reference to the plugin is released

Plugins can declare dependencies (an array of other plugin classes). The manager initializes dependencies first and shuts them down last, so a plugin can safely call engine.plugins.getPlugin(OtherPlugin) in its own startup.

import { EnginePlugin } from "@woosh/meep-engine/src/engine/plugin/EnginePlugin.js";

class AudioPlugin extends EnginePlugin {
    constructor() {
        super();
        this.id = "audio";
    }

    get dependencies() {
        return [];   // list other EnginePlugin classes here
    }

    async startup() {
        await super.startup();
        // set up services on this.engine
    }

    async shutdown() {
        await super.shutdown();
    }
}

Acquiring a plugin

engine.plugins.acquire(PluginClass) is the only way to obtain a plugin instance. It returns a Promise<Reference<T>>:

const ref = await engine.plugins.acquire(AudioPlugin);
const plugin = ref.getValue();     // the AudioPlugin instance

// when you no longer need it:
ref.release();

The manager is reference-counted. The first call to acquire instantiates the plugin and brings it to the running state. Subsequent calls with the same class increment the reference count and return a new Reference to the same instance. When the count reaches zero, the plugin is shut down and removed.

acquireMany(classes) is the parallel form — it calls acquire for each class and resolves when all transitions are complete.

Registering plugins at configuration time

The more common pattern is to declare plugins in the EngineConfiguration callback passed to EngineHarness.bootstrap. That runs acquire for each declared plugin before the engine starts:

import { EngineHarness }   from "@woosh/meep-engine/src/engine/EngineHarness.js";
import { AmbientOcclusionPostProcessEffect }
    from "@woosh/meep-engine/src/engine/graphics/ecs/fx/ao/AmbientOcclusionPostProcessEffect.js";

const engine = await EngineHarness.bootstrap({
    configuration: (config, engine) => {
        config.addPlugin(AmbientOcclusionPostProcessEffect);
    }
});

Locating an active plugin

// by class (returns the instance or undefined)
const ao = engine.plugins.getPlugin(AmbientOcclusionPostProcessEffect);

// by string id
const audio = engine.plugins.getById("audio");

Options

The tree

engine.options is an OptionGroup — the root of the settings tree. The tree is a hierarchy of groups (branches) and leaf Option nodes. Each node has a string id; the path from the root uniquely identifies it.

import { OptionGroup } from "@woosh/meep-engine/src/engine/options/OptionGroup.js";

// engine.options is already an OptionGroup at engine startup.
// Add a sub-group and two options:
const graphics = engine.options.addGroup("graphics");

graphics.add(
    "shadowQuality",
    () => shadowQuality,           // read
    (v) => { shadowQuality = v; }, // write
    { values: ["low", "medium", "high"] }
);

graphics.add(
    "fov",
    () => camera.fov,
    (v) => { camera.fov = v; },
    { min: 30, max: 120 }
);

addGroup(id) returns the new OptionGroup so you can chain child additions. add(id, read, write, settings) attaches a leaf Option and also returns the group, so further .add(…) calls can be chained on the same group.

Option settings

The settings object on a leaf option controls both persistence and optional UI rendering:

Setting keyTypeEffect
transientbooleantrue — excluded from toJSON / fromJSON serialization
minnumberLower bound hint for the auto-generated slider
maxnumberUpper bound hint
valuesstring[] / number[]Allowed discrete values — renders as a dropdown

Reactive writes

Every Option exposes an on.written signal that fires after the value is successfully written, and an on.writeFailed signal that fires if the write throws or rejects:

const fov = engine.options.resolve(["graphics", "fov"]);

fov.on.written.add((v) => console.log("FOV changed to", v));
fov.write(75);   // fires on.written with 75

resolve(path) walks the tree by segment array and returns the matching group or option, throwing if any segment is not found.

Persistence

OptionGroup.attachToStorage(key, storage) wires the whole subtree to a storage backend (the engine’s engine.storage). On attach it loads and deserializes any previously stored values; from that point every subsequent write serializes the tree back to storage automatically:

// The engine does this internally for its own options:
await engine.options.attachToStorage("myapp.options", engine.storage);

Transient options are excluded. toJSON serializes the tree to a plain object; fromJSON restores it without triggering on.written.

Auto-generated UI

OptionsView renders any OptionGroup into a dat.GUI panel. It needs the options tree and a Localization instance (option labels come from localization keys of the form system_option.<path>, e.g. system_option.graphics.fov):

import OptionsView from "@woosh/meep-engine/src/engine/options/OptionsView.js";

const view = new OptionsView({
    options: engine.options,
    localization: engine.localization,
    // inclusions: [["graphics"]]  // optional: only show a sub-tree
});

document.body.appendChild(view.el);
view.link();

Pass inclusions as a list of path arrays to limit which branches appear. When omitted, every non-transient option in the tree is shown. The panel updates immediately when any option is written externally, and localizes its own labels whenever the locale changes.

Traversing options in code

engine.options.traverseOptions((option) => {
    console.log(option.computePath().join("."), "=", option.read());
});

computePath() returns the segment array from the root to the node.