# Plugins

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.

This guide covers source composition, manifests, and packaging. For editor scene APIs, render extensions, and gameplay contracts, see the [Plugin API guide](/docs/guides/plugin-api.md).

## 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`). See the [Plugin API guide](/docs/guides/plugin-api.md) for supported extension points.

| Domain | Concern | Where | What you get |
|--------|---------|-------|--------------|
| — | Enable / compose | this guide | manifests, roles, packaging |
| Gameplay | Runtime APIs | `hikari_game` | same as game code |
| Gameplay | Package settings | `hi.settings` + [Plugin API](/docs/guides/plugin-api.md#package-settings-hisettings) | `configs/plugin_settings.json` |
| Gameplay | Lifecycle | [Lifecycle hooks](/docs/guides/plugins.md#lifecycle-hooks) + `sdk/src/plugin_compose.zig` | create / tick / scene / … |
| Editor | Chrome | `hi.editor` + [UI and editor](/docs/systems/ui-and-editor.md#game-editor-sdk-hieditor) | actions, menus, toolbar, panels, surfaces |
| Editor | Scene authoring | `hi.editor.scene()` + [Plugin API](/docs/guides/plugin-api.md#scene-transactions) | transactional edits, one undo, revision checks |
| Rendering | Temporal upscale | reconstruction provider + [rendering](/docs/systems/rendering.md#dual-resolution-and-temporal-reconstruction) | register in `onLaunch` |
| Rendering | Post-process | `hi.render().registerPostProcessPass` + [rendering](/docs/systems/rendering.md#plugin-post-process-passes) | fixed slots, opaque refs, host encoder tint |
| Gameplay | Explicit native load | `hi.shared_lib` + [native variants](/docs/guides/plugins.md#native-middleware-variants) | stage-only shared libs |
| Gameplay | Optional deps | `hikari_plugin_options` | compile-time `enabled` |

Each surface stays a named, typed seam. There is no unified `hi.plugin` API, and new extension points are added as rows of this table rather than folded into one.

## Layout

Project:

```text
hikari-games/example/
  hikari.project.json
  hikari.plugins.json
  src/
    root.zig
    session.zig
  plugins/
    example_capability/
      hikari.plugin.json
      runtime/root.zig
      editor/root.zig
```

`hikari.plugins.json` enables package directories:

```json
{
  "kind": "com.hikari.plugins",
  "version": 1,
  "plugins": [
    {
      "path": "plugins/example_capability",
      "enabled": true
    }
  ]
}
```

Each package has its own `hikari.plugin.json`:

```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: `hikari-games/example/plugins/_template/` (copy, rename ids/imports, enable in `hikari.plugins.json`).

1. Create a package directory under the project, usually `plugins/<name>/`.
2. Add `hikari.plugin.json`.
3. Add a runtime Zig root for game-facing code.
4. Add optional editor Zig root for authoring surfaces.
5. Enable the package from the project `hikari.plugins.json`.

Runtime module example:

```zig
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:

```zig
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:

```zig
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:

```zig
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](/docs/systems/ui-and-editor.md#game-editor-sdk-hieditor). Working sample (chrome + builtin scene transaction demo): `hikari-games/example/plugins/example_capability/editor/root.zig`. Product overview: [Plugin API](/docs/guides/plugin-api.md).

Rules:

- Call `api.check()` before anything else (`api_version` **4**); it returns `error{IncompatibleEditorApi}` on an older host and installs `hi.editor.scene()` generation state. Entry points added after `api_version` 1 are nullable; reach them through accessors so a missed version check fails cleanly.
- The `*const Api` is a stack temporary. Copy the table (`api.*`) if you need `show_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` (`ActionInvokeFn` with host-filled `ActionContext`) + menu/toolbar entries that reference the action id; use `beginSurface` / `addControl` / `endSurface` for simple panel bodies (labels, buttons, toggles, row/column markers). That surface API is the design’s `SurfaceBuilder` model — a POD command recorder, not retained-widget ownership. There is no `register_command` / `CommandFn` dual path.
- Scene authoring: `hi.editor.scene()` → buffer-until-commit `EditTransaction` → shared host `applyBatch` (one undo label). Do not hold raw `SceneDocument*` / `EditorApp*`.
- Use host actions when they exist; custom actions use open string ids and plugin callbacks.
- Do not call host `contrib.zig` / `bindFill` or 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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "linkage": "shared",
  "load_mode": "explicit",
  "shared_lib": "native/windows/foo_optional.dll",
  "reload_policy": "restart_required"
}
```

Rules:

- `static_lib` is only for `linkage: "static"`.
- `import_lib` is a link input for `shared` + `linked` (mainly Windows).
- `shared_lib` is the runtime file staged **beside the game module** (`dest` = basename).
- `shared` + `explicit` is stage-only and must not declare `import_lib`; same adjacent `dest` rule; open via `hi.shared_lib`.
- `runtime_files` and `licenses` use author-relative `dest` under the same game-module root.

Ship layout (game product, editor install tree, and each hot-reload generation):

```text
game.dll | libgame.dylib
vendor_loader.dll          # linked / explicit shared_lib
licenses/...               # license dests
hikari-plugin-artifacts.json
```

No `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:

```bash
bin/kaji/kaji game --workspace="$PWD" --project="$PWD/../hikari-games/example" --type=dynamic --config=debug
```

Editor with project:

```bash
bin/kaji/kaji editor --workspace="$PWD" --project="$PWD/../hikari-games/example" --type=dynamic --config=debug --run
```

Packed editor that can compile project plugins offline:

```bash
bin/kaji/kaji editor --workspace="$PWD" --type=dynamic --config=release \
  --vendor:zig=fetch+bundle --vendor:engine=bundle --package=bundle
```

Important 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 `--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](/docs/systems/editor-project-selector.md#zig-toolchain-discovery).

## Packaging

Game and editor packaging copy the composed product stage. Plugin native/runtime/license files enter that stage only through:

```text
hikari-plugin-artifacts.json
```

Do 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|loose` / product data layout. |
| Emit host artifact | `--package[=bundle|loose]`. |

## 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_frame` is 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](/docs/guides/plugin-api.md) and the example MetalFX / FSR4 packages.
