Skip to main content

concinnity_core/ecs/
registry.rs

1// src/ecs/registry.rs
2//
3// Single source of truth for the renderer-free half of the engine's asset
4// registry: every Component type paired with its stable u8 discriminant.
5//
6// The list lives in one macro, `for_each_component!`, so both registries built
7// from it stay in lockstep: the runtime value enum + ECS storage
8// (`define_components!`, invoked below in this crate) and the authoring metadata
9// registry (`RegisteredType`, invoked in the build crate from the same list).
10//
11// It arrives in two groups, and which group an entry is in is the whole of what
12// separates a component from a resource:
13//
14//   stored    has a `ComponentTag`, a `ComponentAsset` variant, a column, an
15//             `impl Component`, and an `impl RuntimeComponent`, so a world can
16//             hold one. Split further by the entry's own origin flag into
17//             `external` (declared in a world, survives into a blob) and
18//             `runtime` (only ever minted by a running world).
19//   resource  declared in a world and compiled into the blob's resource stream,
20//             addressed at runtime by a per-kind handle rather than a column.
21//             `impl ResourceAsset`; the entry names its `ResourceKind`. Carries
22//             no origin flag, because being in the group is the origin.
23//
24// A third group -- the types a world declares and the cook expands away before a
25// blob is written -- is the authoring vocabulary, which this crate does not
26// name: its list lives in `concinnity_world::registry::build_only`, and the
27// authoring registry composes the two by passing it through the `$extra` tail
28// below.
29//
30// Components are pure data, registered with one entry each. There is no system
31// registry: every system is internal code, constructed at runtime from world
32// content (see `World::start`), never declared in a world or serialized to a
33// blob. The table that gates and orders the constructed systems is the caller's:
34// this crate's headless table, or the client crate's `ecs::registry`.
35//
36// Each component's discriminant (its on-disk blob tag and in-memory
37// `ComponentId`) is assigned by its position in this list: the runtime
38// `define_components!` builds a `#[repr(u8)] ComponentTag` enum whose variants
39// are these entries in order, so the tag is the list position. Discriminants are
40// therefore not hand-written and not a stable on-disk contract: a build
41// regenerates the blob, so the blob and the engine that loads it always agree.
42// Reordering the list changes every tag, which is only safe alongside a rebuild
43// (`cn build`), which the workflow always does. The tag must stay in 0..128 (the
44// `ComponentMask` ceiling); the list is far shorter, so position keeps it there.
45
46use crate::define_components;
47use crate::ecs::{BlobAssetDef, Component, PayloadLocator};
48use crate::result::CnResult;
49
50/// The one component list. `$cb` is a macro that receives the `Variant => Type`
51/// entries and expands to whatever registry it builds from them. Type paths are
52/// absolute so the list resolves from any crate that consumes it.
53///
54/// The `$cb; $extra` form prepends arbitrary tokens to what `$cb` receives, so a
55/// consumer holding a group this crate does not name (the authoring-only
56/// vocabulary in concinnity-world) can hand its own list to the same callback.
57#[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: every type with a column, a `ComponentTag`, and a
64            // `ComponentAsset` variant. `external` entries are declared in a
65            // world and survive into a blob; `runtime` entries are only ever
66            // minted by a running world.
67            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            },
146
147            // Resource: declared in a world and compiled into the blob's
148            // resource stream, addressed at runtime by a per-kind handle
149            // rather than stored in a column. Each entry names the dense
150            // handle space it is assigned into.
151            resource: {
152                AudioClip => $crate::components::AudioClip { resource: AudioClip, compiled },
153                Texture => $crate::components::Texture { resource: Texture, compiled },
154                CubemapTexture => $crate::components::CubemapTexture { resource: CubemapTexture, compiled },
155                EnvironmentMap => $crate::components::EnvironmentMap { resource: EnvironmentMap, compiled, renders },
156                ColorLut => $crate::components::ColorLut { resource: ColorLut, compiled },
157                Font => $crate::components::Font { resource: Font, compiled, useful_blank },
158                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")] },
159                Mesh => $crate::components::Mesh { resource: Mesh, compiled },
160                SkinnedMesh => $crate::components::SkinnedMesh { resource: SkinnedMesh, compiled, renders },
161            },
162        }
163    };
164}
165
166// The runtime half: the `ComponentTag` enum, the `ComponentAsset` value enum,
167// its blob loader, and the ECS storage. The authoring `RegisteredType` registry
168// is built from the same list in the build crate.
169crate::for_each_component!(define_components);
170
171// Generate the trivial `impl Component` blocks from the shared component list.
172//
173// The runtime trait is small: a NAME, a `from_baked` blob loader, and the
174// optional identity / payload injection hooks. Most components are pure data
175// whose impl is mechanical, generated here from each list entry's compact
176// `{ ... }` metadata block. Entries whose impl is bespoke mark themselves
177// `manual` and keep their impl; their trailing flags (origin, args type, refs)
178// are authoring metadata consumed only by the build-side registry in
179// concinnity-world.
180//
181// Metadata grammar (inside the braces):
182//   manual, <flags...>          -- skip; the impl is hand-written elsewhere
183//   gen, <flags...>             -- generated impl:
184//     external | runtime        -- the authoring origin (world-side only)
185//     compiled                  -- an `inject_locator` that stores into
186//                                  `self.locator` (and marks the payload
187//                                  world-side)
188//     id                        -- an `inject_name` that stores into
189//                                  `self.asset_id`
190//     singleton                 -- at most one instance belongs to a world
191//                                  (world-side only)
192//     useful_blank              -- meaningful when declared with only default
193//                                  args, so authoring tools offer a plain add
194//                                  (world-side only)
195//     renders                   -- presence implies the world renders; drives
196//                                  the GraphicsConfig companion injection at
197//                                  build time (world-side only)
198//     live                      -- the running world re-reads this column
199//                                  every frame, so overwriting a component in
200//                                  place takes effect without reloading the
201//                                  world. Carries a second obligation: no
202//                                  build-time expansion may read the type's
203//                                  args, because an in-place write skips the
204//                                  expansion entirely (world-side only)
205//     consumed [: <Type>]       -- a load-time pass drains this column during
206//                                  `World::start`, so it holds nothing from
207//                                  the first tick; `: <Type>` names the
208//                                  runtime component that survives in its
209//                                  place (world-side only)
210//     validate: <fn>            -- the bake-time validator (world-side only)
211//     refs: [ ("field", "Type"), ... ] -- the reference fields (world-side only)
212//     args: <Asset>             -- names the asset whose authored schema
213//                                  differs from the component it bakes into;
214//                                  the schema is that asset's `cook` form
215//                                  (world-side only)
216//   runtime [, build: <fn>]     -- RuntimeOnly: never authored, never in a
217//                                  blob; the impl is NAME + the default
218//                                  (rejecting) `from_baked`.
219macro_rules! cn_impl_components {
220    // Entry point: one impl per stored entry. The resource group is skipped
221    // whole -- a resource is never loaded from a component record.
222    (
223        stored: { $( $variant:ident => $ty:path { $($meta:tt)* } ),+ $(,)? },
224        resource: { $( $rvariant:ident => $rty:path { $($rmeta:tt)* } ),+ $(,)? } $(,)?
225    ) => {
226        $( cn_impl_components!(@one $variant $ty { $($meta)* }); )+
227    };
228
229    // Bespoke impls opt out here; trailing flags are world-side metadata.
230    (@one $variant:ident $ty:path { manual $($rest:tt)* }) => {};
231
232    // Generated impls: seed an empty method accumulator, then consume the flag
233    // list one token at a time. Only `compiled` and `id` contribute runtime
234    // code; the authoring flags are consumed (and used) by the world registry.
235    (@one $variant:ident $ty:path { gen $($flags:tt)* }) => {
236        cn_impl_components!(@munch $variant $ty [] $($flags)*);
237    };
238
239    // RuntimeOnly components: never authored in a world and never stored in a
240    // blob, so the default (rejecting) `from_baked` is correct.
241    (@one $variant:ident $ty:path { runtime $($rest:tt)* }) => {
242        impl $crate::ecs::Component for $ty {
243            const NAME: &'static str = stringify!($variant);
244        }
245    };
246
247    (@munch $variant:ident $ty:path [$($body:tt)*] , compiled $($rest:tt)*) => {
248        cn_impl_components!(@munch $variant $ty
249            [$($body)*
250             fn inject_locator(&mut self, locator: $crate::ecs::PayloadLocator) {
251                 self.locator = Some(locator);
252             }]
253            $($rest)*);
254    };
255    (@munch $variant:ident $ty:path [$($body:tt)*] , id $($rest:tt)*) => {
256        cn_impl_components!(@munch $variant $ty
257            [$($body)* fn inject_name(&mut self, id: $crate::ecs::asset_id::AssetId) {
258                 self.asset_id = id;
259             }]
260            $($rest)*);
261    };
262    // Authoring-only flags: consumed here, used by the world registry.
263    (@munch $variant:ident $ty:path [$($body:tt)*] , validate: $f:ident $($rest:tt)*) => {
264        cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
265    };
266    (@munch $variant:ident $ty:path [$($body:tt)*] , refs: [ $( ($fld:literal, $tgt:literal) ),+ $(,)? ] $($rest:tt)*) => {
267        cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
268    };
269    (@munch $variant:ident $ty:path [$($body:tt)*] , consumed: $surviving:ident $($rest:tt)*) => {
270        cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
271    };
272    (@munch $variant:ident $ty:path [$($body:tt)*] , $flag:ident $($rest:tt)*) => {
273        cn_impl_components!(@munch $variant $ty [$($body)*] $($rest)*);
274    };
275
276    // No flags left: emit the impl. The baked blob record carries the
277    // serialized component itself.
278    (@munch $variant:ident $ty:path [$($body:tt)*]) => {
279        impl $crate::ecs::Component for $ty {
280            const NAME: &'static str = stringify!($variant);
281            $($body)*
282            fn from_baked(bytes: &[u8]) -> Result<Self, $crate::result::CnResult> {
283                Ok($crate::blob::decode_exact(bytes)?)
284            }
285        }
286    };
287}
288
289// The generated trivial `impl Component` blocks: one per list entry marked
290// `gen`, expanded from its metadata. Entries marked `manual` keep the
291// hand-written impl in their own `components` module. Emitted here (rather than
292// in `components`) so the macro is in textual scope, alongside
293// `define_components`.
294crate::for_each_component!(cn_impl_components);
295
296#[cfg(test)]
297mod tests {
298    use crate::ecs::ComponentTag;
299
300    #[test]
301    fn an_undrained_component_survives_as_itself() {
302        assert_eq!(
303            ComponentTag::Transform.surviving_tag(),
304            Some(ComponentTag::Transform)
305        );
306        assert_eq!(
307            ComponentTag::Behavior.surviving_tag(),
308            Some(ComponentTag::Behavior)
309        );
310    }
311
312    #[test]
313    fn a_consumed_component_survives_as_nothing() {
314        assert_eq!(ComponentTag::Screen.surviving_tag(), None);
315        assert_eq!(ComponentTag::PropBody.surviving_tag(), None);
316    }
317
318    // The one entry whose `consumed` flag names a replacement: the flag is what
319    // keeps "Prop" a usable behavior scope after decomposition drains it.
320    #[test]
321    fn a_consumed_component_can_name_its_replacement() {
322        assert_eq!(
323            ComponentTag::Prop.surviving_tag(),
324            Some(ComponentTag::PropInstance)
325        );
326    }
327}