This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/rhai_engine/rhaibook/patterns/events-3.md
2025-04-03 09:18:05 +02:00

4.7 KiB

Scriptable Event Handler with State
Map Style

{{#include ../links.md}}


A runnable example of this implementation is included.

See the [_Examples_]({{rootUrl}}/start/examples/rust.md) section for details.

I Hate this! How Can I Get Rid of It?

You're using Rust and you don't want people to think you're writing lowly JavaScript?

Taking inspiration from the JS Style, a slight modification of the Main Style is to store all states inside an [object map] inside a custom [Scope].

Nevertheless, instead of writing this.variable_name everywhere to access a state variable (in the JS Style), you'd write state.variable_name instead.

It is up to you to decide whether this is an improvement!

Handler Initialization


Notice that a [variable definition filter] is used to prevent [shadowing] of the states [object map].

Implementation wise, this style follows closely the Main Style, but a single [object map] is added to the custom [Scope] which holds all state values.

Global [constants] can still be added to the custom [Scope] as normal and used through the script.

Calls to the init [function] no longer need to avoid rewinding the [Scope] because state [variables] are added as properties under the states [object map].

impl Handler {
    // Create a new 'Handler'.
    pub fn new(path: impl Into<PathBuf>) -> Self {
        let mut engine = Engine::new();

        // Forbid shadowing of 'state' variable
        engine.on_def_var(|_, info, _| Ok(info.name != "state"));

                    :
            // Code omitted
                    :

        // Use an object map to hold state
        let mut states = Map::new();

        // Default states can be added
        states.insert("bool_state".into(), Dynamic::FALSE);

        // Add the main states-holding object map and call it 'state'
        scope.push("state", states);

        // Just a simple 'call_fn' can do here because we're rewinding the 'Scope'
        // In a real application you'd again be handling errors...
        engine.call_fn(&mut scope, &ast, "init", ()).unwrap();

                    :
            // Code omitted
                    :

        Self { engine, scope, ast }
    }
}

Handler Scripting Style

The stored state is kept in an [object map] in the custom [Scope].

In this example, that [object map] is named state, but it can be any name.

User-defined functions in state

Because an [object map] is used to hold state values, it is even possible to add user-defined [functions], leveraging the [OOP] support for [object maps].

However, within these user-defined [functions], the this pointer binds to the [object map]. Therefore, the variable-accessing syntax is different from the main body of the script.

fn do_action() {
    // Access state: `state.xxx`
    state.number = 42;

    // Add OOP functions - you still need to use `this`...
    state.log = |x| print(`State = ${this.value}, data = ${x}`);
}

Sample script

/// Initialize user-provided state.
/// State is stored inside an object map bound to 'state'.
fn init() {
    // Add 'bool_state' as new state variable if one does not exist
    if "bool_state" !in state {
        state.bool_state = false;
    }
    // Add 'obj_state' as new state variable (overwrites any existing)
    state.obj_state = new_state(0);

    // Can also add OOP-style functions!
    state.log = |x| print(`State = ${this.obj_state.value}, data = ${x}`);
}

/// 'start' event handler
fn start(data) {
    // Can detect system-provided default states!
    // Access state variables in 'state'
    if state.bool_state {
        throw "Already started!";
    }

    // New values can be added to the state
    state.start_mode = data;

    if state.obj_state.func1() || state.obj_state.func2() {
        throw "Conditions not yet ready to start!";
    }
    state.bool_state = true;
    state.obj_state.value = data;

    // Constant 'MY_CONSTANT' in custom scope is also visible!
    print(`MY_CONSTANT = ${MY_CONSTANT}`);
}

/// 'end' event handler
fn end(data) {
    if !state.bool_state || "start_mode" !in state {
        throw "Not yet started!";
    }
    if !state.obj_state.func1() && !state.obj_state.func2() {
        throw "Conditions not yet ready to end!";
    }
    state.bool_state = false;
    state.obj_state.value = data;
}

/// 'update' event handler
fn update(data) {
    state.obj_state.value += process(data);

    // Call user-defined function OOP-style!
    state.log(data);
}