Skip to main content

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