# Brain assets

A Brain is a reusable AI state/task program. Author a `.brain.json` document, assign it through a component's **Brain** picker, and let Shinra compile it to `.shinbrain`. Actors share the immutable compiled program; each actor keeps its own current state, timers, parameter overrides, and decision history.

The [AI playground](/docs/guides/game-ai.md) provides a complete example. Both guards reference `asset://./brains/guard`, while their hearing ranges remain 6 m and 11 m.

## Edit a Brain

In the Asset Browser, choose **New Brain**, or open an existing Brain asset. The editor uses the same graph toolbar, canvas, stage, and inspector controls as the material and animation graph editors.

- **Canvas:** state cards show the task, duration or target slot, and an entry badge. A single directed connection groups every condition and task result between the same two states; its badge shows the number of routes. Hover or select a connection to see its label. Click a grouped connection to choose an individual transition, **Complete**, or **Failure** result in the inspector. Global rules originate at **Any state**. The **Connections** selector filters these layers. Self completion/failure destinations stay in the inspector instead of adding redundant loops; explicit self-transition rules draw a loop above the card.
- **Focus selection:** enabled by default. Selecting a state shows its incoming/outgoing connections and global rules; selecting a connection isolates that connection. Click empty canvas space to restore the overview, or turn off **Focus selection** to keep the complete graph visible. This changes only presentation.
- **State inspector:** click a card to edit its name and task, select completion/failure destinations, and set movement or wait options. **Set as entry** changes the starting state. Its transition list includes outgoing and global rules.
- **Transition inspector:** click a single connection or choose a transition from a grouped connection to select its source and destination, toggle restart, and choose **Any value**, **Must be true**, or **Must be false** for each fact. **Earlier/Later** changes rule priority; the inspector’s numbered transition rows reflect this order.
- **Brain settings:** use the gear button to select the entry state, edit target-slot capacity, and add or edit shared duration parameters and named facts.

Use **State** on the toolbar or double-click empty canvas space to add a state. Drag from an output pin to another state's input pin to create a transition, or use **Transition** in the inspector. Pan, zoom, fit, and reset use the shared graph controls. Drag cards to arrange them; their optional `editor_position` coordinates are saved with the source and excluded from the cooked runtime tables. The draggable **Any state** card uses the root `editor_any_state_position` field.

Names use ASCII letters, digits, and underscores, with a maximum of 47 bytes. Renaming a state, parameter, or fact updates its references. Removing a state also removes its condition transitions and redirects task/entry references to a remaining state; the final state cannot be removed. Removing a duration parameter converts its uses to literal durations. A fact used by a condition must be detached before deletion. **Remove** deletes the selected inspector subject. Delete/Backspace over the canvas deletes the selected state or condition arrow; on an individually selected task result it resets that destination to the source state. Selecting a grouped connection never bulk-deletes its routes; choose an individual member first.

Valid edits save automatically after a short idle delay; the toolbar save button writes immediately. Invalid names, missing destinations, contradictory conditions, invalid numbers, and exceeded capacities prevent saving. Undo/Redo uses the editor's normal document history, including card movement. The canonical JSON Schema is `src/hikari/schemas/com.hikari.brain.v1.schema.json`, staged into the project's `.engine/schemas/` directory.

Live validation sends the latest document to Shinra. If the asset pipeline is offline, edits remain available for saving and the toolbar reports offline status; further edits do not lose that status. Reconnection submits the latest document for validation.

The ordinary Shinra watcher cooks saved changes. Wait for cooking to finish before starting Play. Programs already loaded by a World remain fixed until that World is destroyed; a new Play loads the new program. Invalid or unavailable programs do not start guard movement, and `brain_status` reports the binding problem.

## Decisions and tasks

The game publishes a set of named boolean facts and supplies target positions. The Brain evaluates transitions in authored order. The first matching transition wins; all required facts must be true and all excluded facts false. An empty source matches any state. A transition to the active state is skipped unless **Restart** is enabled. Each decision call can enter at most one state.

