Hikari plugins are source-composed packages. Enabling a plugin adds Zig modules and native middleware declarations to the project game module build; it does not install a runtime Zig plugin loader.
For composition design and invariants, see Source-composed plugin system. For the author-facing product (editor scene API, render extensions, gameplay polish), start at the Plugin API guide; normative design: Plugin API design.
Plugin surfaces
Product name: Hikari Plugin API. Contracts remain federated and typed — there is no runtime hi.plugin mega-vtable. Plugins compose into the game module, then call typed SDK paths (hi.editor, hikari_game, hi.render). Related bounds: design non-goals; front door: Plugin API guide; design: Plugin API design.
| Domain | Concern | Where | What you get |
|---|---|---|---|
| — | Enable / compose | this guide + design | manifests, roles, packaging |
| Gameplay | Runtime APIs | hikari_game | same as game code |
| Gameplay | Package settings | hi.settings + Plugin API | .engine/plugin-settings/<id>.json |
| Gameplay | Lifecycle | Lifecycle hooks + sdk/src/plugin_compose.zig | create / tick / scene / … |
| Editor | Chrome | hi.editor + UI and editor | actions, menus, toolbar, panels, surfaces |
| Editor | Scene authoring | hi.editor.scene() + Plugin API | transactional edits, one undo, revision checks |
| Rendering | Temporal upscale | reconstruction provider + rendering | register in onLaunch |
| Rendering | Post-process | hi.render().registerPostProcessPass + rendering | fixed slots, opaque refs, host encoder tint |
| Gameplay | Explicit native load | hi.shared_lib + native variants | stage-only shared libs |
| Gameplay | Optional deps | hikari_plugin_options | compile-time enabled |
Maintenance: when seams multiply, add a row here. Never consolidate federated plugin runtime surfaces into one API for cleanup alone — keep seams named and typed; a unified hi.plugin is not a tidy-up goal.
Layout
Project:
src/games/example/
hikari.project.json
hikari.plugins.json
src/
root.zig
session.zig
plugins/
example_capability/
hikari.plugin.json
runtime/root.zig
editor/root.zighikari.plugins.json enables package directories:
{
"kind": "com.hikari.plugins",
"version": 1,
"plugins": [
{
"path": "plugins/example_capability",
"enabled": true
}
]
}Each package has its own hikari.plugin.json:
{
"kind": "com.hikari.plugin",
"version": 1,
"id": "com.hikari.example_capability",
"package_version": "1.0.0",
"display_name": "Example Capability",
"sdk_compat": "1",
"modules": [
{
"import": "hikari_plugin_example",
"role": "runtime",
"source": "runtime/root.zig"
},
{
"import": "hikari_plugin_example_editor",
"role": "editor",
"source": "editor/root.zig",
"deps": [
{ "import": "hikari_plugin_example", "public": true }
]
}
]
}version is the manifest file format. sdk_compat is the Hikari SDK generation your Zig sources are written against — it is optional, but when present it must match the SDK exactly, and a mismatch fails the build rather than compiling your package against an API it does not target. Optional supported_os values (windows, macos, linux, any) advertise host compatibility to Project Settings. It is a UI hint rather than a build gate: a cross-platform package may remain enabled while its runtime stays inert on an unsupported host.
Managing plugins in the editor
Project Settings → Plugins lists every package it can find: those listed in hikari.plugins.json, plus any package sitting under plugins/ that nobody has enabled yet. Each card shows the display name, id, version, and path; capability chips derived from the package's module roles (Runtime, Editor UI, Entities, Native, Middleware); and what changing it costs — Reloads with the game module, Stays loaded until the editor restarts (process_pinned), or Restart required to change (restart_required).
A package the build would reject is shown dimmed with the reason (invalid manifest, unreadable manifest, or built for a different SDK generation) and cannot be switched on — only off. A package whose supported_os excludes the current host is marked Incompatible with this platform; if it was already enabled, the editor preserves that project setting so its inert cross-platform wrapper can keep the project portable.
Toggling is an ordinary Project Settings edit: nothing is written until Save, Revert restores the scanned state, and the footer reports the reopen boundary, because enabling or disabling a package recomposes the game module. Save writes hikari.plugins.json — the same file you would hand-edit, and still the only project-level source of truth for enablement.
The page reads manifests for display only. The resolver in sdk/build/plugins/ remains the authority on what actually composes, so the build is still what tells you a graph is invalid.
Create a plugin package
Canonical skeleton: src/games/example/plugins/_template/ (copy, rename ids/imports, enable in hikari.plugins.json).
- Create a package directory under the project, usually
plugins/<name>/. - Add
hikari.plugin.json. - Add a runtime Zig root for game-facing code.
- Add optional editor Zig root for authoring surfaces.
- Enable the package from the project
hikari.plugins.json.
Runtime module example:
const std = @import("hikari_std");
const hi = @import("hikari_game");
pub fn hashBytes(bytes: []const u8) u64 {
return std.hash.Wyhash.hash(0, bytes);
}
pub const Plugin = struct {
allocator: std.mem.Allocator,
pub fn create(allocator: std.mem.Allocator) !*Plugin {
const self = try allocator.create(Plugin);
self.* = .{ .allocator = allocator };
return self;
}
pub fn destroy(self: *Plugin) void {
self.allocator.destroy(self);
}
pub fn onLaunch(self: *Plugin) void {
_ = self;
}
pub fn onTick(self: *Plugin) void {
_ = self;
}
};Product Zig rules still apply: import hikari_std, not Zig std directly.
Use a plugin from game code
The module import field becomes the Zig import name:
const example_plugin = @import("hikari_plugin_example");
pub fn update() void {
const h = example_plugin.hashBytes("hello");
_ = h;
}The plugin is compiled into the same game module generation as the project code. There is no runtime lookup by package id.
Optional dependency presence is a compile-time query:
const plugin_options = @import("hikari_plugin_options");
const has_steam = plugin_options.enabled("hikari_plugin_steam");enabled answers for this product, not for the manifest: it is true only for modules the current build actually composes. An editor module reports false in a game build, and a third_party module reports false when no composed module depends on it. That makes it safe to gate an optional @import on.
Every source plugin receives only hikari_game, hikari_std, hikari_plugin_options, and its declared module dependencies.
Lifecycle hooks
If the runtime root exports pub const Plugin = struct { ... }, generated glue calls optional methods in dependency order:
| Hook | When |
|---|---|
create(allocator) | Game module/plugin container creation. |
destroy() | Reverse order during shutdown or failed create cleanup. |
onLaunch() | Runtime launch. |
onWorldAttach(WorldScope) | A World is fully bound, before its scene actors instantiate. The subsystem is one instance shared by every World — the editor attaches the authored World and the Play copy at the same time — so key per-World state by scope.id rather than assuming a singleton. scope.role is .editor, .play, or .standalone. |
onWorldDetach(WorldScope) | Reverse order immediately before that World's final teardown, with HostApi still bound to it. |
onTerminate() | Runtime terminate. |
onTick() | Per game tick. |
onPlayEnded() | Editor Play -> Edit stop path. |
onSceneWillUnload() | Scene unload notification. |
onSceneLoaded(info: hi.SceneInfo) | Scene loaded. |
onSceneLayerLoaded(key) | Additive layer presented — its content is live. |
onSceneLayerReady(key) | A manual layer reached its gate and awaits presentSceneLayer. |
onSceneLayerWillUnload(key) | Additive layer unload. |
onSceneReadyStage(key, stage) | Scene readiness progress. |
Dependencies are called first; shutdown is reverse order. If two modules are otherwise unordered, the resolver orders by package id then import name.
Module roles
| Role | Use |
|---|---|
runtime | Game-facing API, entities, lifecycle hooks. Included in game products and editor game-module builds. |
editor | Editor menus/panels/actions for the project. Included only when editor contribution is enabled. |
developer | Local development helpers. Not part of product composition by default. |
third_party | Native middleware declaration only; no Zig source. |
Dependency rules:
- Runtime may depend on runtime and third_party modules only.
- Editor may depend on runtime, editor, and third_party modules.
- Developer may depend on anything, but is not selected for product builds by default.
- Third_party may depend only on third_party.
entities and components are valid only on runtime modules.
register_editor is valid only on editor modules.
A package's entities directory is generated into its own module and reaches the game root as hikari_plugin_entities. The project's own sources are discovered wholesale into content_manifest.gen.zig (any path under src/). Both are walked at content registration, so plugin archetypes register alongside project archetypes.
A package's optional components directory follows the same pattern and reaches
the game root as hikari_plugin_components. Plugin files must end in
_entity.zig / _component.zig (each is a separate module root outside the
package sandbox); manifests are sorted. Plugin components register before
project and plugin archetypes.
Editor contributions
Editor modules should use the existing hi.editor contribution ABI. Keep editor-only code gated so standalone game products never analyze it.
Pattern:
const hi = @import("hikari_game");
const runtime = @import("hikari_plugin_example");
pub fn registerEditor(api: *const hi.editor.Api) void {
// Always first: refuses a host whose table this SDK generation cannot read.
api.check() catch return;
_ = runtime.hashBytes("editor");
// Register actions, menus, toolbar items, panels, and surface controls
// through the checked accessors (`api.registerAction`, `api.beginSurface`, …).
}Full capability table and isolation rules: Game editor SDK. Working sample (chrome + builtin scene transaction demo): src/games/example/plugins/example_capability/editor/root.zig. Product overview: Plugin API.
Rules:
- Call
api.check()before anything else (api_version4); it returnserror{IncompatibleEditorApi}on an older host and installshi.editor.scene()generation state. Entry points added afterapi_version1 are nullable; reach them through accessors so a missed version check fails cleanly. - The
*const Apiis a stack temporary. Copy the table (api.*) if you needshow_message/set_control_*later; those stay valid for the life of the generation, but are editor-UI-thread only. - Register structural chrome at create time only (actions, toolbar items, surface controls).
- Host owns retained chrome, menus, toolbars, docks, strings, and accessibility.
- Plugin ids/labels are copied or interned by the host.
- On game-module reload, callbacks rebind; structural panels/menus/toolbars/surfaces do not change after
freezeChrome. - Prefer
registerAction(ActionInvokeFnwith host-filledActionContext) + menu/toolbar entries that reference the action id; usebeginSurface/addControl/endSurfacefor simple panel bodies (labels, buttons, toggles, row/column markers). That surface API is the design’sSurfaceBuildermodel — a POD command recorder, not retained-widget ownership. There is noregister_command/CommandFndual path. - Scene authoring:
hi.editor.scene()→ buffer-until-commitEditTransaction→ shared hostapplyBatch(one undo label). Do not hold rawSceneDocument*/EditorApp*. - Use host actions when they exist; custom actions use open string ids and plugin callbacks.
- Do not call host
contrib.zig/bindFillor keep retained widget pointers from plugin code.
Native middleware variants
Declare native SDKs under native variants. The build selects the best match for OS, arch, config, and product mode.
Static library:
{
"import": "hikari_plugin_foo_native",
"role": "third_party",
"native": [
{
"os": "macos",
"arch": "aarch64",
"libraries": [
{
"linkage": "static",
"static_lib": "native/macos/libfoo.a",
"reload_policy": "generation_local"
}
],
"licenses": [
{ "path": "LICENSE.txt", "dest": "licenses/foo.txt" }
]
}
]
}Windows linked shared library:
{
"linkage": "shared",
"load_mode": "linked",
"import_lib": "native/windows/foo.lib",
"shared_lib": "native/windows/foo.dll",
"reload_policy": "process_pinned"
}Explicitly loaded shared library:
{
"linkage": "shared",
"load_mode": "explicit",
"shared_lib": "native/windows/foo_optional.dll",
"reload_policy": "restart_required"
}Rules:
static_libis only forlinkage: "static".import_libis a link input forshared+linked(mainly Windows).shared_libis the runtime file staged beside the game module (dest= basename).shared+explicitis stage-only and must not declareimport_lib; same adjacentdestrule; open viahi.shared_lib.runtime_filesandlicensesuse author-relativedestunder the same game-module root.
Ship layout (game product, editor install tree, and each hot-reload generation):
game.dll | libgame.dylib
vendor_loader.dll # linked / explicit shared_lib
licenses/... # license dests
hikari-plugin-artifacts.jsonNo PATH / DYLD_* / LD_LIBRARY_PATH for players or tools. Zig installs each artifact once under the module install subdir; Kaji consumes the generated link closure and copies the same dest map into the product root.
Loader / dependency resolution:
| Platform | Mechanism |
|---|---|
| Windows | Host opens game.dll with LoadLibraryEx + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR so dependents resolve from the module directory. Explicit loads in hikari_game.shared_lib use the same flags for absolute paths. |
| macOS / Linux | dlopen plus link-time @loader_path / $ORIGIN rpath on the game module so dependents resolve beside the loaded image. |
Reload policies:
| Policy | Use |
|---|---|
generation_local | Safe with game-module reload. |
process_pinned | Loaded once for the process; editor restart not usually required for code using the same binary. |
restart_required | Changing or rebinding requires a full editor/product restart. |
Build and packed-editor workflow
Normal game build:
bin/kaji/kaji game --workspace="$PWD" --project="$PWD/src/games/example" --type=dynamic --config=debugEditor with project:
bin/kaji/kaji editor --workspace="$PWD" --project="$PWD/src/games/example" --type=dynamic --config=debug --runPacked editor that can compile project plugins offline:
bin/kaji/kaji editor --workspace="$PWD" --type=dynamic --config=release \
--vendor:zig=fetch+bundle --vendor:engine=bundle --package=bundleImportant behavior:
- The packed editor does not ship arbitrary plugin packages. Users open a project, and the editor compiles that project's enabled plugins.
- Project/plugin source is trusted code. It runs in the editor process after compile/load. A packed editor refuses the first compile until the user explicitly grants trust; after reviewing the enabled package sources, relaunch once with
HIKARI_TRUST_PROJECT=<digest>(the digest is printed in the editor log / compile pane). - Trust is stored outside the project in the user's persistent Hikari data and is keyed by that digest of project game source plus every enabled plugin file. Changing a manifest, source file, or native artifact invalidates it. A project cannot self-authorize with a file under
.engine/. - If a plugin declares native middleware, its selected link and stage artifacts come from the generated
hikari-plugin-artifacts.json. - Without bundled Zig, the editor discovers a host toolchain at runtime as described in Editor project selector.
Packaging
Game and editor packaging copy the composed product stage. Plugin native/runtime/license files enter that stage only through:
hikari-plugin-artifacts.jsonDo not add parallel package copy rules for plugin folders. A plugin package's source tree is not product data by default. If a runtime file, license, or shared library must ship, declare it in hikari.plugin.json so it appears in the artifact manifest.
Host packaging remains separate from resource layout:
| Concern | Mechanism |
|---|---|
| Compile/link plugin Zig | Source composition into game module. |
| Link/stage native middleware | hikari-plugin-artifacts.json. |
| Ship cooked game data | `--resources=bundles |
| Emit host artifact | `--package[=bundle |
What plugins are not
- Not runtime-loaded Zig modules.
- Not a service locator.
- Not a way to get raw GPU/native renderer handles (reconstruction
on_frameis the exception). - Not a replacement editor shell.
- Not plugin-owned Shinra asset roots.
- Not special engine cases for Steamworks, platform commerce, analytics, or similar SDKs. Those are plugin packages that consume this foundation.
- Not open render-graph plugins (adding passes / owning frame resources) yet. Temporal reconstruction uses a dedicated Zig-only path:
registerReconstructionProvider+on_config/on_frame(plugin owns any private C/vendor glue) — not general pass injection. See Plugin API — Render contract and the example MetalFX / FSR4 packages.