use crate::ecs::PayloadLocator;
use crate::ecs::asset_id::AssetId;
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BlobAssetDef {
pub name: Option<AssetId>,
pub kind: AssetKind,
pub discriminant: u8,
#[serde(with = "serde_bytes")]
pub args_bytes: Vec<u8>,
pub payload: Option<PayloadLocator>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum AssetKind {
Component,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResourceKind {
Mesh,
Texture,
Material,
Font,
AudioClip,
CubemapTexture,
EnvironmentMap,
ColorLut,
SkinnedMesh,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ResourceRecord {
pub resource_kind: u8,
pub handle: u32,
pub payload: Option<PayloadLocator>,
#[serde(with = "serde_bytes")]
pub data_bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct WorldManifest {
pub component_counts: Vec<(u8, u32)>,
pub max_blob_index: u32,
}
impl WorldManifest {
pub fn from_records(defs: &[BlobAssetDef], resources: &[ResourceRecord]) -> Self {
let mut counts = [0u32; 256];
for def in defs {
counts[def.discriminant as usize] += 1;
}
let component_counts = counts
.iter()
.enumerate()
.filter(|&(_, &n)| n > 0)
.map(|(d, &n)| (d as u8, n))
.collect();
let max_blob_index = defs
.iter()
.filter_map(|d| d.payload.as_ref())
.chain(resources.iter().filter_map(|r| r.payload.as_ref()))
.map(|p| p.blob_index)
.max()
.unwrap_or(0);
WorldManifest {
component_counts,
max_blob_index,
}
}
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct BlobMeta {
pub defs: Vec<BlobAssetDef>,
pub resources: Vec<ResourceRecord>,
pub manifest: WorldManifest,
pub scene_groups: Vec<SceneGroup>,
pub mesh_bounds: Vec<MeshBoundsRecord>,
pub physics_budget: Option<PhysicsBudgetRecord>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct PhysicsBudgetRecord {
pub fixed: u32,
pub dynamic: u32,
pub kinematic: u32,
pub sensors: u32,
pub joints: u32,
pub anchors: u32,
pub spawn_headroom: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MeshBoundsRecord {
pub handle: u32,
pub min: [f32; 3],
pub max: [f32; 3],
pub vertex_count: u32,
pub index_count: u32,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct SceneGroup {
pub scene: AssetId,
pub resources: Vec<(u8, u32)>,
pub defs: Vec<AssetId>,
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn sample_meta() -> BlobMeta {
let defs = vec![BlobAssetDef {
name: Some(AssetId(3)),
kind: AssetKind::Component,
discriminant: 42,
args_bytes: vec![9, 8, 7],
payload: Some(PayloadLocator {
blob_index: 1,
offset: 16,
len: 4,
}),
}];
let resources = vec![ResourceRecord {
resource_kind: ResourceKind::Material as u8,
handle: 5,
payload: None,
data_bytes: vec![1, 2, 3, 4],
}];
let manifest = WorldManifest::from_records(&defs, &resources);
BlobMeta {
defs,
resources,
manifest,
scene_groups: vec![SceneGroup {
scene: AssetId(7),
resources: vec![(ResourceKind::Material as u8, 5)],
defs: vec![AssetId(3)],
}],
mesh_bounds: vec![MeshBoundsRecord {
handle: 2,
min: [-1.0, 0.0, -1.0],
max: [1.0, 2.0, 1.0],
vertex_count: 24,
index_count: 36,
}],
physics_budget: Some(PhysicsBudgetRecord {
fixed: 3,
dynamic: 2,
kinematic: 1,
sensors: 1,
joints: 2,
anchors: 1,
spawn_headroom: 16,
}),
}
}
#[test]
fn blob_meta_round_trips_through_postcard() {
let meta = sample_meta();
let bytes = postcard::to_allocvec(&meta).expect("serialize");
let back: BlobMeta = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(back, meta);
}
#[test]
fn resource_record_round_trips_both_branches() {
let payload_res = ResourceRecord {
resource_kind: ResourceKind::AudioClip as u8,
handle: 0,
payload: Some(PayloadLocator {
blob_index: 0,
offset: 0,
len: 7,
}),
data_bytes: Vec::new(),
};
let data_res = ResourceRecord {
resource_kind: ResourceKind::Material as u8,
handle: 1,
payload: None,
data_bytes: vec![0xAA, 0xBB],
};
for rec in [payload_res, data_res] {
let bytes = postcard::to_allocvec(&rec).expect("serialize");
let back: ResourceRecord = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(back, rec);
}
}
#[test]
fn manifest_derives_counts_and_max_blob_index() {
let def = |disc: u8, blob_index: u32| BlobAssetDef {
name: None,
kind: AssetKind::Component,
discriminant: disc,
args_bytes: Vec::new(),
payload: Some(PayloadLocator {
blob_index,
offset: 0,
len: 1,
}),
};
let defs = vec![def(7, 0), def(7, 2), def(3, 1)];
let resources = vec![ResourceRecord {
resource_kind: ResourceKind::Texture as u8,
handle: 0,
payload: Some(PayloadLocator {
blob_index: 4,
offset: 0,
len: 1,
}),
data_bytes: Vec::new(),
}];
let manifest = WorldManifest::from_records(&defs, &resources);
assert_eq!(manifest.component_counts, vec![(3, 1), (7, 2)]);
assert_eq!(manifest.max_blob_index, 4, "resource payloads count too");
let empty = WorldManifest::from_records(&[], &[]);
assert_eq!(empty, WorldManifest::default());
}
#[test]
fn resource_kind_discriminants_are_stable() {
assert_eq!(ResourceKind::Mesh as u8, 0);
assert_eq!(ResourceKind::Texture as u8, 1);
assert_eq!(ResourceKind::Material as u8, 2);
assert_eq!(ResourceKind::Font as u8, 3);
assert_eq!(ResourceKind::AudioClip as u8, 4);
assert_eq!(ResourceKind::CubemapTexture as u8, 5);
assert_eq!(ResourceKind::EnvironmentMap as u8, 6);
assert_eq!(ResourceKind::ColorLut as u8, 7);
assert_eq!(ResourceKind::SkinnedMesh as u8, 8);
}
}