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);