Rhai¶
Rhai is LunCoSim's language for scenario policy. A scenario is a persistent program attached to an entity. It observes the simulation, reacts to events, and issues typed commands. It does not integrate physics, solve equations, or replace a Modelica model.
Rhai is small, sandboxed, and implemented in Rust. The engine exposes a deliberately narrow host surface; most convenience comes from an editable prelude written in Rhai itself. Browser build coverage is TBD.
What Rhai owns¶
| Fact | Authored as | Consumed by |
|---|---|---|
| Mission phases and objectives | task, mission, on_event |
Scenario runtime |
| Waypoint selection and sequencing | Task-tree constructors | Native behavior kernel |
| Reactions to events | on_event and wait_for |
Event bus |
| User-facing hints and tutorial flow | HUD prelude helpers | Workbench UI |
| Test assertions and verdicts | Authored test scenarios | luncosim test |
| Policy that should change without a rebuild | .rhai assets and tool libraries |
Hot-reload pipeline |
Rhai does not own continuous equations, direct scene-graph mutation outside the command surface, a second implementation of a Modelica controller, or per-tick polling that should be an event or a task predicate.
A scenario is attached, not global¶
A scenario is a prim applying LunCoProgramAPI — usually a child Scope — or
a program started against a specific entity:
def Scope "Mission" (
prepend apiSchemas = ["LunCoProgramAPI"]
) {
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @twin://my-rover/scenarios/drive.rhai@
}
The asset source is loaded and hot-replaced by the asset layer. In the editor
and API workflows you can also run a scenario directly against an entity with
RunScenario, or run a file-backed asset with RunScenarioAsset.
The program prim is the authored attachment point, but the runtime binds me
to its immediate owning prim. Use name(me) for the vessel or mission host that
owns the program; parent(me) refers to the scene hierarchy above that owner
and is not a replacement for the program host.
Lifecycle at a glance¶
| Hook | When it runs |
|---|---|
task(me, ctx) |
Builds a persistent task tree advanced by the fixed-step kernel |
mission(me, ctx) |
Declares objectives and completion conditions |
simulation_dependencies(me, ctx) |
Read-only admission plan before per-instance initialization |
on_visualization(me, ctx) |
One-shot read-only presentation preparation |
on_start(me, ctx) |
Once, after the owner and its required inputs are ready |
on_event(me, evt, ctx) |
When an event reaches the owner on an eligible pass |
on_stop(me, ctx) |
On hot reload, detach, despawn, or scene teardown |
on_tick(me, ctx) |
Test-only sampling callback; avoid it in production |
me is the owning entity id. ctx carries scenario parameters and execution
context. this is the persistent per-instance state map.
fn on_start(me, ctx) {
this.started = true;
notify("Mission ready");
}
fn on_event(me, evt, ctx) {
if evt.name == "MISSION_COMPLETE" {
notify_kind("Mission complete", "success");
}
}
fn on_stop(me, ctx) {
brake(me);
}
The command/query bridge¶
Scripts use the same public surface as the UI and HTTP API:
let result = cmd("SetPorts", #{ target: rover, writes: [["throttle", 0.4]] });
let state = query("QueryPhysicsState", #{ id: rover });
let rover = find("Rover");
cmd(name, params)performs a write with side effects.query(name, params)reads state; no-data is(), failure is an error map.emit(name, value)produces a telemetry event delivered toon_event.- Prelude helpers such as
drive,brake,nav_to,possess, andinput_hintcompose common behavior.
The full command surface is generated by the running app and is intentionally larger than this chapter. Use the app's command reference or discover it over the HTTP API when you need an exact name.
Events, not polling¶
Production scenarios should react to events or task predicates. A fixed-tick
on_tick loop is reserved for test sampling and verdicts. The kernel already
advances tasks, waits, and event leaves; re-implementing that in a polling loop
duplicates the runtime and makes the scenario less deterministic.
Determinism and ownership¶
Scenarios run in a defined owner scope and clock. Twin, application, and core policy are distinct. A script must not assume it can mutate another owner's state on an arbitrary thread.
Directives can select the execution target and timing:
host includes the standalone authoritative process. simulation is the
default; presentation runs on application frames and may submit typed domain
commands, but not direct structural world writes. Use these only when the
behavior genuinely needs that scope.
Reading this chapter¶
| Section | Question it answers |
|---|---|
| Lifecycle | When does each hook run, and what state does it own? |
| Tasks | How is a mission expressed as a task tree? |
| Commands and queries | How does a script read and change the world? |
| Events | How do scripts coordinate without calling each other? |
| Testing | How does a scenario produce a PASS or FAIL verdict? |
| Patterns | What do real mission and controller scripts look like? |
| Tooling | How do the prelude, tool libraries, and authoring tools fit together? |
Practical rules¶
- Put equations and state in Modelica; put sequencing and decisions in Rhai.
- Attach the scenario to the entity that owns the behavior.
- Use
taskfor progression andon_eventfor external reactions. - Use
cmdfor operations with side effects andsetonly for tuning writes. - Release control, stop actuators, and clear owned state in
on_stop. - Keep scenario state in
this; keep source files hot-reloadable. - Test the public command and query result, not a private script detail.