Appearance
Authoring a capability package
Capabilities extend the engine without bloating the kernel. Two shipped packages are the templates:
@bendyline/molen-physics-rapier— a kernel-only plugin (no client half).@bendyline/molen-terrain— a kernel + client capability, split into subpath exports@bendyline/molen-terrain/kerneland@bendyline/molen-terrain/client(it has no.export).
The pattern
Pick your halves. Pure simulation → kernel-only (like physics-rapier). Needs rendering → add a
/clienthalf that depends on@bendyline/molen-client(like terrain). Never let the kernel half import three.js or the DOM.Install onto a world. Export an
install*(world, opts?)that registers systems and (if stateful for snapshots) a snapshot provider. Give the system a name and unregister both in yourdispose()(world.removeSystem(name),world.unregisterSnapshotProvider(name)):tsexport function installMyThing(world: World, opts?: MyOpts): void { world.addSystem((w, ctx) => { /* ... */ }, { phase: 'physics', name: 'my-thing' }); // Opaque state that the legible ECS can't hold (solver warm-start, etc.): world.registerSnapshotProvider('my-thing', () => save(), (blob) => load(blob)); }installKinematics,installCharacterController, andinstallScriptingare concrete examples.Define components in
@bendyline/molen-schema. Register their data shape somolen validateandmolen componentsknow about them (and typos get did-you-mean):tsimport { registerComponent } from '@bendyline/molen-schema'; import { z } from 'zod'; registerComponent('myBody', z.looseObject({ mass: z.number() }), { description: 'My rigid body.', owner: 'my-capability', examples: [{ mass: 1 }], });Register any file formats with
registerSchema(kind, zod, meta)— and add ameta.validate(data)for cross-field checks Zod can't express (cycles, dangling refs); see how@bendyline/molen-materialsvalidates matgraph/pixelgrid.Keep determinism. Kernel-side math goes through
dmath(thecheck-dmathlint enforces it); route randomness through the world RNG. WASM stays behind anawait init()and never enters the kernel core (it lives in the capability/tooling package — see physics-rapier).Reach scripts. Export a
<thing>ScriptApi(handle): object(a frozen bag of functions — seerapierScriptApi,terrainScriptApi) and hand it tobuildWorldthrough thephysics/terrainhooks orscriptExtensions; it appears asmolen.<name>.*in every scene script. Tooling wires the built-in capabilities inprepareSceneBuilder; your own host does the same withbuildWorld(manifest, setup, { scriptExtensions: { mine: myScriptApi(handle) } }).A capability that a scene opts into through its own manifest block is a
capabilitieshook,(world, manifest) => ({ mine: api }), that returnsundefinedwhen the block is absent, so scenes without it pay nothing.ambientCapability()from@bendyline/molen-ambient/kernelis one: it reads the scene'sambientblock (see ambient life).Ground and reads. A capability that owns a height source registers it with
installTerrain(world, field)(any object withsampleHeight+normalAt) so the character controller and kinematics find it. Component reads are frozen stored objects: compare references to detect change (w.get(id, C) !== last), never mutate them.Surface it to agents. If you add an operation, put the logic in
@bendyline/molen-tooling'ssrc/ops/, expose it on both the CLI and MCP server, and add anOPS_CATALOGentry somolen describe/describe_opdocument it. Add a guide here and link it fromllms.txt.
Conventions checklist
- Package name
@bendyline/molen-<thing>; kernel/client halves as subpath exports, no.export if it has both halves. isolatedDeclarationson (every export explicitly typed); ESM-only; build with tsdown.- Kernel halves run
node ../../scripts/check-dmath.mjs srcin theirlintscript. - Ship a smoke test in each target environment for any WASM dep.