khora_data/ecs/bundle.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::any::TypeId;
16use std::collections::HashMap;
17
18use crate::ecs::component::Component;
19use crate::ecs::entity::EntityMetadata;
20use crate::ecs::page::{AnyVec, ComponentPage, PageIndex};
21use crate::ecs::registry::ComponentRegistry;
22
23/// A trait for any collection of components that can be spawned together as a single unit.
24///
25/// This is a key part of the ECS's public API. It is typically implemented on tuples
26/// of components, like `(Position, Velocity)`. It provides the logic for identifying
27/// its component types and safely writing its data into a `ComponentPage`.
28pub trait ComponentBundle {
29 /// Returns the sorted list of `TypeId`s for the components in this bundle.
30 ///
31 /// This provides a canonical "signature" for the bundle, which is used to find
32 /// a matching `ComponentPage` in the `World`. Sorting is crucial to ensure that
33 /// tuples with the same components but in a different order (e.g., (A, B) vs (B, A))
34 /// are treated as identical.
35 fn type_ids() -> Vec<TypeId>;
36
37 /// Creates the set of empty, type-erased `Vec<T>` columns required to store
38 /// this bundle's components.
39 ///
40 /// This is called by the `World` when a new `ComponentPage` needs to be
41 /// created for a specific bundle layout.
42 fn create_columns() -> HashMap<TypeId, Box<dyn AnyVec>>;
43
44 /// Updates the appropriate fields in an `EntityMetadata` struct to point
45 /// to the location of this bundle's data.
46 ///
47 /// This method is called by `World::spawn` to link an entity to its newly
48 /// created component data.
49 fn update_metadata(
50 metadata: &mut EntityMetadata,
51 location: PageIndex,
52 registry: &ComponentRegistry,
53 );
54
55 /// Adds the components from this bundle into the specified `ComponentPage`.
56 ///
57 /// # Safety
58 /// This function is unsafe because it relies on the caller to guarantee that
59 /// the `ComponentPage` is the correct one for this bundle's exact component layout.
60 /// It performs unsafe downcasting to write to the type-erased `Vec<T>`s.
61 unsafe fn add_to_page(self, page: &mut ComponentPage);
62}
63
64/// Implementation for the empty tuple, allowing spawning of empty entities.
65impl ComponentBundle for () {
66 fn type_ids() -> Vec<TypeId> {
67 Vec::new()
68 }
69
70 fn create_columns() -> HashMap<TypeId, Box<dyn AnyVec>> {
71 HashMap::new()
72 }
73
74 fn update_metadata(
75 _metadata: &mut EntityMetadata,
76 _location: PageIndex,
77 _registry: &ComponentRegistry,
78 ) {
79 // No components, so no location information to update.
80 }
81
82 unsafe fn add_to_page(self, _page: &mut ComponentPage) {
83 // No components to add, so this is a no-op.
84 }
85}
86
87// Implementation for a single component.
88impl<C1: Component> ComponentBundle for C1 {
89 fn type_ids() -> Vec<TypeId> {
90 // The signature is just the TypeId of the single component.
91 vec![TypeId::of::<C1>()]
92 }
93
94 fn create_columns() -> HashMap<TypeId, Box<dyn AnyVec>> {
95 let mut columns: HashMap<TypeId, Box<dyn AnyVec>> = HashMap::new();
96 // Route through the component's layout hook (AoS by default, field-SoA
97 // when opted in) — the column type is the component's choice.
98 columns.insert(TypeId::of::<C1>(), C1::make_column());
99 columns
100 }
101
102 fn update_metadata(
103 metadata: &mut EntityMetadata,
104 location: PageIndex,
105 registry: &ComponentRegistry,
106 ) {
107 // Find the domain for this component type in the registry.
108 if let Some(domain) = registry.get_domain(TypeId::of::<Self>()) {
109 // Insert or update the location for that domain.
110 metadata.locations.insert(domain, location);
111 }
112 // Note: We might want to log a warning here if a component is not registered.
113 }
114
115 unsafe fn add_to_page(self, page: &mut ComponentPage) {
116 let column = page.columns.get_mut(&TypeId::of::<C1>()).unwrap().as_mut();
117 self.push_into_column(column);
118 }
119}
120
121// Implementation for tuples of components.
122// We use a macro to handle various tuple sizes efficienty, following the
123// same architectural patterns as `WorldQuery`.
124macro_rules! impl_bundle_tuple {
125 ($(($C:ident, $idx:tt)),*) => {
126 impl<$($C: Component),*> ComponentBundle for ($($C,)*) {
127 fn type_ids() -> Vec<TypeId> {
128 let mut ids = vec![$(TypeId::of::<$C>()),*];
129 ids.sort();
130 ids.dedup();
131 ids
132 }
133
134 fn create_columns() -> HashMap<TypeId, Box<dyn AnyVec>> {
135 let mut columns: HashMap<TypeId, Box<dyn AnyVec>> = HashMap::new();
136 $(
137 columns.insert(TypeId::of::<$C>(), $C::make_column());
138 )*
139 columns
140 }
141
142 fn update_metadata(
143 metadata: &mut EntityMetadata,
144 location: PageIndex,
145 registry: &ComponentRegistry,
146 ) {
147 $(
148 if let Some(domain) = registry.get_domain(TypeId::of::<$C>()) {
149 metadata.locations.insert(domain, location);
150 }
151 )*
152 }
153
154 unsafe fn add_to_page(self, page: &mut ComponentPage) {
155 $(
156 let column = page.columns.get_mut(&TypeId::of::<$C>()).unwrap().as_mut();
157 self.$idx.push_into_column(column);
158 )*
159 }
160 }
161 };
162}
163
164impl_bundle_tuple!((C1, 0), (C2, 1));
165impl_bundle_tuple!((C1, 0), (C2, 1), (C3, 2));
166impl_bundle_tuple!((C1, 0), (C2, 1), (C3, 2), (C4, 3));
167impl_bundle_tuple!((C1, 0), (C2, 1), (C3, 2), (C4, 3), (C5, 4));
168impl_bundle_tuple!((C1, 0), (C2, 1), (C3, 2), (C4, 3), (C5, 4), (C6, 5));
169impl_bundle_tuple!(
170 (C1, 0),
171 (C2, 1),
172 (C3, 2),
173 (C4, 3),
174 (C5, 4),
175 (C6, 5),
176 (C7, 6)
177);
178impl_bundle_tuple!(
179 (C1, 0),
180 (C2, 1),
181 (C3, 2),
182 (C4, 3),
183 (C5, 4),
184 (C6, 5),
185 (C7, 6),
186 (C8, 7)
187);
188impl_bundle_tuple!(
189 (C1, 0),
190 (C2, 1),
191 (C3, 2),
192 (C4, 3),
193 (C5, 4),
194 (C6, 5),
195 (C7, 6),
196 (C8, 7),
197 (C9, 8)
198);
199impl_bundle_tuple!(
200 (C1, 0),
201 (C2, 1),
202 (C3, 2),
203 (C4, 3),
204 (C5, 4),
205 (C6, 5),
206 (C7, 6),
207 (C8, 7),
208 (C9, 8),
209 (C10, 9)
210);
211impl_bundle_tuple!(
212 (C1, 0),
213 (C2, 1),
214 (C3, 2),
215 (C4, 3),
216 (C5, 4),
217 (C6, 5),
218 (C7, 6),
219 (C8, 7),
220 (C9, 8),
221 (C10, 9),
222 (C11, 10)
223);
224impl_bundle_tuple!(
225 (C1, 0),
226 (C2, 1),
227 (C3, 2),
228 (C4, 3),
229 (C5, 4),
230 (C6, 5),
231 (C7, 6),
232 (C8, 7),
233 (C9, 8),
234 (C10, 9),
235 (C11, 10),
236 (C12, 11)
237);