293 lines
10 KiB
Rust
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()
|
|
}
|
|
} |