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 columns.insert(
97 TypeId::of::<C1>(),
98 Box::new(Vec::<C1>::new()) as Box<dyn AnyVec>,
99 );
100 columns
101 }
102
103 fn update_metadata(
104 metadata: &mut EntityMetadata,
105 location: PageIndex,
106 registry: &ComponentRegistry,
107 ) {
108 // Find the domain for this component type in the registry.
109 if let Some(domain) = registry.get_domain(TypeId::of::<Self>()) {
110 // Insert or update the location for that domain.
111 metadata.locations.insert(domain, location);
112 }
113 // Note: We might want to log a warning here if a component is not registered.
114 }
115
116 unsafe fn add_to_page(self, page: &mut ComponentPage) {
117 // Get the column and push the single component.
118 page.columns
119 .get_mut(&TypeId::of::<C1>())
120 .unwrap()
121 .as_any_mut()
122 .downcast_mut::<Vec<C1>>()
123 .unwrap()
124 .push(self);
125 }
126}
127
128// Implementation for tuples of components.
129// We use a macro to handle various tuple sizes efficienty, following the
130// same architectural patterns as `WorldQuery`.
131macro_rules! impl_bundle_tuple {
132 ($(($C:ident, $idx:tt)),*) => {
133 impl<$($C: Component),*> ComponentBundle for ($($C,)*) {
134 fn type_ids() -> Vec<TypeId> {
135 let mut ids = vec![$(TypeId::of::<$C>()),*];
136 ids.sort();
137 ids.dedup();
138 ids
139 }
140
141 fn create_columns() -> HashMap<TypeId, Box<dyn AnyVec>> {
142 let mut columns: HashMap<TypeId, Box<dyn AnyVec>> = HashMap::new();
143 $(
144 columns.insert(TypeId::of::<$C>(), Box::new(Vec::<$C>::new()) as Box<dyn AnyVec>);
145 )*
146 columns
147 }
148
149 fn update_metadata(
150 metadata: &mut EntityMetadata,
151 location: PageIndex,
152 registry: &ComponentRegistry,
153 ) {
154 $(
155 if let Some(domain) = registry.get_domain(TypeId::of::<$C>()) {
156 metadata.locations.insert(domain, location);
157 }
158 )*
159 }
160
161 unsafe fn add_to_page(self, page: &mut ComponentPage) {
162 $(
163 page.columns
164 .get_mut(&TypeId::of::<$C>())
165 .unwrap()
166 .as_any_mut()
167 .downcast_mut::<Vec<$C>>()
168 .unwrap()
169 .push(self.$idx);
170 )*
171 }
172 }
173 };
174}
175
176impl_bundle_tuple!((C1, 0), (C2, 1));
177impl_bundle_tuple!((C1, 0), (C2, 1), (C3, 2));
178impl_bundle_tuple!((C1, 0), (C2, 1), (C3, 2), (C4, 3));
179impl_bundle_tuple!((C1, 0), (C2, 1), (C3, 2), (C4, 3), (C5, 4));
180impl_bundle_tuple!((C1, 0), (C2, 1), (C3, 2), (C4, 3), (C5, 4), (C6, 5));
181impl_bundle_tuple!(
182 (C1, 0),
183 (C2, 1),
184 (C3, 2),
185 (C4, 3),
186 (C5, 4),
187 (C6, 5),
188 (C7, 6)
189);
190impl_bundle_tuple!(
191 (C1, 0),
192 (C2, 1),
193 (C3, 2),
194 (C4, 3),
195 (C5, 4),
196 (C6, 5),
197 (C7, 6),
198 (C8, 7)
199);
200impl_bundle_tuple!(
201 (C1, 0),
202 (C2, 1),
203 (C3, 2),
204 (C4, 3),
205 (C5, 4),
206 (C6, 5),
207 (C7, 6),
208 (C8, 7),
209 (C9, 8)
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);
223impl_bundle_tuple!(
224 (C1, 0),
225 (C2, 1),
226 (C3, 2),
227 (C4, 3),
228 (C5, 4),
229 (C6, 5),
230 (C7, 6),
231 (C8, 7),
232 (C9, 8),
233 (C10, 9),
234 (C11, 10)
235);
236impl_bundle_tuple!(
237 (C1, 0),
238 (C2, 1),
239 (C3, 2),
240 (C4, 3),
241 (C5, 4),
242 (C6, 5),
243 (C7, 6),
244 (C8, 7),
245 (C9, 8),
246 (C10, 9),
247 (C11, 10),
248 (C12, 11)
249);