`custom` runs a registered [gameplay task](/docs/guides/ai-tasks.md), with typed parameters and explicit cancellation. Its implementation is available from both Zig and assets. `move_to` returns a ground destination for the game's motor/navigation adapter. `wait` consumes simulation time. Completion and failure destinations belong to the state. One task step performs at most one completion transition. Failure reports carry the activation serial, so an old movement result cannot complete a replacement task. The game invokes decisions and task steps at its simulation boundary; pausing the playground stops both.

Targets, hearing categories, movement, target selection, and the meaning of facts remain game-owned. The engine executes the shared tables. A Brain does not directly call physics, render, or control an actor transform.

## Brain document format

This small noise probe enters `search` when it hears a stimulus, waits for a parameterized duration, then returns to `idle`. Save it as `assets/brains/noise_probe.brain.json`. It demonstrates the format independently of the seven-state guard adapter.

```json
{
  "$schema": "../../.engine/schemas/com.hikari.brain.v1.schema.json",
  "kind": "com.hikari.brain",
  "version": 1,
  "entry": "idle",
  "target_count": 0,
  "facts": ["noise"],
  "parameters": [{ "name": "search_seconds", "value": 4 }],
  "states": [
    { "name": "idle", "task": "wait", "duration": 3600,
      "next": "idle", "failure": "idle" },
    { "name": "search", "task": "wait", "duration_parameter": "search_seconds",
      "next": "idle", "failure": "idle" }
  ],
  "transitions": [
    { "name": "heard_noise", "to": "search", "restart": true,
      "require": ["noise"] }
  ]
}
```

`entry` selects the initial state. `next` and `failure` name each state's completion and failure destinations. Transition `from` may be omitted or null to match any state; `require` and `exclude` reference the declared boolean facts. Here `restart` makes another noise restart the search timer. Idle is also a timed task and completes back to itself after an hour; there is no indefinite-wait task in this format.

`target_count` declares the positional slots supplied by game code. A `move_to` task selects one with `target_slot` and finishes inside `arrival_radius`, unless `finish_on_arrival` is false. The task returns a destination; the game supplies routing and locomotion. `duration_parameter` selects a named default, with an optional per-instance override. Current Brain parameters are nonnegative duration values; patrol points, speed and hearing settings belong to the consuming component.

