# Gameplay API

`defineActor` defines an actor type by composing components. Spawning creates an
instance. `defineComponent` declares ordinary Zig data, optional lifecycle hooks,
and a generated scene/editor schema. Content discovery registers exported actors
and their components automatically. There are no legacy `defineEntity` or `Entry`
aliases.

For storage tradeoffs and examples, start with
[Choosing component storage](/docs/guides/component-storage.md): attachable, embedded, and swarm.

Gameplay services enter through `hi.world()`, `hi.assets()`, `hi.packs()`,
`hi.render()`, `hi.audio()`, `hi.animation()` and `hi.particle()`. Asset requests,
explicit pack holds and prefab definitions belong to the calling World; see
[Public SDK services and ownership](/docs/systems/session-services.md#public-sdk-services-and-ownership).
Raw host binding and tables are under `hi.abi`, for engine integration and test hosts.

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

pub const Spin = hi.defineComponent(.{
    .name = "spin",
    .data = struct {
        degrees_per_second: f32 = 45,
        state: struct { elapsed: f32 = 0 } = .{},

        pub fn update(self: *@This(), actor: hi.ActorContext, tick: *const hi.TickContext) void {
            self.state.elapsed += tick.dt;
            var rotation = actor.rotationEuler();
            rotation[1] += self.degrees_per_second * tick.dt;
            actor.setRotationEuler(rotation);
        }
    },
});

pub const Beacon = hi.defineActor(.{
    .archetype = "beacon",
    .components = .{ hi.ComponentRender, Spin },
});
```

Components are attachable by default. Their fields have defaults, appear in the
inspector when supported, and serialize under the component's stable name. Use
`.storage = .embedded` for fixed composition when needed: data lives in the actor
payload, with the same scene and inspector schema. Storage does not change the
callback API. Actor component tuple order defines execution order within a tile;
`.requires = .{OtherComponent}` validates presence, not scheduling order.

## Engine components, such as lights

Built-in components join the same actor composition tuple. They declare engine
capabilities; their runtime data is accessed through subsystem APIs.

```zig
pub const LampLogic = hi.defineComponent(.{
    .name = "lamp_logic",
    .requires = .{hi.ComponentLight},
    .data = struct {
        intensity: f32 = 10,

        pub fn start(self: *@This(), actor: hi.ActorContext) void {
            actor.render().setLight(actor.id, .{
                .kind = .point,
                .color = .{ 1, 0.8, 0.6 },
                .intensity = self.intensity,
                .radius = 8,
                .cast_shadow = true,
            });
        }

        pub fn onChanged(self: *@This(), actor: hi.ActorContext, _: []const u8) void {
            actor.render().setLight(actor.id, .{ .intensity = self.intensity });
        }
    },
});

pub const Lamp = hi.defineActor(.{
    .archetype = "lamp",
    .components = .{ hi.ComponentLight, LampLogic },
});
```

`requires` rejects an actor composition missing the light capability. The light
is configured in `start`; `onChanged` keeps it synchronized with inspector edits
and `target.component(LampLogic).set(.intensity, 20)`. `ComponentLight` is a capability
marker, not a public light-data struct, so `target.component(hi.ComponentLight).set(.intensity, 20)` is
not available. Use `actor.render().setLight(actor.id, patch)` or
`hi.render().setLight(target, patch)` to update the engine light directly.
For a scene-authored light without custom behavior, the tuple can contain only
`hi.ComponentLight`. Other built-ins, such as `ComponentRender`, `ComponentCamera`,
and `ComponentPhysics`, compose the same way.

## Scoped actor access

Lifecycle hooks belong to component `Data`, and actor events fan out in component
tuple order. See [Actor and component lifecycle](/docs/guides/component-lifecycle.md) for every
event's dispatch source, activation order, teardown guarantees, and the current
limits of runtime-added components.

All ordinary lifecycle hooks receive `hi.ActorContext` as their second argument:

| Hook | Arguments after `self` | Return |
|---|---|---|
| `awake` | `actor, ?*const hi.SceneActorView` | `void` or error union of `void` |
| `start`, `onDestroy` | `actor` | `void` |
| `onEnable`, `onDisable` | `actor` | `void` |
| `update`, `lateUpdate`, `fixedUpdate` | `actor, *const hi.TickContext` | `void` |
| `onCollision` | `actor, *const hi.CollisionInfo` | `void` |
| `onAnimationEvent` | `actor, *const hi.AnimationEvent` | `void` |
| `onMessage` | `actor, *const hi.Message` | `void` |
| `onChanged` | `actor, []const u8` | `void` |
| `onEnvironmentPreview` | `actor, hi.EnvironmentPreview` | `void` |

`deinit(self, allocator)` releases owned data at storage teardown or replacement
by a custom scene decoder; it does not receive an actor context. Initialization
and scene codecs retain their distinct construction contracts.

Initial activation follows `awake → start → onEnable`. Later effective activity
changes dispatch `onDisable` / `onEnable` without rerunning `start`. These hooks
follow the actor's authored active state combined with layer presentation; they
do not add independent enabled flags to ordinary components.

`actor.id` is the generation-safe weak handle. `actor.get(Health)` returns an
optional `*Health.Data`; `actor.require(Health)` requires presence. These resolve
embedded, column, and instance-added data.

> [!IMPORTANT]
> Contexts and returned pointers are borrowed for the callback only. Retain `ActorRef` across ticks, not pointers. Do not hold sibling pointers across structural world mutations.

`position`, `rotationEuler`, `scale`, and their setters resolve transforms only
when called. No transform lookup is charged to callbacks that do not use one.
`actor.world()`, `actor.render()`, and the event helpers expose world services.
Builtins remain capability markers; use these subsystem services to operate them.

Parallel tick hooks use `hi.ParallelEntry`, with direct transform data and deferred
commands. They cannot use ordinary actor lifecycle hooks. Parallel access remains
explicit through `.parallel = true` and does not grant sibling/world access.

## Authored values and runtime state

Ordinary fields are authored. Put temporary values in an inline `state` field:

```zig
speed: f32 = 5,
state: struct {
    elapsed: f32 = 0,
    grounded: bool = false,
} = .{},
```

The whole `state` field is excluded from inspector schemas, scene output, and
replication. Applying authored settings preserves existing state, and scene
input that tries to author it is rejected. Defaults initialize it on spawn.
This convention adds no allocation, indirection, or generated instance wrapper.
Owned state still needs `deinit`; the generated scene decoder can skip that state
and decode ordinary authored values. Custom scene codecs remain responsible for
their own ownership policy.

`metadata.properties` remains available for labels, ranges, widgets, and individual
transient fields when a flat script-facing path matters. `metadata.runtime_only`
hides the inspector surface; it does not mean all values are transient. Use
`state` for that distinction.

## Runtime component membership

### Entry points

| Caller | Add | Remove | Result |
|---|---|---|---|
| Typed actor view | `target.component(C).add()` | `target.component(C).remove()` | Zig `!void` |
| Typed component | `C.addTo(target)` | `C.removeFrom(target)` | Zig `!void`; same implementation |
| Name-based Zig / tools | `hi.world().componentAdd(target, "health")` | `hi.world().componentRemove(target, "health")` | Zig `!void` |
| Parallel component callback | `entry.cmd.addComponent(C, target)` | `entry.cmd.removeComponent(C, target)` | `void`; records a worker command |
| Kawa | `Actors.add(actor, "health")` | `Actors.remove(actor, "health")` | Boolean request acceptance |

Check committed presence with `target.component(C).has()`, `C.hasOn(target)`,
`hi.world().componentHas(target, "health")`, or Kawa `Actors.has(actor, "health")`.
There is no implicit add-on-write, replacement operation, multiple instances of
the same type, or automatic actor migration. Add/remove changes membership, not
the actor's archetype or identity. Export runtime-only components so content
discovery registers them even when no actor tuple includes them.

```zig
try target.component(Health).add();       // Health.Data defaults
try target.component(Health).remove();
try Health.addTo(target);                 // Equivalent component-first form
try Health.removeFrom(target);
try target.component(hi.ComponentLight).add();
try target.component(hi.ComponentLight).remove();
```

Register the component with the game/module before attaching it. One actor has
at most one instance of each component type. `add` reports `AlreadyPresent`;
`remove` reports `NotPresent`. Missing requirements return `DependencyMissing`;
removing a requirement still used by another component returns `RequiredByOther`.
Requirements are checked in queued command order; the engine does not implicitly
add dependencies or cascade removals.

### Errors: compile time versus runtime

Typed operations on `.embedded` produce a **compile error**, including worker
commands. `catch` cannot intercept a compile error. Passing a `defineSwarm` to
`ActorRef.component` also fails to compile: a swarm has no actor component view.
For names only known at runtime, use the name-based World API and catch its error:

```zig
hi.world().componentRemove(target, "door_controller") catch |err| switch (err) {
    error.NotAttachableStorage => return, // Embedded membership is fixed.
    error.NotPresent => return,           // Already absent.
    else => return err,
};
```

The enclosing function in this example returns `!void`.

| Runtime error | Meaning |
|---|---|
| `EntityNotInWorld` | Target is dead, retiring, stale, or not in this World. |
| `UnknownComponent` | The name is not registered in this World. |
| `NotAttachableStorage` | The registered type is embedded or otherwise not attachable. |
| `MandatoryComponent` | Attempt to structurally change Transform, which every actor requires. |
| `AlreadyPresent` | Add requested for a component already present in projected membership. |
| `NotPresent` | Remove requested for a component absent from projected membership. |
| `DependencyMissing` | A required component would be missing at this point in the command sequence. |
| `RequiredByOther` | A remaining component requires the component being removed. |
| `OutOfMemory` | Storage or command allocation failed. |
| Initialization/script errors | A synchronous attachment failed during initialization; the error propagates. |

These APIs return an open Zig error union; keep an `else` branch when handling a
subset. The table describes public validation and common failure paths, not an
exhaustive list of every user-defined `awake` error.

### Execution and initialization

Outside gameplay callbacks these calls commit synchronously. Within a dispatcher
callback they enqueue a generation-checked command. Commands commit at the next
World tick or tick-group/fixed-phase structural boundary, after borrowed rows and
worker tiles are no longer in use. `try` confirms validation and queue acceptance;
a later allocation or initialization failure is logged. Commands raised by
transition hooks wait for a subsequent boundary. `has`, field reads, and field
writes see **committed** membership: adding and immediately setting a field inside
the same callback does not work. Put initial user values in `Data` defaults or
`awake`; configure newly attached system components in a subsequent callback.

Parallel callbacks record the same changes with
`entry.cmd.addComponent(Health, target)` or
`entry.cmd.removeComponent(Health, target)`. These commands drain in deterministic
slice/append order, then commit at a structural boundary. Parallel code must use
this command buffer; calling ActorRef/HostApi mutation methods from worker threads
is unsupported.

Worker command methods have no catchable return value: recording/drain failures
are diagnosed by the command system. Kawa returns `false` for a rejected request;
it does not expose Zig error values. None of the deferred forms returns a future,
completion receipt, or transaction. A failed queued command is logged and later
commands still run with fresh validation. Queued commands targeting a retired
actor are discarded, never redirected to a recycled actor slot. A sequence of
several operations is not atomic; earlier successful operations stay committed.

Queue required components before dependents, and queue removal of dependents
before requirements. Removing then adding the same type queues a fresh instance.
A retained typed view resolves that replacement; it does not identify a particular
attachment generation. Do not retain raw row pointers across callbacks.

An added component receives `awake(null)`, then `start` and `onEnable` when the
actor's startup/activity permits. Removal sends final `onDisable` when enabled,
then `onDestroy`, then releases scripts, owned data, and system resources.
Re-adding constructs fresh defaults and a fresh lifecycle. This also works for
attachable components declared in `defineActor`: their fixed payload slot becomes
absent and can later hold a new column row. Weak component views remain valid as
views, but resolve whichever instance is currently attached.

| Storage | Runtime membership | Mutable contents |
|---|---|---|
| User `.attachable` | Add/remove, whether declared by the actor or added later | Yes |
| System components | Add/remove with subsystem construction and cleanup | Through their system APIs |
| Transform | Mandatory; cannot remove | Yes |
| `.embedded` | Fixed within the actor payload; cannot add/remove | Yes, including runtime state |
| `defineSwarm` | Fixed population schema; rows are not actors and accept no component attachments | Yes; use `hi.swarm(C).spawn`, `retire`, `get`, `set`, and column operations |

An actor with embedded components can still gain and lose **other attachable
components**. Immutability describes membership/layout, not the actor's values or
the swarm population size. Runtime changes affect the current World; they do not
rewrite the authored scene document. Inspector authoring remains a document edit.

Optional system types are Render, Physics, Light, Camera, Audio, VisualZone,
Decal, FogVolume, Animation, Particle, and Kawa. Use each system's configuration
API after attachment; attaching a row alone does not provide a render asset,
audio clip, particle program, animation graph, or script asset. Resource creation
and publication still follow that subsystem's normal asynchronous work. Physics
removal/re-addition uses attachment generations to reject old simulation replies.

See [Choosing component storage](/docs/guides/component-storage.md) for layout costs and why
embedded and swarm membership stays fixed. The
`hikari-games/example/scenes/runtime_components.README.md`
demonstrates declared and extra attachments, system configuration after commit,
lifecycle counters, fresh defaults, typed call directions, field paths, and
catching an unsupported name-based request.

See [Component lifecycle](/docs/guides/component-lifecycle.md#dynamic-membership) for dispatch
and batching details. Kawa uses `Actors.add(actor, "health")` and
`Actors.remove(actor, "health")`, returning whether the request was accepted.

## Typed cross-actor fields

```zig
const hp: ?u64 = target.component(Health).get(.hp);
_ = target.component(Health).set(.hp, 100);
_ = target.component(Motion).set(.velocity, .{ 1, 0, 0 });
_ = target.component(Motion).setAt(.velocity, 1, 2);
const elapsed: ?f32 = target.component(Health).getPath("state.elapsed");
_ = target.component(Health).setPath("state.elapsed", 2.5);
```

`target.component(Health)` creates a typed view holding only the weak `ActorRef`;
it does not resolve or retain a data pointer. Each operation resolves the current
component afresh, so the view can be retained across ticks. `has()` checks presence;
creation itself does not imply the actor or component exists. Swarms cannot be
accessed through actors.

`get` and `set` accept a compile-time `Component.Field` enum generated from the top-level
fields of `Component.Data`, so `.hp` has a concrete enum type for contextual
completion and misspellings fail to compile. `getPath` and `setPath` accept compile-time
strings for nested exposed fields or computed paths such as `"state." ++ "elapsed"`.
The view methods inline into the same field accessors, with no allocation or
runtime selector or path lookup. The example's `Health.Data.state` contains
`elapsed: f32 = 0`.

Selectors and paths must identify an exposed, safely copyable field; selecting a
whole nested struct still requires direct `ActorContext.get` access. `get` returns
the declared Zig type wrapped in an optional for missing/dead actors or components;
optional fields therefore have an outer presence optional. `set` accepts exactly
the declared type and reports success. Arrays/vectors additionally support
`getAt(.field, index)` / `setAt(.field, index, value)`, and nested arrays/vectors use
`getPathAt("nested.field", index)` / `setPathAt("nested.field", index, value)`.
Indexing checks bounds and copies the field value. Strings and `AssetRef` use the
same `get`/`set` names; setters intern borrowed strings before publication.
Owning pointers are not exposed through typed copies.

Both call directions are supported and share the same implementation:

```zig
_ = Health.field(.hp).set(target, 100);
_ = target.component(Health).set(.hp, 100);

const elapsed = Health.fieldPath("state.elapsed").get(target);
const same_elapsed = target.component(Health).getPath("state.elapsed");
```

The component-level factories also expose accessor types for generic code,
including their `Value` and `Element` types. Prefer the actor-bound view for
ordinary gameplay; component-first access remains a supported public API.
Direct assignment through `actor.require(Health).hp` remains callback-scoped
pointer mutation and does not call `onChanged` automatically.

Field paths resolve at compile time; typed calls use a component ID and field index, without runtime string searches. Values cross the host boundary as exact typed byte copies, validated against the
registered field type and size. Wide integers never pass through `f64`. Writes
notify `onChanged` and retain numeric script synchronization. Direct sibling
pointer mutation is ordinary gameplay data access and does not imply a change
notification. Dynamic tooling and Kawa keep `hi.actors`/`Actors.*` name-keyed APIs.

## Generated declarations and editor Rebuild

`Component.Field`, typed accessors, lifecycle adapters, and scene/editor schemas
are Zig compile-time declarations. They do not produce a separate source file.
Their implementation starts in `src/hikari/sdk/src/define_component.zig` and
`src/hikari/sdk/src/define_component/`; actor composition lives in
`src/hikari/sdk/src/define_actor.zig`. Actor-bound component views live in
`src/hikari/sdk/src/component_ref.zig`. The compiled declarations become part of
the game module.

Content discovery does write `<game-source>/content_manifest.gen.zig`, beside
`root.zig`. `sdk/build/content_manifest.zig` rescans the game's Zig sources and
rewrites this import list on every build-script invocation, including editor
Recompile (the toolbar action whose tooltip is "Rebuild the game module").
Added, renamed, and removed source files are therefore reflected on the next
build. Discovery reads the filesystem and does not use Git tracking, so projects
outside version control work the same way. Do not edit generated
manifests by hand.

The editor invokes `zig build game-module` from the project's resolved SDK root.
Zig tracks imported source changes and reevaluates the affected compile-time
declarations. Under the default project layout, editor build outputs live at:

| Output | Location relative to the project |
|---|---|
| Local/global Zig caches | `.engine/cache/game/zig/local/` and `global/` |
| Plugin composition sources | `.engine/cache/game/zig/local/hikari-plugins/<build-key>/` |
| Built game library on macOS | `.engine/lib/libgame.dylib` |
| Staged reload generation on macOS | `.engine/hot-reload/gen-<n>/game.dylib` |
| Full compiler log | `.engine/logs/game-module-build.log` |

On success, the editor loads a fresh generation, rebuilds scene registrations,
and refreshes the inspector. Replacement waits until the editor can safely swap
modules; compilation failures leave the existing module running.

Recompile uses the selected SDK: an explicit `engine_sdk` path, otherwise a
bundled `sdk/` beside the editor executable, with a monorepo fallback to
`src/hikari/sdk/`. It does not refresh a bundled SDK from engine source or rebuild
the running editor host. Kaji's editor packaging restages bundled SDK inputs when
their content hash changes. Engine/host ABI changes require rebuilding and
restarting the editor; ordinary game component changes use Recompile.

## Swarm populations

```zig
pub const Crowd = hi.defineSwarm(.{
    .name = "crowd",
    .capacity = 100_000,
    .data = struct {
        x: f32 = 0,
        y: f32 = 0,
        z: f32 = 0,

        pub fn integrate(store: *hi.SwarmStore(@This()), dt: f32) void {
            for (store.liveIndices()) |index| store.column("x")[index] += dt;
        }
    },
});
```

Use `hi.swarm(Crowd)` for store operations. A swarm has no actor identity,
hierarchy, actor scripts, or actor lifecycle hooks. Supply bulk `integrate` or
per-row `update(self, SwarmEntry, dt)`, not both. Capacity is required and nonzero.
The existing World-owned SoA storage and deferred retirement path are retained.

## Diagnostics and validation

Unknown declaration options, malformed dependency tuples, unsupported hook
signatures, and invalid typed field paths fail at the declaration. New options
must be explicitly admitted by the SDK; misspellings never silently disable
behavior.

From `src/hikari`, run `zig build test-sdk compile-fail -Dgame-src=<absolute game>`
for authoring contracts. `zig build test` covers host lifecycle and field access;
`zig build entity-bench` measures dispatch. Build actual products with Kaji.

### Live inspection

Play's read-only inspector reflects live transform, system and public user fields,
including components attached after spawn. Added sections carry a **Runtime** pill.
Normal callback assignments and field/system setters participate automatically;
`hidden` fields and `Data.state` remain excluded. Transient fields appear only when
also marked `read_only`, exposing live diagnostics without scene serialization.
Only the selected actor is
observed, with coalesced notifications rather than per-frame field polling. See
[live actor inspection](/docs/systems/ui-and-editor.md#live-actor-inspection-during-play)
for the callback invalidation policy and the prepared Play-only edit boundary.
