This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/rhai_engine/src/rhailoader/script_manager.rs
2025-04-03 12:36:53 +02:00

293 lines
10 KiB
Rust

use rhai::{Engine, AST, Scope, Dynamic, Array};
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, RwLock};
use std::fs;
/// Type alias for a Rhai function that can be called from Rust
pub type RhaiFn = Box<dyn Fn(Vec<Dynamic>) -> Result<Dynamic, String>>;
/// ScriptManager handles loading, compiling, and exposing Rhai scripts
pub struct ScriptManager {
engine: Engine,
scripts: HashMap<String, Arc<RwLock<AST>>>,
functions: HashMap<String, RhaiFn>,
filters: HashMap<String, RhaiFn>,
}
/// Creates a standard engine with all necessary string functions registered
fn create_standard_engine() -> Engine {
// Create a new engine with all default features enabled
let mut engine = Engine::new();
// Set up standard packages
engine.set_fast_operators(true);
// Register essential string functions needed by the scripts
engine.register_fn("substr", |s: &str, start: i64, len: i64| {
let start = start as usize;
let len = len as usize;
if start >= s.len() {
return String::new();
}
let end = (start + len).min(s.len());
s[start..end].to_string()
});
// Register string case conversion functions
engine.register_fn("to_upper", |s: &str| s.to_uppercase());
engine.register_fn("to_lower", |s: &str| s.to_lowercase());
// Register array/string splitting and joining functions
// This form matches exactly what is expected in the script: text.split(" ")
engine.register_fn("split", |text: &str, delimiter: &str| -> Array {
text.split(delimiter).map(|s| Dynamic::from(s.to_string())).collect()
});
// Register split with one parameter (defaults to space delimiter)
engine.register_fn("split", |s: &str| -> Array {
s.split_whitespace()
.map(|part| Dynamic::from(part.to_string()))
.collect()
});
// Register len property as a method for arrays
engine.register_fn("len", |array: &mut Array| -> i64 {
array.len() as i64
});
engine.register_fn("join", |arr: &mut Array, separator: &str| -> String {
arr.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(separator)
});
// Register string replace function
engine.register_fn("replace", |s: &str, from: &str, to: &str| -> String {
s.replace(from, to)
});
// Register push function for arrays
engine.register_fn("push", |arr: &mut Array, value: Dynamic| {
arr.push(value);
Dynamic::from(())
});
// Register len property for strings
engine.register_fn("len", |s: &str| -> i64 {
s.len() as i64
});
// Register range operator for the repeat function
engine.register_fn("..", |start: i64, end: i64| {
let mut arr = Array::new();
for i in start..end { arr.push(Dynamic::from(i)); }
arr
});
engine
}
impl ScriptManager {
/// Create a new ScriptManager
pub fn new() -> Self {
Self {
engine: create_standard_engine(),
scripts: HashMap::new(),
functions: HashMap::new(),
filters: HashMap::new(),
}
}
/// Load a Rhai script from a file
pub fn load_script(&mut self, name: &str, path: impl AsRef<Path>) -> Result<(), String> {
let path = path.as_ref();
// Read the script file
let script_content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read script file {}: {}", path.display(), e))?;
// Compile the script with the current engine
let ast = self.engine.compile(&script_content)
.map_err(|e| format!("Failed to compile script {}: {}", name, e))?;
// Store the compiled AST
self.scripts.insert(name.to_string(), Arc::new(RwLock::new(ast)));
// Extract and map functions
self.map_script_functions(name)?;
Ok(())
}
/// Extract functions from a script and map them to callable wrappers
fn map_script_functions(&mut self, script_name: &str) -> Result<(), String> {
let script_arc = self.scripts.get(script_name)
.ok_or_else(|| format!("Script not found: {}", script_name))?
.clone();
// Get the AST to extract function names dynamically
let ast_lock = script_arc.read().unwrap();
// Use iter_functions to get all defined functions in the script
let function_defs: Vec<_> = ast_lock.iter_functions().collect();
// Log found functions for debugging
println!("Registering functions for script '{}':", script_name);
// Register each function we found
for fn_def in function_defs {
let fn_name = fn_def.name.to_string();
println!(" - {} (params: {})", fn_name, fn_def.params.len());
let full_name = format!("{}:{}", script_name, fn_name);
let script_arc_clone = script_arc.clone();
let fn_name_owned = fn_name.clone();
// Create a closure that will call this function using a shared engine configuration
let function_wrapper: RhaiFn = Box::new(move |args: Vec<Dynamic>| -> Result<Dynamic, String> {
// Create a configured engine for each invocation
let engine = create_standard_engine();
let mut scope = Scope::new();
// Call the function
engine.call_fn::<Dynamic>(
&mut scope,
&script_arc_clone.read().unwrap(),
&fn_name_owned,
args
).map_err(|e| format!("Error calling function {}: {}", fn_name_owned, e))
});
// Store the function wrapper in the maps
self.functions.insert(full_name, function_wrapper);
// Create a new wrapper for the filter
let script_arc_clone = script_arc.clone();
let fn_name_owned = fn_name.clone();
let filter_wrapper: RhaiFn = Box::new(move |args: Vec<Dynamic>| -> Result<Dynamic, String> {
// Use the same engine creation function
let engine = create_standard_engine();
let mut scope = Scope::new();
engine.call_fn::<Dynamic>(
&mut scope,
&script_arc_clone.read().unwrap(),
&fn_name_owned,
args
).map_err(|e| format!("Error calling filter {}: {}", fn_name_owned, e))
});
// Store the filter wrapper
self.filters.insert(fn_name, filter_wrapper);
}
Ok(())
}
/// Get a function by its full name (script:function)
pub fn get_function(&self, name: &str) -> Option<&RhaiFn> {
self.functions.get(name)
}
/// Get a filter by its name
pub fn get_filter(&self, name: &str) -> Option<&RhaiFn> {
self.filters.get(name)
}
/// Get all filters
pub fn get_all_filters(&self) -> &HashMap<String, RhaiFn> {
&self.filters
}
/// Get all functions
pub fn get_all_functions(&self) -> &HashMap<String, RhaiFn> {
&self.functions
}
/// Call a function by its full name (script:function)
pub fn call_function(&self, name: &str, args: Vec<Dynamic>) -> Result<Dynamic, String> {
if let Some(func) = self.functions.get(name) {
func(args)
} else {
Err(format!("Function not found: {}", name))
}
}
/// Call a filter by its name
pub fn call_filter(&self, name: &str, args: Vec<Dynamic>) -> Result<Dynamic, String> {
if let Some(filter) = self.filters.get(name) {
filter(args)
} else {
Err(format!("Filter not found: {}", name))
}
}
/// Reload a script from a file
pub fn reload_script(&mut self, name: &str, path: impl AsRef<Path>) -> Result<(), String> {
let path = path.as_ref();
// Read the script file
let script_content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read script file {}: {}", path.display(), e))?;
// Compile the script
let ast = self.engine.compile(&script_content)
.map_err(|e| format!("Failed to compile script {}: {}", name, e))?;
// Update the stored AST
if let Some(script_arc) = self.scripts.get(name) {
let mut script = script_arc.write().unwrap();
*script = ast;
} else {
self.scripts.insert(name.to_string(), Arc::new(RwLock::new(ast)));
}
// Re-map functions
self.map_script_functions(name)?;
Ok(())
}
/// Load all .rhai scripts from a directory
pub fn load_scripts_from_directory(&mut self, dir_path: impl AsRef<Path>) -> Result<Vec<String>, String> {
let dir_path = dir_path.as_ref();
// Check if directory exists
if !dir_path.exists() || !dir_path.is_dir() {
return Err(format!("Directory does not exist or is not a directory: {}", dir_path.display()));
}
// Get all entries in the directory
let entries = fs::read_dir(dir_path)
.map_err(|e| format!("Failed to read directory {}: {}", dir_path.display(), e))?;
let mut loaded_scripts = Vec::new();
// Process each entry
for entry_result in entries {
let entry = entry_result.map_err(|e| format!("Failed to read directory entry: {}", e))?;
let path = entry.path();
// Only process .rhai files
if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") {
if let Some(stem) = path.file_stem() {
if let Some(script_name) = stem.to_str() {
// Load the script
self.load_script(script_name, &path)?;
loaded_scripts.push(script_name.to_string());
}
}
}
}
Ok(loaded_scripts)
}
/// Get a list of all registered function names
pub fn get_function_names(&self) -> Vec<String> {
self.functions.keys().cloned().collect()
}
}