//! The [`ScriptAsset`] — a loaded script's source — and its [`ScriptLoader`]. //! //! A script lives on disk as a `.rhai` file under the project's `assets/scripts/` //! folder. Loading one yields a [`ScriptAsset`], which is just the source text //! plus its origin path; turning that text into something executable (a compiled //! `rhai` AST) is the job of the [`ScriptEngine`](crate::ScriptEngine), done at //! run time so a live edit can recompile without touching the asset plumbing. //! //! Keeping the asset as plain source (rather than a pre-compiled AST) is what //! makes hot-reload cheap: the file watcher swaps in fresh source on change and //! the host recompiles, with no engine-specific data baked into the asset cache. use std::path::Path; use oxide_engine::asset::{AssetError, AssetLoader}; /// A loaded script: its source text and the path it came from. /// /// This is the *asset* a [`Script`](crate::Script) component points at via an /// [`AssetRef`](oxide_engine::asset::AssetRef). It is deliberately /// inert — holding source, not behaviour — so the same file can be recompiled on /// every live reload. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ScriptAsset { /// The script's source code. pub source: String, /// A human-readable name for diagnostics (the file stem, when loaded from /// disk), used in error messages and the script console. pub name: String, } impl ScriptAsset { /// Builds an asset from in-memory source with the given diagnostic `name`. pub fn from_source(name: impl Into, source: impl Into) -> Self { Self { source: source.into(), name: name.into(), } } } /// The [`AssetServer`](oxide_engine::asset::AssetServer) loader for `.rhai` /// scripts. /// /// Registered by the [`ScriptModule`](crate::ScriptModule) (handles `.rhai`), so /// `assets.load::("scripts/spin.rhai")` works once the module is /// added. It reads the file as UTF-8 and records the file stem as the asset's /// diagnostic name. pub struct ScriptLoader; impl AssetLoader for ScriptLoader { type Asset = ScriptAsset; fn extensions(&self) -> &'static [&'static str] { &["rhai"] } fn load(&self, path: &Path) -> Result { let source = std::fs::read_to_string(path).map_err(|err| AssetError::Load { path: path.to_path_buf(), message: err.to_string(), })?; let name = path .file_stem() .and_then(|s| s.to_str()) .unwrap_or("script") .to_string(); Ok(ScriptAsset { source, name }) } } #[cfg(test)] mod tests { use super::*; #[test] fn from_source_keeps_name_and_text() { let a = ScriptAsset::from_source("spin", "let x = 1;"); assert_eq!(a.name, "spin"); assert_eq!(a.source, "let x = 1;"); } #[test] fn loader_reads_a_file_and_uses_the_stem_as_name() { let dir = std::env::temp_dir().join("oxide-script-loader-test"); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("hello.rhai"); std::fs::write(&path, "print(\"hi\");").unwrap(); let asset = ScriptLoader.load(&path).unwrap(); assert_eq!(asset.name, "hello"); assert!(asset.source.contains("print")); std::fs::remove_file(&path).ok(); } #[test] fn loader_claims_the_rhai_extension() { assert_eq!(ScriptLoader.extensions(), &["rhai"]); } }