1use crate::define_components;
47use crate::ecs::{BlobAssetDef, Component, PayloadLocator};
48use crate::result::CnResult;
49
50#[macro_export]
58macro_rules! for_each_component {
59 ($cb:ident) => { $crate::for_each_component!($cb;); };
60 ($cb:ident; $($extra:tt)*) => {
61 $cb! {
62 $($extra)*
63 stored: {
68 Window => $crate::components::Window { gen, external, singleton, consumed },
69 GraphicsConfig => $crate::components::GraphicsConfig { gen, external, singleton, renders, consumed },
70 Shader => $crate::components::Shader { manual, external, compiled, consumed },
71 Camera3D => $crate::components::Camera3D { manual, external, useful_blank, live, args: Camera3D },
72 FrameInput => $crate::components::FrameInput { gen, runtime },
73 Prop => $crate::components::Prop { gen, external, id, renders, validate: prop, refs: [("model", "Model"), ("material", "Material"), ("texture", "Texture"), ("scene", "Scene"), ("parent", "Prop")], consumed: PropInstance },
74 RigidBody => $crate::components::RigidBody { gen, external, validate: rigid_body },
75 PropBody => $crate::components::PropBody { gen, external, consumed },
76 Room => $crate::components::Room { manual, external, compiled, useful_blank, args: Room, refs: [("texture", "Texture"), ("wall_texture", "Texture"), ("floor_texture", "Texture"), ("ceiling_texture", "Texture")], consumed },
77 DirectionalLight => $crate::components::DirectionalLight { gen, external, useful_blank, validate: directional_light },
78 PointLight => $crate::components::PointLight { gen, external, useful_blank, validate: point_light },
79 SpotLight => $crate::components::SpotLight { gen, external, useful_blank, validate: spot_light },
80 RectAreaLight => $crate::components::RectAreaLight { gen, external, useful_blank, validate: rect_area_light },
81 ProceduralMesh => $crate::components::ProceduralMesh { gen, external, compiled, id },
82 Model => $crate::components::Model { gen, external, id, consumed },
83 Scene => $crate::components::Scene { gen, external, id, refs: [("camera_shot", "Camera3D")], consumed },
84 TextLabel => $crate::components::TextLabel { gen, external, id, useful_blank, renders, live, refs: [("font", "Font"), ("screen", "Screen")] },
85 HitRegion => $crate::components::HitRegion { gen, external, useful_blank, refs: [("label", "TextLabel"), ("screen", "Screen")], consumed },
86 File => $crate::components::File { manual, external, compiled, args: File, consumed },
87 BlockType => $crate::components::BlockType { gen, external, id, useful_blank, consumed },
88 VoxelChunk => $crate::components::VoxelChunk { gen, external, compiled, id, validate: voxel_chunk, consumed },
89 InstancedProp => $crate::components::InstancedProp { gen, external, id, renders, validate: instanced_prop, refs: [("material", "Material"), ("texture", "Texture")], consumed },
90 PostProcessConfig => $crate::components::PostProcessConfig { manual, external, singleton, consumed },
91 Animation => $crate::components::Animation { gen, external, id, consumed },
92 SkeletonPose => $crate::components::SkeletonPose { runtime, build: skeleton_pose },
93 StreamingConfig => $crate::components::StreamingConfig { gen, external, singleton, consumed },
94 VoxelWorld => $crate::components::VoxelWorld { gen, external, renders, refs: [("material", "Material")], consumed },
95 AudioEmitter => $crate::components::AudioEmitter { gen, external, useful_blank, refs: [("clip", "AudioClip"), ("prop", "Prop")] },
96 Sprite => $crate::components::Sprite { gen, external, id, useful_blank, renders, live, refs: [("texture", "Texture"), ("screen", "Screen")] },
97 KeyBinding => $crate::components::KeyBinding { gen, external, useful_blank, refs: [("screen", "Screen")], consumed },
98 Screen => $crate::components::Screen { gen, external, id, useful_blank, refs: [("focus", "TextInput")], consumed },
99 Decal => $crate::components::Decal { gen, external, id, useful_blank, validate: decal, refs: [("texture", "Texture")], consumed },
100 VolumetricFog => $crate::components::VolumetricFog { gen, external, useful_blank, validate: volumetric_fog, consumed },
101 PhysicsJoint => $crate::components::PhysicsJoint { gen, external, id, validate: joint, refs: [("body_a", "Prop"), ("body_b", "Prop")], consumed },
102 ParticleEmitter => $crate::components::ParticleEmitter { gen, external, id, useful_blank, validate: particle_emitter, refs: [("texture", "Texture")], consumed },
103 WaterSurface => $crate::components::WaterSurface { gen, external, id, useful_blank, renders, validate: water_surface, consumed },
104 SdfVolume => $crate::components::SdfVolume { manual, external, compiled, renders, validate: sdf_volume, consumed },
105 GlassPanel => $crate::components::GlassPanel { gen, external, id, useful_blank, validate: glass_panel, consumed },
106 LayoutContainer => $crate::components::LayoutContainer { gen, external, renders, live },
107 PhysicsConfig => $crate::components::PhysicsConfig { gen, external, singleton },
108 FpsCounter => $crate::components::FpsCounter { gen, external, useful_blank, refs: [("label", "TextLabel")] },
109 StatHud => $crate::components::StatHud { gen, external, renders, refs: [("fps_label", "TextLabel"), ("vram_label", "TextLabel"), ("ram_label", "TextLabel"), ("ev_label", "TextLabel"), ("edr_label", "TextLabel")] },
110 ScrollPanel => $crate::components::ScrollPanel { gen, external, refs: [("screen", "Screen")], consumed },
111 ReflectionProbe => $crate::components::ReflectionProbe { gen, external, useful_blank, validate: reflection_probe },
112 Transform => $crate::components::Transform { runtime },
113 PropInstance => $crate::components::PropInstance { runtime },
114 MeshRenderer => $crate::components::MeshRenderer { runtime },
115 ModelRenderer => $crate::components::ModelRenderer { runtime },
116 Collider => $crate::components::Collider { runtime },
117 BodyDynamics => $crate::components::BodyDynamics { runtime },
118 Interactable => $crate::components::Interactable { runtime },
119 Pickup => $crate::components::Pickup { runtime },
120 Parent => $crate::components::Parent { runtime },
121 Children => $crate::components::Children { runtime },
122 SceneMember => $crate::components::SceneMember { runtime },
123 GlobalTransform => $crate::components::GlobalTransform { runtime },
124 RenderHandle => $crate::components::RenderHandle { runtime },
125 Held => $crate::components::Held { runtime },
126 Lifetime => $crate::components::Lifetime { runtime },
127 Spawner => $crate::components::Spawner { manual, external, args: Spawner },
128 DebugHud => $crate::components::DebugHud { gen, external, renders, refs: [("passes_label", "TextLabel"), ("mouse_label", "TextLabel"), ("camera_label", "TextLabel"), ("sys_label", "TextLabel")] },
129 AudioCue => $crate::components::AudioCue { gen, external, useful_blank, refs: [("clip", "AudioClip"), ("screen", "Screen")] },
130 Story => $crate::components::Story { gen, external, id },
131 AppConfig => $crate::components::AppConfig { manual, external, singleton, args: AppConfig },
132 AnimationGraph => $crate::components::AnimationGraph { gen, external, id, consumed },
133 AnimationParams => $crate::components::AnimationParams { runtime, build: anim_params },
134 CharacterRig => $crate::components::CharacterRig { runtime, build: character_rig },
135 GroundProbes => $crate::components::GroundProbes { runtime },
136 CameraProbe => $crate::components::CameraProbe { runtime },
137 TextInput => $crate::components::TextInput { gen, external, id, useful_blank, renders, live, refs: [("font", "Font"), ("screen", "Screen")] },
138 Behavior => $crate::components::Behavior { gen, external, id, useful_blank, live },
139 Variables => $crate::components::Variables { gen, external, singleton, live },
140 TriggerVolume => $crate::components::TriggerVolume { gen, external, id, useful_blank },
141 Hidden => $crate::components::Hidden { runtime },
142 LoadingOverlay => $crate::components::LoadingOverlay { gen, external, singleton, renders, refs: [("screen", "Screen"), ("backdrop", "Sprite"), ("track", "Sprite"), ("fill", "Sprite"), ("label", "TextLabel")] },
143 AudioOcclusionProbe => $crate::components::AudioOcclusionProbe { runtime },
144 CharacterShape => $crate::components::CharacterShape { gen, external, id, refs: [("target", "SkinnedMesh")] },
145 EngineDefaults => $crate::components::EngineDefaults { gen, external, singleton, consumed },
146 },
147
148 resource: {
153 AudioClip => $crate::components::AudioClip { resource: AudioClip, compiled },
154 Texture => $crate::components::Texture { resource: Texture, compiled },
155 CubemapTexture => $crate::components::CubemapTexture { resource: CubemapTexture, compiled },
156 EnvironmentMap => $crate::components::EnvironmentMap { resource: EnvironmentMap, compiled, renders },
157 ColorLut => $crate::components::ColorLut { resource: ColorLut, compiled },
158 Font => $crate::components::Font { resource: Font, compiled, useful_blank },
159 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")] },
160 Mesh => $crate::components::Mesh { resource: Mesh, compiled },
161 SkinnedMesh => $crate::components::SkinnedMesh { resource: SkinnedMesh, compiled, renders },
162 },
163 }
164 };
165}
166
167crate::for_each_component!(define_components);
171
172macro_rules! cn_impl_components {
221 (
224 stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
225 resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? } $(,)?
226 ) => {
227 $( cn_impl_components!(@one $variant $ty { $($meta)* }); )+
228 };
229
230 (@one $variant:ident $ty:path { manual $($rest:tt)* }) => {};
232
233 (@one $variant:ident $ty:path { gen $($flags:tt)* }) => {
237 cn_impl_components!(@munch $variant $ty [] $($flags)*);
238 };
239
240 (@one $variant:ident $ty:path { runtime $($rest:tt)* }) => {
243 impl $crate::ecs::Component for $ty {
244 const NAME: &'static str = stringify!($variant);
245 }
246 };
247
248 (@munch $variant:ident $ty:path [$($body:tt)*] , compiled $($rest:tt)*) => {
249 cn_impl_components!(@munch $variant $ty
250 [$($body)*
251 fn inject_locator(&mut self, locator: $crate::ecs::PayloadLocator) {
252 self.locator = Some(locator);
253 }]
254 $($rest)*);
255 };
256 (@munch $variant:ident $ty:path [$($body:tt)*] , id $($rest:tt)*) => {
257 cn_impl_components!(@munch $variant $ty
258 [$($body)* fn inject_name(&mut self, id: $crate::ecs::asset_id::AssetId) {
259 self.asset_id = id;
260 }]
261 $($rest)*);
262 };
263 (@munch $variant:ident $ty:path [$($body:tt)*] , validate: $f:ident $($rest:tt)*) => {
265 cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
266 };
267 (@munch $variant:ident $ty:path [$($body:tt)*] , refs: [ $( ($fld:literal, $tgt:literal) ),+ $(,)? ] $($rest:tt)*) => {
268 cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
269 };
270 (@munch $variant:ident $ty:path [$($body:tt)*] , consumed: $surviving:ident $($rest:tt)*) => {
271 cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
272 };
273 (@munch $variant:ident $ty:path [$($body:tt)*] , $flag:ident $($rest:tt)*) => {
274 cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
275 };
276
277 (@munch $variant:ident $ty:path [$($body:tt)*]) => {
280 impl $crate::ecs::Component for $ty {
281 const NAME: &'static str = stringify!($variant);
282 $($body)*
283 fn from_baked(bytes: &[u8]) -> Result<Self, $crate::result::CnResult> {
284 Ok($crate::blob::decode_exact(bytes)?)
285 }
286 }
287 };
288}
289
290crate::for_each_component!(cn_impl_components);
296
297#[cfg(test)]
298mod tests {
299 use crate::blob::BlobAssetDef;
300 use crate::components::{Prop, Transform};
301 use crate::ecs::asset_id::AssetId;
302 use crate::ecs::{
303 AssetKind, ComponentAsset, ComponentStorage, ComponentTag, PayloadLocator, ResourceKind,
304 };
305 use crate::result::CnResult;
306 use alloc::vec::Vec;
307
308 macro_rules! registry_names {
312 (
313 stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
314 resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? } $(,)?
315 ) => {
316 const STORED: &[(ComponentTag, &str)] =
317 &[$( (ComponentTag::$variant, stringify!($variant)) ),+];
318 const RESOURCES: &[&str] = &[$( stringify!($rvariant) ),+];
319 };
320 }
321 crate::for_each_component!(registry_names);
322
323 #[test]
324 fn an_undrained_component_survives_as_itself() {
325 assert_eq!(
326 ComponentTag::Transform.surviving_tag(),
327 Some(ComponentTag::Transform)
328 );
329 assert_eq!(
330 ComponentTag::Behavior.surviving_tag(),
331 Some(ComponentTag::Behavior)
332 );
333 }
334
335 #[test]
336 fn a_consumed_component_survives_as_nothing() {
337 assert_eq!(ComponentTag::Screen.surviving_tag(), None);
338 assert_eq!(ComponentTag::PropBody.surviving_tag(), None);
339 }
340
341 #[test]
344 fn a_consumed_component_can_name_its_replacement() {
345 assert_eq!(
346 ComponentTag::Prop.surviving_tag(),
347 Some(ComponentTag::PropInstance)
348 );
349 }
350
351 #[test]
355 fn every_tag_names_itself_and_parses_back() {
356 for (i, (tag, name)) in STORED.iter().enumerate() {
357 assert_eq!(tag.as_str(), *name);
358 assert_eq!(ComponentTag::parse(name), Some(*tag));
359 assert_eq!(*tag as u8, i as u8, "{name} is not at its list position");
360 }
361 }
362
363 #[test]
364 fn a_name_no_component_carries_parses_to_nothing() {
365 assert_eq!(ComponentTag::parse("NotAComponent"), None);
366 assert_eq!(ComponentTag::parse(""), None);
367 }
368
369 #[test]
372 fn every_tag_resolves_a_surviving_tag_and_fits_the_mask() {
373 for (tag, name) in STORED {
374 let surviving = tag.surviving_tag();
375 assert!(
376 surviving.is_none_or(|s| STORED.iter().any(|(t, _)| *t == s)),
377 "{name} survives as a tag that is not in the list"
378 );
379 }
380 assert!(
381 STORED.len() < 128,
382 "the list outgrew the ComponentMask ceiling"
383 );
384 }
385
386 #[test]
387 fn every_resource_name_resolves_to_a_handle_space() {
388 for name in RESOURCES {
389 assert!(
390 ResourceKind::parse(name).is_some(),
391 "{name} is a resource entry with no handle space"
392 );
393 }
394 assert_eq!(ResourceKind::parse("Transform"), None);
395 assert_eq!(ResourceKind::parse("NotAResource"), None);
396 }
397
398 #[test]
399 fn a_loaded_component_reports_the_type_it_holds() {
400 let asset = ComponentAsset::from(Transform::default());
401 assert_eq!(asset.type_name(), "Transform");
402 assert_eq!(asset.tag(), ComponentTag::Transform);
403 }
404
405 #[test]
408 fn injection_dispatches_to_the_variant_it_holds() {
409 let mut asset = ComponentAsset::from(Transform::default());
410 asset.inject_name(AssetId(3));
411 asset.inject_locator(PayloadLocator {
412 blob_index: 0,
413 offset: 0,
414 len: 0,
415 });
416 assert_eq!(asset.tag(), ComponentTag::Transform);
417 }
418
419 #[test]
422 fn validation_runs_only_where_an_entry_declares_a_clamp() {
423 let plain = ComponentAsset::from(Transform::default()).validated();
424 assert_eq!(plain.tag(), ComponentTag::Transform);
425
426 let clamped = ComponentAsset::from(Prop::default()).validated();
427 assert_eq!(clamped.tag(), ComponentTag::Prop);
428 }
429
430 fn baked(discriminant: u8, args_bytes: Vec<u8>, name: Option<AssetId>) -> BlobAssetDef {
431 BlobAssetDef {
432 name,
433 kind: AssetKind::Component,
434 discriminant,
435 args_bytes,
436 payload: None,
437 }
438 }
439
440 #[test]
443 fn a_baked_record_loads_as_the_component_its_discriminant_names() {
444 let prop = Prop {
445 position: [1.0, 2.0, 3.0],
446 ..Prop::default()
447 };
448 let bytes = postcard::to_allocvec(&prop).expect("a prop encodes");
449 let asset =
450 ComponentAsset::from_baked(&baked(ComponentTag::Prop as u8, bytes, Some(AssetId(7))))
451 .expect("the record loads");
452 let ComponentAsset::Prop(loaded) = asset else {
453 panic!("expected a prop");
454 };
455 assert_eq!(loaded.position, [1.0, 2.0, 3.0]);
456 assert_eq!(loaded.asset_id, AssetId(7));
457 }
458
459 #[test]
460 fn a_record_no_tag_claims_is_rejected() {
461 assert_eq!(
462 ComponentAsset::from_baked(&baked(u8::MAX, Vec::new(), None)).err(),
463 Some(CnResult::AssetInvalidType)
464 );
465 }
466
467 #[test]
471 fn the_value_enum_pushes_replaces_and_counts_through_its_column() {
472 let mut storage = ComponentStorage::default();
473 let entity = storage.push(ComponentAsset::from(Transform::default()));
474 assert_eq!(
475 storage.entities_with_tag(ComponentTag::Transform as u8),
476 &[entity]
477 );
478 assert_eq!(
479 storage.component_census(),
480 alloc::vec![(ComponentTag::Transform as u8, 1)]
481 );
482
483 let moved = Transform {
484 position: [4.0, 5.0, 6.0],
485 ..Transform::default()
486 };
487 assert!(storage.replace(entity, ComponentAsset::from(moved)));
488 assert_eq!(
489 storage.get::<Transform>(entity).map(|t| t.position),
490 Some([4.0, 5.0, 6.0])
491 );
492
493 assert!(!storage.replace(entity, ComponentAsset::from(Prop::default())));
496 assert!(storage.entities_with_tag(u8::MAX).is_empty());
497 }
498}