Skip to main content

concinnity_core/ecs/
component.rs

1//! The runtime-facing component contract: the `Component` trait a registered
2//! type implements, and the two plain metadata enums the registry and the blob
3//! format are built from.
4//!
5//! All authoring metadata (reference fields, args schema, validators) lives in
6//! the build-side registry in concinnity-cook, derived from the
7//! `for_each_component!` metadata blocks in [`crate::ecs::registry`].
8
9use crate::ecs::asset_id::AssetId;
10use crate::ecs::{ComponentAsset, PayloadLocator};
11use crate::result::CnResult;
12
13/// A component a world can hold after the cook: every type an authored world
14/// declares that survives into a blob, plus every type only the runtime mints.
15///
16/// The bound on [`World::add_component`](crate::ecs::World::add_component).
17/// Exactly the registry's `stored` group, which is also the group with a
18/// `ComponentTag`, a `ComponentAsset` variant, and a column. The groups
19/// partition the registry, so a type carrying this marker carries no other.
20#[diagnostic::on_unimplemented(
21    message = "`{Self}` cannot be added to a world",
22    label = "not a runtime component",
23    note = "`{Self}` is a build-time asset: either the cook expands it into the components it stands for (`BuildOnlyAsset`), or it compiles into the blob's resource stream and is reached by handle (`ResourceAsset`). Either way it never reaches a world as a component. Declare it in a world.jsonl and build."
24)]
25pub trait RuntimeComponent: Into<ComponentAsset> {}
26
27/// An asset compiled into the blob's resource stream rather than stored as a
28/// component.
29///
30/// A world declares one of these, cook compiles its payload and assigns it a
31/// dense per-kind handle, and the runtime keeps it in the resource table owned
32/// by the system that reads it. Exactly the registry's `resource` group. A
33/// marker only, like [`RuntimeComponent`]: it exists so the groups are
34/// checkable at compile time.
35pub trait ResourceAsset {}
36
37/// Where an asset comes from and whether it persists to a blob.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub enum AssetOrigin {
40    /// Authored in a world and persisted to the blob.
41    External,
42    /// Created by the runtime; never persisted.
43    RuntimeOnly,
44    /// Consumed by the build; never reaches the runtime.
45    BuildOnly,
46}
47
48/// Whether the asset has a compiled binary payload packed into a .cnb blob.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50pub enum AssetPayload {
51    /// No compiled payload; the component is its own data.
52    None,
53    /// A compiled binary payload packed into the blob.
54    Compiled,
55}
56
57/// Component -- pure serializable data, no behavior. The runtime-facing surface
58/// only: a component loads from its baked blob bytes and receives its injected
59/// identity/payload hooks. All authoring metadata (origin, payload kind,
60/// reference fields, args schema, validators) lives in the build-side registry
61/// (concinnity-cook), derived from the `for_each_component!` metadata blocks.
62pub trait Component: Sized + Send + core::fmt::Debug + 'static {
63    /// The registry name a world authors this component under.
64    const NAME: &'static str;
65
66    /// Reconstruct this component from a blob record, whose bytes are the
67    /// serialized runtime component (cook already ran the asset -> component
68    /// translation). The default rejects: runtime-only components are never
69    /// stored in a blob, so only loadable types provide an implementation.
70    fn from_baked(_bytes: &[u8]) -> Result<Self, CnResult> {
71        Err(CnResult::AssetInvalidType)
72    }
73
74    /// Called after construction to inject the payload locator from the blob def.
75    /// Only meaningful for components with a compiled payload.
76    /// The default implementation does nothing (correct for most components).
77    fn inject_locator(&mut self, _locator: PayloadLocator) {}
78
79    /// Called after construction to inject the asset's identity from the blob
80    /// def. Only meaningful for components that look themselves up by id at
81    /// runtime. The default implementation does nothing.
82    fn inject_name(&mut self, _id: AssetId) {}
83}
84
85#[cfg(test)]
86mod tests {
87    use super::{ResourceAsset, RuntimeComponent};
88    use crate::components::{TextLabel, Texture, Transform};
89
90    fn runtime<C: RuntimeComponent>() {}
91    fn resource<A: ResourceAsset>() {}
92
93    // One representative per group. The calls are the assertion: each fails to
94    // compile if an entry moves between the registry's groups, and the storage
95    // half is generated from the same grouping, so a type reaching the wrong
96    // marker also loses (or gains) its column. The authoring-only group's
97    // marker lives with its list in concinnity-cook.
98    #[test]
99    fn origin_markers_follow_the_registry() {
100        runtime::<TextLabel>();
101        runtime::<Transform>();
102
103        resource::<Texture>();
104    }
105}