use crate::define_components;
use crate::ecs::{BlobAssetDef, Component, PayloadLocator};
use crate::result::CnResult;
#[macro_export]
macro_rules! for_each_component {
($cb:ident) => { $crate::for_each_component!($cb;); };
($cb:ident; $($extra:tt)*) => {
$cb! {
$($extra)*
stored: {
Window => $crate::components::Window { gen, external, singleton, consumed },
GraphicsConfig => $crate::components::GraphicsConfig { gen, external, singleton, renders, consumed },
Shader => $crate::components::Shader { manual, external, compiled, consumed },
Camera3D => $crate::components::Camera3D { manual, external, useful_blank, live, args: Camera3D },
FrameInput => $crate::components::FrameInput { gen, runtime },
Prop => $crate::components::Prop { gen, external, id, renders, validate: prop, refs: [("model", "Model"), ("material", "Material"), ("texture", "Texture"), ("scene", "Scene"), ("parent", "Prop")], consumed: PropInstance },
RigidBody => $crate::components::RigidBody { gen, external, validate: rigid_body },
PropBody => $crate::components::PropBody { gen, external, consumed },
Room => $crate::components::Room { manual, external, compiled, useful_blank, args: Room, refs: [("texture", "Texture"), ("wall_texture", "Texture"), ("floor_texture", "Texture"), ("ceiling_texture", "Texture")], consumed },
DirectionalLight => $crate::components::DirectionalLight { gen, external, useful_blank, validate: directional_light },
PointLight => $crate::components::PointLight { gen, external, useful_blank, validate: point_light },
SpotLight => $crate::components::SpotLight { gen, external, useful_blank, validate: spot_light },
RectAreaLight => $crate::components::RectAreaLight { gen, external, useful_blank, validate: rect_area_light },
ProceduralMesh => $crate::components::ProceduralMesh { gen, external, compiled, id },
Model => $crate::components::Model { gen, external, id, consumed },
Scene => $crate::components::Scene { gen, external, id, refs: [("camera_shot", "Camera3D")], consumed },
TextLabel => $crate::components::TextLabel { gen, external, id, useful_blank, renders, live, refs: [("font", "Font"), ("screen", "Screen")] },
HitRegion => $crate::components::HitRegion { gen, external, useful_blank, refs: [("label", "TextLabel"), ("screen", "Screen")], consumed },
File => $crate::components::File { manual, external, compiled, args: File, consumed },
BlockType => $crate::components::BlockType { gen, external, id, useful_blank, consumed },
VoxelChunk => $crate::components::VoxelChunk { gen, external, compiled, id, validate: voxel_chunk, consumed },
InstancedProp => $crate::components::InstancedProp { gen, external, id, renders, validate: instanced_prop, refs: [("material", "Material"), ("texture", "Texture")], consumed },
PostProcessConfig => $crate::components::PostProcessConfig { manual, external, singleton, consumed },
Animation => $crate::components::Animation { gen, external, id, consumed },
SkeletonPose => $crate::components::SkeletonPose { runtime, build: skeleton_pose },
StreamingConfig => $crate::components::StreamingConfig { gen, external, singleton, consumed },
VoxelWorld => $crate::components::VoxelWorld { gen, external, renders, refs: [("material", "Material")], consumed },
AudioEmitter => $crate::components::AudioEmitter { gen, external, useful_blank, refs: [("clip", "AudioClip"), ("prop", "Prop")] },
Sprite => $crate::components::Sprite { gen, external, id, useful_blank, renders, live, refs: [("texture", "Texture"), ("screen", "Screen")] },
KeyBinding => $crate::components::KeyBinding { gen, external, useful_blank, refs: [("screen", "Screen")], consumed },
Screen => $crate::components::Screen { gen, external, id, useful_blank, refs: [("focus", "TextInput")], consumed },
Decal => $crate::components::Decal { gen, external, id, useful_blank, validate: decal, refs: [("texture", "Texture")], consumed },
VolumetricFog => $crate::components::VolumetricFog { gen, external, useful_blank, validate: volumetric_fog, consumed },
PhysicsJoint => $crate::components::PhysicsJoint { gen, external, id, validate: joint, refs: [("body_a", "Prop"), ("body_b", "Prop")], consumed },
ParticleEmitter => $crate::components::ParticleEmitter { gen, external, id, useful_blank, validate: particle_emitter, refs: [("texture", "Texture")], consumed },
WaterSurface => $crate::components::WaterSurface { gen, external, id, useful_blank, renders, validate: water_surface, consumed },
SdfVolume => $crate::components::SdfVolume { manual, external, compiled, renders, validate: sdf_volume, consumed },
GlassPanel => $crate::components::GlassPanel { gen, external, id, useful_blank, validate: glass_panel, consumed },
LayoutContainer => $crate::components::LayoutContainer { gen, external, renders, live },
PhysicsConfig => $crate::components::PhysicsConfig { gen, external, singleton },
FpsCounter => $crate::components::FpsCounter { gen, external, useful_blank, refs: [("label", "TextLabel")] },
StatHud => $crate::components::StatHud { gen, external, renders, refs: [("fps_label", "TextLabel"), ("vram_label", "TextLabel"), ("ram_label", "TextLabel"), ("ev_label", "TextLabel"), ("edr_label", "TextLabel")] },
ScrollPanel => $crate::components::ScrollPanel { gen, external, refs: [("screen", "Screen")], consumed },
ReflectionProbe => $crate::components::ReflectionProbe { gen, external, useful_blank, validate: reflection_probe },
Transform => $crate::components::Transform { runtime },
PropInstance => $crate::components::PropInstance { runtime },
MeshRenderer => $crate::components::MeshRenderer { runtime },
ModelRenderer => $crate::components::ModelRenderer { runtime },
Collider => $crate::components::Collider { runtime },
BodyDynamics => $crate::components::BodyDynamics { runtime },
Interactable => $crate::components::Interactable { runtime },
Pickup => $crate::components::Pickup { runtime },
Parent => $crate::components::Parent { runtime },
Children => $crate::components::Children { runtime },
SceneMember => $crate::components::SceneMember { runtime },
GlobalTransform => $crate::components::GlobalTransform { runtime },
RenderHandle => $crate::components::RenderHandle { runtime },
Held => $crate::components::Held { runtime },
Lifetime => $crate::components::Lifetime { runtime },
Spawner => $crate::components::Spawner { manual, external, args: Spawner },
DebugHud => $crate::components::DebugHud { gen, external, renders, refs: [("passes_label", "TextLabel"), ("mouse_label", "TextLabel"), ("camera_label", "TextLabel"), ("sys_label", "TextLabel")] },
AudioCue => $crate::components::AudioCue { gen, external, useful_blank, refs: [("clip", "AudioClip"), ("screen", "Screen")] },
Story => $crate::components::Story { gen, external, id },
AppConfig => $crate::components::AppConfig { manual, external, singleton, args: AppConfig },
AnimationGraph => $crate::components::AnimationGraph { gen, external, id, consumed },
AnimationParams => $crate::components::AnimationParams { runtime, build: anim_params },
CharacterRig => $crate::components::CharacterRig { runtime, build: character_rig },
GroundProbes => $crate::components::GroundProbes { runtime },
CameraProbe => $crate::components::CameraProbe { runtime },
TextInput => $crate::components::TextInput { gen, external, id, useful_blank, renders, live, refs: [("font", "Font"), ("screen", "Screen")] },
Behavior => $crate::components::Behavior { gen, external, id, useful_blank, live },
Variables => $crate::components::Variables { gen, external, singleton, live },
TriggerVolume => $crate::components::TriggerVolume { gen, external, id, useful_blank },
Hidden => $crate::components::Hidden { runtime },
LoadingOverlay => $crate::components::LoadingOverlay { gen, external, singleton, renders, refs: [("screen", "Screen"), ("backdrop", "Sprite"), ("track", "Sprite"), ("fill", "Sprite"), ("label", "TextLabel")] },
AudioOcclusionProbe => $crate::components::AudioOcclusionProbe { runtime },
CharacterShape => $crate::components::CharacterShape { gen, external, id, refs: [("target", "SkinnedMesh")] },
EngineDefaults => $crate::components::EngineDefaults { gen, external, singleton, consumed },
},
resource: {
AudioClip => $crate::components::AudioClip { resource: AudioClip, compiled },
Texture => $crate::components::Texture { resource: Texture, compiled },
CubemapTexture => $crate::components::CubemapTexture { resource: CubemapTexture, compiled },
EnvironmentMap => $crate::components::EnvironmentMap { resource: EnvironmentMap, compiled, renders },
ColorLut => $crate::components::ColorLut { resource: ColorLut, compiled },
Font => $crate::components::Font { resource: Font, compiled, useful_blank },
Material => $crate::components::Material { resource: Material, data, useful_blank, refs: [("albedo", "Texture"), ("normal_map", "Texture"), ("emissive_map", "Texture"), ("orm_map", "Texture"), ("albedo_secondary", "Texture"), ("normal_secondary", "Texture"), ("shader", "Shader")] },
Mesh => $crate::components::Mesh { resource: Mesh, compiled },
SkinnedMesh => $crate::components::SkinnedMesh { resource: SkinnedMesh, compiled, renders },
},
}
};
}
crate::for_each_component!(define_components);
macro_rules! cn_impl_components {
(
stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? } $(,)?
) => {
$( cn_impl_components!(@one $variant $ty { $($meta)* }); )+
};
(@one $variant:ident $ty:path { manual $($rest:tt)* }) => {};
(@one $variant:ident $ty:path { gen $($flags:tt)* }) => {
cn_impl_components!(@munch $variant $ty [] $($flags)*);
};
(@one $variant:ident $ty:path { runtime $($rest:tt)* }) => {
impl $crate::ecs::Component for $ty {
const NAME: &'static str = stringify!($variant);
}
};
(@munch $variant:ident $ty:path [$($body:tt)*] , compiled $($rest:tt)*) => {
cn_impl_components!(@munch $variant $ty
[$($body)*
fn inject_locator(&mut self, locator: $crate::ecs::PayloadLocator) {
self.locator = Some(locator);
}]
$($rest)*);
};
(@munch $variant:ident $ty:path [$($body:tt)*] , id $($rest:tt)*) => {
cn_impl_components!(@munch $variant $ty
[$($body)* fn inject_name(&mut self, id: $crate::ecs::asset_id::AssetId) {
self.asset_id = id;
}]
$($rest)*);
};
(@munch $variant:ident $ty:path [$($body:tt)*] , validate: $f:ident $($rest:tt)*) => {
cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
};
(@munch $variant:ident $ty:path [$($body:tt)*] , refs: [ $( ($fld:literal, $tgt:literal) ),+ $(,)? ] $($rest:tt)*) => {
cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
};
(@munch $variant:ident $ty:path [$($body:tt)*] , consumed: $surviving:ident $($rest:tt)*) => {
cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
};
(@munch $variant:ident $ty:path [$($body:tt)*] , $flag:ident $($rest:tt)*) => {
cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
};
(@munch $variant:ident $ty:path [$($body:tt)*]) => {
impl $crate::ecs::Component for $ty {
const NAME: &'static str = stringify!($variant);
$($body)*
fn from_baked(bytes: &[u8]) -> Result<Self, $crate::result::CnResult> {
Ok($crate::blob::decode_exact(bytes)?)
}
}
};
}
crate::for_each_component!(cn_impl_components);
#[cfg(test)]
mod tests {
use crate::blob::BlobAssetDef;
use crate::components::{Prop, Transform};
use crate::ecs::asset_id::AssetId;
use crate::ecs::{
AssetKind, ComponentAsset, ComponentStorage, ComponentTag, PayloadLocator, ResourceKind,
};
use crate::result::CnResult;
use alloc::vec::Vec;
macro_rules! registry_names {
(
stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? } $(,)?
) => {
const STORED: &[(ComponentTag, &str)] =
&[$( (ComponentTag::$variant, stringify!($variant)) ),+];
const RESOURCES: &[&str] = &[$( stringify!($rvariant) ),+];
};
}
crate::for_each_component!(registry_names);
#[test]
fn an_undrained_component_survives_as_itself() {
assert_eq!(
ComponentTag::Transform.surviving_tag(),
Some(ComponentTag::Transform)
);
assert_eq!(
ComponentTag::Behavior.surviving_tag(),
Some(ComponentTag::Behavior)
);
}
#[test]
fn a_consumed_component_survives_as_nothing() {
assert_eq!(ComponentTag::Screen.surviving_tag(), None);
assert_eq!(ComponentTag::PropBody.surviving_tag(), None);
}
#[test]
fn a_consumed_component_can_name_its_replacement() {
assert_eq!(
ComponentTag::Prop.surviving_tag(),
Some(ComponentTag::PropInstance)
);
}
#[test]
fn every_tag_names_itself_and_parses_back() {
for (i, (tag, name)) in STORED.iter().enumerate() {
assert_eq!(tag.as_str(), *name);
assert_eq!(ComponentTag::parse(name), Some(*tag));
assert_eq!(*tag as u8, i as u8, "{name} is not at its list position");
}
}
#[test]
fn a_name_no_component_carries_parses_to_nothing() {
assert_eq!(ComponentTag::parse("NotAComponent"), None);
assert_eq!(ComponentTag::parse(""), None);
}
#[test]
fn every_tag_resolves_a_surviving_tag_and_fits_the_mask() {
for (tag, name) in STORED {
let surviving = tag.surviving_tag();
assert!(
surviving.is_none_or(|s| STORED.iter().any(|(t, _)| *t == s)),
"{name} survives as a tag that is not in the list"
);
}
assert!(
STORED.len() < 128,
"the list outgrew the ComponentMask ceiling"
);
}
#[test]
fn every_resource_name_resolves_to_a_handle_space() {
for name in RESOURCES {
assert!(
ResourceKind::parse(name).is_some(),
"{name} is a resource entry with no handle space"
);
}
assert_eq!(ResourceKind::parse("Transform"), None);
assert_eq!(ResourceKind::parse("NotAResource"), None);
}
#[test]
fn a_loaded_component_reports_the_type_it_holds() {
let asset = ComponentAsset::from(Transform::default());
assert_eq!(asset.type_name(), "Transform");
assert_eq!(asset.tag(), ComponentTag::Transform);
}
#[test]
fn injection_dispatches_to_the_variant_it_holds() {
let mut asset = ComponentAsset::from(Transform::default());
asset.inject_name(AssetId(3));
asset.inject_locator(PayloadLocator {
blob_index: 0,
offset: 0,
len: 0,
});
assert_eq!(asset.tag(), ComponentTag::Transform);
}
#[test]
fn validation_runs_only_where_an_entry_declares_a_clamp() {
let plain = ComponentAsset::from(Transform::default()).validated();
assert_eq!(plain.tag(), ComponentTag::Transform);
let clamped = ComponentAsset::from(Prop::default()).validated();
assert_eq!(clamped.tag(), ComponentTag::Prop);
}
fn baked(discriminant: u8, args_bytes: Vec<u8>, name: Option<AssetId>) -> BlobAssetDef {
BlobAssetDef {
name,
kind: AssetKind::Component,
discriminant,
args_bytes,
payload: None,
}
}
#[test]
fn a_baked_record_loads_as_the_component_its_discriminant_names() {
let prop = Prop {
position: [1.0, 2.0, 3.0],
..Prop::default()
};
let bytes = postcard::to_allocvec(&prop).expect("a prop encodes");
let asset =
ComponentAsset::from_baked(&baked(ComponentTag::Prop as u8, bytes, Some(AssetId(7))))
.expect("the record loads");
let ComponentAsset::Prop(loaded) = asset else {
panic!("expected a prop");
};
assert_eq!(loaded.position, [1.0, 2.0, 3.0]);
assert_eq!(loaded.asset_id, AssetId(7));
}
#[test]
fn a_record_no_tag_claims_is_rejected() {
assert_eq!(
ComponentAsset::from_baked(&baked(u8::MAX, Vec::new(), None)).err(),
Some(CnResult::AssetInvalidType)
);
}
#[test]
fn the_value_enum_pushes_replaces_and_counts_through_its_column() {
let mut storage = ComponentStorage::default();
let entity = storage.push(ComponentAsset::from(Transform::default()));
assert_eq!(
storage.entities_with_tag(ComponentTag::Transform as u8),
&[entity]
);
assert_eq!(
storage.component_census(),
alloc::vec![(ComponentTag::Transform as u8, 1)]
);
let moved = Transform {
position: [4.0, 5.0, 6.0],
..Transform::default()
};
assert!(storage.replace(entity, ComponentAsset::from(moved)));
assert_eq!(
storage.get::<Transform>(entity).map(|t| t.position),
Some([4.0, 5.0, 6.0])
);
assert!(!storage.replace(entity, ComponentAsset::from(Prop::default())));
assert!(storage.entities_with_tag(u8::MAX).is_empty());
}
}