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