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
+4 -4
View File
@@ -28,7 +28,7 @@ use crate::{Board, Direction};
use crate::action::BoardAction;
use crate::object_def::ObjectDef;
use crate::script::Registerable;
use crate::tile::{Hookable, Optics, ScriptAttributes, Tile};
use crate::tile::{Hookable, Optics, ScriptAttributes, ScriptKey, Tile};
use crate::utils::{LogSink, ObjectId};
/// A snapshot of one board object, returned by `Board.tagged`, `Board.named`,
@@ -42,7 +42,7 @@ pub struct ObjectInfo {
pub x: i64,
pub y: i64,
pub board: BoardRef,
pub script_name: Option<String>,
pub script_key: ScriptKey,
pub queue: ObjQueue
}
@@ -60,7 +60,7 @@ impl ObjectInfo {
x: x as i64,
y: y as i64,
board,
script_name: hookable.scriptable().script_name.clone(),
script_key: hookable.scriptable().script_name.clone(),
queue: hookable.scriptable().queue.clone(),
}
}
@@ -71,7 +71,7 @@ impl ObjectInfo {
x: x as i64,
y: y as i64,
board: board.clone(),
script_name: obj.scripting.script_name.clone(),
script_key: obj.scripting.script_name.clone(),
queue: obj.scripting.queue.clone()
}
}
+12 -2
View File
@@ -71,13 +71,16 @@ pub struct Board {
impl Board {
/// Return a list of all `ObjectId`s currently on the board.
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 {
Some(def.scripting.id)
} else {
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)`.
@@ -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)
}
+27 -12
View File
@@ -1,9 +1,11 @@
use std::collections::HashMap;
use crate::glyph::Glyph;
use crate::utils::Pushable;
use color::Rgba8;
use lazy_static::lazy_static;
use crate::keys::KeyType;
use serde::{Serialize, Deserialize};
use crate::tile::{EnterResponse, Optics};
use crate::tile::{EnterResponse, Optics, ScriptKey};
/// 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.
pub fn script(self) -> &'static str {
pub fn script(self) -> ScriptKey {
match self {
$( Builtin::$variant => $script, )+
}
@@ -112,12 +114,12 @@ builtins! {
Gem => ["gem" => g(4, 0x50, 0x50, 0xFF)] {
enter: EnterResponse::Grab,
optics: Optics { opaque: false, glow: 0 },
script: include_str!("scripts/gem.rhai"),
script: ScriptKey::Builtin("gem"),
},
Heart => ["heart" => g(3, 0xCC, 0x22, 0x22)] {
enter: EnterResponse::Grab,
optics: Optics { opaque: false, glow: 0 },
script: include_str!("scripts/heart.rhai"),
script: ScriptKey::Builtin("heart"),
},
Pusher => [
"pusher_north" => g(30, 0xAA, 0xAA, 0xAA),
@@ -127,7 +129,7 @@ builtins! {
] {
enter: EnterResponse::Block,
optics: Optics { opaque: true, glow: 0 },
script: include_str!("scripts/pusher.rhai"),
script: ScriptKey::Builtin("pusher"),
},
Spinner => [
"spinner_cw" => g(47, 0xAA, 0xAA, 0xAA),
@@ -135,7 +137,7 @@ builtins! {
] {
enter: EnterResponse::Block,
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
// default glyph is the first frame of its animation loop (see transporter.rhai).
@@ -147,7 +149,7 @@ builtins! {
] {
enter: EnterResponse::Hook,
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_blue" => KeyType::Blue.glyph(),
@@ -161,7 +163,7 @@ builtins! {
] {
enter: EnterResponse::Grab,
optics: Optics { opaque: false, glow: 0 },
script: include_str!("scripts/key.rhai"),
script: ScriptKey::Builtin("key"),
},
Wall => ["wall" => Glyph {
tile: 35,
@@ -169,25 +171,38 @@ builtins! {
bg: Rgba8 { r: 0x60, g: 0x60, b: 0x60, a: 255 }}] {
enter: EnterResponse::Block,
optics: Optics { opaque: true, glow: 0 },
script: ""
script: ScriptKey::None
},
Crate => ["crate" => g(254, 0xaa, 0xaa, 0xaa)] { // CP437 ■ (small filled square)
enter: EnterResponse::Push(Pushable::Any),
optics: Optics { opaque: true, glow: 0 },
script: ""
script: ScriptKey::None
},
HCrate => ["hcrate" => g(29, 0xaa, 0xaa, 0xaa)] { // CP437 ↔ (left-right arrow) — pushable east/west
enter: EnterResponse::Push(Pushable::Horizontal),
optics: Optics { opaque: true, glow: 0 },
script: ""
script: ScriptKey::None
},
VCrate => ["vcrate" => g(18, 0xaa, 0xaa, 0xaa)] { // CP437 ↕ (up-down arrow) — pushable north/south
enter: EnterResponse::Push(Pushable::Vertical),
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)]
mod tests {
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::queue::ObjQueue;
use crate::api::registry::Registry;
use crate::builtin::BUILTIN_SOURCES;
use crate::colors::parse_color;
use crate::glyph::Glyph;
use crate::keys::Keyring;
use crate::player::PlayerRef;
use crate::tile::ScriptKey;
/// Types which can be registered to be sent to Rhai
pub trait Registerable {
@@ -86,7 +88,7 @@ impl CompiledScript {
/// Owns the Rhai engine and per-object script state for a board.
pub struct ScriptHost {
engine: Engine,
scripts: HashMap<String, CompiledScript>,
scripts: HashMap<ScriptKey, CompiledScript>,
scopes: HashMap<ObjectId, Scope<'static>>,
log_sink: LogSink,
board: BoardRef
@@ -123,57 +125,50 @@ impl ScriptHost {
// `script_name`: a world-pool name for named scripts, or a synthetic
// `BUILTIN_*` name for built-ins so identical ones share one AST). The
// source for a built-in still comes from its embedded `builtin_script`.
let mut scripts: HashMap<String, CompiledScript> = HashMap::new();
let mut failed: HashSet<String> = HashSet::new();
let mut scripts: HashMap<ScriptKey, CompiledScript> = HashMap::new();
let mut failed: HashSet<ScriptKey> = HashSet::new();
let all_scriptables = board.sorted_hookables();
for obj in all_scriptables.iter() {
let Some(key) = obj.script_key() else {
continue;
};
if scripts.contains_key(key) || failed.contains(key) {
let script_key = obj.script_key();
// We've already compiled, or failed to compile, this script...
if scripts.contains_key(script_key) || failed.contains(script_key) {
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 {
src
} else {
match script_sources.get(key) {
Some(src) => src,
None => {
failed.insert(key.clone());
log_sink.error(format!("object references unknown script '{key}'"));
if let Some(source) = script_key.source(&script_sources) {
match engine.compile(source) {
Ok(ast) => {
let defines = |n: &str, params: usize| {
ast.iter_functions()
.any(|f| f.name == n && f.params.len() == params)
};
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;
}
}
};
match engine.compile(source) {
Ok(ast) => {
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}"));
}
} else {
// This has no source...
continue;
}
}
// One runtime per object whose script compiled.
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());
}
}
@@ -219,8 +214,7 @@ impl ScriptHost {
fn run_hook_on_one(&mut self, hook: Hook, id: ObjectId, arg: Option<Dynamic>, dt: f64) -> Vec<BoardAction> {
let mut actions = Vec::new();
if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) {
if let Some(script_key) = info.script_name.as_ref()
&& let Some(script) = self.scripts.get(script_key)
if let Some(script) = self.scripts.get(&info.script_key)
&& let Some(scope) = self.scopes.get_mut(&id) {
// `tick` only fires on an object whose previous output has fully
@@ -241,7 +235,7 @@ impl ScriptHost {
hook.to_str(),
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
@@ -294,8 +288,7 @@ impl ScriptHost {
/// a send action we've already seen this tick.
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(script_key) = info.script_name.as_ref()
&& let Some(script) = self.scripts.get(script_key)
if let Some(script) = self.scripts.get(&info.script_key)
&& let Some(scope) = self.scopes.get_mut(&id) {
// Find the function:
@@ -309,7 +302,7 @@ impl ScriptHost {
// If it's not there at all, just bail:
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;
}
@@ -331,7 +324,7 @@ impl ScriptHost {
fn_name,
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 {
+42 -12
View File
@@ -1,7 +1,8 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use crate::api::queue::ObjQueue;
use crate::{Builtin, Direction};
use crate::builtin::BUILTIN_SOURCES;
use crate::floor::{Floor, FloorBiome};
use crate::glyph::Glyph;
use crate::object_def::ObjectDef;
@@ -117,6 +118,38 @@ const fn default_as_below() -> DrawLayer {
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.
/// TODO clean this up some, especially the script_name-vs-builtin_script dichotomy
#[derive(Clone, Default)]
@@ -136,14 +169,13 @@ pub struct ScriptAttributes {
/// or a synthetic `BUILTIN_*` name set when a script-backed archetype is
/// expanded (see [`builtin_script`](ObjectDef::builtin_script)). `None` means
/// 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
/// `pusher_*` or `gem`) is expanded into an object at load time (see
/// [`crate::builtin_scripts`]). When set, this is the object's script *source*
/// (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
/// from the archetype on load.
pub builtin_script: Option<&'static str>,
/// Open-ended string labels for this object. Serialized as a TOML array;
/// not subject to any rate limit — mutations take effect immediately after
/// the frame's action queue is drained.
@@ -202,7 +234,7 @@ impl SensorSpec {
optics: self.optics,
name: self.name,
tags: self.tags.into_iter().collect(),
script_name: self.script,
script_name: self.script.into(),
..Default::default()
}
}
@@ -234,8 +266,8 @@ pub trait Hookable {
/// 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
/// no script.
fn script_key(&self) -> Option<&String> {
self.scriptable().script_name.as_ref()
fn script_key(&self) -> &ScriptKey {
&self.scriptable().script_name
}
}
@@ -346,8 +378,7 @@ impl IntoTile for TileSpec {
id: *next_object_id,
glyph,
optics,
script_name: script,
builtin_script: None,
script_name: script.into(),
tags: tags.into_iter().collect(),
name,
queue: ObjQueue::new(),
@@ -365,8 +396,7 @@ impl IntoTile for TileSpec {
id: *next_object_id,
glyph: glyph.unwrap_or(builtin.default_glyph_for(variant)),
optics: builtin.optics(),
script_name: None,
builtin_script: Some(builtin.script()),
script_name: builtin.script(),
tags: HashSet::from([format!("BUILTIN_{}", variant)]),
name: None,
queue: ObjQueue::new(),
@@ -389,10 +419,10 @@ impl TileSpec {
TileSpec::Builtin { kind: "wall".to_string(), glyph: None }
}
pub fn player() -> Self {
TileSpec::Builtin { kind: "wall".to_string(), glyph: None }
TileSpec::Player
}
pub fn krate() -> Self {
TileSpec::Builtin { kind: "wall".to_string(), glyph: None }
TileSpec::Builtin { kind: "crate".to_string(), glyph: None }
}
pub fn gem() -> Self {
TileSpec::Builtin { kind: "gem".to_string(), glyph: None }