removed queue.push

This commit is contained in:
2026-06-13 01:47:11 -05:00
parent 73c8a9bd3e
commit f327e77e3d
3 changed files with 32 additions and 303 deletions
+21 -277
View File
@@ -1,30 +1,14 @@
//! The [`Action`] enum and its Rhai-facing conversions.
//!
//! This module owns the `Action` type (which scripts enqueue via write host
//! functions), the rate-limiting delay constant, and the Rhai bridge that
//! lets scripts introspect and compose action queues via
//! `Queue.peek()`/`Queue.pop()`/`Queue.push()`.
//!
//! ## Rhai `"Action"` module
//!
//! [`rhai_action_module`] builds a static Rhai module registered as `"Action"`
//! on the engine. Scripts can import it and call constructor functions:
//!
//! ```rhai
//! import "Action" as A;
//! Queue.push(A::Move(North));
//! Queue.push(A::Say("Hello!"));
//! ```
//!
//! Constructors return Rhai maps (`#{ type: "...", ... }`). [`action_to_map`]
//! converts in the same direction for `Queue.peek()` / `Queue.pop()`.
//! [`TryFrom<rhai::Map>`] converts back, used by `Queue.push()`.
//! functions), the rate-limiting delay constant, and [`action_to_map`] which
//! converts an `Action` to a Rhai map for `Queue.peek()` / `Queue.pop()`.
use crate::log::LogLine;
use crate::map_file::{color_to_hex, parse_color};
use crate::map_file::color_to_hex;
use crate::utils::{Direction, ObjectId, ScriptArg};
use color::Rgba8;
use rhai::{Dynamic, ImmutableString, Module};
use rhai::Dynamic;
/// How long a move occupies an object before it can act again, in seconds.
pub(crate) const MOVE_COST: f64 = 0.25;
@@ -70,7 +54,7 @@ pub(crate) enum Action {
Scroll(Vec<ScrollLine>),
}
// ── Direction helpers ─────────────────────────────────────────────────────────
// ── Direction helper ─────────────────────────────────────────────────────────
fn dir_to_str(d: Direction) -> &'static str {
match d {
@@ -81,59 +65,51 @@ fn dir_to_str(d: Direction) -> &'static str {
}
}
fn str_to_dir(s: &str) -> Option<Direction> {
match s {
"North" => Some(Direction::North),
"South" => Some(Direction::South),
"East" => Some(Direction::East),
"West" => Some(Direction::West),
_ => None,
}
}
// ── action_to_map ─────────────────────────────────────────────────────────────
/// Converts an [`Action`] to the Rhai map form used by `Queue.peek()` / `Queue.pop()`.
/// Converts an [`Action`] to a Rhai map for `Queue.peek()` / `Queue.pop()`.
///
/// The map always has a `"type"` key naming the variant; other keys carry the
/// payload. [`Action::Log`] is flattened to its first span's text.
pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
// Dynamic::from(String) avoids rhai 1.x's From<&str> lifetime restriction.
let ds = |v: &'static str| Dynamic::from(v.to_string());
let mut m = rhai::Map::new();
match action {
Action::Move(dir) => {
ins_str(&mut m, "type", "Move");
ins_str(&mut m, "dir", dir_to_str(*dir));
m.insert("type".into(), ds("Move"));
m.insert("dir".into(), ds(dir_to_str(*dir)));
}
Action::SetTile(n) => {
ins_str(&mut m, "type", "SetTile");
m.insert("type".into(), ds("SetTile"));
m.insert("tile".into(), Dynamic::from(*n as i64));
}
Action::Log(line) => {
let text = line.spans.first().map(|s| s.text.as_str()).unwrap_or("").to_string();
ins_str(&mut m, "type", "Log");
m.insert("msg".into(), Dynamic::from(text));
m.insert("type".into(), ds("Log"));
m.insert("msg".into(), Dynamic::from(text));
}
Action::SetTag { target, tag, present } => {
ins_str(&mut m, "type", "SetTag");
m.insert("type".into(), ds("SetTag"));
m.insert("target".into(), Dynamic::from(*target as i64));
m.insert("tag".into(), Dynamic::from(tag.clone()));
m.insert("present".into(), Dynamic::from(*present));
}
Action::Say(text) => {
ins_str(&mut m, "type", "Say");
m.insert("msg".into(), Dynamic::from(text.clone()));
m.insert("type".into(), ds("Say"));
m.insert("msg".into(), Dynamic::from(text.clone()));
}
Action::Delay(secs) => {
ins_str(&mut m, "type", "Delay");
m.insert("type".into(), ds("Delay"));
m.insert("secs".into(), Dynamic::from(*secs));
}
Action::SetColor { fg, bg } => {
ins_str(&mut m, "type", "SetColor");
m.insert("type".into(), ds("SetColor"));
if let Some(c) = fg { m.insert("fg".into(), Dynamic::from(color_to_hex(*c))); }
if let Some(c) = bg { m.insert("bg".into(), Dynamic::from(color_to_hex(*c))); }
}
Action::Send { target, fn_name, arg } => {
ins_str(&mut m, "type", "Send");
m.insert("type".into(), ds("Send"));
m.insert("target".into(), Dynamic::from(*target as i64));
m.insert("name".into(), Dynamic::from(fn_name.clone()));
if let Some(a) = arg {
@@ -144,242 +120,10 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
m.insert("arg".into(), dyn_arg);
}
}
// Scroll lines are not round-trippable via the queue map API; emit type only.
// Scroll lines are not inspectable via the queue map API; emit type only.
Action::Scroll(_) => {
ins_str(&mut m, "type", "Scroll");
m.insert("type".into(), ds("Scroll"));
}
}
m
}
// ── TryFrom<rhai::Map> ────────────────────────────────────────────────────────
impl TryFrom<rhai::Map> for Action {
type Error = String;
/// Converts a Rhai map (as produced by the `"Action"` module constructors or
/// [`action_to_map`]) back into a Rust [`Action`]. Returns `Err(msg)` when
/// the map is structurally invalid or names an unknown variant.
fn try_from(map: rhai::Map) -> Result<Self, Self::Error> {
let type_str = map
.get("type")
.ok_or_else(|| "missing 'type' key".to_string())?
.clone()
.into_string()
.map_err(|_| "'type' must be a string".to_string())?;
match type_str.as_str() {
"Move" => {
let dir_str = get_str(&map, "dir", "Move")?;
let dir = str_to_dir(&dir_str)
.ok_or_else(|| format!("Move: unknown direction '{dir_str}'"))?;
Ok(Action::Move(dir))
}
"SetTile" => {
let n = get_int(&map, "tile", "SetTile")?;
Ok(Action::SetTile(n as u32))
}
"Log" => {
let msg = get_str(&map, "msg", "Log")?;
Ok(Action::Log(LogLine::raw(msg)))
}
"Say" => {
let msg = get_str(&map, "msg", "Say")?;
Ok(Action::Say(msg))
}
"Delay" => {
let secs = get_float(&map, "secs", "Delay")?;
Ok(Action::Delay(secs))
}
"SetTag" => {
let target = get_int(&map, "target", "SetTag")?;
let tag = get_str(&map, "tag", "SetTag")?;
let present = get_bool(&map, "present", "SetTag")?;
Ok(Action::SetTag { target: target as ObjectId, tag, present })
}
"SetColor" => {
let fg = opt_color(&map, "fg", "SetColor")?;
let bg = opt_color(&map, "bg", "SetColor")?;
Ok(Action::SetColor { fg, bg })
}
"Send" => {
let target = get_int(&map, "target", "Send")?;
let fn_name = get_str(&map, "name", "Send")?;
// Accept int, float, or string as the optional arg.
let arg = if let Some(v) = map.get("arg") {
let v = v.clone();
if let Ok(n) = v.clone().as_int() {
Some(ScriptArg::Num(n as f64))
} else if let Ok(f) = v.clone().as_float() {
Some(ScriptArg::Num(f))
} else if let Ok(s) = v.into_string() {
Some(ScriptArg::Str(s))
} else {
None // unrecognised arg type — ignored
}
} else {
None
};
Ok(Action::Send { target: target as ObjectId, fn_name, arg })
}
"Scroll" => Err("Scroll actions cannot be constructed from maps".to_string()),
other => Err(format!("unknown action type: '{other}'")),
}
}
}
// ── Rhai "Action" module ──────────────────────────────────────────────────────
/// Builds the `"Action"` Rhai static module.
///
/// Register on the engine with
/// `engine.register_static_module("Action", rhai_action_module().into())`.
/// Scripts can then write `import "Action" as A; Queue.push(A::Move(North));`.
pub(crate) fn rhai_action_module() -> Module {
let mut m = Module::new();
m.set_native_fn("Move", |dir: Direction| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "Move");
ins_str(&mut map, "dir", dir_to_str(dir));
Ok(Dynamic::from(map))
});
m.set_native_fn("Delay", |secs: f64| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "Delay");
map.insert("secs".into(), Dynamic::from(secs));
Ok(Dynamic::from(map))
});
// Integer overload so A::Delay(2) works as well as A::Delay(2.0).
m.set_native_fn("Delay", |secs: i64| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "Delay");
map.insert("secs".into(), Dynamic::from(secs as f64));
Ok(Dynamic::from(map))
});
m.set_native_fn("SetTile", |n: i64| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "SetTile");
map.insert("tile".into(), Dynamic::from(n));
Ok(Dynamic::from(map))
});
m.set_native_fn("Log", |msg: ImmutableString| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "Log");
map.insert("msg".into(), Dynamic::from(msg));
Ok(Dynamic::from(map))
});
m.set_native_fn("Say", |msg: ImmutableString| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "Say");
map.insert("msg".into(), Dynamic::from(msg));
Ok(Dynamic::from(map))
});
// SetColor — fg only
m.set_native_fn("SetColor", |fg: ImmutableString| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "SetColor");
map.insert("fg".into(), Dynamic::from(fg));
Ok(Dynamic::from(map))
});
// SetColor — fg + bg
m.set_native_fn("SetColor", |fg: ImmutableString, bg: ImmutableString| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "SetColor");
map.insert("fg".into(), Dynamic::from(fg));
map.insert("bg".into(), Dynamic::from(bg));
Ok(Dynamic::from(map))
});
m.set_native_fn("SetTag", |target: i64, tag: ImmutableString, present: bool| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "SetTag");
map.insert("target".into(), Dynamic::from(target));
map.insert("tag".into(), Dynamic::from(tag));
map.insert("present".into(), Dynamic::from(present));
Ok(Dynamic::from(map))
});
// Send — no arg
m.set_native_fn("Send", |target: i64, name: ImmutableString| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "Send");
map.insert("target".into(), Dynamic::from(target));
map.insert("name".into(), Dynamic::from(name));
Ok(Dynamic::from(map))
});
// Send — with arg
m.set_native_fn("Send", |target: i64, name: ImmutableString, arg: Dynamic| {
let mut map = rhai::Map::new();
ins_str(&mut map, "type", "Send");
map.insert("target".into(), Dynamic::from(target));
map.insert("name".into(), Dynamic::from(name));
map.insert("arg".into(), arg);
Ok(Dynamic::from(map))
});
m
}
// ── Small helpers ─────────────────────────────────────────────────────────────
/// Inserts a `&'static str` value into a [`rhai::Map`] by converting via `String`.
/// Using `String` avoids the `From<&str>` lifetime restriction in rhai 1.x.
fn ins_str(m: &mut rhai::Map, key: &'static str, value: &'static str) {
m.insert(key.into(), Dynamic::from(value.to_string()));
}
fn get_str(map: &rhai::Map, key: &str, ctx: &str) -> Result<String, String> {
map.get(key)
.ok_or_else(|| format!("{ctx}: missing '{key}'"))?
.clone()
.into_string()
.map_err(|_| format!("{ctx}: '{key}' must be a string"))
}
fn get_int(map: &rhai::Map, key: &str, ctx: &str) -> Result<i64, String> {
map.get(key)
.ok_or_else(|| format!("{ctx}: missing '{key}'"))?
.clone()
.as_int()
.map_err(|_| format!("{ctx}: '{key}' must be an integer"))
}
fn get_float(map: &rhai::Map, key: &str, ctx: &str) -> Result<f64, String> {
let v = map.get(key)
.ok_or_else(|| format!("{ctx}: missing '{key}'"))?
.clone();
// Accept an integer literal passed where a float is expected.
v.clone()
.as_float()
.or_else(|_| v.as_int().map(|n| n as f64))
.map_err(|_| format!("{ctx}: '{key}' must be a number"))
}
fn get_bool(map: &rhai::Map, key: &str, ctx: &str) -> Result<bool, String> {
map.get(key)
.ok_or_else(|| format!("{ctx}: missing '{key}'"))?
.clone()
.as_bool()
.map_err(|_| format!("{ctx}: '{key}' must be a bool"))
}
/// Returns `Some(Rgba8)` if `key` is present in the map, or `None` if absent.
fn opt_color(map: &rhai::Map, key: &str, ctx: &str) -> Result<Option<Rgba8>, String> {
match map.get(key) {
None => Ok(None),
Some(v) => {
let s = v.clone()
.into_string()
.map_err(|_| format!("{ctx}: '{key}' must be a string"))?;
Ok(Some(parse_color(&s)))
}
}
}
+4 -25
View File
@@ -43,9 +43,9 @@
//!
//! ## Queue API
//!
//! `Queue.length()`, `Queue.clear()`, `Queue.peek()`, `Queue.pop()`, `Queue.push(map)`
//! `Queue.length()`, `Queue.clear()`, `Queue.peek()`, `Queue.pop()`
use crate::action::{action_to_map, rhai_action_module, Action, ScrollLine, MOVE_COST};
use crate::action::{action_to_map, Action, ScrollLine, MOVE_COST};
use crate::board::Board;
use crate::log::LogLine;
use crate::map_file::{color_to_hex, parse_color};
@@ -173,10 +173,9 @@ impl ScriptHost {
let errors: ErrorSink = Rc::new(RefCell::new(Vec::new()));
let mut engine = Engine::new();
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, &errors);
register_queue_api(&mut engine);
register_me_type(&mut engine);
register_object_info_type(&mut engine);
register_glyph_type(&mut engine);
@@ -717,7 +716,7 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
// ── Queue API ─────────────────────────────────────────────────────────────────
fn register_queue_api(engine: &mut Engine, errors: &ErrorSink) {
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());
@@ -738,26 +737,6 @@ fn register_queue_api(engine: &mut Engine, errors: &ErrorSink) {
.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 ────────────────────────────────────────────────────────