Skip to main content

galeon_engine/
commands.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3use std::any::TypeId;
4
5use crate::component::Component;
6use crate::deadline::{DeadlineId, Timestamp};
7use crate::entity::Entity;
8use crate::system_param::Access;
9use crate::world::{Bundle, UnsafeWorldCell, World};
10
11/// A boxed, type-erased command closure.
12type BoxedCommand = Box<dyn FnOnce(&mut World) + Send>;
13
14// =============================================================================
15// CommandBuffer — internal queue of deferred world mutations
16// =============================================================================
17
18/// A buffer of deferred structural mutations applied between schedule stages.
19///
20/// Commands are not the hot iteration path, so boxing each command is
21/// acceptable. The buffer is drained by [`World::apply_commands`].
22pub struct CommandBuffer {
23    queue: Vec<BoxedCommand>,
24}
25
26impl CommandBuffer {
27    pub fn new() -> Self {
28        Self { queue: Vec::new() }
29    }
30
31    /// Push a type-erased command onto the buffer.
32    fn push(&mut self, cmd: impl FnOnce(&mut World) + Send + 'static) {
33        self.queue.push(Box::new(cmd));
34    }
35
36    /// Returns the number of queued commands.
37    pub fn len(&self) -> usize {
38        self.queue.len()
39    }
40
41    /// Returns `true` if no commands are queued.
42    pub fn is_empty(&self) -> bool {
43        self.queue.is_empty()
44    }
45
46    /// Take all queued commands out, leaving the buffer empty.
47    ///
48    /// Used by [`World::apply_commands`] to drain the queue without
49    /// holding a borrow on the buffer while executing commands.
50    pub(crate) fn take(&mut self) -> Vec<BoxedCommand> {
51        std::mem::take(&mut self.queue)
52    }
53}
54
55impl Default for CommandBuffer {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61// =============================================================================
62// Commands — typed system parameter for deferred mutations
63// =============================================================================
64
65/// System parameter that queues structural mutations for deferred application.
66///
67/// `Commands` buffers spawn, despawn, insert, and remove operations. These
68/// are applied between schedule stages via [`World::apply_commands`], avoiding
69/// mid-iteration archetype changes.
70///
71/// ```rust,ignore
72/// fn spawn_units(mut cmds: Commands<'_>) {
73///     cmds.spawn((Position { x: 0.0, y: 0.0 },));
74///     cmds.despawn(old_entity);
75///     cmds.insert(entity, Health(100));
76///     cmds.remove::<Velocity>(entity);
77/// }
78/// ```
79pub struct Commands<'w> {
80    buffer: &'w mut CommandBuffer,
81}
82
83impl<'w> Commands<'w> {
84    /// Spawn an entity with the given component bundle (deferred).
85    pub fn spawn<B: Bundle + Send + 'static>(&mut self, bundle: B) {
86        self.buffer.push(move |world: &mut World| {
87            world.spawn(bundle);
88        });
89    }
90
91    /// Despawn an entity (deferred).
92    pub fn despawn(&mut self, entity: Entity) {
93        self.buffer.push(move |world: &mut World| {
94            world.despawn(entity);
95        });
96    }
97
98    /// Insert a component into an entity (deferred).
99    ///
100    /// If the entity already has this component type, the value is overwritten.
101    pub fn insert<C: Component>(&mut self, entity: Entity, component: C) {
102        self.buffer.push(move |world: &mut World| {
103            world.insert(entity, component);
104        });
105    }
106
107    /// Remove a component from an entity (deferred).
108    pub fn remove<C: Component>(&mut self, entity: Entity) {
109        self.buffer.push(move |world: &mut World| {
110            world.remove::<C>(entity);
111        });
112    }
113
114    /// Schedule a deadline event (deferred).
115    ///
116    /// The event type must have been registered with
117    /// [`World::add_deadline_type::<T>()`].
118    pub fn schedule_deadline<T: Send + 'static>(&mut self, deadline: Timestamp, event: T) {
119        self.buffer.push(move |world: &mut World| {
120            world.schedule_deadline(deadline, event);
121        });
122    }
123
124    /// Cancel a previously scheduled deadline (deferred).
125    pub fn cancel_deadline<T: Send + 'static>(&mut self, id: DeadlineId) {
126        self.buffer.push(move |world: &mut World| {
127            world.cancel_deadline::<T>(id);
128        });
129    }
130
131    /// Returns the number of queued commands.
132    pub fn len(&self) -> usize {
133        self.buffer.len()
134    }
135
136    /// Returns `true` if no commands are queued.
137    pub fn is_empty(&self) -> bool {
138        self.buffer.is_empty()
139    }
140}
141
142// =============================================================================
143// SystemParam implementation
144// =============================================================================
145
146// SAFETY: access() reports a unique marker type. fetch() only touches the
147// command buffer field, which no other SystemParam accesses. The buffer is
148// a separate field from resources and archetypes.
149unsafe impl crate::system_param::SystemParam for Commands<'_> {
150    type Item<'w> = Commands<'w>;
151
152    fn access() -> Vec<Access> {
153        // Use the CommandBuffer TypeId as a marker. This prevents two Commands
154        // params in the same system (which would alias) while not conflicting
155        // with any Res/ResMut/Query/QueryMut.
156        vec![Access::ResWrite(TypeId::of::<CommandBuffer>())]
157    }
158
159    unsafe fn fetch<'w>(world: UnsafeWorldCell) -> Commands<'w> {
160        Commands {
161            buffer: unsafe { world.commands_mut() },
162        }
163    }
164}
165
166// =============================================================================
167// Tests
168// =============================================================================
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::component::Component;
174    use crate::system_param::SystemParam;
175
176    #[derive(Debug, Clone, PartialEq)]
177    struct Pos {
178        x: f32,
179        y: f32,
180    }
181    impl Component for Pos {}
182
183    #[derive(Debug, Clone, PartialEq)]
184    struct Vel {
185        x: f32,
186        y: f32,
187    }
188    impl Component for Vel {}
189
190    #[allow(dead_code)]
191    #[derive(Debug, Clone, PartialEq)]
192    struct Health(i32);
193    impl Component for Health {}
194
195    // -- CommandBuffer --
196
197    #[test]
198    fn command_buffer_starts_empty() {
199        let buf = CommandBuffer::new();
200        assert!(buf.is_empty());
201        assert_eq!(buf.len(), 0);
202    }
203
204    #[test]
205    fn command_buffer_tracks_length() {
206        let mut buf = CommandBuffer::new();
207        buf.push(|_| {});
208        buf.push(|_| {});
209        assert_eq!(buf.len(), 2);
210        assert!(!buf.is_empty());
211    }
212
213    #[test]
214    fn command_buffer_take_drains_all() {
215        let mut buf = CommandBuffer::new();
216        buf.push(|world: &mut World| {
217            world.spawn((Pos { x: 1.0, y: 2.0 },));
218        });
219        buf.push(|world: &mut World| {
220            world.spawn((Pos { x: 3.0, y: 4.0 },));
221        });
222
223        let mut world = World::new();
224        let commands = buf.take();
225        assert!(buf.is_empty());
226        for cmd in commands {
227            cmd(&mut world);
228        }
229        assert_eq!(world.entity_count(), 2);
230    }
231
232    // -- Commands typed API --
233
234    #[test]
235    fn commands_spawn_deferred() {
236        let mut world = World::new();
237        assert_eq!(world.entity_count(), 0);
238
239        // Queue a spawn via Commands.
240        {
241            let buf = world.command_buffer_mut();
242            let mut cmds = Commands { buffer: buf };
243            cmds.spawn((Pos { x: 1.0, y: 2.0 },));
244        }
245
246        // Not spawned yet.
247        assert_eq!(world.entity_count(), 0);
248
249        // Apply commands.
250        world.apply_commands();
251        assert_eq!(world.entity_count(), 1);
252
253        let xs: Vec<f32> = world.query::<&Pos>().map(|(_, p)| p.x).collect();
254        assert_eq!(xs, vec![1.0]);
255    }
256
257    #[test]
258    fn commands_despawn_deferred() {
259        let mut world = World::new();
260        let e = world.spawn((Pos { x: 1.0, y: 2.0 },));
261
262        {
263            let buf = world.command_buffer_mut();
264            let mut cmds = Commands { buffer: buf };
265            cmds.despawn(e);
266        }
267
268        // Still alive until apply.
269        assert!(world.is_alive(e));
270
271        world.apply_commands();
272        assert!(!world.is_alive(e));
273    }
274
275    #[test]
276    fn commands_insert_deferred() {
277        let mut world = World::new();
278        let e = world.spawn((Pos { x: 1.0, y: 2.0 },));
279
280        {
281            let buf = world.command_buffer_mut();
282            let mut cmds = Commands { buffer: buf };
283            cmds.insert(e, Vel { x: 3.0, y: 4.0 });
284        }
285
286        // Vel not yet present.
287        assert!(world.get::<Vel>(e).is_none());
288
289        world.apply_commands();
290        assert_eq!(world.get::<Vel>(e).unwrap().x, 3.0);
291        // Pos preserved.
292        assert_eq!(world.get::<Pos>(e).unwrap().x, 1.0);
293    }
294
295    #[test]
296    fn commands_remove_deferred() {
297        let mut world = World::new();
298        let e = world.spawn((Pos { x: 1.0, y: 2.0 }, Vel { x: 3.0, y: 4.0 }));
299
300        {
301            let buf = world.command_buffer_mut();
302            let mut cmds = Commands { buffer: buf };
303            cmds.remove::<Vel>(e);
304        }
305
306        // Vel still present until apply.
307        assert!(world.get::<Vel>(e).is_some());
308
309        world.apply_commands();
310        assert!(world.get::<Vel>(e).is_none());
311        assert_eq!(world.get::<Pos>(e).unwrap().x, 1.0);
312    }
313
314    #[test]
315    fn commands_multiple_ops_applied_in_order() {
316        let mut world = World::new();
317        let e = world.spawn((Pos { x: 1.0, y: 2.0 },));
318
319        {
320            let buf = world.command_buffer_mut();
321            let mut cmds = Commands { buffer: buf };
322            // Insert then remove — net effect is no Vel.
323            cmds.insert(e, Vel { x: 10.0, y: 20.0 });
324            cmds.remove::<Vel>(e);
325        }
326
327        world.apply_commands();
328        assert!(world.get::<Vel>(e).is_none());
329    }
330
331    #[test]
332    fn commands_on_dead_entity_is_safe() {
333        let mut world = World::new();
334        let e = world.spawn((Pos { x: 1.0, y: 2.0 },));
335        world.despawn(e);
336
337        {
338            let buf = world.command_buffer_mut();
339            let mut cmds = Commands { buffer: buf };
340            cmds.insert(e, Vel { x: 1.0, y: 1.0 });
341            cmds.despawn(e);
342            cmds.remove::<Pos>(e);
343        }
344
345        // Should not panic.
346        world.apply_commands();
347    }
348
349    #[test]
350    fn commands_spawn_multi_component() {
351        let mut world = World::new();
352
353        {
354            let buf = world.command_buffer_mut();
355            let mut cmds = Commands { buffer: buf };
356            cmds.spawn((Pos { x: 1.0, y: 2.0 }, Vel { x: 3.0, y: 4.0 }));
357        }
358
359        world.apply_commands();
360        assert_eq!(world.entity_count(), 1);
361
362        let results: Vec<_> = world.query::<(&Pos, &Vel)>().collect();
363        assert_eq!(results.len(), 1);
364        assert_eq!(results[0].1.0.x, 1.0);
365        assert_eq!(results[0].1.1.x, 3.0);
366    }
367
368    // -- SystemParam integration --
369
370    #[test]
371    fn commands_access_uses_command_buffer_marker() {
372        let access = <Commands<'_> as SystemParam>::access();
373        assert_eq!(access.len(), 1);
374        assert_eq!(access[0], Access::ResWrite(TypeId::of::<CommandBuffer>()));
375    }
376
377    #[test]
378    fn commands_does_not_conflict_with_res() {
379        use crate::system_param::{Res, has_conflicts};
380        let a = <Commands<'_> as SystemParam>::access();
381        let b = <Res<'_, i32> as SystemParam>::access();
382        assert!(!has_conflicts(&a, &b));
383    }
384
385    #[test]
386    fn commands_does_not_conflict_with_query() {
387        use crate::system_param::{Query, has_conflicts};
388        let a = <Commands<'_> as SystemParam>::access();
389        let b = <Query<'_, Pos> as SystemParam>::access();
390        assert!(!has_conflicts(&a, &b));
391    }
392
393    #[test]
394    fn commands_fetch_via_unsafe_world_cell() {
395        let mut world = World::new();
396        let cell = unsafe { UnsafeWorldCell::new(&mut world as *mut World) };
397        unsafe {
398            let mut cmds: Commands<'_> = <Commands<'_> as SystemParam>::fetch(cell);
399            cmds.spawn((Pos { x: 42.0, y: 0.0 },));
400        }
401        world.apply_commands();
402        assert_eq!(world.entity_count(), 1);
403    }
404}