use serde::Deserialize;
use crate::ecs::asset_id::AssetId;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Once;
pub use concinnity_world::registry::RegisteredType;
pub use concinnity_world::resource_type::{ResourceKind, asset_resource_kind, is_mesh_source};
pub(crate) trait ResourceAssetCompile {
fn compile_payload(
self,
args: &serde_json::Value,
assets_dir: Option<&Path>,
) -> std::io::Result<Vec<u8>>;
fn source_files(self, args: &serde_json::Value, assets_dir: Option<&Path>) -> Vec<String>;
fn compile_data(self, name: &str, args: &serde_json::Value)
-> std::io::Result<Option<Vec<u8>>>;
}
impl ResourceAssetCompile for RegisteredType {
fn compile_payload(
self,
args: &serde_json::Value,
assets_dir: Option<&Path>,
) -> std::io::Result<Vec<u8>> {
match self {
Self::AudioClip => crate::audio_clip::compile_audio_clip_payload(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::Texture => crate::texture::compile_texture_payload(args, assets_dir)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::CubemapTexture => crate::cubemap::compile_cubemap_payload(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::EnvironmentMap => {
crate::environment_map::compile_environment_map_payload(args, assets_dir)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
Self::ColorLut => crate::color_lut::compile_color_lut_payload(args, assets_dir)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::Font => crate::font::compile_font_payload(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::Material => compile_material_data(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::Mesh => crate::mesh_compile::compile_mesh_payload(args, assets_dir)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Self::SkinnedMesh => compile_skinned_mesh_payload(args),
other => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("{} is not a resource asset", other.as_str()),
)),
}
}
fn source_files(self, args: &serde_json::Value, assets_dir: Option<&Path>) -> Vec<String> {
match self {
Self::AudioClip
| Self::Texture
| Self::CubemapTexture
| Self::EnvironmentMap
| Self::ColorLut
| Self::Mesh
| Self::SkinnedMesh => {
let mut files: Vec<String> = args
.get("source")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
.into_iter()
.collect();
if let Some(src) = files.first().cloned()
&& src.to_lowercase().ends_with(".gltf")
{
files.extend(crate::gltf_source::referenced_files(&src, assets_dir));
}
if let Some(arg) = args.get("character_model").and_then(|v| {
serde_json::from_value::<crate::character::import::CharacterModelArg>(v.clone())
.ok()
}) {
files.extend(arg.source_files());
}
files
}
Self::Font => args
.get("path")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
.into_iter()
.collect(),
_ => Vec::new(),
}
}
fn compile_data(
self,
name: &str,
args: &serde_json::Value,
) -> std::io::Result<Option<Vec<u8>>> {
match self {
Self::SkinnedMesh => compile_skinned_mesh_data(name, args)
.map(Some)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
_ => Ok(None),
}
}
}
fn compile_material_data(args: &serde_json::Value) -> Result<Vec<u8>, String> {
let mat: crate::components::Material =
Deserialize::deserialize(args).map_err(|e| format!("Material args: {e}"))?;
let mat = concinnity_world::validate::material(mat);
postcard::to_allocvec(&mat).map_err(|e| format!("Material serialize: {e}"))
}
fn compile_skinned_mesh_payload(args: &serde_json::Value) -> std::io::Result<Vec<u8>> {
let mesh: crate::components::SkinnedMesh = Deserialize::deserialize(args)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let skeleton: Vec<crate::components::SkeletonJoint> = match args.get("skeleton") {
Some(v) => Deserialize::deserialize(v).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("SkinnedMesh: invalid skeleton args: {e}"),
)
})?,
None => Vec::new(),
};
crate::geometry::compile_skinned_mesh_payload_with_lods(
&mesh.vertices,
&mesh.indices,
&skeleton,
&mesh.morph_target_names,
&mesh.morph_deltas,
&crate::geometry::SkinnedLods {
levels: mesh.lod_levels,
distances: &mesh.lod_distances,
},
)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
fn compile_skinned_mesh_data(name: &str, args: &serde_json::Value) -> Result<Vec<u8>, String> {
let mut sm: crate::components::SkinnedMesh =
Deserialize::deserialize(args).map_err(|e| format!("SkinnedMesh args: {e}"))?;
if sm.scale == [0.0, 0.0, 0.0] {
sm.scale = [1.0, 1.0, 1.0];
}
if sm.lod_levels == 0 {
sm.lod_levels = 1;
}
sm.lod_levels = sm.lod_levels.min(8);
sm.max_instances = sm.max_instances.min(4096);
sm.vertices = Vec::new();
sm.indices = Vec::new();
let name_id = crate::ecs::asset_id::intern(name);
postcard::to_allocvec(&(name_id.0, sm)).map_err(|e| format!("SkinnedMesh serialize: {e}"))
}
#[derive(Debug, Default, Clone)]
pub(crate) struct ResourceHandles {
next: HashMap<ResourceKind, u32>,
map: HashMap<(ResourceKind, AssetId), u32>,
shader_map: HashMap<AssetId, u32>,
shader_next: u32,
}
impl ResourceHandles {
pub(crate) fn assign(&mut self, kind: ResourceKind, id: AssetId) -> u32 {
let next = self.next.entry(kind).or_insert(0);
let handle = *next;
*next += 1;
self.map.insert((kind, id), handle);
handle
}
pub(crate) fn get(&self, kind: ResourceKind, id: AssetId) -> Option<u32> {
self.map.get(&(kind, id)).copied()
}
#[cfg(test)]
pub(crate) fn count(&self, kind: ResourceKind) -> u32 {
self.next.get(&kind).copied().unwrap_or(0)
}
pub(crate) fn from_assets(assets: impl IntoIterator<Item = (AssetId, ResourceKind)>) -> Self {
let mut handles = Self::default();
for (id, kind) in assets {
handles.assign(kind, id);
}
handles
}
pub(crate) fn assign_shader(&mut self, id: AssetId) -> u32 {
let handle = self.shader_next;
self.shader_next += 1;
self.shader_map.insert(id, handle);
handle
}
pub(crate) fn shader(&self, id: AssetId) -> Option<u32> {
self.shader_map.get(&id).copied()
}
#[cfg(test)]
pub(crate) fn shader_count(&self) -> u32 {
self.shader_next
}
}
pub(crate) fn assign_shader_handles(
handles: &mut ResourceHandles,
assets: &[crate::world::WorldJsonlAsset],
) {
for asset in assets
.iter()
.filter(|a| a.asset_type.to_lowercase().replace('_', "") == "shader")
{
handles.assign_shader(crate::ecs::asset_id::intern(&asset.name));
}
}
pub(crate) fn assign_mesh_source_handles(
handles: &mut ResourceHandles,
assets: &[crate::world::WorldJsonlAsset],
) {
for block in 0..=3u8 {
for asset in assets.iter().filter(|a| {
concinnity_world::resource_type::mesh_source_block(&a.asset_type, &a.args)
== Some(block)
}) {
handles.assign(
ResourceKind::Mesh,
crate::ecs::asset_id::intern(&asset.name),
);
}
}
}
thread_local! {
static RESOURCE_HANDLES: RefCell<ResourceHandles> = RefCell::new(ResourceHandles::default());
}
pub(crate) fn ensure_resource_handle_resolvers() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
crate::ecs::set_texture_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::Texture, id))
});
crate::ecs::set_audio_clip_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::AudioClip, id))
});
crate::ecs::set_font_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::Font, id))
});
crate::ecs::set_mesh_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::Mesh, id))
});
crate::ecs::set_material_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::Material, id))
});
crate::ecs::set_skinned_mesh_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().get(ResourceKind::SkinnedMesh, id))
});
crate::ecs::set_shader_handle_resolver(|name| {
let id = crate::ecs::asset_id::intern(name);
RESOURCE_HANDLES.with(|h| h.borrow().shader(id))
});
});
}
pub(crate) fn install_resource_handles(handles: ResourceHandles) {
ensure_resource_handle_resolvers();
RESOURCE_HANDLES.with(|h| *h.borrow_mut() = handles);
}
pub(crate) fn reset_resource_handles() {
ensure_resource_handle_resolvers();
RESOURCE_HANDLES.with(|h| *h.borrow_mut() = ResourceHandles::default());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::RegisteredType;
#[test]
fn mesh_source_handles_are_block_ordered_across_kinds() {
use crate::ecs::asset_id;
use crate::world::WorldJsonlAsset;
let a = |name: &str, ty: &str, args: serde_json::Value| WorldJsonlAsset {
name: name.to_string(),
asset_type: ty.to_string(),
args,
};
let assets = vec![
a("m0", "Mesh", serde_json::json!({})),
a(
"p0",
"ProceduralMesh",
serde_json::json!({"generator": "box"}),
),
a("f_tex", "File", serde_json::json!({"kind": "png"})),
a("v0", "VoxelChunk", serde_json::json!({})),
a("m1", "Mesh", serde_json::json!({})),
a("f_mesh", "File", serde_json::json!({"kind": "obj"})),
a("light", "PointLight", serde_json::json!({})),
a(
"p1",
"ProceduralMesh",
serde_json::json!({"generator": "sphere"}),
),
];
asset_id::reset_interner();
let names: Vec<&str> = assets.iter().map(|x| x.name.as_str()).collect();
asset_id::intern_all(&names);
let mut handles = ResourceHandles::default();
assign_mesh_source_handles(&mut handles, &assets);
let h = |name: &str| handles.get(ResourceKind::Mesh, asset_id::intern(name));
assert_eq!(h("m0"), Some(0));
assert_eq!(h("m1"), Some(1));
assert_eq!(h("p0"), Some(2));
assert_eq!(h("p1"), Some(3));
assert_eq!(h("v0"), Some(4));
assert_eq!(h("f_mesh"), Some(5));
assert_eq!(h("f_tex"), None);
assert_eq!(h("light"), None);
assert_eq!(handles.count(ResourceKind::Mesh), 6);
}
#[test]
fn a_material_reads_no_source_file_and_resolves_by_declaration_order() {
use crate::ecs::asset_id;
assert!(
RegisteredType::Material
.source_files(
&serde_json::json!({"albedo": "wall_tex", "source": "m.json"}),
None
)
.is_empty()
);
asset_id::reset_interner();
let stone = asset_id::intern("stone");
let wood = asset_id::intern("wood");
reset_resource_handles();
install_resource_handles(ResourceHandles::from_assets([
(stone, ResourceKind::Material),
(wood, ResourceKind::Material),
]));
let bytes = RegisteredType::Prop
.reserialize_args(&serde_json::json!({"material": "wood"}))
.unwrap();
let prop: crate::components::Prop = postcard::from_bytes(&bytes).unwrap();
assert_eq!(prop.material, Some(crate::ecs::MaterialHandle(1)));
let bytes = RegisteredType::Prop
.reserialize_args(&serde_json::json!({"material": "granite"}))
.unwrap();
let prop: crate::components::Prop = postcard::from_bytes(&bytes).unwrap();
assert_eq!(
prop.material,
Some(crate::ecs::MaterialHandle(
asset_id::intern("granite").0 as usize as u32
))
);
}
#[test]
fn handles_are_dense_per_kind_in_order() {
let assets = [
(AssetId(10), ResourceKind::Texture),
(AssetId(11), ResourceKind::Mesh),
(AssetId(12), ResourceKind::Texture),
(AssetId(20), ResourceKind::AudioClip),
(AssetId(14), ResourceKind::Texture),
];
let handles = ResourceHandles::from_assets(assets);
assert_eq!(handles.get(ResourceKind::Texture, AssetId(10)), Some(0));
assert_eq!(handles.get(ResourceKind::Texture, AssetId(12)), Some(1));
assert_eq!(handles.get(ResourceKind::Texture, AssetId(14)), Some(2));
assert_eq!(handles.get(ResourceKind::Mesh, AssetId(11)), Some(0));
assert_eq!(handles.count(ResourceKind::Texture), 3);
assert_eq!(handles.count(ResourceKind::Mesh), 1);
assert_eq!(handles.count(ResourceKind::AudioClip), 1);
assert_eq!(handles.count(ResourceKind::Material), 0);
assert_eq!(handles.get(ResourceKind::AudioClip, AssetId(20)), Some(0));
assert_eq!(handles.get(ResourceKind::Texture, AssetId(99)), None);
}
#[test]
fn a_shader_reference_name_bakes_to_its_declaration_order_handle() {
use crate::ecs::ShaderHandle;
use crate::ecs::asset_id;
use crate::world::WorldJsonlAsset;
asset_id::reset_interner();
let world_asset = |name: &str, ty: &str| WorldJsonlAsset {
name: name.to_string(),
asset_type: ty.to_string(),
args: serde_json::json!({}),
};
let assets = vec![
world_asset("mat", "Material"),
world_asset("shader_a", "Shader"),
world_asset("tex", "Texture"),
world_asset("shader_b", "Shader"),
];
reset_resource_handles();
let mut handles = ResourceHandles::default();
assign_shader_handles(&mut handles, &assets);
assert_eq!(handles.shader_count(), 2);
assert_eq!(handles.shader(asset_id::intern("shader_a")), Some(0));
assert_eq!(handles.shader(asset_id::intern("shader_b")), Some(1));
install_resource_handles(handles);
let bytes = RegisteredType::Material
.compile_payload(&serde_json::json!({"shader": "shader_b"}), None)
.unwrap();
let mat: crate::components::Material = postcard::from_bytes(&bytes).unwrap();
assert_eq!(mat.shader, Some(ShaderHandle(1)));
let bytes = RegisteredType::Material
.compile_payload(&serde_json::json!({}), None)
.unwrap();
let mat: crate::components::Material = postcard::from_bytes(&bytes).unwrap();
assert_eq!(mat.shader, None);
}
#[test]
fn the_same_id_in_two_kinds_gets_independent_handles() {
let mut handles = ResourceHandles::default();
assert_eq!(handles.assign(ResourceKind::Texture, AssetId(1)), 0);
assert_eq!(handles.assign(ResourceKind::Mesh, AssetId(1)), 0);
assert_eq!(handles.get(ResourceKind::Texture, AssetId(1)), Some(0));
assert_eq!(handles.get(ResourceKind::Mesh, AssetId(1)), Some(0));
}
#[test]
fn a_texture_reference_name_reserializes_to_its_declaration_order_handle() {
use crate::ecs::asset_id;
asset_id::reset_interner();
let a = asset_id::intern("tex_a");
let b = asset_id::intern("tex_b");
reset_resource_handles();
install_resource_handles(ResourceHandles::from_assets([
(a, ResourceKind::Texture),
(b, ResourceKind::Texture),
]));
let bake = |field_json: serde_json::Value| -> crate::components::Material {
let bytes = RegisteredType::Material
.compile_payload(&field_json, None)
.unwrap();
postcard::from_bytes(&bytes).unwrap()
};
use crate::ecs::TextureHandle;
assert_eq!(
bake(serde_json::json!({"albedo": "tex_b"})).albedo,
Some(TextureHandle(1))
);
assert_eq!(
bake(serde_json::json!({"albedo": "tex_a"})).albedo,
Some(TextureHandle(0))
);
let decal_bytes = RegisteredType::Decal
.reserialize_args(&serde_json::json!({"texture": "tex_b"}))
.unwrap();
let decal: crate::components::Decal = postcard::from_bytes(&decal_bytes).unwrap();
assert_eq!(decal.texture, Some(TextureHandle(1)));
let prop_bytes = RegisteredType::Prop
.reserialize_args(&serde_json::json!({"texture": "tex_b"}))
.unwrap();
let prop: crate::components::Prop = postcard::from_bytes(&prop_bytes).unwrap();
assert_eq!(prop.texture, Some(TextureHandle(1)));
let inst_bytes = RegisteredType::InstancedProp
.reserialize_args(&serde_json::json!({"texture": "tex_a"}))
.unwrap();
let inst: crate::components::InstancedProp = postcard::from_bytes(&inst_bytes).unwrap();
assert_eq!(inst.texture, Some(TextureHandle(0)));
}
#[test]
fn an_audio_clip_reference_name_reserializes_to_its_declaration_order_handle() {
use crate::ecs::asset_id;
asset_id::reset_interner();
let clip_a = asset_id::intern("clip_a");
let tex = asset_id::intern("tex");
let clip_b = asset_id::intern("clip_b");
reset_resource_handles();
install_resource_handles(ResourceHandles::from_assets([
(clip_a, ResourceKind::AudioClip),
(tex, ResourceKind::Texture),
(clip_b, ResourceKind::AudioClip),
]));
use crate::ecs::AudioClipHandle;
let clip_field = |ct: RegisteredType, name: &str| -> Option<AudioClipHandle> {
let bytes = ct
.reserialize_args(&serde_json::json!({ "clip": name }))
.unwrap();
match ct {
RegisteredType::AudioEmitter => {
postcard::from_bytes::<crate::components::AudioEmitter>(&bytes)
.unwrap()
.clip
}
RegisteredType::AudioCue => {
postcard::from_bytes::<crate::components::AudioCue>(&bytes)
.unwrap()
.clip
}
other => panic!("unexpected type {other:?}"),
}
};
assert_eq!(
clip_field(RegisteredType::AudioEmitter, "clip_a"),
Some(AudioClipHandle(0))
);
assert_eq!(
clip_field(RegisteredType::AudioEmitter, "clip_b"),
Some(AudioClipHandle(1))
);
assert_eq!(
clip_field(RegisteredType::AudioCue, "clip_b"),
Some(AudioClipHandle(1))
);
}
}