Skip to main content

bevy_ecs/bundle/
info.rs

1use alloc::{boxed::Box, vec, vec::Vec};
2use bevy_platform::{
3    collections::{HashMap, HashSet},
4    hash::FixedHasher,
5};
6use bevy_ptr::{MovingPtr, OwningPtr};
7use bevy_utils::TypeIdHashMap;
8use core::{any::TypeId, ptr::NonNull};
9use indexmap::{IndexMap, IndexSet};
10
11use crate::{
12    archetype::{Archetype, BundleComponentStatus, ComponentStatus},
13    bundle::{Bundle, DynamicBundle},
14    change_detection::{MaybeLocation, Tick},
15    component::{
16        ComponentId, Components, ComponentsRegistrator, RequiredComponentConstructor, StorageType,
17    },
18    entity::Entity,
19    query::DebugCheckedUnwrap as _,
20    storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
21};
22
23/// For a specific [`World`], this stores a unique value identifying a type of a registered [`Bundle`].
24///
25/// [`World`]: crate::world::World
26#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
27pub struct BundleId(usize);
28
29impl BundleId {
30    /// Returns the index of the associated [`Bundle`] type.
31    ///
32    /// Note that this is unique per-world, and should not be reused across them.
33    #[inline]
34    pub fn index(self) -> usize {
35        self.0
36    }
37}
38
39impl SparseSetIndex for BundleId {
40    #[inline]
41    fn sparse_set_index(&self) -> usize {
42        self.index()
43    }
44
45    #[inline]
46    fn get_sparse_set_index(value: usize) -> Self {
47        Self(value)
48    }
49}
50
51/// What to do on insertion if a component already exists.
52#[derive(Clone, Copy, Eq, PartialEq)]
53pub enum InsertMode {
54    /// Any existing components of a matching type will be overwritten.
55    Replace,
56    /// Any existing components of a matching type will be left unchanged.
57    Keep,
58}
59
60/// Stores metadata associated with a specific type of [`Bundle`] for a given [`World`].
61///
62/// [`World`]: crate::world::World
63pub struct BundleInfo {
64    pub(super) id: BundleId,
65
66    /// The list of all components contributed by the bundle (including Required Components). This is in
67    /// the order `[EXPLICIT_COMPONENTS][REQUIRED_COMPONENTS]`
68    ///
69    /// # Safety
70    /// Every ID in this list must be valid within the World that owns the [`BundleInfo`],
71    /// must have its storage initialized (i.e. columns created in tables, sparse set created),
72    /// and the range (0..`explicit_components_len`) must be in the same order as the source bundle
73    /// type writes its components in.
74    pub(super) contributed_component_ids: Box<[ComponentId]>,
75
76    /// The list of constructors for all required components indirectly contributed by this bundle.
77    pub(super) required_component_constructors: Box<[RequiredComponentConstructor]>,
78}
79
80impl BundleInfo {
81    /// Create a new [`BundleInfo`].
82    ///
83    /// # Safety
84    ///
85    /// Every ID in `component_ids` must be valid within the World that owns the `BundleInfo`
86    /// and must be in the same order as the source bundle type writes its components in.
87    unsafe fn new(
88        bundle_type_name: &'static str,
89        storages: &mut Storages,
90        components: &Components,
91        mut component_ids: Vec<ComponentId>,
92        id: BundleId,
93    ) -> BundleInfo {
94        let explicit_component_ids = component_ids
95            .iter()
96            .copied()
97            .collect::<IndexSet<_, FixedHasher>>();
98
99        // check for duplicates
100        if explicit_component_ids.len() != component_ids.len() {
101            // TODO: Replace with `Vec::partition_dedup` once https://github.com/rust-lang/rust/issues/54279 is stabilized
102            let mut seen = <HashSet<_>>::default();
103            let mut dups = Vec::new();
104            for id in component_ids {
105                if !seen.insert(id) {
106                    dups.push(id);
107                }
108            }
109
110            let names = dups
111                .into_iter()
112                .map(|id| {
113                    // SAFETY: the caller ensures component_id is valid.
114                    unsafe { components.get_info_unchecked(id).name() }
115                })
116                .collect::<Vec<_>>();
117
118            panic!("Bundle {bundle_type_name} has duplicate components: {names:?}");
119        }
120
121        let mut depth_first_components = IndexMap::<_, _, FixedHasher>::default();
122        for &component_id in &component_ids {
123            // SAFETY: caller has verified that all ids are valid
124            let info = unsafe { components.get_info_unchecked(component_id) };
125
126            for (&required_id, required_component) in &info.required_components().all {
127                depth_first_components
128                    .entry(required_id)
129                    .or_insert_with(|| required_component.clone());
130            }
131
132            storages.prepare_component(component_id, info);
133        }
134
135        let required_components = depth_first_components
136            .into_iter()
137            .filter(|&(required_id, _)| !explicit_component_ids.contains(&required_id))
138            .inspect(|&(required_id, _)| {
139                // SAFETY: These ids came out of the passed `components`, so they must be valid.
140                storages.prepare_component(required_id, unsafe {
141                    components.get_info_unchecked(required_id)
142                });
143                component_ids.push(required_id);
144            })
145            .map(|(_, required_component)| required_component.constructor)
146            .collect::<Box<_>>();
147
148        // SAFETY: The caller ensures that component_ids:
149        // - is valid for the associated world
150        // - has had its storage initialized
151        // - is in the same order as the source bundle type
152        BundleInfo {
153            id,
154            contributed_component_ids: component_ids.into(),
155            required_component_constructors: required_components,
156        }
157    }
158
159    /// Returns a value identifying the associated [`Bundle`] type.
160    #[inline]
161    pub const fn id(&self) -> BundleId {
162        self.id
163    }
164
165    /// Returns the length of the explicit components part of the [`contributed_components`](Self::contributed_components) list.
166    #[inline]
167    pub(super) fn explicit_components_len(&self) -> usize {
168        self.contributed_component_ids.len() - self.required_component_constructors.len()
169    }
170
171    /// Returns the [ID](ComponentId) of each component explicitly defined in this bundle (ex: Required Components are excluded).
172    ///
173    /// For all components contributed by this bundle (including Required Components), see [`BundleInfo::contributed_components`]
174    #[inline]
175    pub fn explicit_components(&self) -> &[ComponentId] {
176        &self.contributed_component_ids[0..self.explicit_components_len()]
177    }
178
179    /// Returns the [ID](ComponentId) of each Required Component needed by this bundle. This _does not include_ Required Components that are
180    /// explicitly provided by the bundle.
181    #[inline]
182    pub fn required_components(&self) -> &[ComponentId] {
183        &self.contributed_component_ids[self.explicit_components_len()..]
184    }
185
186    /// Returns the [ID](ComponentId) of each component contributed by this bundle. This includes Required Components.
187    ///
188    /// For only components explicitly defined in this bundle, see [`BundleInfo::explicit_components`]
189    #[inline]
190    pub fn contributed_components(&self) -> &[ComponentId] {
191        &self.contributed_component_ids
192    }
193
194    /// Returns an iterator over the [ID](ComponentId) of each component explicitly defined in this bundle (ex: this excludes Required Components).
195    /// To iterate all components contributed by this bundle (including Required Components), see [`BundleInfo::iter_contributed_components`]
196    #[inline]
197    pub fn iter_explicit_components(&self) -> impl Iterator<Item = ComponentId> + Clone + '_ {
198        self.explicit_components().iter().copied()
199    }
200
201    /// Returns an iterator over the [ID](ComponentId) of each component contributed by this bundle. This includes Required Components.
202    ///
203    /// To iterate only components explicitly defined in this bundle, see [`BundleInfo::iter_explicit_components`]
204    #[inline]
205    pub fn iter_contributed_components(&self) -> impl Iterator<Item = ComponentId> + Clone + '_ {
206        self.contributed_components().iter().copied()
207    }
208
209    /// Returns an iterator over the [ID](ComponentId) of each Required Component needed by this bundle. This _does not include_ Required Components that are
210    /// explicitly provided by the bundle.
211    pub fn iter_required_components(&self) -> impl Iterator<Item = ComponentId> + '_ {
212        self.required_components().iter().copied()
213    }
214
215    /// This writes components from a given [`Bundle`] to the given entity.
216    ///
217    /// # Safety
218    ///
219    /// `bundle_component_status` must return the "correct" [`ComponentStatus`] for each component
220    /// in the [`Bundle`], with respect to the entity's original archetype (prior to the bundle being added).
221    ///
222    /// For example, if the original archetype already has `ComponentA` and `T` also has `ComponentA`, the status
223    /// should be `Existing`. If the original archetype does not have `ComponentA`, the status should be `Added`.
224    ///
225    /// When "inserting" a bundle into an existing entity, [`ArchetypeAfterBundleInsert`](crate::archetype::SpawnBundleStatus)
226    /// should be used, which will report `Added` vs `Existing` status based on the current archetype's structure.
227    ///
228    /// When spawning a bundle, [`SpawnBundleStatus`](crate::archetype::SpawnBundleStatus) can be used instead,
229    /// which removes the need to look up the [`ArchetypeAfterBundleInsert`](crate::archetype::ArchetypeAfterBundleInsert)
230    /// in the archetype graph, which requires ownership of the entity's current archetype.
231    ///
232    /// Regardless of how this is used, if `T::Effect: !NoBundleEffect` then [`apply_effect`] must be called
233    /// exactly once on `bundle` after this function is called  before returning to user-space safe code.
234    /// This is currently only doable via use of [`MovingPtr::partial_move`].
235    ///
236    /// `table` must be the "new" table for `entity`. `table_row` must have space allocated for the
237    /// `entity`, `bundle` must match this [`BundleInfo`]'s type
238    ///
239    /// [`apply_effect`]: crate::bundle::DynamicBundle::apply_effect
240    #[inline]
241    pub(super) unsafe fn write_components<'a, T: DynamicBundle, S: BundleComponentStatus>(
242        &self,
243        table: &mut Table,
244        sparse_sets: &mut SparseSets,
245        bundle_component_status: &S,
246        required_components: impl Iterator<Item = &'a RequiredComponentConstructor>,
247        entity: Entity,
248        table_row: TableRow,
249        change_tick: Tick,
250        bundle: MovingPtr<'_, T>,
251        insert_mode: InsertMode,
252        caller: MaybeLocation,
253    ) {
254        // NOTE: get_components calls `write_component` on each component in "bundle order".
255        // bundle_info.component_ids are also in "bundle order"
256        let mut bundle_component = 0;
257        let mut write_component = bind_lifetime(|storage_type, component_ptr| {
258            // SAFETY: bundle_component is a valid index per unsafe bundle impl
259            let component_id = *unsafe {
260                self.contributed_component_ids
261                    .get_unchecked(bundle_component)
262            };
263            // SAFETY: bundle_component is a valid index per unsafe bundle impl
264            let status = unsafe { bundle_component_status.get_status(bundle_component) };
265            match storage_type {
266                StorageType::Table => {
267                    let column =
268                        // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that
269                        // the target table contains the component.
270                        unsafe { table.get_column_mut(component_id).debug_checked_unwrap() };
271                    match (status, insert_mode) {
272                        // SAFETY:
273                        // - table_row valid per preconditions
274                        // - component_ptr valid per unsafe bundle impl
275                        (ComponentStatus::Added, _) => unsafe {
276                            column.initialize(table_row, component_ptr, change_tick, caller);
277                        },
278                        // SAFETY:
279                        // - table_row valid per preconditions
280                        // - component_ptr valid per unsafe bundle impl
281                        // - component exists per bundle_component_status requirements
282                        (ComponentStatus::Existing, InsertMode::Replace) => unsafe {
283                            column.replace(table_row, component_ptr, change_tick, caller);
284                        },
285                        (ComponentStatus::Existing, InsertMode::Keep) => {
286                            if let Some(drop_fn) = table.get_drop_for(component_id) {
287                                // SAFETY: component_ptr valid per unsafe bundle impl
288                                unsafe { drop_fn(component_ptr) };
289                            }
290                        }
291                    }
292                }
293                StorageType::SparseSet => {
294                    let sparse_set =
295                        // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that
296                        // a sparse set exists for the component.
297                        unsafe { sparse_sets.get_mut(component_id).debug_checked_unwrap() };
298                    match (status, insert_mode) {
299                        // SAFETY: component_ptr valid per unsafe bundle impl
300                        (ComponentStatus::Added, _) | (_, InsertMode::Replace) => unsafe {
301                            sparse_set.insert(entity, component_ptr, change_tick, caller);
302                        },
303                        (ComponentStatus::Existing, InsertMode::Keep) => {
304                            if let Some(drop_fn) = sparse_set.get_drop() {
305                                // SAFETY: component_ptr valid per unsafe bundle impl
306                                unsafe { drop_fn(component_ptr) };
307                            }
308                        }
309                    }
310                }
311            }
312            bundle_component += 1;
313        });
314        // Remove this once closure_lifetime_binder is stable
315        fn bind_lifetime<F: FnMut(StorageType, OwningPtr<'_>)>(func: F) -> F {
316            func
317        }
318        // SAFETY:
319        // - storage type correct per precondition
320        // - `apply_effect` called if required per precondition
321        unsafe { T::get_components(bundle, &mut write_component) };
322
323        for required_component in required_components {
324            // SAFETY: we're in write_components
325            unsafe {
326                required_component.initialize(
327                    table,
328                    sparse_sets,
329                    change_tick,
330                    table_row,
331                    entity,
332                    caller,
333                );
334            }
335        }
336    }
337
338    /// Internal method to initialize a required component from an [`OwningPtr`]. This should ultimately be called
339    /// in the context of [`BundleInfo::write_components`], via [`RequiredComponentConstructor::initialize`].
340    ///
341    /// # Safety
342    ///
343    /// `component_ptr` must point to a required component value that matches the given `component_id`. The `storage_type` must match
344    /// the type associated with `component_id`. The `entity` and `table_row` must correspond to an entity with an uninitialized
345    /// component matching `component_id`.
346    ///
347    /// This method _should not_ be called outside of [`BundleInfo::write_components`].
348    /// For more information, read the [`BundleInfo::write_components`] safety docs.
349    /// This function inherits the safety requirements defined there.
350    pub(crate) unsafe fn initialize_required_component(
351        table: &mut Table,
352        sparse_sets: &mut SparseSets,
353        change_tick: Tick,
354        table_row: TableRow,
355        entity: Entity,
356        component_id: ComponentId,
357        storage_type: StorageType,
358        component_ptr: OwningPtr,
359        caller: MaybeLocation,
360    ) {
361        // SAFETY:
362        // - if component_id is in required_components, `BundleInfo::new` ensured that the storage exists
363        // - caller ensures row and component_ptr are valid
364        unsafe {
365            match storage_type {
366                StorageType::Table => {
367                    let column = table.get_column_mut(component_id).debug_checked_unwrap();
368                    column.initialize(table_row, component_ptr, change_tick, caller);
369                }
370                StorageType::SparseSet => {
371                    let sparse_set = sparse_sets.get_mut(component_id).debug_checked_unwrap();
372                    sparse_set.insert(entity, component_ptr, change_tick, caller);
373                }
374            }
375        }
376    }
377}
378
379/// The type of archetype move (or lack thereof) that will result from a bundle
380/// being inserted into an entity.
381pub(crate) enum ArchetypeMoveType {
382    /// If the entity already has all of the components that are being inserted,
383    /// its archetype won't change.
384    SameArchetype,
385    /// If only [`sparse set`](StorageType::SparseSet) components are being added,
386    /// the entity's archetype will change while keeping the same table.
387    NewArchetypeSameTable { new_archetype: NonNull<Archetype> },
388    /// If any [`table-stored`](StorageType::Table) components are being added,
389    /// both the entity's archetype and table will change.
390    NewArchetypeNewTable { new_archetype: NonNull<Archetype> },
391}
392
393/// Metadata for bundles. Stores a [`BundleInfo`] for each type of [`Bundle`] in a given world.
394#[derive(Default)]
395pub struct Bundles {
396    bundle_infos: Vec<BundleInfo>,
397    /// Cache static [`BundleId`]
398    bundle_ids: TypeIdHashMap<BundleId>,
399    /// Cache bundles, which contains both explicit and required components of [`Bundle`]
400    contributed_bundle_ids: TypeIdHashMap<BundleId>,
401    /// Cache dynamic [`BundleId`] with multiple components
402    dynamic_bundle_ids: HashMap<Box<[ComponentId]>, BundleId>,
403    dynamic_bundle_storages: HashMap<BundleId, Vec<StorageType>>,
404    /// Cache optimized dynamic [`BundleId`] with single component
405    dynamic_component_bundle_ids: HashMap<ComponentId, BundleId>,
406    dynamic_component_storages: HashMap<BundleId, StorageType>,
407}
408
409impl Bundles {
410    /// The total number of [`Bundle`] registered in [`Storages`].
411    pub fn len(&self) -> usize {
412        self.bundle_infos.len()
413    }
414
415    /// Returns true if no [`Bundle`] registered in [`Storages`].
416    pub fn is_empty(&self) -> bool {
417        self.len() == 0
418    }
419
420    /// Iterate over [`BundleInfo`].
421    pub fn iter(&self) -> impl Iterator<Item = &BundleInfo> {
422        self.bundle_infos.iter()
423    }
424
425    /// Gets the metadata associated with a specific type of bundle.
426    /// Returns `None` if the bundle is not registered with the world.
427    #[inline]
428    pub fn get(&self, bundle_id: BundleId) -> Option<&BundleInfo> {
429        self.bundle_infos.get(bundle_id.index())
430    }
431
432    /// Gets the value identifying a specific type of bundle.
433    /// Returns `None` if the bundle does not exist in the world,
434    /// or if `type_id` does not correspond to a type of bundle.
435    #[inline]
436    pub fn get_id(&self, type_id: TypeId) -> Option<BundleId> {
437        self.bundle_ids.get(&type_id).copied()
438    }
439
440    /// Gets the value identifying a specific type of bundle
441    /// that contains both explicit and required components for a statically known type.
442    ///
443    /// Returns `None` if the bundle does not exist in the world,
444    /// or if `type_id` does not correspond to a type of bundle.
445    pub fn get_contributed_bundle_id(&self, type_id: TypeId) -> Option<BundleId> {
446        self.contributed_bundle_ids.get(&type_id).copied()
447    }
448
449    /// Registers a new [`BundleInfo`] for a statically known type.
450    ///
451    /// Also registers all the components in the bundle.
452    ///
453    /// # Safety
454    ///
455    /// `components` and `storages` must be from the same [`World`] as `self`.
456    ///
457    /// [`World`]: crate::world::World
458    pub(crate) unsafe fn register_info<T: Bundle>(
459        &mut self,
460        components: &mut ComponentsRegistrator,
461        storages: &mut Storages,
462    ) -> BundleId {
463        let bundle_infos = &mut self.bundle_infos;
464        *self.bundle_ids.entry(TypeId::of::<T>()).or_insert_with(|| {
465            let component_ids = T::component_ids(components).collect::<Vec<_>>();
466            let id = BundleId(bundle_infos.len());
467            let bundle_info =
468                // SAFETY: T::component_id ensures:
469                // - its info was created
470                // - appropriate storage for it has been initialized.
471                // - it was created in the same order as the components in T
472                unsafe { BundleInfo::new(core::any::type_name::<T>(), storages, components, component_ids, id) };
473            bundle_infos.push(bundle_info);
474            id
475        })
476    }
477
478    /// Registers a new [`BundleInfo`], which contains both explicit and required components for a statically known type.
479    ///
480    /// Also registers all the components in the bundle.
481    ///
482    /// # Safety
483    ///
484    /// `components` and `storages` must be from the same [`World`] as `self`.
485    ///
486    /// [`World`]: crate::world::World
487    pub(crate) unsafe fn register_contributed_bundle_info<T: Bundle>(
488        &mut self,
489        components: &mut ComponentsRegistrator,
490        storages: &mut Storages,
491    ) -> BundleId {
492        if let Some(id) = self.contributed_bundle_ids.get(&TypeId::of::<T>()).cloned() {
493            id
494        } else {
495            // SAFETY: as per the guarantees of this function, components and
496            // storages are from the same world as self
497            let explicit_bundle_id = unsafe { self.register_info::<T>(components, storages) };
498
499            // SAFETY: reading from `explicit_bundle_id` and creating new bundle in same time. Its valid because bundle hashmap allow this
500            let id = unsafe {
501                let (ptr, len) = {
502                    // SAFETY: `explicit_bundle_id` is valid and defined above
503                    let contributed = self
504                        .get_unchecked(explicit_bundle_id)
505                        .contributed_components();
506                    (contributed.as_ptr(), contributed.len())
507                };
508                // SAFETY: this is sound because the contributed_components Vec for explicit_bundle_id will not be accessed mutably as
509                // part of init_dynamic_info. No mutable references will be created and the allocation will remain valid.
510                self.init_dynamic_info(storages, components, core::slice::from_raw_parts(ptr, len))
511            };
512            self.contributed_bundle_ids.insert(TypeId::of::<T>(), id);
513            id
514        }
515    }
516
517    /// # Safety
518    /// A [`BundleInfo`] with the given [`BundleId`] must have been initialized for this instance of `Bundles`.
519    pub(crate) unsafe fn get_unchecked(&self, id: BundleId) -> &BundleInfo {
520        // SAFETY: Bundle exists per precondition
521        unsafe { self.bundle_infos.get_unchecked(id.0) }
522    }
523
524    /// # Safety
525    /// This [`BundleId`] must have been initialized with a single [`Component`](crate::component::Component)
526    /// via [`init_component_info`](Self::init_dynamic_info)
527    pub(crate) unsafe fn get_storage_unchecked(&self, id: BundleId) -> StorageType {
528        // SAFETY: Added in `init_component_info`
529        *unsafe {
530            self.dynamic_component_storages
531                .get(&id)
532                .debug_checked_unwrap()
533        }
534    }
535
536    /// # Safety
537    /// This [`BundleId`] must have been initialized via [`init_dynamic_info`](Self::init_dynamic_info)
538    pub(crate) unsafe fn get_storages_unchecked(&mut self, id: BundleId) -> &mut Vec<StorageType> {
539        // SAFETY: Added in `init_dynamic_info`
540        unsafe {
541            self.dynamic_bundle_storages
542                .get_mut(&id)
543                .debug_checked_unwrap()
544        }
545    }
546
547    /// Initializes a new [`BundleInfo`] for a dynamic [`Bundle`].
548    ///
549    /// # Panics
550    ///
551    /// Panics if any of the provided [`ComponentId`]s do not exist in the
552    /// provided [`Components`].
553    pub(crate) fn init_dynamic_info(
554        &mut self,
555        storages: &mut Storages,
556        components: &Components,
557        component_ids: &[ComponentId],
558    ) -> BundleId {
559        let bundle_infos = &mut self.bundle_infos;
560
561        // Use `raw_entry_mut` to avoid cloning `component_ids` to access `Entry`
562        let (_, bundle_id) = self
563            .dynamic_bundle_ids
564            .raw_entry_mut()
565            .from_key(component_ids)
566            .or_insert_with(|| {
567                let (id, storages) = initialize_dynamic_bundle(
568                    bundle_infos,
569                    storages,
570                    components,
571                    Vec::from(component_ids),
572                );
573                // SAFETY: The ID always increases when new bundles are added, and so, the ID is unique.
574                unsafe {
575                    self.dynamic_bundle_storages
576                        .insert_unique_unchecked(id, storages);
577                }
578                (component_ids.into(), id)
579            });
580        *bundle_id
581    }
582
583    /// Initializes a new [`BundleInfo`] for a dynamic [`Bundle`] with single component.
584    ///
585    /// # Panics
586    ///
587    /// Panics if the provided [`ComponentId`] does not exist in the provided [`Components`].
588    pub(crate) fn init_component_info(
589        &mut self,
590        storages: &mut Storages,
591        components: &Components,
592        component_id: ComponentId,
593    ) -> BundleId {
594        let bundle_infos = &mut self.bundle_infos;
595        let bundle_id = self
596            .dynamic_component_bundle_ids
597            .entry(component_id)
598            .or_insert_with(|| {
599                let (id, storage_type) = initialize_dynamic_bundle(
600                    bundle_infos,
601                    storages,
602                    components,
603                    vec![component_id],
604                );
605                self.dynamic_component_storages.insert(id, storage_type[0]);
606                id
607            });
608        *bundle_id
609    }
610}
611
612/// Asserts that all components are part of [`Components`]
613/// and initializes a [`BundleInfo`].
614fn initialize_dynamic_bundle(
615    bundle_infos: &mut Vec<BundleInfo>,
616    storages: &mut Storages,
617    components: &Components,
618    component_ids: Vec<ComponentId>,
619) -> (BundleId, Vec<StorageType>) {
620    // Assert component existence
621    let storage_types = component_ids.iter().map(|&id| {
622        components.get_info(id).unwrap_or_else(|| {
623            panic!(
624                "init_dynamic_info called with component id {id:?} which doesn't exist in this world"
625            )
626        }).storage_type()
627    }).collect();
628
629    let id = BundleId(bundle_infos.len());
630    let bundle_info =
631        // SAFETY: `component_ids` are valid as they were just checked
632        unsafe { BundleInfo::new("<dynamic bundle>", storages, components, component_ids, id) };
633    bundle_infos.push(bundle_info);
634
635    (id, storage_types)
636}