# Hikari Plugin API

Product front door for **author-facing** plugin contracts. Composition (manifests, roles, packaging) lives in [Plugins](/docs/guides/plugins.md).

## 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 |

```text
                    Hikari Plugin API  (product name + docs)
                    ┌─────────────────────────────────────┐
                    │  Source composition (unchanged)     │
                    │  hikari.plugins.json / plugin.json  │
                    └──────────────┬──────────────────────┘
           ┌───────────────────────┼───────────────────────┐
           ▼                       ▼                       ▼
     hi.editor              hikari_game               hi.render
      Editor                 Gameplay                 Rendering
```

## Composition in one page

1. Enable a package in project `hikari.plugins.json`.
2. Package `hikari.plugin.json` declares modules by **role** (`runtime`, `editor`, `developer`, `third_party`).
3. Build composes Zig sources into the game module; native link and staging inputs come from generated `hikari-plugin-artifacts.json` only.
4. No runtime Zig package loader.

Full steps and composition rules: [Plugins guide](/docs/guides/plugins.md).

## Choose your domain

| Need | Start here |
|------|------------|
| Actions, menus, panels, scene edits | [Editor](/docs/guides/plugin-api.md#editor) · [UI and editor SDK](/docs/systems/ui-and-editor.md#game-editor-sdk-hieditor) |
| Entities, tick, libraries, settings, open components | [Gameplay](/docs/guides/plugin-api.md#gameplay) |
| Temporal reconstruction; typed post-process slots | [Rendering](/docs/guides/plugin-api.md#rendering) · [Rendering system](/docs/systems/rendering.md) |

## Editor

### Chrome

In an `editor` role module, export `registerEditor` and always call `api.check()` first:

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

```zig
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: [Editor MCP](/docs/systems/ui-and-editor.md#agent-mcp-per-project).

## 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](/docs/guides/plugins.md#lifecycle-hooks).
- Entities and components under runtime modules (`entities` / `components` paths on the runtime module).
- Explicit native shared libraries via `hi.shared_lib` when declared as stage-only middleware.
- Optional compile-time deps via `hikari_plugin_options`.
- Package settings via `hi.settings` (below).

Working open-component sample: `hikari-games/example/plugins/example_capability/` registers `example_marker` (attachable) with document-backed inspector fields (mutation plane / undo). Canonical skeleton: `hikari-games/example/plugins/_template/`.

### Package settings (`hi.settings`)

Project-owned values for enabled packages, authored in the editor and shipped with the product. Game code reads them; it never writes them. **Does not** dirty `hikari.project.json`.

| Item | Value |
|------|--------|
| Path | `<project>/configs/plugin_settings.json` (`com.hikari.config.plugin_settings` v1), staged to the product's `data/configs/` with the other configs |
| Authoring | **Project Settings → Plugins** and MCP `plugins_edit` (immediate write) |
| API | `hi.settings` — read-only; **not** `hi.plugin_*` (no service bus) |
| Values | `packages.<package_id>.<key>`: bool / number / string |
| Boundary | Typed host API; plugin code never receives filesystem access |

```json
{
  "kind": "com.hikari.config.plugin_settings",
  "version": 1,
  "packages": {
    "com.hikari.example_capability": { "radius": 8.0, "enabled_feature": true }
  }
}
```

```zig
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;
// Strings: getStringAlloc(allocator, …) returns an owned copy.
```

Absent values return `null`, so the manifest default belongs at the call site. Per-player preferences are not package settings; store them through `hi.config` (writable user layer).

Optional schema in the package manifest (drives **Project Settings → Plugins** package-settings rows when present):

```json
{
  "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 reads do not require the schema. Schema keys and keys already present in `configs/plugin_settings.json` both surface in Project Settings (immediate write; does not dirty Save / `hikari.project.json`). A running game module sees an editor write on its next `hi.settings` read.

### 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 MCP `set_field` all use `component.field` paths through the shared mutation plane (`scene_mutation` → document payload). No live-column / `localScalar` poke for open-component fields.
- **Live world:** document apply re-spawns the actor so decode reloads the new payload; save also runs `syncOpenComponentPayloads` as 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 | **Available** — 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](/docs/systems/rendering.md#dual-resolution-and-temporal-reconstruction), [plugin post-process](/docs/systems/rendering.md#plugin-post-process-passes).

## 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](/docs/guides/plugins.md) | Composition, manifests, lifecycle, surfaces table |
| [UI and editor](/docs/systems/ui-and-editor.md#game-editor-sdk-hieditor) | `hi.editor` ABI table |
| [Rendering](/docs/systems/rendering.md) | Reconstruction and PP |
