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"), ("parent", "SkyRotation")], 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 SkyRotation => $crate::components::SkyRotation { gen, external, singleton, id, validate: sky_rotation },
147 },
148
149 resource: {
154 AudioClip => $crate::components::AudioClip { resource: AudioClip, compiled },
155 Texture => $crate::components::Texture { resource: Texture, compiled },
156 CubemapTexture => $crate::components::CubemapTexture { resource: CubemapTexture, compiled },
157 EnvironmentMap => $crate::components::EnvironmentMap { resource: EnvironmentMap, compiled, renders },
158 ColorLut => $crate::components::ColorLut { resource: ColorLut, compiled },
159 Font => $crate::components::Font { resource: Font, compiled, useful_blank },
160 Material => $crate::components::Material { resource: Material, data, useful_blank, refs: [("albedo", "Texture"), ("normal_map", "Texture"), ("emissive_map", "Texture"), ("orm_map", "Texture"), ("shader", "Shader")] },
161 Mesh => $crate::components::Mesh { resource: Mesh, compiled },
162 SkinnedMesh => $crate::components::SkinnedMesh { resource: SkinnedMesh, compiled, renders },
163 },
164 }
165 };
166}
167
168crate::for_each_component!(define_components);
172
173macro_rules! cn_impl_components {
222 (
225 stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
226 resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? } $(,)?
227 ) => {
228 $( cn_impl_components!(@one $variant $ty { $($meta)* }); )+
229 };
230
231 (@one $variant:ident $ty:path { manual $($rest:tt)* }) => {};
233
234 (@one $variant:ident $ty:path { gen $($flags:tt)* }) => {
238 cn_impl_components!(@munch $variant $ty [] $($flags)*);
239 };
240
241 (@one $variant:ident $ty:path { runtime $($rest:tt)* }) => {
244 impl $crate::ecs::Component for $ty {
245 const NAME: &'static str = stringify!($variant);
246 }
247 };
248
249 (@munch $variant:ident $ty:path [$($body:tt)*] , compiled $($rest:tt)*) => {
250 cn_impl_components!(@munch $variant $ty
251 [$($body)*
252 fn inject_locator(&mut self, locator: $crate::ecs::PayloadLocator) {
253 self.locator = Some(locator);
254 }]
255 $($rest)*);
256 };
257 (@munch $variant:ident $ty:path [$($body:tt)*] , id $($rest:tt)*) => {
258 cn_impl_components!(@munch $variant $ty
259 [$($body)* fn inject_name(&mut self, id: $crate::ecs::asset_id::AssetId) {
260 self.asset_id = id;
261 }]
262 $($rest)*);
263 };
264 (@munch $variant:ident $ty:path [$($body:tt)*] , validate: $f:ident $($rest:tt)*) => {
266 cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
267 };
268 (@munch $variant:ident $ty:path [$($body:tt)*] , refs: [ $( ($fld:literal, $tgt:literal) ),+ $(,)? ] $($rest:tt)*) => {
269 cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
270 };
271 (@munch $variant:ident $ty:path [$($body:tt)*] , consumed: $surviving:ident $($rest:tt)*) => {
272 cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
273 };
274 (@munch $variant:ident $ty:path [$($body:tt)*] , $flag:ident $($rest:tt)*) => {
275 cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
276 };
277
278 (@munch $variant:ident $ty:path [$($body:tt)*]) => {
281 impl $crate::ecs::Component for $ty {
282 const NAME: &'static str = stringify!($variant);
283 $($body)*
284 fn from_baked(bytes: &[u8]) -> Result<Self, $crate::result::CnResult> {
285 Ok($crate::blob::decode_exact(bytes)?)
286 }
287 }
288 };
289}
290
291crate::for_each_component!(cn_impl_components);
297
298#[cfg(test)]
299mod tests {
300 use crate::blob::BlobAssetDef;
301 use crate::components::{Prop, Transform};
302 use crate::ecs::asset_id::AssetId;
303 use crate::ecs::{
304 AssetKind, ComponentAsset, ComponentStorage, ComponentTag, PayloadLocator, ResourceKind,
305 };
306 use crate::result::CnResult;
307 use alloc::vec::Vec;
308
309 macro_rules! registry_names {
313 (
314 stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
315 resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? } $(,)?
316 ) => {
317 const STORED: &[(ComponentTag, &str)] =
318 &[$( (ComponentTag::$variant, stringify!($variant)) ),+];
319 const RESOURCES: &[&str] = &[$( stringify!($rvariant) ),+];
320 };
321 }
322 crate::for_each_component!(registry_names);
323
324 #[test]
325 fn an_undrained_component_survives_as_itself() {
326 assert_eq!(
327 ComponentTag::Transform.surviving_tag(),
328 Some(ComponentTag::Transform)
329 );
330 assert_eq!(
331 ComponentTag::Behavior.surviving_tag(),
332 Some(ComponentTag::Behavior)
333 );
334 }
335
336 #[test]
337 fn a_consumed_component_survives_as_nothing() {
338 assert_eq!(ComponentTag::Screen.surviving_tag(), None);
339 assert_eq!(ComponentTag::PropBody.surviving_tag(), None);
340 }
341
342 #[test]
345 fn a_consumed_component_can_name_its_replacement() {
346 assert_eq!(
347 ComponentTag::Prop.surviving_tag(),
348 Some(ComponentTag::PropInstance)
349 );
350 }
351
352 #[test]
356 fn every_tag_names_itself_and_parses_back() {
357 for (i, (tag, name)) in STORED.iter().enumerate() {
358 assert_eq!(tag.as_str(), *name);
359 assert_eq!(ComponentTag::parse(name), Some(*tag));
360 assert_eq!(*tag as u8, i as u8, "{name} is not at its list position");
361 }
362 }
363
364 #[test]
365 fn a_name_no_component_carries_parses_to_nothing() {
366 assert_eq!(ComponentTag::parse("NotAComponent"), None);
367 assert_eq!(ComponentTag::parse(""), None);
368 }
369
370 #[test]
373 fn every_tag_resolves_a_surviving_tag_and_fits_the_mask() {
374 for (tag, name) in STORED {
375 let surviving = tag.surviving_tag();
376 assert!(
377 surviving.is_none_or(|s| STORED.iter().any(|(t, _)| *t == s)),
378 "{name} survives as a tag that is not in the list"
379 );
380 }
381 assert!(
382 STORED.len() < 128,
383 "the list outgrew the ComponentMask ceiling"
384 );
385 }
386
387 #[test]
388 fn every_resource_name_resolves_to_a_handle_space() {
389 for name in RESOURCES {
390 assert!(
391 ResourceKind::parse(name).is_some(),
392 "{name} is a resource entry with no handle space"
393 );
394 }
395 assert_eq!(ResourceKind::parse("Transform"), None);
396 assert_eq!(ResourceKind::parse("NotAResource"), None);
397 }
398
399 #[test]
400 fn a_loaded_component_reports_the_type_it_holds() {
401 let asset = ComponentAsset::from(Transform::default());
402 assert_eq!(asset.type_name(), "Transform");
403 assert_eq!(asset.tag(), ComponentTag::Transform);
404 }
405
406 #[test]
409 fn injection_dispatches_to_the_variant_it_holds() {
410 let mut asset = ComponentAsset::from(Transform::default());
411 asset.inject_name(AssetId(3));
412 asset.inject_locator(PayloadLocator {
413 blob_index: 0,
414 offset: 0,
415 len: 0,
416 });
417 assert_eq!(asset.tag(), ComponentTag::Transform);
418 }
419
420 #[test]
423 fn validation_runs_only_where_an_entry_declares_a_clamp() {
424 use crate::components::SdfVolume;
425
426 let plain = ComponentAsset::from(Transform::default()).validated();
427 assert_eq!(plain.tag(), ComponentTag::Transform);
428
429 let clamped = ComponentAsset::from(Prop::default()).validated();
430 assert_eq!(clamped.tag(), ComponentTag::Prop);
431
432 let volume = SdfVolume {
435 volumetric: true,
436 cast_shadows: true,
437 max_steps: 9999,
438 ..Default::default()
439 };
440 let ComponentAsset::SdfVolume(baked) = ComponentAsset::from(volume).validated() else {
441 panic!("the value keeps its variant through validation");
442 };
443 assert!(!baked.cast_shadows);
444 assert_eq!(
445 baked.max_steps,
446 crate::components::sdf_volume::SDF_MAX_STEPS_CEILING
447 );
448 }
449
450 fn baked(discriminant: u8, args_bytes: Vec<u8>, name: Option<AssetId>) -> BlobAssetDef {
451 BlobAssetDef {
452 name,
453 kind: AssetKind::Component,
454 discriminant,
455 args_bytes,
456 payload: None,
457 }
458 }
459
460 #[test]
463 fn a_baked_record_loads_as_the_component_its_discriminant_names() {
464 let prop = Prop {
465 position: [1.0, 2.0, 3.0],
466 ..Prop::default()
467 };
468 let bytes = postcard::to_allocvec(&prop).expect("a prop encodes");
469 let asset =
470 ComponentAsset::from_baked(&baked(ComponentTag::Prop as u8, bytes, Some(AssetId(7))))
471 .expect("the record loads");
472 let ComponentAsset::Prop(loaded) = asset else {
473 panic!("expected a prop");
474 };
475 assert_eq!(loaded.position, [1.0, 2.0, 3.0]);
476 assert_eq!(loaded.asset_id, AssetId(7));
477 }
478
479 #[test]
480 fn a_record_no_tag_claims_is_rejected() {
481 assert_eq!(
482 ComponentAsset::from_baked(&baked(u8::MAX, Vec::new(), None)).err(),
483 Some(CnResult::AssetInvalidType)
484 );
485 }
486
487 #[test]
491 fn the_value_enum_pushes_replaces_and_counts_through_its_column() {
492 let mut storage = ComponentStorage::default();
493 let entity = storage.push(ComponentAsset::from(Transform::default()));
494 assert_eq!(
495 storage.entities_with_tag(ComponentTag::Transform as u8),
496 &[entity]
497 );
498 assert_eq!(
499 storage.component_census(),
500 alloc::vec![(ComponentTag::Transform as u8, 1)]
501 );
502
503 let moved = Transform {
504 position: [4.0, 5.0, 6.0],
505 ..Transform::default()
506 };
507 assert!(storage.replace(entity, ComponentAsset::from(moved)));
508 assert_eq!(
509 storage.get::<Transform>(entity).map(|t| t.position),
510 Some([4.0, 5.0, 6.0])
511 );
512
513 assert!(!storage.replace(entity, ComponentAsset::from(Prop::default())));
516 assert!(storage.entities_with_tag(u8::MAX).is_empty());
517 }
518}