Skip to main content

cougr_core/simple_world/
mod.rs

1mod indexing;
2#[cfg(test)]
3mod tests;
4
5use crate::component::{ComponentStorage, ComponentTrait};
6use soroban_sdk::{contracttype, Bytes, Env, Map, Symbol, Vec};
7
8/// Simple entity ID type for Soroban-optimized ECS.
9pub type EntityId = u32;
10
11/// Simplified game world optimized for Soroban on-chain storage.
12///
13/// Uses `Map`-based storage for O(log n) component lookups instead of
14/// linear scans. This is the recommended ECS container for Soroban contracts.
15///
16/// ## Dual-Map storage
17///
18/// Components are split into two maps based on their `ComponentStorage` kind:
19/// - **Table** (`components`): Frequently-iterated components (e.g., Position, Velocity).
20///   Queried by `get_entities_with_component()`.
21/// - **Sparse** (`sparse_components`): Infrequently-accessed marker or tag components.
22///   Not included in the default entity query; use `get_all_entities_with_component()` to include them.
23///
24/// Both maps are transparent to `get_component()`, `has_component()`, and `remove_component()`.
25///
26/// # Example
27/// ```
28/// use cougr_core::component::ComponentStorage;
29/// use cougr_core::simple_world::SimpleWorld;
30/// use soroban_sdk::{symbol_short, Bytes, Env};
31///
32/// let env = Env::default();
33/// let mut world = SimpleWorld::new(&env);
34/// let entity_id = world.spawn_entity();
35/// world.add_component(entity_id, symbol_short!("position"), Bytes::new(&env));
36/// world.add_component_with_storage(
37///     entity_id,
38///     symbol_short!("marker"),
39///     Bytes::new(&env),
40///     ComponentStorage::Sparse,
41/// );
42/// assert!(world.has_component(entity_id, &symbol_short!("position")));
43/// ```
44#[contracttype]
45#[derive(Clone, Debug)]
46pub struct SimpleWorld {
47    pub(crate) next_entity_id: u32,
48    /// Table component data keyed by (entity_id, component_type).
49    pub(crate) components: Map<(u32, Symbol), Bytes>,
50    /// Sparse component data keyed by (entity_id, component_type).
51    pub(crate) sparse_components: Map<(u32, Symbol), Bytes>,
52    /// Tracks which component types each entity has.
53    pub(crate) entity_components: Map<u32, Vec<Symbol>>,
54    /// Direct index for frequently queried table-backed components.
55    pub(crate) table_index: Map<Symbol, Vec<u32>>,
56    /// Direct index for all components regardless of backing storage.
57    pub(crate) all_index: Map<Symbol, Vec<u32>>,
58    /// Version counter incremented on structural changes (add/remove/despawn).
59    /// Used for query cache invalidation.
60    pub(crate) version: u64,
61}
62
63impl SimpleWorld {
64    pub fn new(env: &Env) -> Self {
65        Self {
66            next_entity_id: 1,
67            components: Map::new(env),
68            sparse_components: Map::new(env),
69            entity_components: Map::new(env),
70            table_index: Map::new(env),
71            all_index: Map::new(env),
72            version: 0,
73        }
74    }
75
76    /// Returns the current world version for cache invalidation.
77    pub fn version(&self) -> u64 {
78        self.version
79    }
80
81    /// Returns the next entity ID that will be assigned on spawn.
82    pub fn next_entity_id(&self) -> EntityId {
83        self.next_entity_id
84    }
85
86    /// Returns the environment backing this world storage.
87    pub fn env(&self) -> &Env {
88        self.components.env()
89    }
90
91    pub fn spawn_entity(&mut self) -> EntityId {
92        let id = self.next_entity_id;
93        self.next_entity_id += 1;
94        id
95    }
96
97    fn has_component_in_table(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
98        self.components
99            .contains_key((entity_id, component_type.clone()))
100    }
101
102    fn has_component_in_sparse(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
103        self.sparse_components
104            .contains_key((entity_id, component_type.clone()))
105    }
106
107    /// Add a component using the default **Table** storage.
108    pub fn add_component(&mut self, entity_id: EntityId, component_type: Symbol, data: Bytes) {
109        self.add_component_with_storage(entity_id, component_type, data, ComponentStorage::Table);
110    }
111
112    /// Add a component, routing to the Table or Sparse map based on `storage`.
113    pub fn add_component_with_storage(
114        &mut self,
115        entity_id: EntityId,
116        component_type: Symbol,
117        data: Bytes,
118        storage: ComponentStorage,
119    ) {
120        self.version += 1;
121        let was_in_table = self.has_component_in_table(entity_id, &component_type);
122        let was_in_sparse = self.has_component_in_sparse(entity_id, &component_type);
123
124        match storage {
125            ComponentStorage::Table => {
126                self.components
127                    .set((entity_id, component_type.clone()), data);
128                if was_in_sparse {
129                    self.sparse_components
130                        .remove((entity_id, component_type.clone()));
131                }
132            }
133            ComponentStorage::Sparse => {
134                self.sparse_components
135                    .set((entity_id, component_type.clone()), data);
136                if was_in_table {
137                    self.components.remove((entity_id, component_type.clone()));
138                }
139            }
140        }
141
142        let mut types = self
143            .entity_components
144            .get(entity_id)
145            .unwrap_or_else(|| Vec::new(self.components.env()));
146
147        let mut found = false;
148        for i in 0..types.len() {
149            if let Some(t) = types.get(i) {
150                if t == component_type {
151                    found = true;
152                    break;
153                }
154            }
155        }
156        if !found {
157            types.push_back(component_type.clone());
158        }
159        self.entity_components.set(entity_id, types);
160
161        indexing::push_index(&mut self.all_index, &component_type, entity_id);
162        match storage {
163            ComponentStorage::Table => {
164                indexing::push_index(&mut self.table_index, &component_type, entity_id);
165            }
166            ComponentStorage::Sparse => {
167                indexing::remove_from_index(&mut self.table_index, &component_type, entity_id);
168            }
169        }
170    }
171
172    /// Get a component's data, checking both Table and Sparse maps transparently.
173    pub fn get_component(&self, entity_id: EntityId, component_type: &Symbol) -> Option<Bytes> {
174        self.components
175            .get((entity_id, component_type.clone()))
176            .or_else(|| {
177                self.sparse_components
178                    .get((entity_id, component_type.clone()))
179            })
180    }
181
182    /// Remove a component from both Table and Sparse maps transparently.
183    pub fn remove_component(&mut self, entity_id: EntityId, component_type: &Symbol) -> bool {
184        self.version += 1;
185        let removed = self
186            .components
187            .remove((entity_id, component_type.clone()))
188            .or_else(|| {
189                self.sparse_components
190                    .remove((entity_id, component_type.clone()))
191            });
192
193        if removed.is_some() {
194            if let Some(types) = self.entity_components.get(entity_id) {
195                let env = self.components.env();
196                let mut new_types = Vec::new(env);
197                for i in 0..types.len() {
198                    if let Some(t) = types.get(i) {
199                        if &t != component_type {
200                            new_types.push_back(t);
201                        }
202                    }
203                }
204                if new_types.is_empty() {
205                    self.entity_components.remove(entity_id);
206                } else {
207                    self.entity_components.set(entity_id, new_types);
208                }
209            }
210            indexing::remove_from_index(&mut self.all_index, component_type, entity_id);
211            indexing::remove_from_index(&mut self.table_index, component_type, entity_id);
212            true
213        } else {
214            false
215        }
216    }
217
218    /// Check if an entity has a component in either Table or Sparse storage.
219    pub fn has_component(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
220        self.has_component_in_table(entity_id, component_type)
221            || self.has_component_in_sparse(entity_id, component_type)
222    }
223
224    pub fn get_entities_with_component(&self, component_type: &Symbol, env: &Env) -> Vec<EntityId> {
225        self.table_index
226            .get(component_type.clone())
227            .unwrap_or_else(|| Vec::new(env))
228    }
229
230    /// Get entities that have the given component in **Table** storage only.
231    /// This is the fast path for querying frequently-iterated components.
232    pub fn get_table_entities_with_component(
233        &self,
234        component_type: &Symbol,
235        env: &Env,
236    ) -> Vec<EntityId> {
237        self.table_index
238            .get(component_type.clone())
239            .unwrap_or_else(|| Vec::new(env))
240    }
241
242    /// Get entities that have the given component in **either** Table or Sparse storage.
243    pub fn get_all_entities_with_component(
244        &self,
245        component_type: &Symbol,
246        env: &Env,
247    ) -> Vec<EntityId> {
248        self.all_index
249            .get(component_type.clone())
250            .unwrap_or_else(|| Vec::new(env))
251    }
252
253    /// Returns the number of entities indexed for a component in table storage only.
254    pub fn table_component_count(&self, component_type: &Symbol) -> usize {
255        self.table_index
256            .get(component_type.clone())
257            .map(|entities| entities.len())
258            .unwrap_or(0)
259            .try_into()
260            .unwrap()
261    }
262
263    /// Returns the number of entities indexed for a component across both storage classes.
264    pub fn component_count(&self, component_type: &Symbol) -> usize {
265        self.all_index
266            .get(component_type.clone())
267            .map(|entities| entities.len())
268            .unwrap_or(0)
269            .try_into()
270            .unwrap()
271    }
272
273    /// Get a component and deserialize it into the concrete type.
274    ///
275    /// # Example
276    /// ```
277    /// use cougr_core::component::Position;
278    /// use cougr_core::simple_world::SimpleWorld;
279    /// use soroban_sdk::Env;
280    ///
281    /// let env = Env::default();
282    /// let mut world = SimpleWorld::new(&env);
283    /// let entity_id = world.spawn_entity();
284    /// world.set_typed(&env, entity_id, &Position::new(10, 20));
285    /// let pos: Option<Position> = world.get_typed::<Position>(&env, entity_id);
286    /// assert_eq!(pos.unwrap().x, 10);
287    /// ```
288    pub fn get_typed<T: ComponentTrait>(&self, env: &Env, entity_id: EntityId) -> Option<T> {
289        let bytes = self.get_component(entity_id, &T::component_type())?;
290        T::deserialize(env, &bytes)
291    }
292
293    /// Serialize a component and store it, using the type's default storage kind.
294    ///
295    /// # Example
296    /// ```
297    /// use cougr_core::component::Position;
298    /// use cougr_core::simple_world::SimpleWorld;
299    /// use soroban_sdk::Env;
300    ///
301    /// let env = Env::default();
302    /// let mut world = SimpleWorld::new(&env);
303    /// let entity_id = world.spawn_entity();
304    /// world.set_typed(&env, entity_id, &Position::new(10, 20));
305    /// assert!(world.has_typed::<Position>(entity_id));
306    /// ```
307    pub fn set_typed<T: ComponentTrait>(&mut self, env: &Env, entity_id: EntityId, component: &T) {
308        let symbol = T::component_type();
309        let data = component.serialize(env);
310        let storage = T::default_storage();
311        self.add_component_with_storage(entity_id, symbol, data, storage);
312    }
313
314    /// Check if an entity has a component of the given type.
315    pub fn has_typed<T: ComponentTrait>(&self, entity_id: EntityId) -> bool {
316        self.has_component(entity_id, &T::component_type())
317    }
318
319    /// Remove a component of the given type from an entity.
320    pub fn remove_typed<T: ComponentTrait>(&mut self, entity_id: EntityId) -> bool {
321        self.remove_component(entity_id, &T::component_type())
322    }
323
324    pub fn despawn_entity(&mut self, entity_id: EntityId) {
325        self.version += 1;
326        if let Some(types) = self.entity_components.get(entity_id) {
327            for i in 0..types.len() {
328                if let Some(t) = types.get(i) {
329                    self.components.remove((entity_id, t.clone()));
330                    self.sparse_components.remove((entity_id, t.clone()));
331                    indexing::remove_from_index(&mut self.all_index, &t, entity_id);
332                    indexing::remove_from_index(&mut self.table_index, &t, entity_id);
333                }
334            }
335        }
336        self.entity_components.remove(entity_id);
337    }
338}