better scripting api
This commit is contained in:
+471
-257
@@ -9,186 +9,217 @@
|
||||
//! - `bump(id)` — run when another mover steps into this object's cell, with the
|
||||
//! bumper's object index (`-1` for the player); see [`ScriptHost::run_bump`].
|
||||
//!
|
||||
//! ## Actions, queues, and rate limiting
|
||||
//! ## Script constants in scope
|
||||
//!
|
||||
//! Scripts don't mutate the world directly. Each write host function (`move`,
|
||||
//! `set_tile`, `log`, `say`) appends an [`Action`] to the issuing object's **output
|
||||
//! queue** (a per-object FIFO).
|
||||
//! Every object scope contains:
|
||||
//!
|
||||
//! Timing is encoded **inline** in the queue as [`Action::Delay`] entries rather than
|
||||
//! as an external per-object timer. `move(dir)` appends both a `Move` and a
|
||||
//! `Delay(MOVE_COST)` so the object pauses before its next action. Adjacent delays are
|
||||
//! merged at push time so the queue never contains two consecutive `Delay`s.
|
||||
//! | Name | Type | Description |
|
||||
//! |---|---|---|
|
||||
//! | `Board` | `Board` | Read-only world handle. |
|
||||
//! | `Queue` | `Queue` | This object's pending-action queue. |
|
||||
//! | `Me` | `Me` | Self-reference (id, name, x, y, tags, glyph). |
|
||||
//! | `North`/`South`/`East`/`West` | `Direction` | Cardinal directions. |
|
||||
//! | `Black`..`White` | `String` | 16 EGA/VGA color hex strings. |
|
||||
//!
|
||||
//! Each tick, [`ScriptHost::drain`] advances the queue for each object:
|
||||
//! it promotes non-delay actions onto the shared **board queue** until it hits a live
|
||||
//! `Delay`, which it decrements (and removes when expired). `dt` is consumed on the
|
||||
//! first `Delay` encountered so multiple delays in one tick are not double-decremented.
|
||||
//! ## Board read API (`Board.*`)
|
||||
//!
|
||||
//! The `now()` host function pops the most recently enqueued action to the front of
|
||||
//! the queue, letting scripts prioritize a zero-cost action (e.g. `say`) over a
|
||||
//! pending delay.
|
||||
//! - `Board.player_x / player_y / width / height` — world properties
|
||||
//! - `Board.tagged(tag) -> Array[ObjectInfo]` — objects carrying a tag
|
||||
//! - `Board.named(name) -> ObjectInfo | ()` — object with that name (or unit)
|
||||
//! - `Board.get(id) -> ObjectInfo | ()` — look up by id; `-1` returns player info
|
||||
//!
|
||||
//! [`crate::game::GameState`] drains the board queue ([`take_board_queue`]) and applies
|
||||
//! each action *after* the script batch, preserving the borrow discipline (scripts only
|
||||
//! ever read the board during execution). Which object issued an action is read from the
|
||||
//! per-call **tag** (set to the object index in [`ScriptHost::run`]).
|
||||
//! ## Self-reference API (`Me.*`)
|
||||
//!
|
||||
//! [`take_board_queue`]: ScriptHost::take_board_queue
|
||||
//! [`GameState`]: crate::game::GameState
|
||||
//! - `Me.id / name / x / y` — getters
|
||||
//! - `Me.has_tag(s) -> bool`
|
||||
//! - `Me.tags() -> Array`
|
||||
//! - `Me.glyph() -> Glyph` (`Glyph.tile`, `Glyph.fg`, `Glyph.bg`)
|
||||
//!
|
||||
//! ## Write functions
|
||||
//!
|
||||
//! `move(dir)`, `delay(secs)`, `now()`, `set_tile(n)`, `log(msg)`, `say(msg)`,
|
||||
//! `set_fg(fg)`, `set_bg(bg)`, `set_color(fg, bg)`, `set_tag(target, tag, present)`,
|
||||
//! `send(target_id, fn_name [, arg])`
|
||||
//!
|
||||
//! ## Queue API
|
||||
//!
|
||||
//! `Queue.length()`, `Queue.clear()`, `Queue.peek()`, `Queue.pop()`, `Queue.push(map)`
|
||||
|
||||
use crate::action::{action_to_map, rhai_action_module, Action, MOVE_COST};
|
||||
use crate::board::Board;
|
||||
use crate::log::LogLine;
|
||||
use rhai::{CallFnOptions, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope, AST};
|
||||
use crate::map_file::{color_to_hex, parse_color};
|
||||
use crate::utils::{Direction, ObjectId, ScriptArg};
|
||||
use rhai::{CallFnOptions, Dynamic, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope, AST};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::rc::Rc;
|
||||
use crate::utils::{Action, Direction, ObjectId, MOVE_COST};
|
||||
|
||||
/// An action promoted from an object's output queue onto the board queue, tagged
|
||||
/// with the object that issued it.
|
||||
pub struct BoardAction {
|
||||
pub(crate) struct BoardAction {
|
||||
/// The stable [`ObjectId`] of the object that issued the action.
|
||||
pub source: ObjectId,
|
||||
pub(crate) source: ObjectId,
|
||||
/// The action to apply.
|
||||
pub action: Action,
|
||||
pub(crate) action: Action,
|
||||
}
|
||||
|
||||
/// A read-only handle to the world, exposed to scripts as `Board`. Holds a shared
|
||||
/// reference to the [`Board`]; only getters are registered, so it is
|
||||
/// read-only by construction.
|
||||
/// A read-only handle to the world, exposed to scripts as `Board`.
|
||||
type BoardRef = Rc<RefCell<Board>>;
|
||||
|
||||
/// A single object's output queue, shared between the host (which pumps it between
|
||||
/// script calls) and the script (which reads/mutates it through the `Queue` object).
|
||||
/// A single object's output queue.
|
||||
type ObjQueue = Rc<RefCell<VecDeque<Action>>>;
|
||||
|
||||
/// The board queue: actions promoted from object queues, awaiting resolution. Shared
|
||||
/// so the `blocked` host function can scan pending moves during script execution.
|
||||
/// The board queue: actions promoted from object queues, awaiting resolution.
|
||||
type BoardQueue = Rc<RefCell<Vec<BoardAction>>>;
|
||||
|
||||
/// Maps an [`ObjectId`] to its output queue, so the global write host functions can
|
||||
/// route an emitted action to the right object via the per-call tag.
|
||||
/// Maps an [`ObjectId`] to its output queue.
|
||||
type QueueMap = Rc<RefCell<HashMap<ObjectId, ObjQueue>>>;
|
||||
|
||||
/// Channel for engine/compile/runtime errors, surfaced straight to the game log
|
||||
/// (errors never go through the action queues).
|
||||
/// Channel for engine/compile/runtime errors.
|
||||
type ErrorSink = Rc<RefCell<Vec<LogLine>>>;
|
||||
|
||||
/// A compiled script plus which lifecycle hooks it defines.
|
||||
struct CompiledScript {
|
||||
/// The compiled Rhai AST (function definitions plus any top-level code).
|
||||
ast: AST,
|
||||
/// Whether the script defines `fn init()` (zero parameters).
|
||||
has_init: bool,
|
||||
/// Whether the script defines `fn tick(dt)` (one parameter).
|
||||
has_tick: bool,
|
||||
/// Whether the script defines `fn bump(id)` (one parameter).
|
||||
has_bump: bool,
|
||||
}
|
||||
|
||||
/// The runtime state of one scripted object: which script it runs, its persistent
|
||||
/// Rhai scope, and its pending action queue.
|
||||
/// The runtime state of one scripted object.
|
||||
struct ObjectRuntime {
|
||||
/// The stable [`ObjectId`] of this object; passed to scripts as the per-call
|
||||
/// tag so host functions know which object issued an action.
|
||||
object_id: ObjectId,
|
||||
/// Key into [`ScriptHost::scripts`] naming this object's compiled script.
|
||||
script_name: String,
|
||||
/// Per-object scope (holds the `Board`/`Queue` views + direction constants, and
|
||||
/// can hold object-local state in the future). Persists across calls.
|
||||
scope: Scope<'static>,
|
||||
/// This object's output queue (shared with its scope's `Queue` object).
|
||||
/// May contain [`Action::Delay`] entries that pace the release of other actions.
|
||||
output_queue: ObjQueue,
|
||||
}
|
||||
|
||||
// ── Rhai types exposed to scripts ────────────────────────────────────────────
|
||||
|
||||
/// A snapshot of one board object, returned by `Board.tagged`, `Board.named`,
|
||||
/// and `Board.get`. Passed by value — scripts read fields, not a live reference.
|
||||
#[derive(Clone)]
|
||||
struct ObjectInfo {
|
||||
id: i64,
|
||||
x: i64,
|
||||
y: i64,
|
||||
name: Option<String>,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// The glyph (tile + colors) of a board object, returned by `Me.glyph()`.
|
||||
/// Colors are `"#RRGGBB"` hex strings matching the map-file format.
|
||||
#[derive(Clone)]
|
||||
struct RhaiGlyph {
|
||||
tile: i64,
|
||||
fg: String,
|
||||
bg: String,
|
||||
}
|
||||
|
||||
/// A script's read-only reference to itself, pushed into scope as the constant `Me`.
|
||||
/// Holds the object id and a shared board handle so getters can look up live values.
|
||||
#[derive(Clone)]
|
||||
struct Me {
|
||||
object_id: ObjectId,
|
||||
board: BoardRef,
|
||||
}
|
||||
|
||||
// ── ObjectInfo helper ─────────────────────────────────────────────────────────
|
||||
|
||||
fn object_info_from(id: ObjectId, board: &Board) -> Option<ObjectInfo> {
|
||||
let obj = board.objects.get(&id)?;
|
||||
Some(ObjectInfo {
|
||||
id: id as i64,
|
||||
x: obj.x as i64,
|
||||
y: obj.y as i64,
|
||||
name: obj.name.clone(),
|
||||
tags: obj.tags.iter().cloned().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn object_info_player(board: &Board) -> ObjectInfo {
|
||||
ObjectInfo {
|
||||
id: -1,
|
||||
x: board.player.x as i64,
|
||||
y: board.player.y as i64,
|
||||
name: None,
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── ScriptHost ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Owns the Rhai engine and per-object script state for a board.
|
||||
pub struct ScriptHost {
|
||||
/// The Rhai engine, with the read getters and write host functions registered.
|
||||
engine: Engine,
|
||||
/// Compiled scripts, keyed by script name; compiled once per referenced name.
|
||||
scripts: HashMap<String, CompiledScript>,
|
||||
/// One entry per object that has a successfully compiled script.
|
||||
objects: Vec<ObjectRuntime>,
|
||||
/// Actions promoted from object queues, drained by [`Self::take_board_queue`].
|
||||
board_queue: BoardQueue,
|
||||
/// Errors collected during compile/run, drained by [`Self::take_errors`].
|
||||
errors: ErrorSink,
|
||||
}
|
||||
|
||||
impl ScriptHost {
|
||||
/// Builds a host for the board behind `board_cell`: registers the read/write API,
|
||||
/// Builds a host for the board behind `board_cell`: registers the full script API,
|
||||
/// compiles every script referenced by an object (once each), and creates a fresh
|
||||
/// scope and output queue per scripted object. Compile errors and references to
|
||||
/// unknown scripts are queued onto the error sink (retrievable via
|
||||
/// [`Self::take_errors`]); no script is run here.
|
||||
/// unknown scripts are queued onto the error sink; no script is run here.
|
||||
pub fn new(board_cell: &BoardRef) -> Self {
|
||||
let board_queue: BoardQueue = Rc::new(RefCell::new(Vec::new()));
|
||||
let queues: QueueMap = Rc::new(RefCell::new(HashMap::new()));
|
||||
let errors: ErrorSink = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let mut engine = Engine::new();
|
||||
register_read_api(&mut engine, board_cell, &board_queue);
|
||||
engine.register_static_module("Action", rhai_action_module().into());
|
||||
register_read_api(&mut engine, board_cell, &board_queue, &errors);
|
||||
register_write_api(&mut engine, &queues);
|
||||
register_queue_api(&mut engine);
|
||||
register_queue_api(&mut engine, &errors);
|
||||
register_me_type(&mut engine);
|
||||
register_object_info_type(&mut engine);
|
||||
register_glyph_type(&mut engine);
|
||||
|
||||
let board = board_cell.borrow();
|
||||
|
||||
// Compile each referenced script once; attribute failures to the first
|
||||
// object that references the script.
|
||||
// Compile each referenced script once.
|
||||
let mut scripts: HashMap<String, CompiledScript> = HashMap::new();
|
||||
let mut failed: HashSet<String> = HashSet::new();
|
||||
for obj in board.objects.values() {
|
||||
let Some(name) = obj.script_name.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if scripts.contains_key(name) || failed.contains(name) {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = obj.script_name.as_ref() else { continue; };
|
||||
if scripts.contains_key(name) || failed.contains(name) { continue; }
|
||||
match board.scripts.get(name) {
|
||||
Some(src) => match engine.compile(src) {
|
||||
Ok(ast) => {
|
||||
// Detect the optional hooks by name and arity.
|
||||
let defines = |n: &str, params: usize| {
|
||||
ast.iter_functions()
|
||||
.any(|f| f.name == n && f.params.len() == params)
|
||||
ast.iter_functions().any(|f| f.name == n && f.params.len() == params)
|
||||
};
|
||||
let compiled = CompiledScript {
|
||||
scripts.insert(name.clone(), CompiledScript {
|
||||
has_init: defines("init", 0),
|
||||
has_tick: defines("tick", 1),
|
||||
has_bump: defines("bump", 1),
|
||||
ast,
|
||||
};
|
||||
scripts.insert(name.clone(), compiled);
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
failed.insert(name.clone());
|
||||
errors.borrow_mut().push(LogLine::raw(format!(
|
||||
errors.borrow_mut().push(LogLine::error(format!(
|
||||
"script '{name}' failed to compile: {err}"
|
||||
)));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
failed.insert(name.clone());
|
||||
errors.borrow_mut().push(LogLine::raw(format!(
|
||||
errors.borrow_mut().push(LogLine::error(format!(
|
||||
"object references unknown script '{name}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One runtime (independent scope + output queue) per object whose script
|
||||
// compiled. The queue is registered in `queues` so host functions can find
|
||||
// it by the object's index, and shared into the scope as the `Queue` object.
|
||||
// One runtime per object whose script compiled.
|
||||
let mut objects = Vec::new();
|
||||
for (&id, obj) in board.objects.iter() {
|
||||
let Some(name) = obj.script_name.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if !scripts.contains_key(name) {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = obj.script_name.as_ref() else { continue; };
|
||||
if !scripts.contains_key(name) { continue; }
|
||||
let queue: ObjQueue = Rc::new(RefCell::new(VecDeque::new()));
|
||||
queues.borrow_mut().insert(id, queue.clone());
|
||||
objects.push(ObjectRuntime {
|
||||
@@ -200,35 +231,22 @@ impl ScriptHost {
|
||||
}
|
||||
|
||||
drop(board);
|
||||
|
||||
Self {
|
||||
engine,
|
||||
scripts,
|
||||
objects,
|
||||
board_queue,
|
||||
errors,
|
||||
}
|
||||
Self { engine, scripts, objects, board_queue, errors }
|
||||
}
|
||||
|
||||
/// Calls `init()` on every scripted object that defines it and drains each queue
|
||||
/// with `dt = 0` (no time advance — just flush any zero-cost front actions).
|
||||
/// Calls `init()` on every scripted object that defines it and drains each queue.
|
||||
pub fn run_init(&mut self) {
|
||||
self.run("init", |c| c.has_init, (), 0.0);
|
||||
}
|
||||
|
||||
/// Calls `tick(dt)` on every scripted object that defines it, then drains each
|
||||
/// queue, advancing inline [`Action::Delay`]s by `dt`.
|
||||
/// Calls `tick(dt)` on every scripted object that defines it, then drains queues.
|
||||
pub fn run_tick(&mut self, dt: f64) {
|
||||
self.run("tick", |c| c.has_tick, (dt,), dt);
|
||||
}
|
||||
|
||||
/// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines the hook.
|
||||
///
|
||||
/// `bumper` is the id of the object that moved into it, or `-1` for the player.
|
||||
/// After the hook, drains the object's queue with `dt = 0` so any zero-cost actions
|
||||
/// the hook enqueued (e.g. `say(); now()`) take effect in the same frame.
|
||||
/// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines
|
||||
/// the hook. After the hook, drains the object's queue with `dt = 0`.
|
||||
pub fn run_bump(&mut self, object_id: ObjectId, bumper: i64) {
|
||||
// Find the index first so we can drain after the split-borrow block.
|
||||
let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else {
|
||||
return;
|
||||
};
|
||||
@@ -241,17 +259,64 @@ impl ScriptHost {
|
||||
if let Err(err) = engine.call_fn_with_options::<()>(
|
||||
options, &mut obj.scope, &compiled.ast, "bump", (bumper,),
|
||||
) {
|
||||
errors.borrow_mut().push(LogLine::raw(format!(
|
||||
errors.borrow_mut().push(LogLine::error(format!(
|
||||
"script '{}' bump error: {err}", obj.script_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
// dt=0: flush zero-cost front actions without advancing any delays.
|
||||
self.drain(i, 0.0);
|
||||
}
|
||||
|
||||
/// Calls the named function on the object with [`ObjectId`] `target_id`.
|
||||
///
|
||||
/// If `arg` is `Some` and the function accepts one parameter, the arg is passed.
|
||||
/// If the function accepts zero parameters (or arg is `None`), it is called with
|
||||
/// no args. If neither arity exists, the call is silently skipped.
|
||||
/// After the call, drains the object's queue with `dt = 0`.
|
||||
pub(crate) fn run_send(&mut self, target_id: ObjectId, fn_name: &str, arg: Option<ScriptArg>) {
|
||||
let Some(i) = self.objects.iter().position(|o| o.object_id == target_id) else {
|
||||
return;
|
||||
};
|
||||
{
|
||||
let Self { engine, scripts, objects, errors, .. } = self;
|
||||
let obj = &mut objects[i];
|
||||
let Some(compiled) = scripts.get(&obj.script_name) else { return; };
|
||||
|
||||
let has_1 = compiled.ast.iter_functions()
|
||||
.any(|f| f.name == fn_name && f.params.len() == 1);
|
||||
let has_0 = compiled.ast.iter_functions()
|
||||
.any(|f| f.name == fn_name && f.params.len() == 0);
|
||||
|
||||
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
|
||||
let result = if has_1 {
|
||||
// Call with arg (or unit if arg is absent).
|
||||
let dyn_arg: Dynamic = match &arg {
|
||||
Some(ScriptArg::Str(s)) => Dynamic::from(s.clone()),
|
||||
Some(ScriptArg::Num(n)) => Dynamic::from(*n),
|
||||
None => Dynamic::UNIT,
|
||||
};
|
||||
engine.call_fn_with_options::<()>(
|
||||
options, &mut obj.scope, &compiled.ast, fn_name, (dyn_arg,),
|
||||
)
|
||||
} else if has_0 {
|
||||
engine.call_fn_with_options::<()>(
|
||||
options, &mut obj.scope, &compiled.ast, fn_name, (),
|
||||
)
|
||||
} else {
|
||||
return; // Function not defined on this object
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
errors.borrow_mut().push(LogLine::error(format!(
|
||||
"script '{}' send('{}') error: {err}", obj.script_name, fn_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
self.drain(i, 0.0);
|
||||
}
|
||||
|
||||
/// Removes and returns the actions promoted onto the board queue.
|
||||
pub fn take_board_queue(&mut self) -> Vec<BoardAction> {
|
||||
pub(crate) fn take_board_queue(&mut self) -> Vec<BoardAction> {
|
||||
std::mem::take(&mut self.board_queue.borrow_mut())
|
||||
}
|
||||
|
||||
@@ -262,31 +327,23 @@ impl ScriptHost {
|
||||
|
||||
/// Drains object `i`'s output queue onto the board queue, advancing inline
|
||||
/// [`Action::Delay`]s by `dt`.
|
||||
///
|
||||
/// Non-delay actions are promoted immediately. When a `Delay` is encountered it is
|
||||
/// decremented by the remaining `dt` (which is then consumed so subsequent delays
|
||||
/// in the same call are not also decremented). An expired delay is removed and
|
||||
/// draining continues; a live delay stops the loop for this call.
|
||||
fn drain(&mut self, i: usize, mut dt: f64) {
|
||||
let oid = self.objects[i].object_id;
|
||||
loop {
|
||||
let queue = self.objects[i].output_queue.clone();
|
||||
// Peek at the front to decide what to do without holding the borrow.
|
||||
let is_delay = matches!(queue.borrow().front(), Some(Action::Delay(_)));
|
||||
if is_delay {
|
||||
// Decrement the delay; pop it only if it expires.
|
||||
let expired = {
|
||||
let mut q = queue.borrow_mut();
|
||||
let Some(Action::Delay(r)) = q.front_mut() else { break };
|
||||
*r -= dt;
|
||||
dt = 0.0; // consume so later delays in this call aren't also decremented
|
||||
dt = 0.0;
|
||||
*r <= 0.0
|
||||
};
|
||||
if expired {
|
||||
queue.borrow_mut().pop_front();
|
||||
// Continue — drain actions after the expired delay.
|
||||
} else {
|
||||
break; // Live delay: stop draining.
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
let Some(action) = queue.borrow_mut().pop_front() else { break };
|
||||
@@ -295,10 +352,7 @@ impl ScriptHost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared driver for `init`/`tick`: for each object, call the hook if its script
|
||||
/// defines it (tagging the call with the object's id so host functions know the
|
||||
/// source), then drain that object's queue so actions reach the board queue before
|
||||
/// the next object's `blocked` check runs.
|
||||
/// Shared driver for `init`/`tick`.
|
||||
fn run<A: FuncArgs + Copy>(
|
||||
&mut self,
|
||||
hook: &str,
|
||||
@@ -307,7 +361,6 @@ impl ScriptHost {
|
||||
drain_dt: f64,
|
||||
) {
|
||||
for i in 0..self.objects.len() {
|
||||
// Scope the split borrow so it ends before the drain reborrow below.
|
||||
{
|
||||
let Self { engine, scripts, objects, errors, .. } = self;
|
||||
let obj = &mut objects[i];
|
||||
@@ -318,7 +371,7 @@ impl ScriptHost {
|
||||
if let Err(err) = engine.call_fn_with_options::<()>(
|
||||
options, &mut obj.scope, &compiled.ast, hook, args,
|
||||
) {
|
||||
errors.borrow_mut().push(LogLine::raw(format!(
|
||||
errors.borrow_mut().push(LogLine::error(format!(
|
||||
"script '{}' {hook} error: {err}", obj.script_name
|
||||
)));
|
||||
}
|
||||
@@ -329,91 +382,155 @@ impl ScriptHost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the read-only `Board` API (getters plus the `blocked` query).
|
||||
///
|
||||
/// The getters operate on the `Board` value in scope; `blocked` instead captures a
|
||||
/// clone of the world and the board queue, so it can resolve the caller's position and
|
||||
/// scan pending moves.
|
||||
fn register_read_api(engine: &mut Engine, board: &BoardRef, board_queue: &BoardQueue) {
|
||||
// ── Register Me type ──────────────────────────────────────────────────────────
|
||||
|
||||
fn register_me_type(engine: &mut Engine) {
|
||||
engine.register_type_with_name::<Me>("Me");
|
||||
|
||||
engine.register_get("id", |me: &mut Me| me.object_id as i64);
|
||||
|
||||
engine.register_get("name", |me: &mut Me| {
|
||||
me.board.borrow()
|
||||
.objects.get(&me.object_id)
|
||||
.and_then(|o| o.name.as_deref())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
});
|
||||
|
||||
engine.register_get("x", |me: &mut Me| {
|
||||
me.board.borrow()
|
||||
.objects.get(&me.object_id)
|
||||
.map(|o| o.x as i64)
|
||||
.unwrap_or(0)
|
||||
});
|
||||
|
||||
engine.register_get("y", |me: &mut Me| {
|
||||
me.board.borrow()
|
||||
.objects.get(&me.object_id)
|
||||
.map(|o| o.y as i64)
|
||||
.unwrap_or(0)
|
||||
});
|
||||
|
||||
engine.register_fn("has_tag", |me: &mut Me, tag: ImmutableString| {
|
||||
me.board.borrow()
|
||||
.objects.get(&me.object_id)
|
||||
.is_some_and(|o| o.tags.contains(tag.as_str()))
|
||||
});
|
||||
|
||||
engine.register_fn("tags", |me: &mut Me| -> rhai::Array {
|
||||
me.board.borrow()
|
||||
.objects.get(&me.object_id)
|
||||
.map(|o| o.tags.iter().map(|t| Dynamic::from(t.clone())).collect())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
engine.register_fn("glyph", |me: &mut Me| -> RhaiGlyph {
|
||||
let board = me.board.borrow();
|
||||
if let Some(obj) = board.objects.get(&me.object_id) {
|
||||
RhaiGlyph {
|
||||
tile: obj.glyph.tile as i64,
|
||||
fg: color_to_hex(obj.glyph.fg),
|
||||
bg: color_to_hex(obj.glyph.bg),
|
||||
}
|
||||
} else {
|
||||
RhaiGlyph { tile: 0, fg: "#000000".to_string(), bg: "#000000".to_string() }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Register ObjectInfo type ──────────────────────────────────────────────────
|
||||
|
||||
fn register_object_info_type(engine: &mut Engine) {
|
||||
engine.register_type_with_name::<ObjectInfo>("ObjectInfo");
|
||||
|
||||
engine.register_get("id", |o: &mut ObjectInfo| o.id);
|
||||
engine.register_get("x", |o: &mut ObjectInfo| o.x);
|
||||
engine.register_get("y", |o: &mut ObjectInfo| o.y);
|
||||
|
||||
engine.register_get("name", |o: &mut ObjectInfo| {
|
||||
o.name.as_deref().unwrap_or("").to_string()
|
||||
});
|
||||
|
||||
engine.register_get("tags", |o: &mut ObjectInfo| -> rhai::Array {
|
||||
o.tags.iter().map(|t| Dynamic::from(t.clone())).collect()
|
||||
});
|
||||
}
|
||||
|
||||
// ── Register RhaiGlyph type ───────────────────────────────────────────────────
|
||||
|
||||
fn register_glyph_type(engine: &mut Engine) {
|
||||
engine.register_type_with_name::<RhaiGlyph>("Glyph");
|
||||
engine.register_get("tile", |g: &mut RhaiGlyph| g.tile);
|
||||
engine.register_get("fg", |g: &mut RhaiGlyph| g.fg.clone());
|
||||
engine.register_get("bg", |g: &mut RhaiGlyph| g.bg.clone());
|
||||
}
|
||||
|
||||
// ── Board read API ────────────────────────────────────────────────────────────
|
||||
|
||||
fn register_read_api(
|
||||
engine: &mut Engine,
|
||||
board: &BoardRef,
|
||||
board_queue: &BoardQueue,
|
||||
errors: &ErrorSink,
|
||||
) {
|
||||
engine.register_type_with_name::<BoardRef>("Board");
|
||||
engine.register_get("player_x", |b: &mut BoardRef| b.borrow().player.x as i64);
|
||||
engine.register_get("player_y", |b: &mut BoardRef| b.borrow().player.y as i64);
|
||||
engine.register_get("width", |b: &mut BoardRef| b.borrow().width as i64);
|
||||
engine.register_get("height", |b: &mut BoardRef| b.borrow().height as i64);
|
||||
engine.register_get("width", |b: &mut BoardRef| b.borrow().width as i64);
|
||||
engine.register_get("height", |b: &mut BoardRef| b.borrow().height as i64);
|
||||
|
||||
// blocked(dir) — true if moving in dir would be impossible for the calling object.
|
||||
let b = board.clone();
|
||||
let bq = board_queue.clone();
|
||||
engine.register_fn("blocked", move |ctx: NativeCallContext, dir: Direction| {
|
||||
is_blocked(&b, &bq, source_of(&ctx), dir)
|
||||
});
|
||||
|
||||
// has_tag(s) -> bool: does the calling object have this tag?
|
||||
// Board.tagged(tag) -> Array[ObjectInfo]
|
||||
let b = board.clone();
|
||||
engine.register_fn("has_tag", move |ctx: NativeCallContext, tag: ImmutableString| {
|
||||
let src = source_of(&ctx);
|
||||
b.borrow().objects.get(&src).is_some_and(|o| o.tags.contains(tag.as_str()))
|
||||
engine.register_fn("tagged", move |_board: &mut BoardRef, tag: ImmutableString| -> rhai::Array {
|
||||
let board = b.borrow();
|
||||
board.objects.iter()
|
||||
.filter(|(_, o)| o.tags.contains(tag.as_str()))
|
||||
.filter_map(|(&id, _)| object_info_from(id, &board))
|
||||
.map(Dynamic::from)
|
||||
.collect()
|
||||
});
|
||||
|
||||
// get_tags() -> Array of strings: the calling object's current tag set.
|
||||
// Board.named(name) -> ObjectInfo | ()
|
||||
let b = board.clone();
|
||||
engine.register_fn("get_tags", move |ctx: NativeCallContext| -> rhai::Array {
|
||||
let src = source_of(&ctx);
|
||||
b.borrow()
|
||||
.objects
|
||||
.get(&src)
|
||||
.map(|o| o.tags.iter().map(|t| rhai::Dynamic::from(t.clone())).collect())
|
||||
.unwrap_or_default()
|
||||
engine.register_fn("named", move |_board: &mut BoardRef, name: ImmutableString| -> Dynamic {
|
||||
let board = b.borrow();
|
||||
board.objects.iter()
|
||||
.find(|(_, o)| o.name.as_deref() == Some(name.as_str()))
|
||||
.and_then(|(&id, _)| object_info_from(id, &board))
|
||||
.map(Dynamic::from)
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
});
|
||||
|
||||
// objects_with_tag(s) -> Array of i64: ids of all objects carrying the tag.
|
||||
// Board.get(id) -> ObjectInfo | () (-1 returns player; unknown id logs error)
|
||||
let b = board.clone();
|
||||
engine.register_fn(
|
||||
"objects_with_tag",
|
||||
move |_ctx: NativeCallContext, tag: ImmutableString| -> rhai::Array {
|
||||
b.borrow()
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|(_, o)| o.tags.contains(tag.as_str()))
|
||||
.map(|(&id, _)| rhai::Dynamic::from(id as i64))
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
|
||||
// my_name() -> String: the calling object's name, or "" if unnamed.
|
||||
let b = board.clone();
|
||||
engine.register_fn("my_name", move |ctx: NativeCallContext| -> String {
|
||||
let src = source_of(&ctx);
|
||||
b.borrow()
|
||||
.objects
|
||||
.get(&src)
|
||||
.and_then(|o| o.name.as_deref())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
let errs = errors.clone();
|
||||
engine.register_fn("get", move |_board: &mut BoardRef, id: i64| -> Dynamic {
|
||||
let board = b.borrow();
|
||||
if id == -1 {
|
||||
return Dynamic::from(object_info_player(&board));
|
||||
}
|
||||
if id <= 0 {
|
||||
errs.borrow_mut().push(LogLine::error(format!("Board.get: invalid id {id}")));
|
||||
return Dynamic::UNIT;
|
||||
}
|
||||
match object_info_from(id as ObjectId, &board) {
|
||||
Some(info) => Dynamic::from(info),
|
||||
None => {
|
||||
errs.borrow_mut().push(LogLine::error(format!("Board.get: no object with id {id}")));
|
||||
Dynamic::UNIT
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// object_id_for_name(s) -> i64: id of the object with the given name, or 0 if none.
|
||||
let b = board.clone();
|
||||
engine.register_fn(
|
||||
"object_id_for_name",
|
||||
move |_ctx: NativeCallContext, name: ImmutableString| -> i64 {
|
||||
b.borrow()
|
||||
.objects
|
||||
.iter()
|
||||
.find(|(_, o)| o.name.as_deref() == Some(name.as_str()))
|
||||
.map(|(&id, _)| id as i64)
|
||||
.unwrap_or(0)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether the object at `source` would bump something by moving in `dir`.
|
||||
///
|
||||
/// Returns `true` if the target cell is off the board, already holds a solid, or is
|
||||
/// the destination of any pending move on the board queue. The caller's own move is
|
||||
/// never on the queue yet (its pump runs after its script), so it can't block itself.
|
||||
///
|
||||
/// Pending moves are matched by their *immediate* target cell only; a queued move that
|
||||
/// would push a chain of solids into the cell is not accounted for (an approximation).
|
||||
/// Whether the object at `source` would be blocked moving in `dir`.
|
||||
fn is_blocked(
|
||||
board: &BoardRef,
|
||||
board_queue: &BoardQueue,
|
||||
@@ -421,19 +538,12 @@ fn is_blocked(
|
||||
dir: Direction,
|
||||
) -> bool {
|
||||
let board = board.borrow();
|
||||
let Some(obj) = board.objects.get(&source) else {
|
||||
return true;
|
||||
};
|
||||
let Some(obj) = board.objects.get(&source) else { return true; };
|
||||
let (dx, dy): (i32, i32) = dir.into();
|
||||
let target = (obj.x as i32 + dx, obj.y as i32 + dy);
|
||||
if !board.in_bounds(target) {
|
||||
return true;
|
||||
}
|
||||
if !board.in_bounds(target) { return true; }
|
||||
let (tx, ty) = (target.0 as usize, target.1 as usize);
|
||||
if board.solid_at(tx, ty).is_some() {
|
||||
return true;
|
||||
}
|
||||
// Any pending move whose immediate target is this cell would put a solid here.
|
||||
if board.solid_at(tx, ty).is_some() { return true; }
|
||||
board_queue.borrow().iter().any(|ba| {
|
||||
if let Action::Move(d) = &ba.action
|
||||
&& let Some(mover) = board.objects.get(&ba.source)
|
||||
@@ -445,12 +555,8 @@ fn is_blocked(
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers the write API: the `Direction` type and the `move`/`set_tile`/`log`/
|
||||
/// `say`/`now` host functions.
|
||||
///
|
||||
/// `move(dir)` pushes both a `Move` and a `Delay(MOVE_COST)` so the object's queue
|
||||
/// paces itself automatically. `now()` promotes the most recently enqueued action to
|
||||
/// the front of the queue, letting a zero-cost action bypass a pending delay.
|
||||
// ── Write API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
||||
engine.register_type_with_name::<Direction>("Direction");
|
||||
|
||||
@@ -464,7 +570,7 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
||||
}
|
||||
});
|
||||
|
||||
// delay(secs): insert an explicit pause into the queue, merged with any trailing Delay.
|
||||
// delay(secs): insert an explicit pause. Two overloads so integer literals work.
|
||||
let q = queues.clone();
|
||||
engine.register_fn("delay", move |ctx: NativeCallContext, secs: f64| {
|
||||
let src = source_of(&ctx);
|
||||
@@ -472,9 +578,15 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
||||
push_delay(queue, secs.max(0.0));
|
||||
}
|
||||
});
|
||||
let q = queues.clone();
|
||||
engine.register_fn("delay", move |ctx: NativeCallContext, secs: i64| {
|
||||
let src = source_of(&ctx);
|
||||
if let Some(queue) = q.borrow().get(&src) {
|
||||
push_delay(queue, (secs.max(0)) as f64);
|
||||
}
|
||||
});
|
||||
|
||||
// now(): pop the back of the queue and push it to the front.
|
||||
// Use after a zero-cost action (say, log) to bypass any pending Delay.
|
||||
// now(): promote the most recently enqueued action to the front.
|
||||
let q = queues.clone();
|
||||
engine.register_fn("now", move |ctx: NativeCallContext| {
|
||||
let src = source_of(&ctx);
|
||||
@@ -492,53 +604,173 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
||||
});
|
||||
|
||||
let q = queues.clone();
|
||||
engine.register_fn(
|
||||
"log",
|
||||
move |ctx: NativeCallContext, msg: ImmutableString| {
|
||||
emit(
|
||||
&q,
|
||||
source_of(&ctx),
|
||||
Action::Log(LogLine::raw(msg.to_string())),
|
||||
);
|
||||
},
|
||||
);
|
||||
engine.register_fn("log", move |ctx: NativeCallContext, msg: ImmutableString| {
|
||||
emit(&q, source_of(&ctx), Action::Log(LogLine::raw(msg.to_string())));
|
||||
});
|
||||
|
||||
let q = queues.clone();
|
||||
engine.register_fn(
|
||||
"say",
|
||||
move |ctx: NativeCallContext, msg: ImmutableString| {
|
||||
emit(&q, source_of(&ctx), Action::Say(msg.to_string()));
|
||||
},
|
||||
);
|
||||
engine.register_fn("say", move |ctx: NativeCallContext, msg: ImmutableString| {
|
||||
emit(&q, source_of(&ctx), Action::Say(msg.to_string()));
|
||||
});
|
||||
|
||||
// set_tag(target_id, tag, present): add (true) or remove (false) a tag on any object.
|
||||
// Use MY_ID as target_id to mutate the calling object's own tags.
|
||||
let q = queues.clone();
|
||||
engine.register_fn(
|
||||
"set_tag",
|
||||
move |ctx: NativeCallContext, target: i64, tag: ImmutableString, present: bool| {
|
||||
emit(
|
||||
&q,
|
||||
source_of(&ctx),
|
||||
Action::SetTag {
|
||||
target: target as ObjectId,
|
||||
tag: tag.to_string(),
|
||||
present,
|
||||
},
|
||||
);
|
||||
emit(&q, source_of(&ctx), Action::SetTag {
|
||||
target: target as ObjectId,
|
||||
tag: tag.to_string(),
|
||||
present,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// set_fg(fg): change foreground color only.
|
||||
let q = queues.clone();
|
||||
engine.register_fn("set_fg", move |ctx: NativeCallContext, fg: ImmutableString| {
|
||||
emit(&q, source_of(&ctx), Action::SetColor {
|
||||
fg: Some(parse_color(fg.as_str())),
|
||||
bg: None,
|
||||
});
|
||||
});
|
||||
|
||||
// set_bg(bg): change background color only.
|
||||
let q = queues.clone();
|
||||
engine.register_fn("set_bg", move |ctx: NativeCallContext, bg: ImmutableString| {
|
||||
emit(&q, source_of(&ctx), Action::SetColor {
|
||||
fg: None,
|
||||
bg: Some(parse_color(bg.as_str())),
|
||||
});
|
||||
});
|
||||
|
||||
// set_color(fg, bg): change both colors.
|
||||
let q = queues.clone();
|
||||
engine.register_fn(
|
||||
"set_color",
|
||||
move |ctx: NativeCallContext, fg: ImmutableString, bg: ImmutableString| {
|
||||
emit(&q, source_of(&ctx), Action::SetColor {
|
||||
fg: Some(parse_color(fg.as_str())),
|
||||
bg: Some(parse_color(bg.as_str())),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// send(target, fn_name): call a named function on another object.
|
||||
let q = queues.clone();
|
||||
engine.register_fn(
|
||||
"send",
|
||||
move |ctx: NativeCallContext, target: i64, name: ImmutableString| {
|
||||
emit(&q, source_of(&ctx), Action::Send {
|
||||
target: target as ObjectId,
|
||||
fn_name: name.to_string(),
|
||||
arg: None,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// send(target, fn_name, arg): same, with an optional string or number argument.
|
||||
let q = queues.clone();
|
||||
engine.register_fn(
|
||||
"send",
|
||||
move |ctx: NativeCallContext, target: i64, name: ImmutableString, arg: Dynamic| {
|
||||
let script_arg = if let Ok(n) = arg.clone().as_int() {
|
||||
Some(ScriptArg::Num(n as f64))
|
||||
} else if let Ok(f) = arg.clone().as_float() {
|
||||
Some(ScriptArg::Num(f))
|
||||
} else if let Ok(s) = arg.into_string() {
|
||||
Some(ScriptArg::Str(s))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
emit(&q, source_of(&ctx), Action::Send {
|
||||
target: target as ObjectId,
|
||||
fn_name: name.to_string(),
|
||||
arg: script_arg,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Registers the `Queue` object: a handle to an object's output queue with `length()`
|
||||
/// and `clear()`. Safe to mutate from a script because the host only touches these
|
||||
/// queues between script calls (during [`ScriptHost::pump`]).
|
||||
fn register_queue_api(engine: &mut Engine) {
|
||||
// ── Queue API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn register_queue_api(engine: &mut Engine, errors: &ErrorSink) {
|
||||
engine.register_type_with_name::<ObjQueue>("Queue");
|
||||
engine.register_fn("length", |q: &mut ObjQueue| q.borrow().len() as i64);
|
||||
engine.register_fn("clear", |q: &mut ObjQueue| q.borrow_mut().clear());
|
||||
engine.register_fn("clear", |q: &mut ObjQueue| q.borrow_mut().clear());
|
||||
|
||||
// peek(): front action as a Rhai map, or () if empty.
|
||||
engine.register_fn("peek", |q: &mut ObjQueue| -> Dynamic {
|
||||
q.borrow()
|
||||
.front()
|
||||
.map(|a| Dynamic::from(action_to_map(a)))
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
});
|
||||
|
||||
// pop(): same, but removes the front.
|
||||
engine.register_fn("pop", |q: &mut ObjQueue| -> Dynamic {
|
||||
q.borrow_mut()
|
||||
.pop_front()
|
||||
.as_ref()
|
||||
.map(|a| Dynamic::from(action_to_map(a)))
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
});
|
||||
|
||||
// push(map): parse the map into an Action and enqueue; log an error on failure.
|
||||
let errs = errors.clone();
|
||||
// We need the queues map here to route push to the right object's queue.
|
||||
// Since push() is called on the Queue handle directly, we use that handle.
|
||||
engine.register_fn("push", move |q: &mut ObjQueue, map: Dynamic| {
|
||||
let rhai_map = match map.try_cast::<rhai::Map>() {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
errs.borrow_mut().push(LogLine::error(
|
||||
"Queue.push: argument must be an Action map".to_string(),
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
match Action::try_from(rhai_map) {
|
||||
Ok(action) => q.borrow_mut().push_back(action),
|
||||
Err(msg) => errs.borrow_mut().push(LogLine::error(format!("Queue.push: {msg}"))),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Scope construction ────────────────────────────────────────────────────────
|
||||
|
||||
/// Builds a fresh per-object scope with the Board view, Queue, Me, direction
|
||||
/// constants, and the 16 EGA/VGA named color constants.
|
||||
fn new_object_scope(board: &BoardRef, queue: &ObjQueue, object_id: ObjectId) -> Scope<'static> {
|
||||
let mut scope = Scope::new();
|
||||
scope.push_constant("Board", board.clone());
|
||||
scope.push_constant("Queue", queue.clone());
|
||||
scope.push_constant("Me", Me { object_id, board: board.clone() });
|
||||
scope.push_constant("North", Direction::North);
|
||||
scope.push_constant("South", Direction::South);
|
||||
scope.push_constant("East", Direction::East);
|
||||
scope.push_constant("West", Direction::West);
|
||||
// 16 EGA/VGA color constants as "#RRGGBB" strings.
|
||||
scope.push_constant("Black", "#000000");
|
||||
scope.push_constant("Blue", "#0000AA");
|
||||
scope.push_constant("Green", "#00AA00");
|
||||
scope.push_constant("Cyan", "#00AAAA");
|
||||
scope.push_constant("Red", "#AA0000");
|
||||
scope.push_constant("Magenta", "#AA00AA");
|
||||
scope.push_constant("Brown", "#AA5500");
|
||||
scope.push_constant("LightGray", "#AAAAAA");
|
||||
scope.push_constant("DarkGray", "#555555");
|
||||
scope.push_constant("BrightBlue", "#5555FF");
|
||||
scope.push_constant("BrightGreen", "#55FF55");
|
||||
scope.push_constant("BrightCyan", "#55FFFF");
|
||||
scope.push_constant("BrightRed", "#FF5555");
|
||||
scope.push_constant("BrightMagenta","#FF55FF");
|
||||
scope.push_constant("Yellow", "#FFFF55");
|
||||
scope.push_constant("White", "#FFFFFF");
|
||||
scope
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Appends a [`Action::Delay`] to the back of `queue`, merging with an existing
|
||||
/// trailing delay to prevent adjacent delays from accumulating.
|
||||
fn push_delay(queue: &ObjQueue, secs: f64) {
|
||||
@@ -557,28 +789,10 @@ fn emit(queues: &QueueMap, source: ObjectId, action: Action) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the issuing object's [`ObjectId`] from the call tag (set in [`ScriptHost::run`]).
|
||||
///
|
||||
/// Falls back to `0` (never a valid id, since ids start at 1) if the tag is missing
|
||||
/// or out of range, which makes [`emit`] a harmless no-op for an unknown source.
|
||||
/// Reads the issuing object's [`ObjectId`] from the call tag.
|
||||
fn source_of(ctx: &NativeCallContext) -> ObjectId {
|
||||
ctx.tag()
|
||||
.and_then(|t| t.as_int().ok())
|
||||
.and_then(|n| ObjectId::try_from(n).ok())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Builds a fresh per-object scope, seeded with the read-only `Board` view, this
|
||||
/// object's `Queue`, the four direction constants, and `MY_ID` (the object's own
|
||||
/// stable [`ObjectId`] as `i64`, for use with `set_tag` and similar calls).
|
||||
fn new_object_scope(board: &BoardRef, queue: &ObjQueue, object_id: ObjectId) -> Scope<'static> {
|
||||
let mut scope = Scope::new();
|
||||
scope.push_constant("Board", board.clone());
|
||||
scope.push_constant("Queue", queue.clone());
|
||||
scope.push_constant("North", Direction::North);
|
||||
scope.push_constant("South", Direction::South);
|
||||
scope.push_constant("East", Direction::East);
|
||||
scope.push_constant("West", Direction::West);
|
||||
scope.push_constant("MY_ID", object_id as i64);
|
||||
scope
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user