use std::{alloc::Allocator, io::ErrorKind};
use serde_json::{Map, Value};
pub mod asset_handler;
pub mod asset_manager;
mod audio_utils;
pub mod bema_asset_handler;
pub mod besl_shader_asset_handler;
pub mod exr_asset_handler;
pub mod fbx_asset_handler;
pub mod gltf_asset_handler;
pub mod lut_asset_handler;
pub mod ogg_asset_handler;
pub mod png_asset_handler;
pub mod wav_asset_handler;
#[cfg(debug_assertions)]
pub mod resource_trace;
#[cfg(debug_assertions)]
pub use resource_trace::{ResourceTrace, ResourceTraceItem, ResourceTraceLevel};
pub type BEADType = Value;
pub type JsonObject = Map<String, Value>;
pub(crate) fn parse_json(source: &str) -> Result<BEADType, json5::Error> {
json5::from_str(source)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ContainerDefaultResource {
Mesh,
Animation,
}
pub(crate) fn container_default_resource(spec: Option<&BEADType>) -> Result<Option<ContainerDefaultResource>, String> {
let Some(value) = spec.and_then(|spec| spec.get("default_resource")) else {
return Ok(None);
};
let Some(value) = value.as_str() else {
return Err("`default_resource` must be the string `mesh` or `animation`".to_string());
};
if value.eq_ignore_ascii_case("mesh") {
Ok(Some(ContainerDefaultResource::Mesh))
} else if value.eq_ignore_ascii_case("animation") {
Ok(Some(ContainerDefaultResource::Animation))
} else {
Err(format!(
"`default_resource` is '{value}', but only `mesh` and `animation` are supported; skeletons require an explicit fragment"
))
}
}
pub(crate) fn store_model<M: crate::Model>(
context: asset_handler::BakeContext<'_>,
id: &str,
model: M,
data: &[u8],
) -> Result<crate::ReferenceModel<M>, asset_handler::LoadErrors> {
context
.store_generated(crate::ProcessedAsset::new(ResourceId::new(id), model), data)
.map(Into::into)
}
pub(crate) fn sanitize_material_name(name: &str) -> String {
let sanitized = name
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() || character == '_' || character == '-' {
character
} else {
'_'
}
})
.collect::<String>();
if sanitized.is_empty() {
"material".to_string()
} else {
sanitized
}
}
#[cfg(test)]
mod container_default_resource_tests {
use super::{container_default_resource, ContainerDefaultResource};
#[test]
fn bead_default_resource_accepts_mesh_and_animation_but_never_skeleton() {
for (value, expected) in [
("mesh", ContainerDefaultResource::Mesh),
("Animation", ContainerDefaultResource::Animation),
] {
let spec = super::parse_json(&format!(r#"{{ "default_resource": "{value}" }}"#)).unwrap();
assert_eq!(container_default_resource(Some(&spec)), Ok(Some(expected)));
}
let skeleton = super::parse_json(r#"{ "default_resource": "skeleton" }"#).unwrap();
assert!(container_default_resource(Some(&skeleton)).is_err());
}
}
pub mod resource_id;
pub mod storage_backend;
pub use resource_id::ResourceId;
pub use storage_backend::FileStorageBackend;
pub use storage_backend::{AssetStorageBytes, StorageBackend};
use crate::r#async::read;
use crate::resource::reader::MappedFileBacking;
pub async fn read_asset_from_source<'a>(
url: ResourceId<'a>,
base_path: Option<&'a std::path::Path>,
allocator: &'a dyn Allocator,
) -> Result<(AssetStorageBytes<'a>, Option<BEADType>, String), ()> {
let base = url.get_base();
let resource_origin = if base.as_ref().starts_with("http://") || base.as_ref().starts_with("https://") {
"network"
} else {
"local"
};
match resource_origin {
"local" => {
let path = base_path.unwrap_or(std::path::Path::new(""));
let path = path.join(base.as_ref());
let spec_path = path.with_added_extension("bead");
let format = path
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default()
.to_string();
let spec = read_asset_spec(&spec_path);
let source_bytes = read_asset_bytes(&path, allocator);
let (spec, source_bytes) = std::future::join!(spec, source_bytes).await;
return Ok((source_bytes?, spec?, format));
}
_ => {
Err(())
}
}
}
async fn read_asset_spec(spec_path: &std::path::Path) -> Result<Option<BEADType>, ()> {
let spec_bytes = match read(spec_path).await {
Ok(bytes) => Some(bytes),
Err(err) if err.kind() == ErrorKind::NotFound => None,
Err(_) => return Err(()),
};
if let Some(spec_bytes) = spec_bytes {
let spec = std::str::from_utf8(&spec_bytes).or(Err(()))?;
let spec = parse_json(spec).or(Err(()))?;
Ok(Some(spec))
} else {
Ok(None)
}
}
async fn read_asset_bytes<'a>(path: &std::path::Path, allocator: &'a dyn Allocator) -> Result<AssetStorageBytes<'a>, ()> {
match std::fs::File::open(path)
.map_err(|_| ())
.and_then(|file| MappedFileBacking::new(&file))
{
Ok(mapped_file) => Ok(AssetStorageBytes::MappedFile(mapped_file)),
Err(_) => {
let source_bytes = read(path).await.or(Err(()))?;
let mut source_data = Vec::with_capacity_in(source_bytes.len(), allocator);
source_data.extend_from_slice(&source_bytes);
Ok(AssetStorageBytes::Allocated(source_data.into_boxed_slice()))
}
}
}