more scripting, objects moving
This commit is contained in:
+327
-161
@@ -1,62 +1,81 @@
|
||||
//! Rhai scripting runtime for board objects.
|
||||
//!
|
||||
//! [`ScriptHost`] owns the Rhai [`Engine`], the compiled scripts referenced by a
|
||||
//! board's objects, and a persistent per-object [`Scope`]. It drives two optional
|
||||
//! board's objects, and a persistent per-object [`Scope`]. It drives the optional
|
||||
//! lifecycle hooks on each scripted object:
|
||||
//!
|
||||
//! - `init()` — run once after the whole map is loaded (see [`ScriptHost::run_init`]).
|
||||
//! - `tick(dt)` — run every frame with the elapsed seconds (see [`ScriptHost::run_tick`]).
|
||||
//! - `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`].
|
||||
//!
|
||||
//! ## Reads vs. writes
|
||||
//! ## Actions, queues, and rate limiting
|
||||
//!
|
||||
//! Scripts **read** the world directly through a read-only `Rc<RefCell<Board>>` (type aliased
|
||||
//! as [`BoardRef`], registering getters only) pushed into each scope as `Board`, e.g.
|
||||
//! `Board.player_x`. Functions borrow it briefly per getter.
|
||||
//! Scripts don't mutate the world directly. Each write host function (`move`,
|
||||
//! `set_tile`, `log`) appends an [`Action`] to the issuing object's **output
|
||||
//! queue** (a per-object FIFO). Actions leave that queue one batch at a time via
|
||||
//! [`ScriptHost::pump`], which promotes them onto the shared **board queue**:
|
||||
//!
|
||||
//! Scripts **write** by enqueuing [`Command`]s: host functions (`move`,
|
||||
//! `set_tile`, `log`) push into a shared queue that [`GameState`] drains and
|
||||
//! applies *after* each batch ([`ScriptHost::take_commands`]). This keeps script
|
||||
//! execution from ever needing a mutable borrow of the game state, and makes
|
||||
//! structural/ordering effects deterministic. Which object issued a command is
|
||||
//! read from the per-call **tag** (set to the object index in [`ScriptHost::run`]),
|
||||
//! so scripts write `move(North)` without naming themselves.
|
||||
//! - A [`pump`](ScriptHost::pump) pulls the leading run of zero-cost actions plus at
|
||||
//! most one action with a time cost (currently only [`Action::Move`], 250 ms).
|
||||
//! - Pulling a timed action arms the object's **ready timer**; while it is running no
|
||||
//! further actions are pulled (this is what caps an object's speed). [`tick`] ticks
|
||||
//! the timer down via [`advance_timers`](ScriptHost::advance_timers).
|
||||
//!
|
||||
//! [`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`]).
|
||||
//!
|
||||
//! [`tick`]: ScriptHost::run_tick
|
||||
//! [`take_board_queue`]: ScriptHost::take_board_queue
|
||||
//! [`GameState`]: crate::game::GameState
|
||||
|
||||
use crate::game::Board;
|
||||
use crate::log::LogLine;
|
||||
use rhai::{AST, CallFnOptions, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Shared queue the write host functions push into; drained by [`ScriptHost::take_commands`].
|
||||
type CommandQueue = Rc<RefCell<Vec<Command>>>;
|
||||
/// How long a move occupies an object before it can act again, in seconds.
|
||||
pub const MOVE_COST: f64 = 0.25;
|
||||
|
||||
/// A mutation requested by a script, applied by [`crate::game::GameState`] after
|
||||
/// the script batch finishes.
|
||||
pub struct Command {
|
||||
/// The object that issued the command.
|
||||
// TODO: `source` is an index into `Board::objects`, which is a stopgap — it
|
||||
// breaks under object spawn/destroy/reorder. Replace with a monotonically
|
||||
// increasing unique object id, with objects stored keyed by it (e.g.
|
||||
// `BTreeMap<ObjectId, ObjectDef>`). Mirrored on `ObjectRuntime::object_index`.
|
||||
pub source: usize,
|
||||
/// What the command does.
|
||||
pub kind: GameCommand,
|
||||
}
|
||||
|
||||
/// The kinds of mutation a script can request.
|
||||
pub enum GameCommand {
|
||||
/// One deferred mutation emitted by a script, applied by [`crate::game::GameState`]
|
||||
/// after it is promoted onto the board queue.
|
||||
pub enum Action {
|
||||
/// Move the source object one cell in a direction (subject to passability).
|
||||
Move(Direction),
|
||||
/// Set the source object's glyph tile index.
|
||||
SetTile(u32),
|
||||
/// Append a plain (uncolored) line to the game log.
|
||||
Log(LogLine),
|
||||
/// A script/engine failure. Applying it logs the message for now; kept a
|
||||
/// distinct variant so execution can later be halted on error.
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl Action {
|
||||
/// How long performing this action keeps the object busy, in seconds.
|
||||
///
|
||||
/// Only [`Action::Move`] has a cost ([`MOVE_COST`]); zero-cost actions can be
|
||||
/// drained several-per-pump and never arm the ready timer.
|
||||
pub fn time_cost(&self) -> f64 {
|
||||
match self {
|
||||
Action::Move(_) => MOVE_COST,
|
||||
Action::SetTile(_) | Action::Log(_) => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An action promoted from an object's output queue onto the board queue, tagged
|
||||
/// with the object that issued it.
|
||||
pub struct BoardAction {
|
||||
/// The object that issued the action.
|
||||
// TODO: `source` is an index into `Board::objects`, which is a stopgap — it
|
||||
// breaks under object spawn/destroy/reorder. Replace with a monotonically
|
||||
// increasing unique object id, with objects stored keyed by it (e.g.
|
||||
// `BTreeMap<ObjectId, ObjectDef>`). Mirrored on `ObjectRuntime::object_index`.
|
||||
pub source: usize,
|
||||
/// The action to apply.
|
||||
pub action: Action,
|
||||
}
|
||||
|
||||
/// A cardinal movement direction.
|
||||
@@ -85,6 +104,22 @@ impl From<Direction> for (i32, i32) {
|
||||
/// read-only by construction.
|
||||
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).
|
||||
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.
|
||||
type BoardQueue = Rc<RefCell<Vec<BoardAction>>>;
|
||||
|
||||
/// Maps an object index to its output queue, so the global write host functions can
|
||||
/// route an emitted action to the right object via the per-call tag.
|
||||
type QueueMap = Rc<RefCell<HashMap<usize, ObjQueue>>>;
|
||||
|
||||
/// Channel for engine/compile/runtime errors, surfaced straight to the game log
|
||||
/// (errors never go through the action queues).
|
||||
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).
|
||||
@@ -93,20 +128,27 @@ struct CompiledScript {
|
||||
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 and its own
|
||||
/// persistent Rhai scope.
|
||||
/// The runtime state of one scripted object: which script it runs, its persistent
|
||||
/// Rhai scope, its pending actions, and its cooldown timer.
|
||||
struct ObjectRuntime {
|
||||
/// Index into [`crate::game::Board::objects`]; passed to scripts as the
|
||||
/// per-call tag so host functions know which object issued a command.
|
||||
// TODO: see [`Command::source`] — array indices should become stable ids.
|
||||
/// per-call tag so host functions know which object issued an action.
|
||||
// TODO: see [`BoardAction::source`] — array indices should become stable ids.
|
||||
object_index: usize,
|
||||
/// Key into [`ScriptHost::scripts`] naming this object's compiled script.
|
||||
script_name: String,
|
||||
/// Per-object scope (holds the `board` view + direction constants, and can
|
||||
/// hold object-local state in the future). Persists across calls.
|
||||
/// 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).
|
||||
output_queue: ObjQueue,
|
||||
/// Seconds remaining before this object may pull another action. While `> 0`
|
||||
/// nothing is pulled from [`output_queue`](Self::output_queue).
|
||||
ready_timer: f64,
|
||||
}
|
||||
|
||||
/// Owns the Rhai engine and per-object script state for a board.
|
||||
@@ -117,22 +159,27 @@ pub struct ScriptHost {
|
||||
scripts: HashMap<String, CompiledScript>,
|
||||
/// One entry per object that has a successfully compiled script.
|
||||
objects: Vec<ObjectRuntime>,
|
||||
/// Queue the write host functions push into; drained by [`Self::take_commands`].
|
||||
commands: CommandQueue,
|
||||
/// 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 `state`: registers the read/write API,
|
||||
/// compiles every script referenced by an object (once each), and creates a
|
||||
/// fresh scope per scripted object. Compile errors and references to unknown
|
||||
/// scripts are queued as [`GameCommand::Error`] (retrievable via
|
||||
/// [`Self::take_commands`]); no script is run here.
|
||||
/// Builds a host for the board behind `board_cell`: registers the read/write 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.
|
||||
pub fn new(board_cell: &BoardRef) -> Self {
|
||||
let commands: CommandQueue = Rc::new(RefCell::new(Vec::new()));
|
||||
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);
|
||||
register_write_api(&mut engine, &commands);
|
||||
register_read_api(&mut engine, board_cell, &board_queue);
|
||||
register_write_api(&mut engine, &queues);
|
||||
register_queue_api(&mut engine);
|
||||
|
||||
let board = board_cell.borrow();
|
||||
|
||||
@@ -140,7 +187,7 @@ impl ScriptHost {
|
||||
// object that references the script.
|
||||
let mut scripts: HashMap<String, CompiledScript> = HashMap::new();
|
||||
let mut failed: HashSet<String> = HashSet::new();
|
||||
for (i, obj) in board.objects.iter().enumerate() {
|
||||
for obj in board.objects.iter() {
|
||||
let Some(name) = obj.script_name.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
@@ -151,57 +198,55 @@ impl ScriptHost {
|
||||
Some(src) => match engine.compile(src) {
|
||||
Ok(ast) => {
|
||||
// Detect the optional hooks by name and arity.
|
||||
let has_init = ast
|
||||
.iter_functions()
|
||||
.any(|f| f.name == "init" && f.params.is_empty());
|
||||
let has_tick = ast
|
||||
.iter_functions()
|
||||
.any(|f| f.name == "tick" && f.params.len() == 1);
|
||||
scripts.insert(
|
||||
name.clone(),
|
||||
CompiledScript {
|
||||
ast,
|
||||
has_init,
|
||||
has_tick,
|
||||
},
|
||||
);
|
||||
let defines = |n: &str, params: usize| {
|
||||
ast.iter_functions()
|
||||
.any(|f| f.name == n && f.params.len() == params)
|
||||
};
|
||||
let compiled = 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());
|
||||
commands.borrow_mut().push(Command {
|
||||
source: i,
|
||||
kind: GameCommand::Error(format!(
|
||||
"script '{name}' failed to compile: {err}"
|
||||
)),
|
||||
});
|
||||
errors
|
||||
.borrow_mut()
|
||||
.push(LogLine::raw(format!("script '{name}' failed to compile: {err}")));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
failed.insert(name.clone());
|
||||
commands.borrow_mut().push(Command {
|
||||
source: i,
|
||||
kind: GameCommand::Error(format!(
|
||||
"object references unknown script '{name}'"
|
||||
)),
|
||||
});
|
||||
errors
|
||||
.borrow_mut()
|
||||
.push(LogLine::raw(format!("object references unknown script '{name}'")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One runtime (independent scope) per object whose script compiled.
|
||||
let objects = board
|
||||
.objects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, obj)| {
|
||||
let name = obj.script_name.as_ref()?;
|
||||
scripts.contains_key(name).then(|| ObjectRuntime {
|
||||
object_index: i,
|
||||
script_name: name.clone(),
|
||||
scope: new_object_scope(board_cell),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// 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.
|
||||
let mut objects = Vec::new();
|
||||
for (i, obj) in board.objects.iter().enumerate() {
|
||||
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(i, queue.clone());
|
||||
objects.push(ObjectRuntime {
|
||||
object_index: i,
|
||||
script_name: name.clone(),
|
||||
scope: new_object_scope(board_cell, &queue),
|
||||
output_queue: queue,
|
||||
ready_timer: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
drop(board);
|
||||
|
||||
@@ -209,112 +254,232 @@ impl ScriptHost {
|
||||
engine,
|
||||
scripts,
|
||||
objects,
|
||||
commands,
|
||||
board_queue,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls `init()` on every scripted object that defines it.
|
||||
/// Calls `init()` on every scripted object that defines it, pumping each object's
|
||||
/// queue afterward.
|
||||
pub fn run_init(&mut self) {
|
||||
self.run("init", |c| c.has_init, ());
|
||||
}
|
||||
|
||||
/// Calls `tick(dt)` on every scripted object that defines it, passing the
|
||||
/// elapsed seconds since the last tick.
|
||||
/// Calls `tick(dt)` on every scripted object that defines it, passing the elapsed
|
||||
/// seconds, then pumps each object's queue.
|
||||
pub fn run_tick(&mut self, dt: f64) {
|
||||
self.run("tick", |c| c.has_tick, (dt,));
|
||||
}
|
||||
|
||||
/// Removes and returns the commands queued by scripts since the last drain.
|
||||
pub fn take_commands(&mut self) -> Vec<Command> {
|
||||
std::mem::take(&mut self.commands.borrow_mut())
|
||||
}
|
||||
|
||||
/// Shared driver for the lifecycle hooks: for each object whose script
|
||||
/// `defined` reports the hook, call it with `args`, tagging the call with the
|
||||
/// object index so host functions know the command source. Runtime errors are
|
||||
/// captured as [`GameCommand::Error`] rather than aborting the batch.
|
||||
fn run<A: FuncArgs + Copy>(
|
||||
&mut self,
|
||||
hook: &str,
|
||||
defined: fn(&CompiledScript) -> bool,
|
||||
args: A,
|
||||
) {
|
||||
// Split the borrow so we can call the engine while mutating each scope.
|
||||
/// Calls `bump(id)` on the object at `object_index`, if it defines the hook.
|
||||
///
|
||||
/// `bumper` is the index of the object that moved into it, or `-1` for the player.
|
||||
/// Does **not** pump: any actions the hook emits wait for the next tick's pump, so
|
||||
/// a bump can't cascade within the same resolution pass.
|
||||
pub fn run_bump(&mut self, object_index: usize, bumper: i64) {
|
||||
let Self {
|
||||
engine,
|
||||
scripts,
|
||||
objects,
|
||||
commands,
|
||||
errors,
|
||||
..
|
||||
} = self;
|
||||
for obj in objects.iter_mut() {
|
||||
let Some(compiled) = scripts.get(&obj.script_name) else {
|
||||
continue;
|
||||
let Some(obj) = objects.iter_mut().find(|o| o.object_index == object_index) else {
|
||||
return;
|
||||
};
|
||||
let Some(compiled) = scripts.get(&obj.script_name) else {
|
||||
return;
|
||||
};
|
||||
if !compiled.has_bump {
|
||||
return;
|
||||
}
|
||||
let options = CallFnOptions::default().with_tag(object_index as i64);
|
||||
if let Err(err) =
|
||||
engine.call_fn_with_options::<()>(options, &mut obj.scope, &compiled.ast, "bump", (bumper,))
|
||||
{
|
||||
errors
|
||||
.borrow_mut()
|
||||
.push(LogLine::raw(format!("script '{}' bump error: {err}", obj.script_name)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts every object's ready timer down by `dt` (floored at zero). Call once per
|
||||
/// frame before [`run_tick`](Self::run_tick).
|
||||
pub fn advance_timers(&mut self, dt: f64) {
|
||||
for obj in &mut self.objects {
|
||||
obj.ready_timer = (obj.ready_timer - dt).max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes and returns the actions promoted onto the board queue.
|
||||
pub fn take_board_queue(&mut self) -> Vec<BoardAction> {
|
||||
std::mem::take(&mut self.board_queue.borrow_mut())
|
||||
}
|
||||
|
||||
/// Removes and returns the errors collected since the last drain.
|
||||
pub fn take_errors(&mut self) -> Vec<LogLine> {
|
||||
std::mem::take(&mut self.errors.borrow_mut())
|
||||
}
|
||||
|
||||
/// Promotes ready actions from object `i`'s output queue onto the board queue.
|
||||
///
|
||||
/// While the object's ready timer is running, nothing is pulled. Otherwise the
|
||||
/// leading run of zero-cost actions is promoted, followed by at most one timed
|
||||
/// action (which arms the timer and ends the pump). The board-queue guard keeps a
|
||||
/// second pump in the same tick from double-promoting.
|
||||
fn pump(&mut self, i: usize) {
|
||||
let oid = self.objects[i].object_index;
|
||||
// Don't re-promote if this object already has actions awaiting resolution.
|
||||
if self.board_queue.borrow().iter().any(|ba| ba.source == oid) {
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
// A running cooldown blocks all further pulls (timed or not).
|
||||
if self.objects[i].ready_timer > 0.0 {
|
||||
break;
|
||||
}
|
||||
let queue = self.objects[i].output_queue.clone();
|
||||
let cost = match queue.borrow().front() {
|
||||
Some(action) => action.time_cost(),
|
||||
None => break, // queue drained
|
||||
};
|
||||
if !defined(compiled) {
|
||||
continue;
|
||||
let action = queue.borrow_mut().pop_front().expect("front checked above");
|
||||
if cost > 0.0 {
|
||||
self.objects[i].ready_timer = cost;
|
||||
}
|
||||
// The tag carries this object's index to the host functions.
|
||||
let options = CallFnOptions::default().with_tag(obj.object_index as i64);
|
||||
if let Err(err) = engine.call_fn_with_options::<()>(
|
||||
options,
|
||||
&mut obj.scope,
|
||||
&compiled.ast,
|
||||
hook,
|
||||
args,
|
||||
) {
|
||||
commands.borrow_mut().push(Command {
|
||||
source: obj.object_index,
|
||||
kind: GameCommand::Error(format!(
|
||||
"script '{}' {hook} error: {err}",
|
||||
obj.script_name
|
||||
)),
|
||||
});
|
||||
self.board_queue.borrow_mut().push(BoardAction { source: oid, action });
|
||||
// A timed action ends the pump; zero-cost ones keep draining.
|
||||
if cost > 0.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared driver for `init`/`tick`: for each object, call the hook if its script
|
||||
/// `defined` it (tagging the call with the object index so host functions know the
|
||||
/// source), then pump that object so backlogged actions drain and later objects'
|
||||
/// `blocked` checks can see earlier movers. Runtime errors are captured rather than
|
||||
/// aborting the batch.
|
||||
fn run<A: FuncArgs + Copy>(&mut self, hook: &str, defined: fn(&CompiledScript) -> bool, args: A) {
|
||||
for i in 0..self.objects.len() {
|
||||
// Scope the split borrow so it ends before the `pump` reborrow below.
|
||||
{
|
||||
let Self {
|
||||
engine,
|
||||
scripts,
|
||||
objects,
|
||||
errors,
|
||||
..
|
||||
} = self;
|
||||
let obj = &mut objects[i];
|
||||
if let Some(compiled) = scripts.get(&obj.script_name)
|
||||
&& defined(compiled)
|
||||
{
|
||||
// The tag carries this object's index to the host functions.
|
||||
let options = CallFnOptions::default().with_tag(obj.object_index as i64);
|
||||
if let Err(err) =
|
||||
engine.call_fn_with_options::<()>(options, &mut obj.scope, &compiled.ast, hook, args)
|
||||
{
|
||||
errors.borrow_mut().push(LogLine::raw(format!(
|
||||
"script '{}' {hook} error: {err}",
|
||||
obj.script_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.pump(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the read-only `Board` API: a [`BoardRef`] type with getters that
|
||||
/// borrow the shared state briefly.
|
||||
fn register_read_api(engine: &mut Engine) {
|
||||
/// 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) {
|
||||
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);
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
/// Registers the write API: the `Direction` type and the `move`/`set_tile`/`log`
|
||||
/// host functions, each enqueuing a [`Command`] tagged with its source object.
|
||||
fn register_write_api(engine: &mut Engine, commands: &CommandQueue) {
|
||||
/// 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).
|
||||
fn is_blocked(board: &BoardRef, board_queue: &BoardQueue, source: usize, dir: Direction) -> bool {
|
||||
let board = board.borrow();
|
||||
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;
|
||||
}
|
||||
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.
|
||||
board_queue.borrow().iter().any(|ba| {
|
||||
if let Action::Move(d) = &ba.action
|
||||
&& let Some(mover) = board.objects.get(ba.source)
|
||||
{
|
||||
let (mdx, mdy): (i32, i32) = (*d).into();
|
||||
return (mover.x as i32 + mdx, mover.y as i32 + mdy) == target;
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers the write API: the `Direction` type and the `move`/`set_tile`/`log` host
|
||||
/// functions, each appending an [`Action`] to the issuing object's output queue.
|
||||
fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
||||
engine.register_type_with_name::<Direction>("Direction");
|
||||
|
||||
let q = commands.clone();
|
||||
let q = queues.clone();
|
||||
engine.register_fn("move", move |ctx: NativeCallContext, dir: Direction| {
|
||||
q.borrow_mut().push(Command {
|
||||
source: source_of(&ctx),
|
||||
kind: GameCommand::Move(dir),
|
||||
});
|
||||
emit(&q, source_of(&ctx), Action::Move(dir));
|
||||
});
|
||||
|
||||
let q = commands.clone();
|
||||
let q = queues.clone();
|
||||
engine.register_fn("set_tile", move |ctx: NativeCallContext, tile: i64| {
|
||||
q.borrow_mut().push(Command {
|
||||
source: source_of(&ctx),
|
||||
kind: GameCommand::SetTile(tile as u32),
|
||||
});
|
||||
emit(&q, source_of(&ctx), Action::SetTile(tile as u32));
|
||||
});
|
||||
|
||||
let q = commands.clone();
|
||||
engine.register_fn(
|
||||
"log",
|
||||
move |ctx: NativeCallContext, msg: ImmutableString| {
|
||||
q.borrow_mut().push(Command {
|
||||
source: source_of(&ctx),
|
||||
kind: GameCommand::Log(LogLine::raw(msg.to_string())),
|
||||
});
|
||||
},
|
||||
);
|
||||
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())));
|
||||
});
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
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());
|
||||
}
|
||||
|
||||
/// Appends `action` to the output queue of the object identified by `source`.
|
||||
fn emit(queues: &QueueMap, source: usize, action: Action) {
|
||||
if let Some(queue) = queues.borrow().get(&source) {
|
||||
queue.borrow_mut().push_back(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the issuing object's index from the call tag (set in [`ScriptHost::run`]).
|
||||
@@ -325,11 +490,12 @@ fn source_of(ctx: &NativeCallContext) -> usize {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Builds a fresh per-object scope, seeded with the read-only `board` view and
|
||||
/// the four direction constants (`north`/`south`/`east`/`west`).
|
||||
fn new_object_scope(board: &BoardRef) -> Scope<'static> {
|
||||
/// Builds a fresh per-object scope, seeded with the read-only `Board` view, this
|
||||
/// object's `Queue`, and the four direction constants (`North`/`South`/`East`/`West`).
|
||||
fn new_object_scope(board: &BoardRef, queue: &ObjQueue) -> 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);
|
||||
|
||||
Reference in New Issue
Block a user