Skip to main content

cougr_core/
commands.rs

1use crate::component::ComponentStorage;
2use crate::simple_world::{EntityId, SimpleWorld};
3use alloc::vec::Vec;
4use soroban_sdk::{Bytes, Symbol};
5
6/// The kind of structural change to apply.
7enum CommandKind {
8    Spawn,
9    Despawn,
10    AddComponent,
11    RemoveComponent,
12}
13
14/// A single queued structural change.
15struct CommandEntry {
16    kind: CommandKind,
17    entity_id: Option<EntityId>,
18    component_type: Option<Symbol>,
19    data: Option<Bytes>,
20    storage: ComponentStorage,
21}
22
23/// A deferred command queue for safe structural changes during system iteration.
24///
25/// Instead of mutating the world directly while iterating entities,
26/// systems can queue commands and apply them after iteration completes.
27///
28/// # Example
29/// ```
30/// use cougr_core::commands::CommandQueue;
31/// use cougr_core::simple_world::SimpleWorld;
32/// use soroban_sdk::{symbol_short, Bytes, Env};
33///
34/// let env = Env::default();
35/// let mut world = SimpleWorld::new(&env);
36/// let mut commands = CommandQueue::new();
37///
38/// commands.spawn();
39/// let spawned = commands.apply(&mut world);
40/// commands = CommandQueue::new();
41/// commands.add_component(spawned[0], symbol_short!("bullet"), Bytes::new(&env));
42/// commands.apply(&mut world);
43///
44/// assert!(world.has_component(spawned[0], &symbol_short!("bullet")));
45/// ```
46pub struct CommandQueue {
47    commands: Vec<CommandEntry>,
48}
49
50impl CommandQueue {
51    /// Create an empty command queue.
52    pub fn new() -> Self {
53        Self {
54            commands: Vec::new(),
55        }
56    }
57
58    /// Queue a new entity spawn. The entity ID is assigned when `apply()` is called.
59    pub fn spawn(&mut self) {
60        self.commands.push(CommandEntry {
61            kind: CommandKind::Spawn,
62            entity_id: None,
63            component_type: None,
64            data: None,
65            storage: ComponentStorage::Table,
66        });
67    }
68
69    /// Queue an entity despawn.
70    pub fn despawn(&mut self, entity_id: EntityId) {
71        self.commands.push(CommandEntry {
72            kind: CommandKind::Despawn,
73            entity_id: Some(entity_id),
74            component_type: None,
75            data: None,
76            storage: ComponentStorage::Table,
77        });
78    }
79
80    /// Queue adding a component (Table storage) to an entity.
81    pub fn add_component(&mut self, entity_id: EntityId, component_type: Symbol, data: Bytes) {
82        self.commands.push(CommandEntry {
83            kind: CommandKind::AddComponent,
84            entity_id: Some(entity_id),
85            component_type: Some(component_type),
86            data: Some(data),
87            storage: ComponentStorage::Table,
88        });
89    }
90
91    /// Queue adding a component with Sparse storage to an entity.
92    pub fn add_sparse_component(
93        &mut self,
94        entity_id: EntityId,
95        component_type: Symbol,
96        data: Bytes,
97    ) {
98        self.commands.push(CommandEntry {
99            kind: CommandKind::AddComponent,
100            entity_id: Some(entity_id),
101            component_type: Some(component_type),
102            data: Some(data),
103            storage: ComponentStorage::Sparse,
104        });
105    }
106
107    /// Queue removing a component from an entity.
108    pub fn remove_component(&mut self, entity_id: EntityId, component_type: Symbol) {
109        self.commands.push(CommandEntry {
110            kind: CommandKind::RemoveComponent,
111            entity_id: Some(entity_id),
112            component_type: Some(component_type),
113            data: None,
114            storage: ComponentStorage::Table,
115        });
116    }
117
118    /// Apply all queued commands to the world in order.
119    ///
120    /// Returns the IDs of any spawned entities (in spawn order).
121    /// Consumes the queue.
122    pub fn apply(self, world: &mut SimpleWorld) -> Vec<EntityId> {
123        let mut spawned_ids = Vec::new();
124
125        for entry in self.commands {
126            match entry.kind {
127                CommandKind::Spawn => {
128                    let id = world.spawn_entity();
129                    spawned_ids.push(id);
130                }
131                CommandKind::Despawn => {
132                    if let Some(entity_id) = entry.entity_id {
133                        world.despawn_entity(entity_id);
134                    }
135                }
136                CommandKind::AddComponent => {
137                    if let (Some(entity_id), Some(component_type), Some(data)) =
138                        (entry.entity_id, entry.component_type, entry.data)
139                    {
140                        world.add_component_with_storage(
141                            entity_id,
142                            component_type,
143                            data,
144                            entry.storage,
145                        );
146                    }
147                }
148                CommandKind::RemoveComponent => {
149                    if let (Some(entity_id), Some(component_type)) =
150                        (entry.entity_id, entry.component_type)
151                    {
152                        world.remove_component(entity_id, &component_type);
153                    }
154                }
155            }
156        }
157
158        spawned_ids
159    }
160
161    /// Returns whether the queue is empty.
162    pub fn is_empty(&self) -> bool {
163        self.commands.is_empty()
164    }
165
166    /// Returns the number of queued commands.
167    pub fn len(&self) -> usize {
168        self.commands.len()
169    }
170}
171
172impl Default for CommandQueue {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use soroban_sdk::{symbol_short, Env};
182
183    #[test]
184    fn test_empty_queue() {
185        let queue = CommandQueue::new();
186        assert!(queue.is_empty());
187        assert_eq!(queue.len(), 0);
188
189        let env = Env::default();
190        let mut world = SimpleWorld::new(&env);
191        let spawned = queue.apply(&mut world);
192        assert!(spawned.is_empty());
193    }
194
195    #[test]
196    fn test_spawn_via_queue() {
197        let env = Env::default();
198        let mut world = SimpleWorld::new(&env);
199        let mut queue = CommandQueue::new();
200
201        queue.spawn();
202        queue.spawn();
203        assert_eq!(queue.len(), 2);
204
205        let spawned = queue.apply(&mut world);
206        assert_eq!(spawned.len(), 2);
207        assert_eq!(spawned[0], 1);
208        assert_eq!(spawned[1], 2);
209    }
210
211    #[test]
212    fn test_despawn_via_queue() {
213        let env = Env::default();
214        let mut world = SimpleWorld::new(&env);
215        let e1 = world.spawn_entity();
216        let data = Bytes::from_array(&env, &[1]);
217        world.add_component(e1, symbol_short!("pos"), data);
218
219        let mut queue = CommandQueue::new();
220        queue.despawn(e1);
221        queue.apply(&mut world);
222
223        assert!(!world.has_component(e1, &symbol_short!("pos")));
224    }
225
226    #[test]
227    fn test_add_component_via_queue() {
228        let env = Env::default();
229        let mut world = SimpleWorld::new(&env);
230        let e1 = world.spawn_entity();
231
232        let mut queue = CommandQueue::new();
233        let data = Bytes::from_array(&env, &[1, 2, 3]);
234        queue.add_component(e1, symbol_short!("pos"), data.clone());
235        queue.apply(&mut world);
236
237        assert!(world.has_component(e1, &symbol_short!("pos")));
238        assert_eq!(world.get_component(e1, &symbol_short!("pos")), Some(data));
239    }
240
241    #[test]
242    fn test_add_sparse_component_via_queue() {
243        let env = Env::default();
244        let mut world = SimpleWorld::new(&env);
245        let e1 = world.spawn_entity();
246
247        let mut queue = CommandQueue::new();
248        let data = Bytes::from_array(&env, &[0xAA]);
249        queue.add_sparse_component(e1, symbol_short!("tag"), data.clone());
250        queue.apply(&mut world);
251
252        assert!(world.has_component(e1, &symbol_short!("tag")));
253        assert_eq!(world.table_component_count(&symbol_short!("tag")), 0);
254        assert_eq!(world.component_count(&symbol_short!("tag")), 1);
255    }
256
257    #[test]
258    fn test_remove_component_via_queue() {
259        let env = Env::default();
260        let mut world = SimpleWorld::new(&env);
261        let e1 = world.spawn_entity();
262        let data = Bytes::from_array(&env, &[1]);
263        world.add_component(e1, symbol_short!("pos"), data);
264
265        let mut queue = CommandQueue::new();
266        queue.remove_component(e1, symbol_short!("pos"));
267        queue.apply(&mut world);
268
269        assert!(!world.has_component(e1, &symbol_short!("pos")));
270    }
271
272    #[test]
273    fn test_mixed_operations() {
274        let env = Env::default();
275        let mut world = SimpleWorld::new(&env);
276
277        // Pre-existing entity
278        let e1 = world.spawn_entity();
279        let data = Bytes::from_array(&env, &[1]);
280        world.add_component(e1, symbol_short!("old"), data);
281
282        let mut queue = CommandQueue::new();
283        // Queue: spawn new entity, add component to e1, remove "old" from e1
284        queue.spawn();
285        let new_data = Bytes::from_array(&env, &[2, 3]);
286        queue.add_component(e1, symbol_short!("new"), new_data.clone());
287        queue.remove_component(e1, symbol_short!("old"));
288
289        let spawned = queue.apply(&mut world);
290        assert_eq!(spawned.len(), 1);
291        assert_eq!(spawned[0], 2); // second entity
292        assert!(world.has_component(e1, &symbol_short!("new")));
293        assert!(!world.has_component(e1, &symbol_short!("old")));
294    }
295
296    #[test]
297    fn test_queue_len_tracking() {
298        let env = Env::default();
299        let mut queue = CommandQueue::new();
300        assert_eq!(queue.len(), 0);
301        assert!(queue.is_empty());
302
303        queue.spawn();
304        assert_eq!(queue.len(), 1);
305        assert!(!queue.is_empty());
306
307        let data = Bytes::from_array(&env, &[1]);
308        queue.add_component(1, symbol_short!("test"), data);
309        assert_eq!(queue.len(), 2);
310
311        queue.remove_component(1, symbol_short!("test"));
312        assert_eq!(queue.len(), 3);
313
314        queue.despawn(1);
315        assert_eq!(queue.len(), 4);
316    }
317}