Skip to main content

moirai/world/
bundle.rs

1//! Component [`Bundle`] insertion for spawn and deferred commands.
2//!
3//! [`BundleWriter`] routes writes through immediate world mutation, queued commands,
4//! or query-side command enqueue depending on the active spawn path.
5
6use crate::command::{CommandOp, ErasedComponentValue};
7use crate::component::ComponentId;
8use crate::entity::EntityId;
9use crate::world::{World, WorldError};
10use alloc::boxed::Box;
11use alloc::vec::Vec;
12
13/// Write one or more components onto an entity through [`BundleWriter`].
14pub trait Bundle {
15    fn write(self, writer: &mut BundleWriter<'_>) -> Result<(), WorldError>;
16}
17
18/// Runtime-assembled bundle of validated [`ComponentId`] entries and owned values.
19pub struct DynamicBundle {
20    entries: Vec<DynamicEntry>,
21}
22
23struct DynamicEntry {
24    component_id: ComponentId,
25    value: Option<Box<dyn ErasedComponentValue>>,
26}
27
28impl DynamicBundle {
29    /// Empty dynamic bundle.
30    pub fn new() -> Self {
31        Self {
32            entries: Vec::new(),
33        }
34    }
35
36    /// Append typed component `T` resolved against `world`'s registry.
37    pub fn push<T: 'static>(&mut self, world: &World, value: T) -> Result<(), WorldError> {
38        let component_id = world.component_id::<T>()?;
39        if world.is_tag_component(&component_id) {
40            return Err(WorldError::WrongStorageKind {
41                name: alloc::string::String::from("tag components cannot carry values"),
42            });
43        }
44        self.push_entry(component_id, Some(Box::new(value)))
45    }
46
47    /// Append tag component without a stored value.
48    pub fn push_tag(&mut self, tag: &ComponentId) -> Result<(), WorldError> {
49        self.push_entry(tag.clone(), None)
50    }
51
52    fn push_entry(
53        &mut self,
54        component_id: ComponentId,
55        value: Option<Box<dyn ErasedComponentValue>>,
56    ) -> Result<(), WorldError> {
57        if self
58            .entries
59            .iter()
60            .any(|entry| entry.component_id.index() == component_id.index())
61        {
62            return Err(WorldError::WrongStorageKind {
63                name: alloc::string::String::from("duplicate component in dynamic bundle"),
64            });
65        }
66        self.entries.push(DynamicEntry {
67            component_id,
68            value,
69        });
70        Ok(())
71    }
72}
73
74impl Default for DynamicBundle {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80impl Bundle for DynamicBundle {
81    fn write(self, writer: &mut BundleWriter<'_>) -> Result<(), WorldError> {
82        for entry in self.entries {
83            entry.component_id.validate_owner(writer.world_owner())?;
84            if writer.is_tag_component(&entry.component_id) {
85                if entry.value.is_some() {
86                    return Err(WorldError::WrongStorageKind {
87                        name: alloc::string::String::from("tag components cannot carry values"),
88                    });
89                }
90                writer.insert_tag_id(entry.component_id)?;
91            } else if let Some(value) = entry.value {
92                writer.insert_dynamic(entry.component_id, value)?;
93            } else {
94                return Err(WorldError::WrongStorageKind {
95                    name: alloc::string::String::from("table/sparse components require values"),
96                });
97            }
98        }
99        Ok(())
100    }
101}
102
103/// Checked bundle write surface for immediate, deferred, and query enqueue paths.
104pub struct BundleWriter<'w> {
105    entity: EntityId,
106    target: BundleTarget<'w>,
107}
108
109enum BundleTarget<'w> {
110    Immediate(&'w mut World),
111    Deferred(&'w mut World),
112    Query {
113        allocator: &'w crate::entity::EntityAllocator,
114        queue: &'w mut crate::command::CommandQueue,
115    },
116}
117
118impl<'w> BundleWriter<'w> {
119    pub(crate) fn new(world: &'w mut World, entity: EntityId) -> Self {
120        Self {
121            entity,
122            target: BundleTarget::Immediate(world),
123        }
124    }
125
126    pub(crate) fn deferred(world: &'w mut World, entity: EntityId) -> Self {
127        Self {
128            entity,
129            target: BundleTarget::Deferred(world),
130        }
131    }
132
133    pub(crate) fn query(
134        allocator: &'w crate::entity::EntityAllocator,
135        queue: &'w mut crate::command::CommandQueue,
136        entity: EntityId,
137    ) -> Self {
138        Self {
139            entity,
140            target: BundleTarget::Query { allocator, queue },
141        }
142    }
143
144    /// Insert component `T` for the bundle's target entity.
145    pub fn insert<T: 'static>(&mut self, value: T) -> Result<(), WorldError> {
146        match &mut self.target {
147            BundleTarget::Immediate(world) => world.insert(self.entity, value).map(|_| ()),
148            BundleTarget::Deferred(world) => {
149                world.ensure_mutable()?;
150                world.ensure_command_target(self.entity)?;
151                world.command_queue_mut().enqueue_insert(self.entity, value)
152            }
153            BundleTarget::Query { allocator, queue } => {
154                ensure_query_target(allocator, self.entity)?;
155                queue.enqueue_insert(self.entity, value)
156            }
157        }
158    }
159
160    pub(crate) fn insert_dynamic(
161        &mut self,
162        component_id: ComponentId,
163        value: Box<dyn ErasedComponentValue>,
164    ) -> Result<(), WorldError> {
165        match &mut self.target {
166            BundleTarget::Immediate(world) => world
167                .insert_dynamic(self.entity, component_id, value)
168                .map(|_| ()),
169            BundleTarget::Deferred(world) => {
170                world.ensure_mutable()?;
171                world.ensure_command_target(self.entity)?;
172                world.validate_component_insert(
173                    self.entity,
174                    component_id.index() as u32,
175                    value.as_ref().type_id(),
176                )?;
177                world.command_queue_mut().push(CommandOp::Insert {
178                    entity: self.entity,
179                    component_index: component_id.index() as u32,
180                    value,
181                });
182                Ok(())
183            }
184            BundleTarget::Query { allocator, queue } => {
185                ensure_query_target(allocator, self.entity)?;
186                queue.enqueue_dynamic_insert(self.entity, component_id.index(), value)
187            }
188        }
189    }
190
191    pub(crate) fn insert_tag_id(&mut self, component_id: ComponentId) -> Result<(), WorldError> {
192        match &mut self.target {
193            BundleTarget::Immediate(world) => world.add_tag_id(self.entity, component_id),
194            BundleTarget::Deferred(world) => {
195                world.ensure_mutable()?;
196                world.ensure_command_target(self.entity)?;
197                world
198                    .command_queue_mut()
199                    .enqueue_tag(self.entity, component_id.index())
200            }
201            BundleTarget::Query { allocator, queue } => {
202                ensure_query_target(allocator, self.entity)?;
203                queue.enqueue_tag(self.entity, component_id.index())
204            }
205        }
206    }
207
208    pub(crate) fn world_owner(&self) -> &crate::world::WorldOwner {
209        match &self.target {
210            BundleTarget::Immediate(world) | BundleTarget::Deferred(world) => world.owner(),
211            BundleTarget::Query { queue, .. } => queue.owner(),
212        }
213    }
214
215    pub(crate) fn is_tag_component(&self, component_id: &ComponentId) -> bool {
216        match &self.target {
217            BundleTarget::Immediate(world) | BundleTarget::Deferred(world) => {
218                world.is_tag_component(component_id)
219            }
220            BundleTarget::Query { queue, .. } => queue.is_tag_component(component_id.index()),
221        }
222    }
223
224    #[cfg(test)]
225    pub(crate) fn test_entity(&self) -> EntityId {
226        self.entity
227    }
228
229    #[cfg(test)]
230    pub(crate) fn test_world(&mut self) -> &mut World {
231        match &mut self.target {
232            BundleTarget::Immediate(world) | BundleTarget::Deferred(world) => world,
233            BundleTarget::Query { .. } => panic!("query bundle writer has no world"),
234        }
235    }
236}
237
238fn ensure_query_target(
239    allocator: &crate::entity::EntityAllocator,
240    entity: EntityId,
241) -> Result<(), WorldError> {
242    if allocator.is_alive(entity) || allocator.is_reserved(entity) {
243        Ok(())
244    } else {
245        Err(WorldError::StaleEntity { entity })
246    }
247}
248
249macro_rules! impl_bundle_tuple {
250    ($($name:ident),+) => {
251        #[allow(non_snake_case)]
252        impl<$($name: 'static),+> Bundle for ($($name,)+) {
253            fn write(self, writer: &mut BundleWriter<'_>) -> Result<(), WorldError> {
254                let ($($name,)+) = self;
255                $(writer.insert($name)?;)+
256                Ok(())
257            }
258        }
259    };
260}
261
262impl_bundle_tuple!(A);
263impl_bundle_tuple!(A, B);
264impl_bundle_tuple!(A, B, C);
265impl_bundle_tuple!(A, B, C, D);
266impl_bundle_tuple!(A, B, C, D, E);
267impl_bundle_tuple!(A, B, C, D, E, F);
268impl_bundle_tuple!(A, B, C, D, E, F, G);
269impl_bundle_tuple!(A, B, C, D, E, F, G, H);
270impl_bundle_tuple!(A, B, C, D, E, F, G, H, I);
271impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J);
272impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K);
273impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
274impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M);
275impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
276impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
277impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use crate::component::ComponentOptions;
283    use crate::world::WorldBuilder;
284    use alloc::vec;
285
286    #[derive(Clone, Copy)]
287    struct Health(i32);
288
289    #[derive(Clone, Copy)]
290    struct Marker;
291
292    #[test]
293    fn dynamic_bundle_default_and_write_validation_errors() {
294        assert_eq!(DynamicBundle::default().entries.len(), 0);
295        let mut builder = WorldBuilder::new();
296        let tag = builder
297            .register_component::<Marker>(ComponentOptions::tag())
298            .expect("tag");
299        builder
300            .register_component::<Health>(ComponentOptions::sparse())
301            .expect("health");
302        let mut world = builder.build().expect("build");
303        let entity = world.spawn().expect("spawn");
304
305        let mut tag_with_value = DynamicBundle::new();
306        tag_with_value.push_tag(&tag).expect("tag");
307        tag_with_value.entries[0].value = Some(Box::new(Health(1)));
308        assert!(matches!(
309            tag_with_value.write(&mut BundleWriter::new(&mut world, entity)),
310            Err(WorldError::WrongStorageKind { .. })
311        ));
312
313        let health_id = world.component_id::<Health>().expect("health");
314        let mut missing_value = DynamicBundle::new();
315        missing_value.push_entry(health_id, None).expect("entry");
316        assert!(matches!(
317            missing_value.write(&mut BundleWriter::new(&mut world, entity)),
318            Err(WorldError::WrongStorageKind { .. })
319        ));
320    }
321
322    #[test]
323    fn deferred_bundle_writer_queues_inserts() {
324        let mut builder = WorldBuilder::new();
325        builder
326            .register_component::<Health>(ComponentOptions::sparse())
327            .expect("health");
328        let mut world = builder.build().expect("build");
329        let entity = world
330            .commands()
331            .expect("commands")
332            .spawn()
333            .expect("reserve");
334        BundleWriter::deferred(&mut world, entity)
335            .insert(Health(3))
336            .expect("queue");
337        assert!(world.has_pending_commands());
338    }
339
340    #[test]
341    fn deferred_dynamic_bundle_queues_validated_erased_value() {
342        let mut builder = WorldBuilder::new();
343        builder
344            .register_component::<Health>(ComponentOptions::sparse())
345            .expect("health");
346        let mut world = builder.build().expect("world");
347        let health = world.component_id::<Health>().expect("id");
348        let entity = world
349            .commands()
350            .expect("commands")
351            .spawn()
352            .expect("reserve");
353        let mut dynamic = DynamicBundle::new();
354        dynamic
355            .push_entry(health, Some(Box::new(Health(7))))
356            .expect("entry");
357        dynamic
358            .write(&mut BundleWriter::deferred(&mut world, entity))
359            .expect("deferred dynamic");
360
361        let mut wrong = DynamicBundle::new();
362        wrong
363            .push_entry(
364                world.component_id::<Health>().expect("health id"),
365                Some(Box::new(7_u32)),
366            )
367            .expect("wrong entry");
368        assert!(matches!(
369            wrong.write(&mut BundleWriter::deferred(&mut world, entity)),
370            Err(WorldError::WrongStorageKind { .. })
371        ));
372    }
373
374    #[test]
375    fn tuple_bundle_writes_components() {
376        let mut builder = WorldBuilder::new();
377        builder
378            .register_component::<Health>(ComponentOptions::sparse())
379            .expect("health");
380        let mut world = builder.build().expect("build");
381        let entity = world.spawn().expect("spawn");
382        (Health(4),)
383            .write(&mut BundleWriter::new(&mut world, entity))
384            .expect("tuple");
385        assert_eq!(
386            world.get::<Health>(entity).expect("get").map(|h| h.0),
387            Some(4)
388        );
389    }
390
391    #[test]
392    fn query_bundle_writer_validates_and_enqueues_all_component_shapes() {
393        let mut builder = WorldBuilder::new();
394        let health = builder
395            .register_component::<Health>(ComponentOptions::sparse())
396            .expect("health");
397        let marker = builder
398            .register_component::<Marker>(ComponentOptions::tag())
399            .expect("marker");
400        let mut world = builder.build().expect("world");
401        let entity = world.spawn().expect("entity");
402        let mut queue = crate::command::CommandQueue::configured(
403            world.owner.clone(),
404            vec![
405                (Some(core::any::TypeId::of::<Health>()), false),
406                (None, true),
407            ],
408        );
409
410        let mut writer = BundleWriter::query(&world.allocator, &mut queue, entity);
411        assert_eq!(writer.test_entity(), entity);
412        assert!(writer.world_owner().same(world.owner()));
413        assert!(!writer.is_tag_component(&health));
414        assert!(writer.is_tag_component(&marker));
415        writer.insert(Health(1)).expect("typed insert");
416        writer
417            .insert_dynamic(health, Box::new(Health(2)))
418            .expect("dynamic insert");
419        writer.insert_tag_id(marker).expect("tag insert");
420
421        let stale = EntityId::from_parts(99, 1);
422        let mut stale_writer = BundleWriter::query(&world.allocator, &mut queue, stale);
423        assert!(matches!(
424            stale_writer.insert(Health(3)),
425            Err(WorldError::StaleEntity { .. })
426        ));
427    }
428
429    #[test]
430    #[should_panic(expected = "query bundle writer has no world")]
431    fn query_bundle_test_world_rejects_world_access() {
432        let mut world = WorldBuilder::new().build().expect("world");
433        let entity = world.spawn().expect("entity");
434        let mut queue = crate::command::CommandQueue::configured(world.owner.clone(), vec![]);
435        BundleWriter::query(&world.allocator, &mut queue, entity).test_world();
436    }
437}