Scripting with Rhai

Behaviour rules handle the common cases — greet on arrival, abort on trespass, respond to keywords. But some behaviours need logic that declarative rules cannot express. For these, the archipelago provides Rhai — a sandboxed scripting language that runs inside the engine.

What Rhai is

Rhai is a small, safe scripting language designed for embedding. It looks like Rust, runs fast, and cannot access the filesystem, network, or anything outside the sandbox. Scripts execute with strict resource limits — a budget of operations, a maximum stack depth, a memory ceiling. If a script exceeds its budget, it is terminated. The player sees fiction (“The spell unravels before completion”), not an error.

Entering script mode

> /script

Mode: script
rig>

The prompt changes to rig>. You are now wiring behaviour — attaching scripts to entities that fire when signals arrive.

A simple script

Attach a script to an NPC that responds when given an item:

rig> edit grak react:give

// Grak examines anything given to him
let item = world.signal().args["item"];
let origin = world.property(item, "origin");

if origin == "ironholt" {
    emit(effect::say("Good steel. Ironholt mark. I'll take it."));
} else {
    emit(effect::say("Foreign make. Interesting."));
    emit(effect::emote("turns the item over in his hands."));
}

This script fires whenever someone gives Grak an item. It checks the item’s origin property and responds differently based on where it was made.

The world API

Scripts can read the world but not mutate it directly. All changes go through effects.

Read-only queries:

FunctionReturns
world.self()The entity this script is attached to
world.room()The room the entity is in
world.occupants()Who else is in the room
world.inventory(entity)What an entity is carrying
world.property(entity, key)A specific property value
world.time()The in-world time
world.random(min, max)A random number in range
world.signal()The signal that triggered this script

Effect emission:

FunctionWhat it does
emit(effect::say("..."))Speak aloud
emit(effect::emote("..."))Perform an action
emit(effect::move(direction))Move to another room
emit(effect::abort(reason))Prevent the triggering action
emit(effect::give(item, target))Give an item
log(message)Write to the operator log (not visible to players)

Capability scoping

Scripts execute with the capability bag of the entity they are attached to. A script on an NPC can do what that NPC is authorised to do — no more. If a script tries to emit an effect that exceeds its entity’s capabilities, the Effect Interpreter rejects it.

This means a script cannot escalate privileges. A shopkeeper’s script cannot open locked doors. A guard’s script cannot spend the realm’s treasury. Authority is bounded by the entity’s role.

Resource limits

Every script invocation has a budget:

If a script hits any limit, it is terminated immediately. The player sees a fiction-appropriate message. The operator sees a structured log entry identifying the script, the entity, and the limit that was hit.

Write scripts that do one thing and finish. Long-running computation does not belong in a signal handler.

When to use Rhai vs. rules

Use caseApproach
Greet on arrivalRule
Respond to a keywordRule
Random chance of actionRule
Abort if actor lacks a tagRule
Check item provenance and respond conditionallyRhai
Count items in inventory and change behaviourRhai
Complex multi-step logic with branchingRhai
Anything that needs arithmetic or string manipulationRhai

Rules are cheaper, faster, and easier to read. Use them first. Reach for Rhai when the logic genuinely requires it.

Attaching scripts

Scripts attach to entities at specific signal hooks:

rig> edit grak react:enter     # fires when someone enters Grak's room
rig> edit grak react:say       # fires when someone speaks in Grak's room
rig> edit grak react:give      # fires when someone gives Grak something
rig> edit grak act:tick        # fires on Grak's periodic tick

The hook name follows the signal dispatch pattern: act, react, react_using, react_in, witness. Most NPC scripts use react — they respond to what happens around them.

Testing scripts

Switch to play mode and trigger the behaviour:

rig> /play

> give sword to grak

Grak turns the sword over in his hands. "Foreign make. Interesting."

If the script fails, you see nothing in play mode (silence is the fallback). Switch to admin mode to see the error:

> /admin

watch> errors last

[error] Script react:give on grak — operation budget exceeded at line 12

Fix the script, try again.