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