Skip to content

Commit 3004915

Browse files
authored
refactor: extract bevy_mod_scripting_asset crate, simplify supported extensions logic (#475)
1 parent 421fc0d commit 3004915

File tree

34 files changed

+397
-407
lines changed

34 files changed

+397
-407
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ bevy_mod_scripting_lua = { workspace = true, optional = true }
9898
bevy_mod_scripting_rhai = { workspace = true, optional = true }
9999
bevy_mod_scripting_functions = { workspace = true }
100100
bevy_mod_scripting_derive = { workspace = true }
101+
bevy_mod_scripting_asset = { workspace = true }
101102

102103
[workspace.dependencies]
103104
# local crates
@@ -110,6 +111,8 @@ ladfile = { path = "crates/ladfile", version = "0.5.0" }
110111
ladfile_builder = { path = "crates/ladfile_builder", version = "0.5.1" }
111112
bevy_mod_scripting_lua = { path = "crates/languages/bevy_mod_scripting_lua", version = "0.15.1", default-features = false }
112113
bevy_mod_scripting_rhai = { path = "crates/languages/bevy_mod_scripting_rhai", version = "0.15.1", default-features = false }
114+
bevy_mod_scripting_asset = { path = "crates/bevy_mod_scripting_asset", version = "0.15.1", default-features = false }
115+
113116
# bevy
114117

115118
bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.15.1" }
@@ -241,6 +244,7 @@ members = [
241244
"crates/ladfile_builder",
242245
"crates/bevy_system_reflection",
243246
"crates/bindings/*",
247+
"crates/bevy_mod_scripting_asset",
244248
]
245249
resolver = "2"
246250
exclude = ["codegen", "crates/macro_tests", "xtask"]

assets/tests/add_system/added_systems_run_in_parallel.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ digraph {
2525
node_0 [label="bevy_asset::assets::Assets<bevy_asset::folder::LoadedFolder>::asset_events"];
2626
node_1 [label="bevy_asset::assets::Assets<bevy_asset::assets::LoadedUntypedAsset>::asset_events"];
2727
node_2 [label="bevy_asset::assets::Assets<()>::asset_events"];
28-
node_3 [label="bevy_asset::assets::Assets<bevy_mod_scripting_core::asset::ScriptAsset>::asset_events"];
28+
node_3 [label="bevy_asset::assets::Assets<bevy_mod_scripting_asset::script_asset::ScriptAsset>::asset_events"];
2929
node_4 [label="bevy_mod_scripting_core::bindings::allocator::garbage_collector"];
3030
node_5 [label="script_integration_test_harness::dummy_before_post_update_system"];
3131
node_6 [label="script_integration_test_harness::dummy_post_update_system"];

assets/tests/add_system/added_systems_run_in_parallel.rhai

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ digraph {
2424
node_0 [label="bevy_asset::assets::Assets<bevy_asset::folder::LoadedFolder>::asset_events"];
2525
node_1 [label="bevy_asset::assets::Assets<bevy_asset::assets::LoadedUntypedAsset>::asset_events"];
2626
node_2 [label="bevy_asset::assets::Assets<()>::asset_events"];
27-
node_3 [label="bevy_asset::assets::Assets<bevy_mod_scripting_core::asset::ScriptAsset>::asset_events"];
27+
node_3 [label="bevy_asset::assets::Assets<bevy_mod_scripting_asset::script_asset::ScriptAsset>::asset_events"];
2828
node_4 [label="bevy_mod_scripting_core::bindings::allocator::garbage_collector"];
2929
node_5 [label="script_integration_test_harness::dummy_before_post_update_system"];
3030
node_6 [label="script_integration_test_harness::dummy_post_update_system"];
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "bevy_mod_scripting_asset"
3+
version = "0.15.1"
4+
authors = ["Maksymilian Mozolewski <makspl17@gmail.com>"]
5+
edition = "2024"
6+
license = "MIT OR Apache-2.0"
7+
description = "Core traits and structures required for other parts of bevy_mod_scripting"
8+
repository = "https://github.com/makspll/bevy_mod_scripting"
9+
homepage = "https://github.com/makspll/bevy_mod_scripting"
10+
categories = ["game-development"]
11+
readme = "readme.md"
12+
13+
14+
[dependencies]
15+
bevy_reflect = { workspace = true }
16+
bevy_asset = { workspace = true }
17+
bevy_log = { workspace = true }
18+
serde = { workspace = true, features = ["derive"] }
19+
[lints]
20+
workspace = true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# bevy_mod_scripting_core
2+
3+
This crate is a part of the ["bevy_mod_scripting" workspace](https://github.com/makspll/bevy_mod_scripting).
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//! Error definitions for the scripting asset pipeline.
2+
3+
use std::fmt::Display;
4+
5+
use bevy_asset::AssetPath;
6+
7+
#[derive(Debug)]
8+
/// An error that can occur when loading or processing a script asset.
9+
pub struct ScriptAssetError {
10+
pub(crate) phrase: &'static str,
11+
pub(crate) asset_path: Option<AssetPath<'static>>,
12+
pub(crate) inner: Box<dyn std::error::Error + Send + Sync + 'static>,
13+
}
14+
impl ScriptAssetError {
15+
/// Create a new script asset error
16+
pub fn new(
17+
phrase: &'static str,
18+
asset_path: Option<&AssetPath<'static>>,
19+
inner: Box<dyn std::error::Error + Send + Sync + 'static>,
20+
) -> Self {
21+
Self {
22+
phrase,
23+
asset_path: asset_path.cloned(),
24+
inner,
25+
}
26+
}
27+
}
28+
29+
impl Display for ScriptAssetError {
30+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31+
if let Some(path) = &self.asset_path {
32+
write!(
33+
f,
34+
"Error {}. while processing script asset '{}': {}",
35+
self.phrase, path, self.inner
36+
)
37+
} else {
38+
write!(
39+
f,
40+
"Error {}. while processing script asset: {}",
41+
self.phrase, self.inner
42+
)
43+
}
44+
}
45+
}
46+
47+
impl std::error::Error for ScriptAssetError {
48+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
49+
Some(self.inner.as_ref())
50+
}
51+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//! Defines supported scripting languages and their file extensions.
2+
3+
use serde::{Deserialize, Serialize};
4+
use std::borrow::Cow;
5+
6+
/// Represents a scripting language. Languages which compile into another language should use the target language as their language.
7+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
8+
pub enum Language {
9+
/// The Rhai scripting language
10+
Rhai,
11+
/// The Lua scripting language
12+
Lua,
13+
/// The Rune scripting language
14+
Rune,
15+
/// An external scripting language
16+
External(Cow<'static, str>),
17+
/// Set if none of the asset path to language mappers match
18+
#[default]
19+
Unknown,
20+
}
21+
22+
impl std::fmt::Display for Language {
23+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24+
match self {
25+
Language::Rhai => "Rhai".fmt(f),
26+
Language::Lua => "Lua".fmt(f),
27+
Language::Rune => "Rune".fmt(f),
28+
Language::External(cow) => cow.fmt(f),
29+
Language::Unknown => "Unknown".fmt(f),
30+
}
31+
}
32+
}
33+
34+
/// Collect the language extensions supported during initialization.
35+
///
36+
/// NOTE: This resource is removed after plugin setup.
37+
#[derive(Debug)]
38+
pub struct LanguageExtensions(Vec<&'static str>, Vec<Language>);
39+
40+
impl LanguageExtensions {
41+
/// Create a new language extensions mapping from an iterator of (extension, language) pairs.
42+
pub fn new(iter: impl IntoIterator<Item = (&'static str, Language)>) -> Self {
43+
let (extensions, languages): (Vec<&'static str>, Vec<Language>) = iter.into_iter().unzip();
44+
Self(extensions, languages)
45+
}
46+
47+
/// Retrieves the language for the given file extension, if it exists.
48+
pub fn get(&self, extension: &str) -> Option<&Language> {
49+
self.0
50+
.iter()
51+
.position(|&ext| ext.eq_ignore_ascii_case(extension))
52+
.and_then(|index| self.1.get(index))
53+
}
54+
55+
/// Inserts a new (extension, language) pair into the mapping.
56+
pub fn insert(&mut self, extension: &'static str, language: Language) {
57+
if let Some(pos) = self
58+
.0
59+
.iter()
60+
.position(|&ext| ext.eq_ignore_ascii_case(extension))
61+
{
62+
self.1[pos] = language;
63+
} else {
64+
self.0.push(extension);
65+
self.1.push(language);
66+
}
67+
}
68+
69+
/// Returns a slice of all supported file extensions.
70+
pub fn extensions(&self) -> &[&str] {
71+
self.0.as_slice()
72+
}
73+
}
74+
75+
impl Default for LanguageExtensions {
76+
fn default() -> Self {
77+
LanguageExtensions::new([
78+
("lua", Language::Lua),
79+
("luau", Language::Lua),
80+
("rhai", Language::Rhai),
81+
("rn", Language::Rune),
82+
])
83+
}
84+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//! All things asset and scripting related.
2+
3+
pub mod error;
4+
pub mod language;
5+
pub mod loader;
6+
pub mod script_asset;
7+
8+
pub use {error::*, language::*, loader::*, script_asset::*};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//! A loader pipeline for script assets
2+
3+
use bevy_asset::AssetLoader;
4+
use bevy_log::warn;
5+
use serde::{Deserialize, Serialize};
6+
7+
use crate::{Language, LanguageExtensions, ScriptAsset, ScriptAssetError};
8+
9+
/// Script settings
10+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11+
pub struct ScriptSettings {
12+
/// Define the language for a script or use the extension if None.
13+
pub language: Option<Language>,
14+
}
15+
16+
/// A loader for script assets
17+
pub struct ScriptAssetLoader {
18+
/// The file extensions this loader should handle
19+
language_extensions: &'static LanguageExtensions,
20+
/// preprocessor to run on the script before saving the content to an asset
21+
pub preprocessor: Option<Box<dyn Fn(&mut [u8]) -> Result<(), ScriptAssetError> + Send + Sync>>,
22+
}
23+
24+
impl ScriptAssetLoader {
25+
/// Create a new script asset loader for the given extensions.
26+
pub fn new(language_extensions: &'static LanguageExtensions) -> Self {
27+
Self {
28+
language_extensions,
29+
preprocessor: None,
30+
}
31+
}
32+
33+
/// Add a preprocessor
34+
pub fn with_preprocessor(
35+
mut self,
36+
preprocessor: Box<dyn Fn(&mut [u8]) -> Result<(), ScriptAssetError> + Send + Sync>,
37+
) -> Self {
38+
self.preprocessor = Some(preprocessor);
39+
self
40+
}
41+
}
42+
43+
impl AssetLoader for ScriptAssetLoader {
44+
type Asset = ScriptAsset;
45+
46+
type Settings = ScriptSettings;
47+
48+
type Error = ScriptAssetError;
49+
50+
async fn load(
51+
&self,
52+
reader: &mut dyn bevy_asset::io::Reader,
53+
settings: &Self::Settings,
54+
load_context: &mut bevy_asset::LoadContext<'_>,
55+
) -> Result<Self::Asset, Self::Error> {
56+
let mut content = Vec::new();
57+
reader.read_to_end(&mut content).await.map_err(|e| {
58+
ScriptAssetError::new(
59+
"reading from disk",
60+
Some(load_context.asset_path()),
61+
Box::new(e),
62+
)
63+
})?;
64+
if let Some(processor) = &self.preprocessor {
65+
processor(&mut content)?;
66+
}
67+
let language = settings.language.clone().unwrap_or_else(|| {
68+
let ext = load_context
69+
.path()
70+
.extension()
71+
.and_then(|e| e.to_str())
72+
.unwrap_or_default();
73+
self.language_extensions
74+
.get(ext)
75+
.cloned()
76+
.unwrap_or_else(|| {
77+
warn!("Unknown language for {:?}", load_context.path().display());
78+
Language::Unknown
79+
})
80+
});
81+
// if language == Language::Lua && cfg!(not(feature = "mlua")) {
82+
// warn_once!(
83+
// "Script {:?} is a Lua script but the {:?} feature is not enabled; the script will not be evaluated.",
84+
// load_context.path().display(),
85+
// "mlua"
86+
// );
87+
// }
88+
// if language == Language::Rhai && cfg!(not(feature = "rhai")) {
89+
// warn_once!(
90+
// "Script {:?} is a Rhai script but the {:?} feature is not enabled; the script will not be evaluated.",
91+
// load_context.path().display(),
92+
// "rhai"
93+
// );
94+
// }
95+
let asset = ScriptAsset {
96+
content: content.into_boxed_slice(),
97+
language,
98+
};
99+
Ok(asset)
100+
}
101+
102+
fn extensions(&self) -> &[&str] {
103+
self.language_extensions.extensions()
104+
}
105+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Scripting asset definitions
2+
3+
use bevy_asset::Asset;
4+
use bevy_reflect::Reflect;
5+
6+
use crate::Language;
7+
8+
/// Represents a script loaded into memory as an asset
9+
#[derive(Asset, Clone, Reflect)]
10+
#[reflect(opaque)]
11+
pub struct ScriptAsset {
12+
/// The body of the script
13+
pub content: Box<[u8]>, // Any chance a Cow<'static, ?> could work here?
14+
/// The language of the script
15+
pub language: Language,
16+
}
17+
18+
impl From<String> for ScriptAsset {
19+
fn from(s: String) -> ScriptAsset {
20+
ScriptAsset {
21+
content: s.into_bytes().into_boxed_slice(),
22+
language: Language::default(),
23+
}
24+
}
25+
}
26+
27+
impl ScriptAsset {
28+
/// Create a new script asset with an unknown language.
29+
pub fn new(s: impl Into<String>) -> Self {
30+
s.into().into()
31+
}
32+
}

0 commit comments

Comments
 (0)