use bevy::asset::io::Reader;
use bevy::asset::{Asset, AssetLoader, LoadContext};
use bevy::prelude::TypePath;
use bevy::tasks::ConditionalSendFuture;
use mortar_compiler::{Deserializer, Language, MortaredData, ParseHandler, Serializer};
use std::path::Path;
#[derive(Asset, TypePath, Debug)]
pub struct MortarAsset {
pub data: MortaredData,
}
#[derive(Default, bevy::prelude::TypePath)]
pub struct MortarAssetLoader;
impl MortarAssetLoader {
fn detect_language() -> Language {
let locale = std::env::var("LANG")
.or_else(|_| std::env::var("LANGUAGE"))
.unwrap_or_default()
.to_lowercase();
if locale.starts_with("zh") {
Language::Chinese
} else {
Language::English
}
}
async fn compile_mortar_source(
reader: &mut dyn Reader,
source_path: &Path,
) -> Result<MortaredData, Box<dyn std::error::Error + Send + Sync>> {
dev_info!("Compiling .mortar file: {:?}", source_path);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let source_content = std::str::from_utf8(&bytes)?;
let language = Self::detect_language();
let (parse_result, diagnostics) =
ParseHandler::parse_source_code_with_diagnostics_and_language(
source_content,
source_path.to_string_lossy().to_string(),
false,
language,
);
if diagnostics.has_errors() {
diagnostics.print_diagnostics(source_content);
return Err("Mortar compilation failed with errors".into());
}
let program = parse_result?;
let json = Serializer::serialize_to_json(&program, true)?;
Deserializer::from_json(&json).map_err(Into::into)
}
async fn load_mortared_direct(
reader: &mut dyn Reader,
path: &Path,
) -> Result<MortaredData, Box<dyn std::error::Error + Send + Sync>> {
#[cfg(not(feature = "dev-logs"))]
let _ = path;
dev_info!("Loading .mortared file: {:?}", path);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let json = std::str::from_utf8(&bytes)?;
Deserializer::from_json(json).map_err(Into::into)
}
#[cfg_attr(not(feature = "dev-logs"), allow(unused_variables))]
fn log_public_constants(path: &Path, data: &MortaredData) {
let public_constants: Vec<_> = data
.constants
.iter()
.filter(|constant| constant.public)
.collect();
if public_constants.is_empty() {
return;
}
#[cfg(not(feature = "dev-logs"))]
let _ = path;
dev_info!("Public constants exported by {}:", path.display());
for constant in public_constants {
#[cfg(not(feature = "dev-logs"))]
let _ = constant;
dev_info!(
" {} ({}): {}",
constant.name,
constant.const_type,
Self::format_constant_value(&constant.value)
);
}
}
#[allow(dead_code)]
fn format_constant_value(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Array(items) => {
let formatted_items: Vec<String> =
items.iter().map(Self::format_constant_value).collect();
format!("[{}]", formatted_items.join(", "))
}
serde_json::Value::Object(map) => {
let formatted_pairs: Vec<String> = map
.iter()
.map(|(key, val)| format!("{}: {}", key, Self::format_constant_value(val)))
.collect();
format!("{{{}}}", formatted_pairs.join(", "))
}
serde_json::Value::Null => "null".to_string(),
}
}
}
impl AssetLoader for MortarAssetLoader {
type Asset = MortarAsset;
type Settings = ();
type Error = Box<dyn std::error::Error + Send + Sync>;
fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> impl ConditionalSendFuture<Output = Result<Self::Asset, Self::Error>> {
Box::pin(async move {
let asset_path = load_context.path().clone();
let path = asset_path.path().to_path_buf();
let data = match path.extension().and_then(std::ffi::OsStr::to_str) {
Some("mortar") => {
Self::compile_mortar_source(reader, &path).await?
}
Some("mortared") => Self::load_mortared_direct(reader, &path).await?,
_ => return Err("Unsupported file extension".into()),
};
dev_info!(
"Successfully loaded mortar asset: {:?} (nodes: {}, functions: {}, variables: {})",
asset_path,
data.nodes.len(),
data.functions.len(),
data.variables.len()
);
Self::log_public_constants(&path, &data);
Ok(MortarAsset { data })
})
}
fn extensions(&self) -> &[&str] {
&["mortar", "mortared"]
}
}