Fixed three script host bugs; scriptkey
This commit is contained in:
@@ -11,3 +11,4 @@ serde = { version = "1", features = ["derive"] }
|
||||
tinyrand = "0.5"
|
||||
toml = { version = "0.8", features = ["preserve_order"] }
|
||||
log = "0.4.33"
|
||||
lazy_static = "1.5.0"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user