concinnity_core/ecs/define_components.rs
1//! The macros that build the runtime half of the component registry.
2//!
3//! `define_components!` is invoked in [`crate::ecs::registry`] over the shared
4//! `for_each_component!` list: it emits the `ComponentTag` discriminants, the
5//! `ComponentAsset` value enum (via the `__define_asset_kind!` helper), the blob
6//! loader, and the `ComponentStorage` / `ComponentSlot` pair, whose storage half
7//! comes from [`define_component_storage!`](crate::define_component_storage).
8//!
9//! The authoring `RegisteredType` registry is built from the same list in
10//! concinnity-cook. Systems are registered separately, client-side, by the
11//! `define_systems!` table.
12
13// Internal helper. Resolves an entry's `consumed` flag into the tag that still
14// holds its entities once the world has started: the entry's own tag when no
15// load-time pass drains it, the `consumed: <Type>` substitute when one does but
16// leaves a runtime marker behind, and `None` when nothing survives.
17#[macro_export]
18#[doc(hidden)]
19macro_rules! __cn_surviving_tag {
20 ($variant:ident;) => { Some($crate::ecs::ComponentTag::$variant) };
21 ($variant:ident; consumed: $surviving:ident $($rest:tt)*) => {
22 Some($crate::ecs::ComponentTag::$surviving)
23 };
24 ($variant:ident; consumed $($rest:tt)*) => { None };
25 ($variant:ident; $skip:tt $($rest:tt)*) => { $crate::__cn_surviving_tag!($variant; $($rest)*) };
26}
27
28// Internal helper. Applies an entry's `validate: <fn>` clamp to a value of the
29// component's own type, or leaves it alone when the entry declares none. A
30// `validate_for: <fn>` entry names a clamp whose result depends on the shader
31// platform the world is cooked for, so it takes that platform alongside the
32// value. The clamps live in `crate::components::validate`, the same ones the
33// authored JSON path runs, so a typed build and a cooked build land on the
34// same component.
35#[macro_export]
36#[doc(hidden)]
37macro_rules! __cn_validate {
38 ($val:expr, $p:expr;) => { $val };
39 ($val:expr, $p:expr; validate: $f:ident $($r:tt)*) => {
40 $crate::components::validate::$f($val)
41 };
42 ($val:expr, $p:expr; validate_for: $f:ident $($r:tt)*) => {
43 $crate::components::validate::$f($val, $p)
44 };
45 ($val:expr, $p:expr; $t:tt $($r:tt)*) => { $crate::__cn_validate!($val, $p; $($r)*) };
46}
47
48// Internal helper. Resolves a resource entry's `resource: <Kind>` flag into the
49// handle space it is assigned into.
50#[macro_export]
51#[doc(hidden)]
52macro_rules! __cn_resource_kind {
53 (resource: $kind:ident $($r:tt)*) => { $crate::ecs::ResourceKind::$kind };
54 ($t:tt $($r:tt)*) => { $crate::__cn_resource_kind!($($r)*) };
55}
56
57// Internal helper. Emits the runtime `<Kind>Asset` value enum the ECS stores
58// and the `From<$ty>` conversions. The authoring metadata registry (`<Kind>Type`)
59// is emitted separately so the two can live in different crates.
60#[macro_export]
61#[doc(hidden)]
62macro_rules! __define_asset_kind {
63 (
64 asset_enum: $asset_enum:ident,
65 asset_kind: $kind_variant:ident,
66 $( $variant:ident => $ty:path, $disc:expr_2021 ),+ $(,)?
67 ) => {
68 // One variant per component type; each is named for the component it
69 // wraps, so the list itself is the documentation.
70 /// A loaded component of any registered type.
71 #[derive(Debug)]
72 #[expect(missing_docs, reason = "one variant per component type, each named for the component it wraps")]
73 pub enum $asset_enum {
74 $( $variant($ty) ),+
75 }
76
77 $( impl From<$ty> for $asset_enum { fn from(c: $ty) -> Self { $asset_enum::$variant(c) } } )+
78 };
79}
80
81/// Generate the runtime component registry from the engine's component list:
82/// the `ComponentTag` discriminants, the `ComponentAsset` value enum, and the
83/// `ComponentStorage` / `ComponentSlot` pair the ECS stores rows in.
84///
85/// Each list entry carries a `{ ... }` metadata block the authoring registry
86/// consumes; this macro captures and ignores it.
87#[macro_export]
88macro_rules! define_components {
89 // Only the `stored` group gets a tag, an enum variant, and a column; the
90 // `resource` group is named here solely to mark it, since a resource is
91 // reached by handle rather than stored in one. Each entry's `{ ... }`
92 // metadata block is authoring metadata for `cn_impl_components!` and the
93 // world-side registry; this macro captures and ignores it.
94 (
95 stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
96 resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? } $(,)?
97 ) => {
98 /// The component type tag: one fieldless variant per component, in list
99 /// order, so each variant's `#[repr(u8)]` discriminant is its list
100 /// position (0, 1, 2, ...). `ComponentTag::$variant as u8` is that tag,
101 /// used both as the on-disk blob discriminant and as the in-memory ECS
102 /// `ComponentId`. The tag is assigned by position, not hand-written, and
103 /// is not a stable on-disk contract: a build regenerates the blob, so the
104 /// blob and the engine that loads it always agree. The authoring
105 /// `RegisteredType` registry derives the same tag from this enum.
106 // One variant per component type, named for that component.
107 #[repr(u8)]
108 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
109 #[expect(missing_docs, reason = "one variant per component type, named for that component")]
110 pub enum ComponentTag {
111 $( $variant ),+
112 }
113
114 impl ComponentTag {
115 /// The registry name of this tag, as a world authors it.
116 pub fn as_str(self) -> &'static str {
117 match self {
118 $( ComponentTag::$variant => stringify!($variant) ),+
119 }
120 }
121
122
123 /// The tag that still holds this component's entities once the
124 /// world has started, or `None` if nothing does.
125 ///
126 /// A load-time pass drains some columns during `World::start`, so
127 /// they match nothing from the first tick onward. Where such a pass
128 /// leaves a runtime marker behind, this returns that marker's tag:
129 /// `Prop` resolves to `PropInstance`, which decomposition puts on
130 /// every prop's entity. Anything not drained resolves to itself.
131 pub fn surviving_tag(self) -> Option<ComponentTag> {
132 match self {
133 $( ComponentTag::$variant => $crate::__cn_surviving_tag!($variant; $($meta)*) ),+
134 }
135 }
136
137 /// The tag a component name denotes. Resolves the component names a
138 /// Behavior declares in its `scope` and `queries`.
139 pub fn parse(name: &str) -> Option<ComponentTag> {
140 $(
141 if name == stringify!($variant) {
142 return Some(ComponentTag::$variant);
143 }
144 )+
145 None
146 }
147 }
148
149 $crate::__define_asset_kind! {
150 asset_enum: ComponentAsset,
151 asset_kind: Component,
152 $( $variant => $ty, ComponentTag::$variant as u8 ),+
153 }
154
155 impl ComponentAsset {
156 /// Reconstruct a component from a blob def: dispatch on the tag and
157 /// deserialize the runtime component via `Component::from_baked`
158 /// (every record is baked -- cook already ran the asset -> component
159 /// translation).
160 pub fn from_baked(def: &BlobAssetDef) -> Result<Self, CnResult> {
161 $(
162 if def.discriminant == ComponentTag::$variant as u8 {
163 let mut c = <$ty as Component>::from_baked(&def.args_bytes)?;
164 if let Some(id) = def.name {
165 <$ty as Component>::inject_name(&mut c, id);
166 }
167 return Ok(ComponentAsset::$variant(c));
168 }
169 )+
170 Err(CnResult::AssetInvalidType)
171 }
172
173 /// Inject a payload locator into the component after construction.
174 /// Delegates to `Component::inject_locator`; a no-op for types
175 /// that don't override that method.
176 pub fn inject_locator(&mut self, locator: PayloadLocator) {
177 match self {
178 $( ComponentAsset::$variant(c) => c.inject_locator(locator) ),+
179 }
180 }
181
182 /// Inject the asset's declared identity after construction.
183 /// Delegates to `Component::inject_name`; a no-op for types that
184 /// never look themselves up by id.
185 pub fn inject_name(&mut self, id: $crate::ecs::asset_id::AssetId) {
186 match self {
187 $( ComponentAsset::$variant(c) => c.inject_name(id) ),+
188 }
189 }
190
191 /// Apply the component's registered validation clamps, the ones a
192 /// cooked world's args pass through on their way to the blob.
193 /// `platform` is the shader platform the world is cooked for; a
194 /// clamp that resolves a per-backend source reads it. A type
195 /// declaring no clamp is returned unchanged.
196 pub fn validated(self, platform: $crate::platform::Platform) -> Self {
197 match self {
198 $(
199 ComponentAsset::$variant(c) => ComponentAsset::$variant(
200 $crate::__cn_validate!(c, platform; $($meta)*)
201 ),
202 )+
203 }
204 }
205
206 /// The registry name of the component this value holds.
207 pub fn type_name(&self) -> &'static str {
208 match self {
209 $( ComponentAsset::$variant(_) => stringify!($variant) ),+
210 }
211 }
212
213 /// The tag of the component this value holds.
214 pub fn tag(&self) -> ComponentTag {
215 match self {
216 $( ComponentAsset::$variant(_) => ComponentTag::$variant ),+
217 }
218 }
219 }
220
221 impl $crate::ecs::ResourceKind {
222 /// The handle space an asset type name compiles into, or `None`
223 /// when the name is not a resource asset. The names are the
224 /// registry's own, so `parse` and the authoring registry agree on
225 /// what counts as a resource.
226 pub fn parse(name: &str) -> Option<$crate::ecs::ResourceKind> {
227 $(
228 if name == stringify!($rvariant) {
229 return Some($crate::__cn_resource_kind!($($rmeta)*));
230 }
231 )+
232 None
233 }
234 }
235
236 // Per-type runtime storage. The `Column`-backed storage struct, the
237 // `ComponentSlot` access trait, and the generic storage operations
238 // (typed push, drain, mutable access, counts) are generated by
239 // `define_component_storage!` -- shared and engine-agnostic. The
240 // asset-enum dispatch (`push`, `all_defs`) is engine-specific and
241 // added in the impl below.
242 $crate::define_component_storage! {
243 storage: ComponentStorage,
244 slot: ComponentSlot,
245 $( $variant => $ty, ComponentTag::$variant as u8 ),+
246 }
247
248 impl ComponentStorage {
249 /// Dispatch a `ComponentAsset` variant into its typed column via the
250 /// generic typed push (which mints the Entity and stamps the tick).
251 /// Returns the minted Entity so loaders can index it by name.
252 pub fn push(&mut self, asset: ComponentAsset) -> $crate::ecs::Entity {
253 match asset {
254 $( ComponentAsset::$variant(c) => self.push_typed(c), )+
255 }
256 }
257
258 /// Overwrite the component the asset's variant addresses on
259 /// `entity`, keeping the entity and its other components, and
260 /// stamping the change tick so the frame's readers see it.
261 /// `false` when the entity holds no component of that type.
262 pub fn replace(&mut self, entity: $crate::ecs::Entity, asset: ComponentAsset) -> bool {
263 match asset {
264 $(
265 ComponentAsset::$variant(c) => match self.get_mut::<$ty>(entity) {
266 Some(slot) => {
267 *slot = c;
268 true
269 }
270 None => false,
271 },
272 )+
273 }
274 }
275
276 /// Every entity carrying the component with this tag, in column
277 /// order. Serves the declared-query resolution in BehaviorSystem,
278 /// which selects components by authored name rather than by type.
279 pub fn entities_with_tag(&self, tag: u8) -> &[$crate::ecs::Entity] {
280 $(
281 if tag == ComponentTag::$variant as u8 {
282 return self.$variant.entities();
283 }
284 )+
285 &[]
286 }
287
288 /// How many components of each type are stored: one `(tag, count)`
289 /// entry per populated type, in tag order. The debug WS snapshot
290 /// reports these; nothing re-serializes stored components back to
291 /// defs. Counted rather than listed per instance, so the snapshot
292 /// is sized by the number of component types rather than by the
293 /// world.
294 pub fn component_census(&self) -> ::alloc::vec::Vec<(u8, u32)> {
295 let mut out = ::alloc::vec::Vec::new();
296 $(
297 let count = self.$variant.len();
298 if count > 0 {
299 out.push((ComponentTag::$variant as u8, count as u32));
300 }
301 )+
302 out
303 }
304 }
305
306 // Which group an entry is in decides whether a world can hold it.
307 $( impl $crate::ecs::RuntimeComponent for $ty {} )+
308 $( impl $crate::ecs::ResourceAsset for $rty {} )+
309 };
310}