f56a1eea3b
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.5 KiB
Rust
102 lines
3.5 KiB
Rust
//! 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<ScriptAsset>`](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<String>, source: impl Into<String>) -> 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::<ScriptAsset>("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<ScriptAsset, AssetError> {
|
|
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"]);
|
|
}
|
|
}
|