# Scripting with Kawa

Editor highlighting for `.kawa`: [`tools/vscode-extensions/kawa`](https://github.com/Flying-Rat/hikari/blob/develop/tools/vscode-extensions/kawa) (VS Code / Cursor), [`tools/nvim-plugins/kawa`](https://github.com/Flying-Rat/hikari/blob/develop/tools/nvim-plugins/kawa) (Neovim / Vim).

## Layers

The engine's scene-scripting seam is VM-agnostic. `src/hikari/src/scripting/api.zig` defines the engine-level native-binding payload, while `src/hikari/src/scripting/kawa/public.zig` is the supported Kawa integration barrel. Only the Kawa implementation folder may touch translated C declarations directly.

```text
World scene scripting hooks
  → SceneScriptBackend (VM-agnostic VTable + registerNative)
  → Kawa adapter (kawa_host/* marshaling)
  → Kawa C runtime and VM
```

Engine domain natives live under `scene/scripting/kawa_host/` and bind via
`kawa_backend` `engine_host_bindings`, **except** `Audio.*`: its adapter
`kawa_host/audio.zig` is registered from the application session so the
scripting dylib never links audio. A future non-Kawa VM reimplements only
the adapter and keeps the same `registerNative` path.

Each engine native is a body `fn (h: *ScriptHost, argv: []const kawa.RawValue) ?kawa.RawValue`
wrapped by `host.native(arity, fail, body)` from `kawa_host/host.zig`. The wrapper
checks the argument count and resolves the script host once; a body that returns
`null` (or a native whose arity or host check fails) returns the declared `Fail`
value. Asset paths from scripts are validated with the same rules as game-code
`AssetRef`s (`value_helpers.isAssetUri`).

Kaji builds Kawa as a native library first. The engine imports its headers for declarations and links the prebuilt artifact, matching the integration model used for native middleware.

## Attaching scripts

Scripts attach at two levels, and an actor may carry any mix of them:

- **Entity-level** — `defineActor` `.script`. One slot per actor, attached last.
- **Component-level** — `defineComponent` `.script` on a game logic component. One slot per component instance, in archetype declaration order. The script's `data` scope is seeded from **that component's** fields, not the whole-actor `user_data` blob.

An entity can therefore have Zig logic components, a Kawa script, scripted components, or all three (open form — no `.behaviour` field). Engine capability builtins (`transform`, `physics`, …) and `.swarm` components reject `.script` at comptime; the builtin `kawa` component is the one empty script host, used by scene JSON.

The following declarations assume `const hi = @import("hikari_game");` and a project-defined `PlayerLogic` component. They demonstrate alternative actor compositions.

```zig
// Zig logic only
pub const Player = hi.defineActor(.{
    .archetype = "player",
    .components = .{ hi.ComponentPhysics, PlayerLogic },
});

// Hybrid: Zig component hooks + Kawa (script path is an asset URI)
pub const ScriptedPlayer = hi.defineActor(.{
    .archetype = "scripted_player",
    .components = .{ hi.ComponentPhysics, PlayerLogic },
    .script = "asset://./scripts/player",
});

// Script-primary actor with engine capabilities
pub const FlyCamera = hi.defineActor(.{
    .archetype = "fly_camera",
    .components = .{hi.ComponentCamera},
    .script = "asset://./scripts/camera_controller",
});

// Component-level: data in Zig, behaviour in Kawa, one slot per instance
const Patrol = hi.defineComponent(.{
    .name = "patrol",
    .storage = .embedded,
    .script = "asset://./scripts/patrol",
    .data = struct {
        speed: f32 = 2.0,
        radius: f32 = 12.0,
    },
});

pub const Guard = hi.defineActor(.{
    .archetype = "guard",
    .components = .{ hi.ComponentRender, Patrol },
    // Entity-level .script may still be added alongside.
});
```

A scene instance overrides either level: entity-level `"script": { "path" }` at the actor root, component-level `"script": { "path", "params" }` inside that component's object under `"components"`. Zig component batches, then all script slots, run within each tick group; `on_message` fires on every slot that declares it.

**In the editor**, the inspector shows the script that will actually *run* on each slot, not only what the scene file authored. The actor's **Script** section shows the archetype's `.script` as a default (with "Change script" to override it on this actor) and, for an authored override, names the default it replaces so "Remove script" reads as "return to the archetype default". Each game component is its own section; a component that declares `.script` or `allows_script` opens with a **Script** row showing its declared default or the instance's override. The row is a drop target for `.kawa` assets, the pencil opens the picker, and × clears an override. Both go through the mutation plane: `set_asset` (`kind: "script"`) for the actor slot and `set_component_script` (`id`, `component`, `path` string or `null`) for a component slot, so agents and the panel write the same thing.

Default script / session script paths are **asset** URIs (`AssetRef` when attaching from Zig). See [Game-facing refs](/docs/systems/refs.md).

World scene code starts and updates entity scripts through `scene/scripting/backend.zig`. In editor Edit mode, simulation scripts do not run; they begin in Play.

### Tick phases

Actor scripts expose two hooks, mapped to game-thread tick groups (full schedule:
[Lifecycle — Tick groups](/docs/systems/lifecycle.md#tick-groups)):

| Hook | Kawa fn | Zig fn | Tick group |
|---|---|---|---|
| Gameplay | `update(dt, total_time)` | `update` | `.update` (default; override with `defineActor` `.tick_group`) |
| Late | `late_update(dt, total_time)` | `lateUpdate` | `.post_update` (fixed) |

Every actor's `update` runs before any actor's `late_update`, so a transform
written during `update` is readable in `late_update` no matter which actor wrote
it or what order the scene declares them in. Zig component batches run before Kawa
in the same group.

That ordering is the whole point. Anything that **reads a transform another
entity writes** belongs in `late_update`: camera-relative aiming and picking,
world-space HUD markers, attachments and sockets, IK. Put it in `update` and it
silently reads whatever the *previous* tick left behind — which looks correct
while standing still and drifts badly the moment things move.

Both hooks are optional; declaring neither costs nothing.

```kawa
fn update(dt, total_time) {
    // move, simulate — write your own transform here
}

fn late_update(dt, total_time) {
    // the camera has already moved this tick, so this ray matches what is drawn
}
```

Session scripts have `update` only — they are not part of the entity phase pass.

## Session scripts

A **session script** is not bound to an actor. Attach once from game code with an
`AssetRef` (typically from `GameSubsystem.onLaunch` after the asset store is ready):

```zig
hi.world().attachSessionScript(
    hi.AssetRef.must(.script, "asset://./scripts/session"),
) catch |err| {
    hi.std.log.err("Session script could not attach: {s}", .{@errorName(err)});
};
```

Engine internals use `script_backend.attachSession(world, path)`. Session hooks:

| Kawa fn | When |
|---|---|
| `start` | After attach |
| `update(dt, total_time)` | Each Play frame (session tick) |
| `on_scene_will_unload` | Before **replace** scene unload |
| `on_scene_loaded(path)` | After a successful **replace** scene load |
| `on_layer_loaded(key)` | An **additive** layer presented — its content is live (`key` = request path) |
| `on_layer_ready(key)` | A `manual` layer reached its gate; open it with `Scene.present_layer(key)` |
| `on_layer_will_unload(key)` | Before **additive** layer unload |
| `on_scene_ready_stage(key, stage)` | Readiness change (see stage table below; empty `key` = world) |
| `destroy` | When the session script is torn down |

`on_scene_ready_stage` / `Scene.ready_stage()` / `Scene.layer_ready_stage(key)` stage ints:

| Int | Name | Notes |
|---|---|---|
| 0 | `none` | No layers / cleared |
| 1 | `entities` | Actors spawned |
| 2 | `assets` | Soft refs resolved |
| 3 | `gpu` | Per-layer terminal (GPU resident) |
| 4 | `all` | World only — every layer at `gpu` |

Scene unload clears **actor** script caches but keeps the session script entry. `Actor.*` is unavailable (nil) in session scope. Use `Scene.reload`, `Scene.load` / `load_additive` / `unload_layer` / `cancel_load`, `Scene.ready_stage` / `layer_ready_stage`, `Scene.stream_warming_up` / `set_stream_focus`, `Input.*`, and `Debug.log`. Sample: `hikari-games/example/assets/scripts/session.kawa`. Full load/layer API: [Session services](/docs/systems/session-services.md).

### Actors communication (preferred)

Unified find / message / name-keyed component fields. Full guide: [Actor communication](/docs/systems/entity-messaging.md).

| API | Role |
|-----|------|
| `Actors.find` / `find_by_name` | Scene id or display name → handle or nil |
| `Actors.is_alive` / `is_active` | Weak-handle checks (never pin unload) |
| `Actors.send` / `emit` | Entity message bus (Zig + all Kawa slots with `on_message`) |
| `Actors.add` / `remove` | Runtime component membership by registry name; boolean request acceptance, deferred inside callbacks ([semantics](/docs/guides/gameplay-api.md#runtime-component-membership)) |
| `Actors.has` / `get_number` / `set_number` | Component fields by registry name (numbers / bools / enums) |

```kawa
let door = Actors.find("msg_door_zig");
if (Actors.is_alive(door)) {
    Actors.send(door, "open");
}
// Self handle when needed for Actors.*:
// Actors.find(Actor.get_scene_id())
```

`Actor.*` remains **this** scripted actor (transform, scene id, …). `World.find` / `World.send` / `Entity.*` are aliases of the same host ops.

Receive:

```kawa
fn on_message(name, from, a0, a1, a2, a3) {
    if (name == "open") { Debug.log("open requested"); }
}
```

### Refs from Kawa

| API | Role |
|-----|------|
| `Actors.find` / `World.find` / `find_by_name` | Actor ref (or nil) — prefer `Actors.*` |
| `World.spawn({...})` | Transactional runtime spawn with the same initial state as Zig `SpawnDesc` |
| `Actor.is_alive` / `send` / `name_of` | Safe on dead refs |
| `Content.read("content://…")` | ContentRef-style file read → string |
| `Render.set_material(actor, mat_uri [, albedo_uri])` | AssetRef material (+ optional albedo) |
| `Render.has_mesh(actor)` | True when store mesh is bound/pending or CPU geometry is present (Zig: `Render.hasMesh`) |
| `Scene.set_skybox_texture("asset://…")` | AssetRef primary cubemap (empty clears) |
| `Scene.set_skybox_texture_secondary("asset://…")` | AssetRef secondary cubemap (empty clears) |
| `Scene.set_skybox_blend(t)` | Dual-skybox blend in [0, 1] |
| `VisualZone.state(actor)` / `VisualZone.update(actor, {…})` | Look zone read/write (Zig `visualZoneState` / `setVisualZone`); `update({…})` alone uses the owning actor |
| `Light.state([actor])` / `Light.update([actor,] {…})` | Light read/write; any field of the light component (`intensity`, `color`, `radius`, cones, `cast_shadow`, `volumetric_scattering_intensity`, `enabled`, …). Without the actor both act on the owner. Per-frame updates are the intended use — a flickering fixture is one `update({ intensity = … })` a frame on a light authored `"is_static": false` |
| `Decal.state([actor])` / `Decal.update([actor,] {…})` | Projected decal read/write (Zig `decalState` / `setDecal`): `color`, `opacity`, `normal_strength`, `angle_fade`, `sort_order`, `enabled`, `is_static`, `important`, `resample_transform`. Texture maps stay authored. Without the actor both act on the owner |
| `FogVolume.state([actor])` / `FogVolume.update([actor,] {…})` | Fog volume read/write (Zig `fogVolumeState` / `setFogVolume`): `density_scale`, `enabled`, `is_static`, `important`, `resample_transform`. Without the actor both act on the owner |
| other component `state` / `update` | Same pattern |
| `Animation.root_motion(actor)` | `{x, y, z, yaw}` the graph took out of the pose last step — yaw in radians, because a script wants a heading change rather than a quaternion |
| `Animation.set_root_motion(actor, "transform" \| "script" \| "off")` | Who applies it; see [Skeletal animation](/docs/systems/animation.md#root-motion) |
| `Animation.state(actor [, layer])` / `in_state(actor, "Attack")` | `{name, hash, time, progress, length, blend, previous}` for one graph layer; nil without a graph |
| `Animation.joint(actor, "hand_r" [, "world" \| "model"])` / `joint_count` / `joint_name(actor, i)` | Where a skeleton joint stands after the last tick — `{x, y, z, rx, ry, rz, rw, sx, sy, sz}`; nil until the rig has posed |
| `Actor.set_parent(actor, parent \| nil)` / `attach_to_joint(actor, parent, "hand_r")` / `set_parent_joint(actor, joint \| nil)` / `parent_joint(actor)` | Parenting and joint sockets; see [Skeletal animation](/docs/systems/animation.md#joint-sockets) |
| `Audio.play(cue [, opts])` / `stop` / `fade_stop` / `crossfade` / `is_playing` | Fire-and-forget SFX (posts to audio worker); opts include `loop` / `hold`, spatial v2 (`inner_cone_deg` / `outer_cone_deg` / `forward` / `occlusion` / `reverb_send`); see [Audio](/docs/systems/audio.md#game-api) |
| `Audio.set_master_mute` / `master_mute` / `set_master_gain` / `set_bus_gain` / `set_bus_mute` / `set_duck_amounts` / `set_reverb_mix` | Mix + mute + duck + global reverb wet (gains ≥ 0, duck 0..1, worker clamp); posts only |

Sample scene: `scenes/refs_demo.json` + `assets/scripts/ref_demo.kawa`.
Zig ↔ Kawa interop (component scripts, entity scripts, messages with arguments, name-keyed fields): `scenes/script_interop.json` + `hikari-games/example/scenes/script_interop.README.md`.
Look zones: `scenes/look_showcase.json` + `assets/scripts/look_zone_pulse.kawa`.
Audio one-shot sample: `scenes/audio_demo.json` + `assets/scripts/sfx_oneshot.kawa`.

`World.spawn` accepts identity, parent/layer membership, active state, transform,
component blocks (including `visual_zone`), `user_data_json`, and script override/parameters (entity-level, or per component inside its block). Component and enum
field names are snake_case; asset fields are AssetRef URI/stem strings. See
[First runtime spawn](/docs/tutorials/first-runtime-spawn.md) for the full example.

## Input contexts

`Input.*` action reads (`held`, `pressed`, `released`, `value`, `vector`) pass through the input context stack, so a script obeys the same gating as Zig game code — a script cannot keep firing while a pause menu is up. `Input.vector` gates each axis independently, since a layer may allow strafing while locking forward motion.

Scripts can drive the stack too, and share the one stack on `World`:

```kawa
Input.push_context({ name = "menu", actions = ["ui_cancel"], exclusive = true, priority = 100, pointer = "free" });
Input.pop_context("menu");
Input.has_context("menu");
Input.clear_contexts();
Input.allows("look"); // Gate raw mouse/gamepad input too.
```

Only `name` is required. `pointer` is `"free"` or `"captured"`. `push_context` returns false rather than truncating when the stack is full or a layer is oversized. Semantics and the raw-input idiom: [Input](/docs/systems/input.md#input-contexts).

## Native bindings

**Games** register numeric natives through the thin SDK before scripts that call them attach:

```zig
fn myNative(argc: u32, argv: [*]const f64, out: [*]f64, out_count: *u32) callconv(.c) void {
    if (argc != 1) {
        out_count.* = 0;
        return;
    }
    out[0] = argv[0] * 2;
    out_count.* = 1;
}
try hi.world().registerScriptNatives(&.{
    .{ .namespace = "Game", .name = "double", .impl = myNative },
});
```

Numbers in → numbers out (`out_count == 0` → nil, `1` → number, `>1` → array). Domain namespaces (`Inventory.*`, terrain helpers, …) live in the **game module**, not the engine.

Register from `GameSubsystem.onWorldAttach(scope)` (before entity scripts attach). Each editor Play World has its own scripting backend, so world-native bindings must be installed for every attachment. Re-registering the same `namespace`+`name` replaces the stored implementation (safe after game-module recompile). Namespace/name string slices must outlive the world (static literals are the usual pattern).

**Engine** code that needs full Kawa value types still builds VM-agnostic `NativeBinding` values with `bindNamespace` from `kawa/public.zig` and registers via the host `World.registerScriptNatives`. Do not expose raw C-layer declarations to ordinary engine or game files.

## Kawa runtime

Kawa is a C VM/compiler with optional type annotations, bytecode archive/import-export support, native C bindings, script ownership, events, debug symbols, and tracing. Its implementation has compiled modules with private internal headers for VM execution and archive functionality; its public API and serialized archive format are preserved across those internal splits.

Scripts use lifecycle/event behavior supplied by the host. Kawa's event system supports normal and one-shot listeners, deferred posting/flush, owner keys, and automatic cleanup when a script is destroyed.

### Bytecode cook and VM lifetime

- **Offline compiler:** `src/kawa/tools/kawac` → `bin/kawa/<platform-arch>/kawac` (`kawac <in.kawa> <out.kawabc>`). Kaji stages it to `bin/game/tools/kawac` for Shinra (`--kawac=<path>`), which cooks `.kawa` → `.kawabc` with the same options as runtime compile (`emit_symbols`, SIMD tier-1).
- **Compiled script cache:** the engine retains bytecode across scene unload (actor instances clear; session script entry stays). Do not destroy/reload compiled scripts on every scene swap — `KAWA_MAX_SCRIPTS` (256) is a per-VM slot budget and careless destroy can **poison** the VM when user-function meta strings still borrow chunk tables.
- **Editor Stop:** retires the disposable Play world and returns to the retained Edit world. Native bindings belong to each world and must be registered on every attachment. Script-asset invalidation and backend teardown release their owned compiled state.

## Entity messaging from Kawa

Prefer `Actors.send` / `Actors.emit` for actor-to-actor gameplay (works with Zig handlers too). `World.send` / `World.emit` are the same bus:

```kawa
let door = Actors.find("msg_door_zig");
if (Actors.is_alive(door)) {
    Actors.send(door, "open");
}
Actors.emit("ping");

fn on_message(name, from, a0, a1, a2, a3) {
    if (name == "open") { Debug.log("open requested"); }
}
```

Hikari creates Kawa VMs with `enable_events = false`, including after resets. The standalone VM’s `Events.*` namespace and `events_*` aliases are therefore unavailable in Hikari; gameplay messages use `Actors.send` / `Actors.emit` through the World dispatcher. Details: [Entity messaging](/docs/systems/entity-messaging.md).

## Where to work

| Task | Location |
|---|---|
| VM-agnostic engine API | `src/hikari/src/scripting/api.zig` |
| Kawa integration API | `src/hikari/src/scripting/kawa/public.zig` |
| Scene + session script contract | `src/hikari/src/scene/scripting/backend.zig` |
| Kawa scene host / `Scene.load` | `src/hikari/src/scene/scripting/kawa_host.zig` (+ `kawa_host/` domains) |
| Entity messaging | [entity-messaging.md](/docs/systems/entity-messaging.md) |
| Kawa source/runtime | `src/kawa/src/` |
| Sample actor + session scripts | `hikari-games/example/assets/scripts/` |
