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.domain_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 a 2-component tuple
129impl<C1: Component, C2: Component> ComponentBundle for (C1, C2) {
130 fn type_ids() -> Vec<TypeId> {
131 let mut ids = vec![TypeId::of::<C1>(), TypeId::of::<C2>()];
132 // It's crucial to sort the IDs to have a canonical signature.
133 ids.sort();
134 ids
135 }
136
137 fn create_columns() -> HashMap<TypeId, Box<dyn AnyVec>> {
138 // Create a HashMap to hold the type-erased component vectors.
139 let mut columns: HashMap<TypeId, Box<dyn AnyVec>> = HashMap::new();
140
141 // For each component in the bundle, create an empty `Vec<T>`,
142 // box it, and insert it into the map with its `TypeId` as the key.
143 columns.insert(
144 TypeId::of::<C1>(),
145 Box::new(Vec::<C1>::new()) as Box<dyn AnyVec>,
146 );
147 columns.insert(
148 TypeId::of::<C2>(),
149 Box::new(Vec::<C2>::new()) as Box<dyn AnyVec>,
150 );
151
152 columns
153 }
154
155 fn update_metadata(
156 metadata: &mut EntityMetadata,
157 location: PageIndex,
158 registry: &ComponentRegistry,
159 ) {
160 // Assumption: all components in a bundle belong to the same semantic domain.
161 // We look up the domain of the *first* component type.
162 if let Some(domain) = registry.domain_of::<C1>() {
163 metadata.locations.insert(domain, location);
164 }
165 }
166
167 unsafe fn add_to_page(self, page: &mut ComponentPage) {
168 // --- THIS IS A CRITICAL SAFETY REGION ---
169
170 // 1. Store the TypeIds in local variables to create references with a valid lifetime.
171 let type_id1 = TypeId::of::<C1>();
172 let type_id2 = TypeId::of::<C2>();
173
174 // 2. Assert that we are not trying to mutably alias the same component type.
175 // This is a critical runtime safety check for our logic.
176 assert_ne!(
177 type_id1, type_id2,
178 "Bundles cannot contain duplicate component types."
179 );
180
181 // 3. Get mutable references to both columns simultaneously using the correct, stable API.
182 let [c1_anyvec, c2_anyvec] = page.columns.get_disjoint_mut([&type_id1, &type_id2]);
183
184 // 4. Safely unwrap and downcast each reference.
185 // We can safely unwrap here because the `unsafe` contract of this function
186 // guarantees that the columns exist.
187 let c1_vec = c1_anyvec
188 .unwrap()
189 .as_any_mut()
190 .downcast_mut::<Vec<C1>>()
191 .unwrap();
192 let c2_vec = c2_anyvec
193 .unwrap()
194 .as_any_mut()
195 .downcast_mut::<Vec<C2>>()
196 .unwrap();
197
198 // 5. Push the component data.
199 c1_vec.push(self.0);
200 c2_vec.push(self.1);
201 }
202}
203
204// Implementation for a 3-component tuple.
205impl<C1: Component, C2: Component, C3: Component> ComponentBundle for (C1, C2, C3) {
206 fn type_ids() -> Vec<TypeId> {
207 let mut ids = vec![TypeId::of::<C1>(), TypeId::of::<C2>(), TypeId::of::<C3>()];
208 ids.sort();
209 ids.dedup();
210 ids
211 }
212
213 fn create_columns() -> HashMap<TypeId, Box<dyn AnyVec>> {
214 let mut columns: HashMap<TypeId, Box<dyn AnyVec>> = HashMap::new();
215 columns.insert(
216 TypeId::of::<C1>(),
217 Box::new(Vec::<C1>::new()) as Box<dyn AnyVec>,
218 );
219 columns.insert(
220 TypeId::of::<C2>(),
221 Box::new(Vec::<C2>::new()) as Box<dyn AnyVec>,
222 );
223 columns.insert(
224 TypeId::of::<C3>(),
225 Box::new(Vec::<C3>::new()) as Box<dyn AnyVec>,
226 );
227 columns
228 }
229
230 fn update_metadata(
231 metadata: &mut EntityMetadata,
232 location: PageIndex,
233 registry: &ComponentRegistry,
234 ) {
235 // Assumption: all components in a bundle belong to the same semantic domain.
236 // We look up the domain of the *first* component type.
237 if let Some(domain) = registry.domain_of::<C1>() {
238 metadata.locations.insert(domain, location);
239 }
240 }
241
242 unsafe fn add_to_page(self, page: &mut ComponentPage) {
243 let type_id1 = TypeId::of::<C1>();
244 let type_id2 = TypeId::of::<C2>();
245 let type_id3 = TypeId::of::<C3>();
246 assert_ne!(
247 type_id1, type_id2,
248 "Bundles cannot contain duplicate component types."
249 );
250 assert_ne!(
251 type_id1, type_id3,
252 "Bundles cannot contain duplicate component types."
253 );
254 assert_ne!(
255 type_id2, type_id3,
256 "Bundles cannot contain duplicate component types."
257 );
258
259 let [c1_anyvec, c2_anyvec, c3_anyvec] = page
260 .columns
261 .get_disjoint_mut([&type_id1, &type_id2, &type_id3]);
262
263 c1_anyvec
264 .unwrap()
265 .as_any_mut()
266 .downcast_mut::<Vec<C1>>()
267 .unwrap()
268 .push(self.0);
269 c2_anyvec
270 .unwrap()
271 .as_any_mut()
272 .downcast_mut::<Vec<C2>>()
273 .unwrap()
274 .push(self.1);
275 c3_anyvec
276 .unwrap()
277 .as_any_mut()
278 .downcast_mut::<Vec<C3>>()
279 .unwrap()
280 .push(self.2);
281 }
282}