Product front door for author-facing plugin contracts. Composition (manifests, roles, packaging) lives in Plugins and plugin-system design. Normative contracts and residual deferred work: Plugin API design.
One product name; three domains
There is no runtime hi.plugin mega-vtable. Plugins compose into the game module, then call typed SDK surfaces:
| Domain | Surface | Threading | Lifetime |
|---|---|---|---|
| Editor | hi.editor | Editor main / UI thread only | Game-module generation; chrome frozen after freezeChrome |
| Gameplay | hikari_game + lifecycle Plugin + hi.settings | Game thread (and existing component rules) | Process / scene as today |
| Rendering | hi.render() | Register on game thread (onLaunch); frame callbacks on render thread | Registration for module generation; frame resources frame-scoped |
Hikari Plugin API (product name + docs)
┌─────────────────────────────────────┐
│ Source composition (unchanged) │
│ hikari.plugins.json / plugin.json │
└──────────────┬──────────────────────┘
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
hi.editor hikari_game hi.render
Editor Gameplay RenderingComposition in one page
- Enable a package in project
hikari.plugins.json. - Package
hikari.plugin.jsondeclares modules by role (runtime,editor,developer,third_party). - Build composes Zig sources into the game module; native link and staging inputs come from generated
hikari-plugin-artifacts.jsononly. - No runtime Zig package loader.
Full steps: Plugins guide. Invariants: plugin-system design.
Choose your domain
| Need | Start here |
|---|---|
| Actions, menus, panels, scene edits | Editor · UI and editor SDK |
| Entities, tick, libraries, settings, open components | Gameplay |
| Temporal reconstruction; typed post-process slots | Rendering · Rendering system |
Editor
Chrome
In an editor role module, export registerEditor and always call api.check() first:
const hi = @import("hikari_game");
const editor = hi.editor;
pub fn registerEditor(api: *const editor.Api) void {
api.check() catch return;
// registerAction / menus / toolbar / panels / surfaces …
}ActionDesc.invoke always receives host-filled ActionContext (playing, has_project, can_author, …). There is no register_command / CommandFn path.
Working sample: src/games/example/plugins/example_capability/editor/root.zig.
Scene transactions
Main-thread only. Use hi.editor.scene() → buffer-until-commit EditTransaction → shared host applyBatch (one undo label). Do not hold raw SceneDocument* / EditorApp*.
Builtin-only sketch (matches the example plugin demo):
fn onCreateDemoLight(user: ?*anyopaque, ctx: *const editor.ActionContext) callconv(.c) void {
_ = user;
if (ctx.playing or !ctx.has_project or !ctx.can_author) return;
const scene = editor.scene() orelse return;
var edit = scene.beginEdit(.{
.label = "Create Demo Light",
.expected_revision = scene.revision(),
}) catch return;
defer edit.cancel(); // no-op after successful commit
const actor = edit.createActor(.{
.name = "Demo Light",
.archetype = "light", // engine ids are lowercase ("light", "empty", …)
}) catch return;
edit.setField(actor, "light.intensity", 0, .{ .number = 5.0 }) catch return;
edit.setTransform(actor, .{ .position = .{ 0, 3, 0 } }) catch return;
edit.commit() catch return;
// Selection is not an undoable document op — post-commit side effect only.
scene.setSelection(&.{actor}) catch {};
}| Rule | Behavior |
|---|---|
| Live document | Unchanged until successful commit |
| Ops | Buffered until commit; max 64 (shared with MCP) |
| Mid-tx reads | See committed state only |
setSelection | Post-commit side effect; not in the undo stack |
| Archetype ids | Catalog ids ("light"), never UI labels ("Light") |
Shared mutation plane with MCP: Plugin API design §3.1, Editor MCP.
Gameplay
Plugins use the same hikari_game surface as project game code:
- Lifecycle methods on
Plugin(create/destroy/onLaunch/onTick/ scene hooks) via compose glue — see Lifecycle hooks. - Entities and components under runtime modules (
entities/componentspaths on the runtime module). - Explicit native shared libraries via
hi.shared_libwhen declared as stage-only middleware. - Optional compile-time deps via
hikari_plugin_options. - Package settings via
hi.settings(below).
Working open-component sample: example_capability registers example_marker (attachable) with document-backed inspector fields (mutation plane / undo). Canonical skeleton: plugins/_template/.
Package settings (hi.settings)
Project-owned key/value storage for enabled packages. Does not dirty hikari.project.json.
| Item | Value |
|---|---|
| Path | <project>/.engine/plugin-settings/<package_id>.json |
| API | hi.settings — not hi.plugin_* (no service bus) |
| Values | JSON object of bool / number / string keys |
| Boundary | Typed host API; plugin code never receives filesystem access |
const package_id = "com.hikari.example_capability";
const radius = hi.settings.getFloat(package_id, "radius") orelse 5.0;
const enabled = hi.settings.getBool(package_id, "enabled_feature") orelse true;
try hi.settings.setFloat(package_id, "radius", 8.0);
try hi.settings.setBool(package_id, "enabled_feature", true);
// Strings: getStringAlloc(allocator, …) returns owned copy; setString writes.Optional schema in the package manifest (drives Project Settings → Plugins package-settings rows when present):
"settings": {
"schema_version": 1,
"keys": [
{ "key": "enabled_feature", "type": "bool", "default": true, "label": "Enable feature" },
{ "key": "radius", "type": "float", "default": 5.0, "min": 0.1, "max": 100.0 }
]
}Runtime read/write does not require the schema. Schema keys and file-only keys under .engine/plugin-settings/ both surface in Project Settings (immediate write; does not dirty Save / hikari.project.json).
Open components and the inspector
Attachable components from game/plugin *_component.zig files register by name. Authors Add Component by registry name; defineComponent reflects supported Data fields into the same normalized authoring schema used by engine components. Data.metadata.properties supplies optional labels, tooltips, ranges, ordering, visibility, and widget overrides. Numeric scalars/vectors of any supported arity, booleans, colors, text, ordinary/optional enums, and typed resources all use the generic inspector path. Resource fields share picker, typed-path, remove, and drag/drop behavior. Runtime field offsets remain a separate generated table for fast live access.
- One write path (undoable): inspector rows,
hi.editor.scene(), and MCPset_fieldall usecomponent.fieldpaths through the shared mutation plane (scene_mutation→ document payload). No live-column /localScalarpoke for open-component fields. - Live world: document apply re-spawns the actor so decode reloads the new payload; save also runs
syncOpenComponentPayloadsas a safety net for any other column drift. - Undo: begin/write/commit on inspector widgets joins one history transaction (same as builtins).
- Policy:
hidden,read_only, field kind/arity, enum tags, and numeric bounds are enforced at the shared mutation boundary, not only by disabled inspector controls. - Runtime cost: schema rows are static per component declaration. There is no per-instance property storage, allocation, lookup map, or tick work; standalone gameplay still uses typed rows and generated offset accessors.
Automated place → edit → save for open components: document tests in editor/scene_document/tests.zig (place edit save open component document round-trip); binding undo in editor/property_binding.zig.
Typed plugin-to-plugin dependencies
Already legal via module deps + @import. Rules:
| Rule | Detail |
|---|---|
| Import name | Dependents @import the module import name from hikari.plugin.json (e.g. hikari_plugin_example), never package id strings at runtime |
| Optional | "optional": true on a dep + compile-time hikari_plugin_options.enabled("…") |
| Visibility | "public": true|false follows the resolver — private deps are not re-exported |
| Roles | Runtime may depend on runtime / third_party only; editor may depend on runtime / editor / third_party |
Package id is for enablement, settings storage, and packaging — not a runtime service lookup key.
Rendering
| Extension | Status | Register |
|---|---|---|
| Temporal reconstruction | Shipped (native escape hatch) | hi.render().registerReconstructionProvider in onLaunch |
| Typed post-process passes | Shipped (P1 v1) — five fixed slots + host tint | hi.render().registerPostProcessPass in onLaunch |
Samples: metalfx_temporal (macOS), fsr4 (Windows), host_tint (after_tonemap host fullscreen tint). Details: dual resolution, plugin post-process.
Threading and lifetime
| Call site | Thread | Notes |
|---|---|---|
registerEditor, action invoke, panel update, SceneApi / EditTransaction | Editor UI / main | Never from game or render thread |
Lifecycle onTick / scene hooks | Game | Same rules as game subsystems |
registerReconstructionProvider / registerPostProcessPass | Game (onLaunch) | |
| Reconstruction / PP frame callbacks | Render | Frame-scoped opaque resources only |
show_message / setControl* | Editor UI | Copy api.* during registerEditor; do not keep the registration stack pointer |
Game-module reload rebinds callbacks; structural chrome does not change after freezeChrome.
Related docs
| Doc | Role |
|---|---|
| Plugins | Composition, manifests, lifecycle, surfaces table |
| Plugin API design | Normative contracts and key decisions |
| Plugin system design | Source composition invariants |
| UI and editor | hi.editor ABI table |
| Rendering | Reconstruction and PP |