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}