diff --git a/Cargo.lock b/Cargo.lock index f8e583c..6d0a23d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,6 +764,7 @@ version = "0.1.0" dependencies = [ "color", "doryen-fov", + "lazy_static", "log", "rhai", "serde", diff --git a/kiln-core/Cargo.toml b/kiln-core/Cargo.toml index 2378efc..855d820 100644 --- a/kiln-core/Cargo.toml +++ b/kiln-core/Cargo.toml @@ -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" diff --git a/kiln-core/src/api/object_info.rs b/kiln-core/src/api/object_info.rs index a1c2cee..7b95e61 100644 --- a/kiln-core/src/api/object_info.rs +++ b/kiln-core/src/api/object_info.rs @@ -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, + 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() } } diff --git a/kiln-core/src/board.rs b/kiln-core/src/board.rs index 68b9562..389df33 100644 --- a/kiln-core/src/board.rs +++ b/kiln-core/src/board.rs @@ -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 { - 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::>(); + 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) } diff --git a/kiln-core/src/builtin.rs b/kiln-core/src/builtin.rs index 034fb17..290ac59 100644 --- a/kiln-core/src/builtin.rs +++ b/kiln-core/src/builtin.rs @@ -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 = { + 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; diff --git a/kiln-core/src/script.rs b/kiln-core/src/script.rs index 06fcd3b..fd52497 100644 --- a/kiln-core/src/script.rs +++ b/kiln-core/src/script.rs @@ -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, + scripts: HashMap, scopes: HashMap>, 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 = HashMap::new(); - let mut failed: HashSet = HashSet::new(); + let mut scripts: HashMap = HashMap::new(); + let mut failed: HashSet = 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, dt: f64) -> Vec { 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 { diff --git a/kiln-core/src/tile.rs b/kiln-core/src/tile.rs index 528c723..117b253 100644 --- a/kiln-core/src/tile.rs +++ b/kiln-core/src/tile.rs @@ -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 => "", + ScriptKey::World(name) => name.as_str(), + ScriptKey::Builtin(name) => *name + } + } + + pub fn source<'a>(&self, sources: &'a HashMap) -> 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> for ScriptKey { + fn from(s: Option) -> 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, + 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 }