Fixed three script host bugs; scriptkey

This commit is contained in:
2026-07-24 23:52:03 -05:00
parent 917f4b1bf0
commit 855b287a88
7 changed files with 127 additions and 77 deletions
Generated
+1
View File
@@ -764,6 +764,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"color", "color",
"doryen-fov", "doryen-fov",
"lazy_static",
"log", "log",
"rhai", "rhai",
"serde", "serde",
+1
View File
@@ -11,3 +11,4 @@ serde = { version = "1", features = ["derive"] }
tinyrand = "0.5" tinyrand = "0.5"
toml = { version = "0.8", features = ["preserve_order"] } toml = { version = "0.8", features = ["preserve_order"] }
log = "0.4.33" log = "0.4.33"
lazy_static = "1.5.0"
+4 -4
View File
@@ -28,7 +28,7 @@ use crate::{Board, Direction};
use crate::action::BoardAction; use crate::action::BoardAction;
use crate::object_def::ObjectDef; use crate::object_def::ObjectDef;
use crate::script::Registerable; use crate::script::Registerable;
use crate::tile::{Hookable, Optics, ScriptAttributes, Tile}; use crate::tile::{Hookable, Optics, ScriptAttributes, ScriptKey, Tile};
use crate::utils::{LogSink, ObjectId}; use crate::utils::{LogSink, ObjectId};
/// A snapshot of one board object, returned by `Board.tagged`, `Board.named`, /// A snapshot of one board object, returned by `Board.tagged`, `Board.named`,
@@ -42,7 +42,7 @@ pub struct ObjectInfo {
pub x: i64, pub x: i64,
pub y: i64, pub y: i64,
pub board: BoardRef, pub board: BoardRef,
pub script_name: Option<String>, pub script_key: ScriptKey,
pub queue: ObjQueue pub queue: ObjQueue
} }
@@ -60,7 +60,7 @@ impl ObjectInfo {
x: x as i64, x: x as i64,
y: y as i64, y: y as i64,
board, board,
script_name: hookable.scriptable().script_name.clone(), script_key: hookable.scriptable().script_name.clone(),
queue: hookable.scriptable().queue.clone(), queue: hookable.scriptable().queue.clone(),
} }
} }
@@ -71,7 +71,7 @@ impl ObjectInfo {
x: x as i64, x: x as i64,
y: y as i64, y: y as i64,
board: board.clone(), board: board.clone(),
script_name: obj.scripting.script_name.clone(), script_key: obj.scripting.script_name.clone(),
queue: obj.scripting.queue.clone() queue: obj.scripting.queue.clone()
} }
} }
+12 -2
View File
@@ -71,13 +71,16 @@ pub struct Board {
impl Board { impl Board {
/// Return a list of all `ObjectId`s currently on the board. /// Return a list of all `ObjectId`s currently on the board.
pub fn all_ids(&self) -> Vec<ObjectId> { pub fn all_ids(&self) -> Vec<ObjectId> {
self.grid.iter().filter_map(|cell| { let mut grid_ids = self.grid.iter().filter_map(|cell| {
if let Some(Tile::Object(def)) = cell { if let Some(Tile::Object(def)) = cell {
Some(def.scripting.id) Some(def.scripting.id)
} else { } else {
None None
} }
}).collect() }).collect::<Vec<_>>();
grid_ids.extend(self.sensors.iter().map(|s| s.scripting.id));
grid_ids.sort();
grid_ids
} }
/// Returns a reference to the `(Glyph, Archetype)` cell at `(x, y)`. /// Returns a reference to the `(Glyph, Archetype)` cell at `(x, y)`.
@@ -234,6 +237,13 @@ impl Board {
} }
} }
} }
// Glowing sensors
for s in self.sensors.iter() {
if s.optics().glow > 0 {
add_source(&mut lighting, s.x, s.y, s.optics().glow, color_to_rgb(s.scripting.glyph.fg));
}
}
Some(lighting) Some(lighting)
} }
+27 -12
View File
@@ -1,9 +1,11 @@
use std::collections::HashMap;
use crate::glyph::Glyph; use crate::glyph::Glyph;
use crate::utils::Pushable; use crate::utils::Pushable;
use color::Rgba8; use color::Rgba8;
use lazy_static::lazy_static;
use crate::keys::KeyType; use crate::keys::KeyType;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::tile::{EnterResponse, Optics}; use crate::tile::{EnterResponse, Optics, ScriptKey};
/// Declares the set of script-backed archetype families. /// Declares the set of script-backed archetype families.
/// ///
@@ -89,7 +91,7 @@ macro_rules! builtins {
} }
/// Returns the embedded Rhai source shared by all aliases in this family. /// Returns the embedded Rhai source shared by all aliases in this family.
pub fn script(self) -> &'static str { pub fn script(self) -> ScriptKey {
match self { match self {
$( Builtin::$variant => $script, )+ $( Builtin::$variant => $script, )+
} }
@@ -112,12 +114,12 @@ builtins! {
Gem => ["gem" => g(4, 0x50, 0x50, 0xFF)] { Gem => ["gem" => g(4, 0x50, 0x50, 0xFF)] {
enter: EnterResponse::Grab, enter: EnterResponse::Grab,
optics: Optics { opaque: false, glow: 0 }, optics: Optics { opaque: false, glow: 0 },
script: include_str!("scripts/gem.rhai"), script: ScriptKey::Builtin("gem"),
}, },
Heart => ["heart" => g(3, 0xCC, 0x22, 0x22)] { Heart => ["heart" => g(3, 0xCC, 0x22, 0x22)] {
enter: EnterResponse::Grab, enter: EnterResponse::Grab,
optics: Optics { opaque: false, glow: 0 }, optics: Optics { opaque: false, glow: 0 },
script: include_str!("scripts/heart.rhai"), script: ScriptKey::Builtin("heart"),
}, },
Pusher => [ Pusher => [
"pusher_north" => g(30, 0xAA, 0xAA, 0xAA), "pusher_north" => g(30, 0xAA, 0xAA, 0xAA),
@@ -127,7 +129,7 @@ builtins! {
] { ] {
enter: EnterResponse::Block, enter: EnterResponse::Block,
optics: Optics { opaque: true, glow: 0 }, optics: Optics { opaque: true, glow: 0 },
script: include_str!("scripts/pusher.rhai"), script: ScriptKey::Builtin("pusher"),
}, },
Spinner => [ Spinner => [
"spinner_cw" => g(47, 0xAA, 0xAA, 0xAA), "spinner_cw" => g(47, 0xAA, 0xAA, 0xAA),
@@ -135,7 +137,7 @@ builtins! {
] { ] {
enter: EnterResponse::Block, enter: EnterResponse::Block,
optics: Optics { opaque: true, glow: 0 }, optics: Optics { opaque: true, glow: 0 },
script: include_str!("scripts/spinner.rhai"), script: ScriptKey::Builtin("spinner"),
}, },
// Solid, see-through (opaque: false), unpushable teleporters. Each direction's // Solid, see-through (opaque: false), unpushable teleporters. Each direction's
// default glyph is the first frame of its animation loop (see transporter.rhai). // default glyph is the first frame of its animation loop (see transporter.rhai).
@@ -147,7 +149,7 @@ builtins! {
] { ] {
enter: EnterResponse::Hook, enter: EnterResponse::Hook,
optics: Optics { opaque: false, glow: 0 }, optics: Optics { opaque: false, glow: 0 },
script: include_str!("scripts/transporter.rhai"), script: ScriptKey::Builtin("transporter"),
}, },
Key => [ // TODO these should refer to the key colors in Keyring Key => [ // TODO these should refer to the key colors in Keyring
"key_blue" => KeyType::Blue.glyph(), "key_blue" => KeyType::Blue.glyph(),
@@ -161,7 +163,7 @@ builtins! {
] { ] {
enter: EnterResponse::Grab, enter: EnterResponse::Grab,
optics: Optics { opaque: false, glow: 0 }, optics: Optics { opaque: false, glow: 0 },
script: include_str!("scripts/key.rhai"), script: ScriptKey::Builtin("key"),
}, },
Wall => ["wall" => Glyph { Wall => ["wall" => Glyph {
tile: 35, tile: 35,
@@ -169,25 +171,38 @@ builtins! {
bg: Rgba8 { r: 0x60, g: 0x60, b: 0x60, a: 255 }}] { bg: Rgba8 { r: 0x60, g: 0x60, b: 0x60, a: 255 }}] {
enter: EnterResponse::Block, enter: EnterResponse::Block,
optics: Optics { opaque: true, glow: 0 }, optics: Optics { opaque: true, glow: 0 },
script: "" script: ScriptKey::None
}, },
Crate => ["crate" => g(254, 0xaa, 0xaa, 0xaa)] { // CP437 ■ (small filled square) Crate => ["crate" => g(254, 0xaa, 0xaa, 0xaa)] { // CP437 ■ (small filled square)
enter: EnterResponse::Push(Pushable::Any), enter: EnterResponse::Push(Pushable::Any),
optics: Optics { opaque: true, glow: 0 }, optics: Optics { opaque: true, glow: 0 },
script: "" script: ScriptKey::None
}, },
HCrate => ["hcrate" => g(29, 0xaa, 0xaa, 0xaa)] { // CP437 ↔ (left-right arrow) — pushable east/west HCrate => ["hcrate" => g(29, 0xaa, 0xaa, 0xaa)] { // CP437 ↔ (left-right arrow) — pushable east/west
enter: EnterResponse::Push(Pushable::Horizontal), enter: EnterResponse::Push(Pushable::Horizontal),
optics: Optics { opaque: true, glow: 0 }, optics: Optics { opaque: true, glow: 0 },
script: "" script: ScriptKey::None
}, },
VCrate => ["vcrate" => g(18, 0xaa, 0xaa, 0xaa)] { // CP437 ↕ (up-down arrow) — pushable north/south VCrate => ["vcrate" => g(18, 0xaa, 0xaa, 0xaa)] { // CP437 ↕ (up-down arrow) — pushable north/south
enter: EnterResponse::Push(Pushable::Vertical), enter: EnterResponse::Push(Pushable::Vertical),
optics: Optics { opaque: true, glow: 0 }, optics: Optics { opaque: true, glow: 0 },
script: "" script: ScriptKey::None
}, },
} }
lazy_static! {
pub static ref BUILTIN_SOURCES: HashMap<ScriptKey, &'static str> = {
let mut m = HashMap::new();
m.insert(ScriptKey::Builtin("gem"), include_str!("scripts/gem.rhai"));
m.insert(ScriptKey::Builtin("heart"), include_str!("scripts/heart.rhai"));
m.insert(ScriptKey::Builtin("pusher"), include_str!("scripts/pusher.rhai"));
m.insert(ScriptKey::Builtin("spinner"), include_str!("scripts/spinner.rhai"));
m.insert(ScriptKey::Builtin("transporter"), include_str!("scripts/transporter.rhai"));
m.insert(ScriptKey::Builtin("key"), include_str!("scripts/key.rhai"));
m
};
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Builtin; use super::Builtin;
+40 -47
View File
@@ -48,10 +48,12 @@ use crate::api::object_info::ObjectInfo;
use crate::api::player::PlayerWithPos; use crate::api::player::PlayerWithPos;
use crate::api::queue::ObjQueue; use crate::api::queue::ObjQueue;
use crate::api::registry::Registry; use crate::api::registry::Registry;
use crate::builtin::BUILTIN_SOURCES;
use crate::colors::parse_color; use crate::colors::parse_color;
use crate::glyph::Glyph; use crate::glyph::Glyph;
use crate::keys::Keyring; use crate::keys::Keyring;
use crate::player::PlayerRef; use crate::player::PlayerRef;
use crate::tile::ScriptKey;
/// Types which can be registered to be sent to Rhai /// Types which can be registered to be sent to Rhai
pub trait Registerable { pub trait Registerable {
@@ -86,7 +88,7 @@ impl CompiledScript {
/// Owns the Rhai engine and per-object script state for a board. /// Owns the Rhai engine and per-object script state for a board.
pub struct ScriptHost { pub struct ScriptHost {
engine: Engine, engine: Engine,
scripts: HashMap<String, CompiledScript>, scripts: HashMap<ScriptKey, CompiledScript>,
scopes: HashMap<ObjectId, Scope<'static>>, scopes: HashMap<ObjectId, Scope<'static>>,
log_sink: LogSink, log_sink: LogSink,
board: BoardRef board: BoardRef
@@ -123,57 +125,50 @@ impl ScriptHost {
// `script_name`: a world-pool name for named scripts, or a synthetic // `script_name`: a world-pool name for named scripts, or a synthetic
// `BUILTIN_*` name for built-ins so identical ones share one AST). The // `BUILTIN_*` name for built-ins so identical ones share one AST). The
// source for a built-in still comes from its embedded `builtin_script`. // source for a built-in still comes from its embedded `builtin_script`.
let mut scripts: HashMap<String, CompiledScript> = HashMap::new(); let mut scripts: HashMap<ScriptKey, CompiledScript> = HashMap::new();
let mut failed: HashSet<String> = HashSet::new(); let mut failed: HashSet<ScriptKey> = HashSet::new();
let all_scriptables = board.sorted_hookables(); let all_scriptables = board.sorted_hookables();
for obj in all_scriptables.iter() { for obj in all_scriptables.iter() {
let Some(key) = obj.script_key() else { let script_key = obj.script_key();
continue; // We've already compiled, or failed to compile, this script...
}; if scripts.contains_key(script_key) || failed.contains(script_key) {
if scripts.contains_key(key) || failed.contains(key) {
continue; continue;
} }
// Source: the embedded built-in, or a lookup in the world script pool.
let source: &str = if let Some(src) = obj.scriptable().builtin_script { if let Some(source) = script_key.source(&script_sources) {
src match engine.compile(source) {
} else { Ok(ast) => {
match script_sources.get(key) { let defines = |n: &str, params: usize| {
Some(src) => src, ast.iter_functions()
None => { .any(|f| f.name == n && f.params.len() == params)
failed.insert(key.clone()); };
log_sink.error(format!("object references unknown script '{key}'")); scripts.insert(
script_key.clone(),
CompiledScript {
has_init: defines("init", 1),
has_tick: defines("tick", 2),
has_bump: defines("bump", 2),
has_grab: defines("grab", 1),
has_enter: defines("enter", 2),
ast,
},
);
}
Err(err) => { // It didn't compile...
failed.insert(script_key.clone());
log_sink.error(format!("script '{}' failed to compile: {err}", script_key.name()));
continue; continue;
} }
} }
}; } else {
match engine.compile(source) { // This has no source...
Ok(ast) => { continue;
let defines = |n: &str, params: usize| {
ast.iter_functions()
.any(|f| f.name == n && f.params.len() == params)
};
scripts.insert(
key.clone(),
CompiledScript {
has_init: defines("init", 1),
has_tick: defines("tick", 2),
has_bump: defines("bump", 2),
has_grab: defines("grab", 1),
has_enter: defines("enter", 2),
ast,
},
);
}
Err(err) => {
failed.insert(key.clone());
log_sink.error(format!("script '{key}' failed to compile: {err}"));
}
} }
} }
// One runtime per object whose script compiled. // One runtime per object whose script compiled.
for obj in all_scriptables.iter() { for obj in all_scriptables.iter() {
if let Some(key) = obj.script_key() && scripts.contains_key(key) { if let key = obj.script_key() && scripts.contains_key(key) {
scopes.insert(obj.id(), Scope::new()); scopes.insert(obj.id(), Scope::new());
} }
} }
@@ -219,8 +214,7 @@ impl ScriptHost {
fn run_hook_on_one(&mut self, hook: Hook, id: ObjectId, arg: Option<Dynamic>, dt: f64) -> Vec<BoardAction> { fn run_hook_on_one(&mut self, hook: Hook, id: ObjectId, arg: Option<Dynamic>, dt: f64) -> Vec<BoardAction> {
let mut actions = Vec::new(); let mut actions = Vec::new();
if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) { if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) {
if let Some(script_key) = info.script_name.as_ref() if let Some(script) = self.scripts.get(&info.script_key)
&& let Some(script) = self.scripts.get(script_key)
&& let Some(scope) = self.scopes.get_mut(&id) { && let Some(scope) = self.scopes.get_mut(&id) {
// `tick` only fires on an object whose previous output has fully // `tick` only fires on an object whose previous output has fully
@@ -241,7 +235,7 @@ impl ScriptHost {
hook.to_str(), hook.to_str(),
args, args,
) { ) {
self.log_sink.error(format!("script '{}' {} error: {err}", script_key, hook)); self.log_sink.error(format!("script '{}' {} error: {err}", info.script_key.name(), hook));
} }
} }
// Run the drain regardless of if we have the hook, otherwise // Run the drain regardless of if we have the hook, otherwise
@@ -294,8 +288,7 @@ impl ScriptHost {
/// a send action we've already seen this tick. /// a send action we've already seen this tick.
pub(crate) fn run_send(&mut self, id: ObjectId, fn_name: &str, arg: SendArg) { pub(crate) fn run_send(&mut self, id: ObjectId, fn_name: &str, arg: SendArg) {
if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) { if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) {
if let Some(script_key) = info.script_name.as_ref() if let Some(script) = self.scripts.get(&info.script_key)
&& let Some(script) = self.scripts.get(script_key)
&& let Some(scope) = self.scopes.get_mut(&id) { && let Some(scope) = self.scopes.get_mut(&id) {
// Find the function: // Find the function:
@@ -309,7 +302,7 @@ impl ScriptHost {
// If it's not there at all, just bail: // If it's not there at all, just bail:
if arities.is_empty() { if arities.is_empty() {
self.log_sink.error(format!("script '{}' send({}) error: function not found", script_key, fn_name)); self.log_sink.error(format!("script '{}' send({}) error: function not found", info.script_key.name(), fn_name));
return; return;
} }
@@ -331,7 +324,7 @@ impl ScriptHost {
fn_name, fn_name,
args, args,
) { ) {
self.log_sink.error(format!("script '{}' send({}) error: {err}", script_key, fn_name)); self.log_sink.error(format!("script '{}' send({}) error: {err}", info.script_key.name(), fn_name));
} }
} }
} else { } else {
+42 -12
View File
@@ -1,7 +1,8 @@
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::api::queue::ObjQueue; use crate::api::queue::ObjQueue;
use crate::{Builtin, Direction}; use crate::{Builtin, Direction};
use crate::builtin::BUILTIN_SOURCES;
use crate::floor::{Floor, FloorBiome}; use crate::floor::{Floor, FloorBiome};
use crate::glyph::Glyph; use crate::glyph::Glyph;
use crate::object_def::ObjectDef; use crate::object_def::ObjectDef;
@@ -117,6 +118,38 @@ const fn default_as_below() -> DrawLayer {
DrawLayer::Below DrawLayer::Below
} }
#[derive(Hash, PartialEq, Eq, Clone, Debug, Default)]
pub enum ScriptKey {
#[default]
None,
World(String),
Builtin(&'static str),
}
impl ScriptKey {
pub fn name(&self) -> &str {
match self {
ScriptKey::None => "<none>",
ScriptKey::World(name) => name.as_str(),
ScriptKey::Builtin(name) => *name
}
}
pub fn source<'a>(&self, sources: &'a HashMap<String, String>) -> Option<&'a str> {
match self {
ScriptKey::None => None,
ScriptKey::World(name) => sources.get(name).map(String::as_str),
key @ ScriptKey::Builtin(_) => BUILTIN_SOURCES.get(key).copied(),
}
}
}
impl From<Option<String>> for ScriptKey {
fn from(s: Option<String>) -> Self {
s.map_or(Self::None, |s| Self::World(s))
}
}
/// Everything an object-or-sensor needs to have a Rhai script attached. /// Everything an object-or-sensor needs to have a Rhai script attached.
/// TODO clean this up some, especially the script_name-vs-builtin_script dichotomy /// TODO clean this up some, especially the script_name-vs-builtin_script dichotomy
#[derive(Clone, Default)] #[derive(Clone, Default)]
@@ -136,14 +169,13 @@ pub struct ScriptAttributes {
/// or a synthetic `BUILTIN_*` name set when a script-backed archetype is /// or a synthetic `BUILTIN_*` name set when a script-backed archetype is
/// expanded (see [`builtin_script`](ObjectDef::builtin_script)). `None` means /// expanded (see [`builtin_script`](ObjectDef::builtin_script)). `None` means
/// this object has no script yet. /// this object has no script yet.
pub script_name: Option<String>, pub script_name: ScriptKey,
/// Embedded built-in script source, set when a script-backed archetype (e.g. a /// Embedded built-in script source, set when a script-backed archetype (e.g. a
/// `pusher_*` or `gem`) is expanded into an object at load time (see /// `pusher_*` or `gem`) is expanded into an object at load time (see
/// [`crate::builtin_scripts`]). When set, this is the object's script *source* /// [`crate::builtin_scripts`]). When set, this is the object's script *source*
/// (its compile-key is the synthetic `BUILTIN_*` [`script_name`](ObjectDef::script_name) /// (its compile-key is the synthetic `BUILTIN_*` [`script_name`](ObjectDef::script_name)
/// the same expansion assigns). Not part of the map file — it is regenerated /// the same expansion assigns). Not part of the map file — it is regenerated
/// from the archetype on load. /// from the archetype on load.
pub builtin_script: Option<&'static str>,
/// Open-ended string labels for this object. Serialized as a TOML array; /// Open-ended string labels for this object. Serialized as a TOML array;
/// not subject to any rate limit — mutations take effect immediately after /// not subject to any rate limit — mutations take effect immediately after
/// the frame's action queue is drained. /// the frame's action queue is drained.
@@ -202,7 +234,7 @@ impl SensorSpec {
optics: self.optics, optics: self.optics,
name: self.name, name: self.name,
tags: self.tags.into_iter().collect(), tags: self.tags.into_iter().collect(),
script_name: self.script, script_name: self.script.into(),
..Default::default() ..Default::default()
} }
} }
@@ -234,8 +266,8 @@ pub trait Hookable {
/// for an expanded built-in (so identical built-ins share one compiled AST, /// for an expanded built-in (so identical built-ins share one compiled AST,
/// while the source still comes from `builtin_script`). `None` if the object has /// while the source still comes from `builtin_script`). `None` if the object has
/// no script. /// no script.
fn script_key(&self) -> Option<&String> { fn script_key(&self) -> &ScriptKey {
self.scriptable().script_name.as_ref() &self.scriptable().script_name
} }
} }
@@ -346,8 +378,7 @@ impl IntoTile for TileSpec {
id: *next_object_id, id: *next_object_id,
glyph, glyph,
optics, optics,
script_name: script, script_name: script.into(),
builtin_script: None,
tags: tags.into_iter().collect(), tags: tags.into_iter().collect(),
name, name,
queue: ObjQueue::new(), queue: ObjQueue::new(),
@@ -365,8 +396,7 @@ impl IntoTile for TileSpec {
id: *next_object_id, id: *next_object_id,
glyph: glyph.unwrap_or(builtin.default_glyph_for(variant)), glyph: glyph.unwrap_or(builtin.default_glyph_for(variant)),
optics: builtin.optics(), optics: builtin.optics(),
script_name: None, script_name: builtin.script(),
builtin_script: Some(builtin.script()),
tags: HashSet::from([format!("BUILTIN_{}", variant)]), tags: HashSet::from([format!("BUILTIN_{}", variant)]),
name: None, name: None,
queue: ObjQueue::new(), queue: ObjQueue::new(),
@@ -389,10 +419,10 @@ impl TileSpec {
TileSpec::Builtin { kind: "wall".to_string(), glyph: None } TileSpec::Builtin { kind: "wall".to_string(), glyph: None }
} }
pub fn player() -> Self { pub fn player() -> Self {
TileSpec::Builtin { kind: "wall".to_string(), glyph: None } TileSpec::Player
} }
pub fn krate() -> Self { pub fn krate() -> Self {
TileSpec::Builtin { kind: "wall".to_string(), glyph: None } TileSpec::Builtin { kind: "crate".to_string(), glyph: None }
} }
pub fn gem() -> Self { pub fn gem() -> Self {
TileSpec::Builtin { kind: "gem".to_string(), glyph: None } TileSpec::Builtin { kind: "gem".to_string(), glyph: None }