Shinra validates names, references and capacities, then cooks the source to immutable numeric tables in `.shinbrain`. Product asset loading reads that binary, without parsing JSON. See [HBR2 layout](/docs/systems/asset-formats.md#ai-brain-programs-hbr2) for the wire format.

## Run an asset from Zig

This attachable component consumes the noise-probe asset above. It filters ordinary gameplay stimuli, supplies the `noise` fact, and advances the shared program using simulation time. It has no movement task or physics requirement.

```zig
const hi = @import("hikari_game");

pub const NoiseProbe = hi.defineComponent(.{
    .name = "noise_probe",
    .data = struct {
        brain: hi.AssetRef = hi.AssetRef.must(.brain, "asset://./brains/noise_probe"),
        hearing_range: f32 = 8,
        search_seconds: f32 = -1,
        state: struct {
            execution: hi.ai.brain.Instance = .{},
            ready: bool = false,
            pending_noise: bool = false,
            noise_bit: u5 = 0,
            retry_in: f32 = 0,
        } = .{},

        pub const metadata = .{ .properties = .{
            .brain = hi.UserDataPropertyOptions{ .widget = .{ .resource = .brain } },
        } };

        pub fn onEnable(self: *@This(), _: hi.ActorContext) void {
            self.state.ready = false;
            self.state.pending_noise = false;
            self.state.retry_in = 0;
        }

        pub fn onDisable(self: *@This(), _: hi.ActorContext) void {
            self.state.execution.cancel();
            self.state.ready = false;
            self.state.pending_noise = false;
        }

        pub fn onChanged(self: *@This(), actor: hi.ActorContext, _: []const u8) void {
            self.onDisable(actor);
            self.onEnable(actor);
        }

        pub fn onMessage(self: *@This(), actor: hi.ActorContext, message: *const hi.Message) void {
            if (!self.state.ready) return;
            const noise = hi.ai.Stimulus.fromMessage(message) orelse return;
            if (noise.heardBy(actor.position(), self.hearing_range, 3) == .heard)
                self.state.pending_noise = true;
        }

        pub fn update(self: *@This(), actor: hi.ActorContext, tick: *const hi.TickContext) void {
            if (!(tick.dt > 0)) return;
            if (!self.state.ready) {
                self.state.retry_in -= tick.dt;
                if (self.state.retry_in > 0) return;
                self.state.retry_in = 0.25;
            }
            const def = actor.world().brainProgram(self.brain) orelse return;
            if (!self.state.ready) {
                if (def.target_count != 0) return;
                self.state.noise_bit = def.fact("noise") orelse return;
                self.state.execution.overrides = @splat(null);
                const duration: ?f32 = if (self.search_seconds == -1) null else self.search_seconds;
                if (!self.state.execution.setParameter(def, "search_seconds", duration)) return;
                self.state.execution.start(def);
                self.state.ready = self.state.execution.machine.enabled;
                if (!self.state.ready) return;
            }
            const facts: u32 = if (self.state.pending_noise)
                @as(u32, 1) << self.state.noise_bit
            else
                0;
            self.state.pending_noise = false;
            _ = self.state.execution.decide(def, facts);
            _ = self.state.execution.step(def, actor.position(), &.{}, tick.dt);
        }
    },
});
```

Export the component through your game's normal [gameplay registration](/docs/guides/gameplay-api.md), attach it to an actor, and emit a stimulus as shown in [Game AI](/docs/guides/game-ai.md#emit-a-stimulus-from-game-code). The mask `3` accepts categories 0 and 1. This probe coalesces noises into one fact per update and ignores messages until its program is ready. It retries unavailable assets or invalid bindings at most four times per simulation second. A production adapter should expose the binding failure, as the guard does through `brain_status`.

Each actor owns its `Instance`; the World owns the shared definition. `search_seconds = -1` inherits the asset default, and a nonnegative value overrides it. The definition pointer is local to the callback. Keep the `AssetRef`, reacquire the definition on each callback that needs it, and resolve named fact/parameter bindings again when changing the asset. Do not reset the instance every frame: that would restart its task timer and history.

For a movement adapter, consume `step(...).destination`, retain the current `execution.machine.activation` with the route request, and pass that captured serial to `execution.fail(def, activation)` if the request fails. Cancel obsolete movement when a decision changes state. Inspect `execution.recent(0)` for the latest reason and `def.state_names[execution.machine.current]` for the active state while the instance is enabled. The complete guard adapter also supplies visual facts, target slots, a motor and read-only inspector diagnostics.

## Author a Brain entirely in Zig

Game code can build the same immutable tables at compile time. This is useful for code-owned behavior and host-free tests. Changes require recompiling the game; this definition has no Asset Browser document or picker entry. It still supports per-instance overrides, named conditions and the same bounded trace.

```zig
const std = @import("hikari_std");
const hi = @import("hikari_game");

const search_brain = blk: {
    @setEvalBranchQuota(10_000);
    break :blk hi.ai.brain.compile(.{
        .entry = "idle",
        .target_count = 0,
        .facts = &.{"noise"},
        .parameters = &.{.{ .name = "search_seconds", .value = 4 }},
        .states = &.{
            .{ .name = "idle", .duration = 3600, .next = "idle", .failure = "idle" },
            .{ .name = "search", .duration_parameter = "search_seconds", .next = "idle", .failure = "idle" },
        },
        .transitions = &.{.{ .name = "heard_noise", .to = "search", .restart = true, .require = &.{"noise"} }},
    }) catch @compileError("Invalid search Brain");
};

test "two actors share code-owned behavior with independent timers" {
    var first: hi.ai.brain.Instance = .{};
    var second: hi.ai.brain.Instance = .{};
    first.start(&search_brain);
    second.start(&search_brain);
    try std.testing.expect(first.setParameter(&search_brain, "search_seconds", 1));
    const noise = @as(u32, 1) << search_brain.fact("noise").?;
    _ = first.decide(&search_brain, noise);
    _ = second.decide(&search_brain, noise);
    _ = first.step(&search_brain, .{ 0, 0, 0 }, &.{}, 1);
    _ = second.step(&search_brain, .{ 0, 0, 0 }, &.{}, 1);
    try std.testing.expectEqual(search_brain.state("idle").?, first.machine.current);
    try std.testing.expectEqual(search_brain.state("search").?, second.machine.current);
}
```

For small index-based programs that do not need named conditions, parameters or history, `hi.ai.Program` + `hi.ai.Machine` remain available. `Machine.start`, `signal`, `step`, `fail` and `cancel` execute directly over borrowed `hi.ai.State` slices. Brain instances wrap this same task executor and add the authored decision tables and trace.

## Parameters and the guard example

The guard supplies `visible`, `remembered`, `retry_ready`, and `noise` facts. Target slot 0 is its next patrol point, slot 1 its last heard position, and slot 2 its visual pursuit/remembered position. The sample adapter binds the named states `patrol`, `investigate`, `look_around`, `return_to_patrol`, `pursue`, `last_seen`, and `search`.

Patrol offsets, speed, hearing range, and vision settings remain ordinary per-actor properties. The Brain's `search_seconds` parameter defaults to 4. The guard property of the same name defaults to **−1**, which inherits the Brain default; a nonnegative actor value overrides it. The two authored guards both inherit the shared default.

To check reuse:

1. Open `brains/guard.brain.json`, select `search_seconds`, change **Default seconds**, and Save.
2. Start a new Play in **AI Perception & Brains**. Both guards use the edited duration.
3. Make noise near a guard, then move a detectable target into its cone. Check the transition reasons as investigation gives way to pursuit.
4. Break sight and watch last-seen movement, search, and return to patrol. Give one guard an explicit `search_seconds` override in Edit to compare independent instances.

## Inspect a running guard

Select a guard during Play. Its read-only component fields expose:

- `brain_status` and `active_state_name`;
- selected target handle, visibility, last known position, and observation age;
- `transition_name`, `decision_reason`, and activation count;
- `decision_history`: recent transitions, newest first, with simulation time, source/destination names, and reasons.

The SDK keeps the last **16** transition records, including the fact mask and activation serial. The guard formats them into a bounded inspector text buffer; long histories may show fewer full lines. The in-game panel also shows the latest transition name and Brain binding status. Task completion, movement failure, explicit cancellation, and authored conditions all produce history entries. This is a decision trace, not World rewind.

## SDK and ownership

`hi.ai.brain.Document` describes authoring data. `compile` validates code-owned definitions and resolves names to table indices. The editor asks Shinra to validate authored documents asynchronously. Asset-backed runtime programs read HBR2 through `decode`; code-owned definitions may be compiled into the game as shown above.

`world.brainProgram(asset)` returns a callback-scoped borrow of a World-owned program, or null when unavailable/invalid. Keep the `AssetRef` across callbacks and reacquire the borrow when needed. Never store the returned pointer in actor state. The World caches at most **32 distinct programs**, loading each once and releasing its owned storage at destruction. Play and Edit caches are independent.

`hi.ai.brain.Instance` owns execution state. Use `start`, `setParameter`, `decide`, `step`, `complete`, `fail`, `cancel`, and `recent`. Parameter overrides are indexed against the bound definition; clear them when rebinding to a different program. No instance retains a program, World, or actor pointer. Warm decisions and task steps allocate nothing.

Each program supports up to **64 states, 128 transitions, 32 boolean facts, 16 duration parameters, and 32 target slots**. The existing perception scheduler budgets sensing; Brain decisions and navigation still run at the game's chosen cadence. Hierarchy, utility scoring, and custom task catalogs are not supported by this asset format.

## Swarm populations

The same Brain executor also runs in swarm rows. See [Brain execution for swarm populations](/docs/guides/swarm-ai.md) for compact state, shared asset binding, mixed actor/swarm sensing, bounded decisions, and the moving 1,000-member acceptance scene.
