hi.ai.tasks extends the Brain executor with game-defined actions. The same action can run from a cooked Brain asset or a hi.ai.brain.compile definition in Zig. Built-in move_to and wait remain available. Tasks are ordinary gameplay code; the engine owns binding, activation receipts, cancellation reconciliation, completion/failure transitions and bounded diagnostics.
Define and register a task
A task declares a stable identifier, a capability mask and a Params struct. Parameters currently support defaulted f32, i32, and bool fields, at most eight per task. Use integer slots to address game-owned targets or resources; borrowed actor/component pointers never belong in parameters.
const hi = @import("hikari_game");
const tasks = hi.ai.tasks;
pub const Charge = struct {
pub const name = "charge";
pub const capabilities: u64 = 1; // Game-defined: this adapter can charge.
pub const Params = struct { seconds: f32 = 2 };
pub fn start(_: hi.ActorContext, p: Params, _: tasks.Ticket) tasks.Result {
if (p.seconds <= 0) return tasks.Result.done(.failure, "invalid_duration");
return .{};
}
pub fn tick(_: hi.ActorContext, p: Params, _: tasks.Ticket, elapsed: f32, dt: f32) tasks.Result {
if (elapsed + dt >= p.seconds) return tasks.Result.done(.success, "charged");
return .{ .progress = (elapsed + dt) / p.seconds };
}
pub fn cancel(_: hi.ActorContext, _: tasks.Ticket) void {
// Release any game-owned interaction/reservation here.
}
};
pub const Tasks = tasks.Registry(.{Charge});
// Export this declaration from the game's root.zig.
pub const ai_tasks = Tasks.descriptors;The game module copies this catalog into each World during content registration. The Brain editor uses a copy of the metadata, with no game function pointers. There are at most 32 task types per registry/catalog. A game composing plugin tasks exports one combined registry. Rebuild the game and reopen an already open Brain editor when changing the task catalog.
The context type belongs to the adapter. It may be ActorContext or a small borrowed struct providing operations for a swarm member. Handler context is supplied afresh for every call and must never escape the callback. Keep persistent action-specific state in the owning component/member, and clean it in cancel. An optional finish(context, ticket, result) hook runs once for terminal success/failure, including an accepted delayed result. Use it for cleanup or simulation-thread effects common to synchronous and asynchronous completion. It never runs for a stale receipt.
Author an action in a Brain
In the Brain editor, select a state and choose the registered action in Task. Its parameters appear as shared numeric fields, integer text fields and checkboxes. The normal completion/failure destinations, transitions, validation, Save and Undo/Redo still apply. Unknown tasks/arguments or mismatched argument types prevent saving against the loaded catalog.
The corresponding state is:
{
"name": "charge_weapon",
"task": "custom",
"action": {
"name": "charge",
"arguments": [
{ "name": "seconds", "value": { "kind": "number", "number": 2.5 } }
]
},
"next": "fire",
"failure": "retreat"
}Argument values carry a kind and its matching payload (number, integer, or boolean). Other payload fields must remain at their defaults. Omitted arguments use the task's declared defaults. Shinra validates and encodes this data; the game registry validates task identities, field names, types and required capabilities when binding. Shinra does not execute or link gameplay handlers.
For a code-only Brain, use the same state in Zig:
const definition = try hi.ai.brain.compile(.{
.entry = "charge_weapon",
.target_count = 0,
.states = &.{
.{ .name = "charge_weapon", .task = .custom,
.action = .{ .name = "charge", .arguments = &.{
.{ .name = "seconds", .value = .{ .number = 2.5 } },
} }, .next = "idle", .failure = "idle" },
.{ .name = "idle", .next = "idle", .failure = "idle" },
},
});
const bound = try Tasks.bind(&definition, 1);No asset, JSON parsing, or editor is required for this path.
Drive the lifecycle
Bind before starting the Brain. Tasks.bind(definition, capabilities) validates the complete program and produces an immutable table of task/argument indices. Share this table across adapters using that definition and registry. It contains no function or asset pointers. Rebind whenever the program or registry changes; never combine a binding with a different definition.
At actor startup, create a driver using a fresh World receipt:
const Driver = Tasks.Driver(8); // Eight lifecycle trace entries.
var driver: Driver = .{
.owner = actor.world().aiTaskOwner(hi.ai.population.Subject.actor(actor.id)),
};Store the driver and Brain instance in the component's transient state. Acquire the owner once per component/member lifetime, never once per tick. Each simulation update:
_ = execution.decide(def, facts);
driver.step(actor, &bound, def, &execution, dt);
const movement = execution.step(def, actor.position(), targets, dt);
// Consume movement through the game's navigation/motor adapter.The driver cancels an interrupted activation before starting the new custom task. start runs once; subsequent positive simulation steps call tick. A task may finish immediately from start. Success follows next; failure follows failure. Built-in/custom execution is bounded and does not recursively drain a chain of instant actions. Zero/nonfinite time does not advance a task.
If a transition occurs outside that update sequence, call driver.sync(context, &execution) immediately after it. On disable, destruction, retirement or rebinding, call execution.cancel() and driver.cancel(context) before releasing gameplay resources. Cancellation runs at most once for an active task; it drops any pending completion. Retired swarm rows must do this before recycling their storage.
Delayed work and diagnostics
Handlers receive a Ticket containing the World, actor/swarm identity, World-issued incarnation, Brain activation and driver serial. A worker may retain that value and copied inputs, never an ActorContext, driver pointer, component pointer, or borrowed definition. Marshal its result back to the owning simulation thread, resolve the still-live subject, then call:
const accepted = driver.report(&execution, ticket,
tasks.Result.done(.success, "animation_finished"));The driver rejects old activations, other owners/Worlds, retired/replaced lifetimes, duplicate pending deliveries and invalid progress. Accepted results are consumed by the next positive simulation step. Reconcile cancellation before the delivery can cause gameplay effects; the worker must not perform those effects itself. report is not a cross-thread queue.
driver.latest reports progress (0–1), status and a bounded identifier explaining the result. recent(age) returns lifecycle history with its ticket. History records starts, finishes and cancellations rather than a record every tick. Driver(0) removes history storage for populations. Tasks are opt-in: ordinary brain.CompactInstance stays unchanged, and populations do not need actor objects to implement a task. Capability masks are game-defined contracts checked at binding, not runtime object type guesses.
Playable acceptance test
Open Tutorials → AI Gameplay Tasks (scenes/ai_tasks.json) in the example game. Its one guard uses brains/guard_door.brain.json; the shared two-guard Brain demonstration remains in AI Perception & Brains.
- Disable Targets detectable, choose Noise, radius 16 m, and click behind the blue door around
(0, 0, -2). - The guard approaches its authored interaction point, runs
open_doorfor two seconds, opens the door, investigates, looks around and returns. - Enable Door locked, press Reset door encounter, and repeat.
door_lockedexplains the failure and the guard returns without opening it. - Unlock/reset. With detection still off, use Move player to place the green player near
(0, 0, 3), on the near side of the door. Begin opening with another noise, then enable Targets detectable. Sight interrupts opening; the door remains closed.
The panel shows active state, task progress/reason and the last eight task lifecycle events. Play inspection also exposes task_progress, task_reason, task_history, and the existing Brain decision history/selected target. Reset cancels the current task and returns the guard to its origin.
This fixture uses a game-owned interaction with one named door. It demonstrates task integration, not a general smart-object/reservation system. Open doors are omitted from newly planned routes; already issued routes retain their geometry snapshot. See Game AI, Brain assets, and Swarm AI.