Skip to main content

cougr_core/
ecs.rs

1//! Curated ECS runtime surface.
2//!
3//! This module defines the stable conceptual model for Cougr's Soroban-first ECS path:
4//!
5//! - `SimpleWorld` and `ArchetypeWorld` are the supported runtime backends
6//! - `GameApp` is the recommended orchestration entrypoint
7//! - `SimpleQuery` is the default query model for Soroban gameplay loops
8//! - `CommandQueue` is the deferred structural mutation primitive
9
10use crate::archetype_world::ArchetypeWorld;
11use crate::component::ComponentTrait;
12use crate::query::QueryStorage;
13use crate::simple_world::{EntityId, SimpleWorld};
14use soroban_sdk::{Env, Symbol, Vec};
15
16/// Supported runtime backends for Cougr's ECS layer.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum WorldBackend {
19    Simple,
20    Archetype,
21}
22
23/// Shared read-only runtime contract for Cougr world backends.
24///
25/// This trait does not attempt to erase every difference between `SimpleWorld`
26/// and `ArchetypeWorld`. It captures the stable overlap that gameplay systems
27/// and tooling can rely on without committing to internal storage details.
28pub trait RuntimeWorld {
29    fn backend(&self) -> WorldBackend;
30    fn entity_count(&self) -> usize;
31    fn version(&self) -> u64;
32    fn has_component(&self, entity_id: EntityId, component_type: &Symbol) -> bool;
33    fn entities_with_component(
34        &self,
35        component_type: &Symbol,
36        storage: QueryStorage,
37        env: &Env,
38    ) -> Vec<EntityId>;
39}
40
41/// Shared mutable runtime contract for Cougr's Soroban-first world backends.
42///
43/// This trait represents the stable gameplay mutation surface common to
44/// `SimpleWorld` and `ArchetypeWorld`. It is intentionally narrower than the
45/// full implementation of either backend.
46pub trait RuntimeWorldMut: RuntimeWorld {
47    fn spawn_entity(&mut self) -> EntityId;
48    fn despawn_entity(&mut self, entity_id: EntityId, env: &Env);
49    fn get_component(
50        &self,
51        entity_id: EntityId,
52        component_type: &Symbol,
53    ) -> Option<soroban_sdk::Bytes>;
54    fn add_component(
55        &mut self,
56        entity_id: EntityId,
57        component_type: Symbol,
58        data: soroban_sdk::Bytes,
59        env: &Env,
60    );
61    fn remove_component(&mut self, entity_id: EntityId, component_type: &Symbol, env: &Env)
62        -> bool;
63
64    fn get_typed<T: ComponentTrait>(&self, env: &Env, entity_id: EntityId) -> Option<T> {
65        let bytes = self.get_component(entity_id, &T::component_type())?;
66        T::deserialize(env, &bytes)
67    }
68
69    fn set_typed<T: ComponentTrait>(&mut self, env: &Env, entity_id: EntityId, component: &T) {
70        self.add_component(
71            entity_id,
72            T::component_type(),
73            component.serialize(env),
74            env,
75        );
76    }
77
78    fn has_typed<T: ComponentTrait>(&self, entity_id: EntityId) -> bool {
79        self.has_component(entity_id, &T::component_type())
80    }
81
82    fn remove_typed<T: ComponentTrait>(&mut self, env: &Env, entity_id: EntityId) -> bool {
83        self.remove_component(entity_id, &T::component_type(), env)
84    }
85}
86
87impl RuntimeWorld for SimpleWorld {
88    fn backend(&self) -> WorldBackend {
89        WorldBackend::Simple
90    }
91
92    fn entity_count(&self) -> usize {
93        self.entity_components.len().try_into().unwrap()
94    }
95
96    fn version(&self) -> u64 {
97        self.version()
98    }
99
100    fn has_component(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
101        self.has_component(entity_id, component_type)
102    }
103
104    fn entities_with_component(
105        &self,
106        component_type: &Symbol,
107        storage: QueryStorage,
108        env: &Env,
109    ) -> Vec<EntityId> {
110        match storage {
111            QueryStorage::Table => self.get_table_entities_with_component(component_type, env),
112            QueryStorage::Any => self.get_all_entities_with_component(component_type, env),
113        }
114    }
115}
116
117impl RuntimeWorldMut for SimpleWorld {
118    fn spawn_entity(&mut self) -> EntityId {
119        SimpleWorld::spawn_entity(self)
120    }
121
122    fn despawn_entity(&mut self, entity_id: EntityId, _env: &Env) {
123        SimpleWorld::despawn_entity(self, entity_id);
124    }
125
126    fn get_component(
127        &self,
128        entity_id: EntityId,
129        component_type: &Symbol,
130    ) -> Option<soroban_sdk::Bytes> {
131        SimpleWorld::get_component(self, entity_id, component_type)
132    }
133
134    fn add_component(
135        &mut self,
136        entity_id: EntityId,
137        component_type: Symbol,
138        data: soroban_sdk::Bytes,
139        _env: &Env,
140    ) {
141        SimpleWorld::add_component(self, entity_id, component_type, data);
142    }
143
144    fn remove_component(
145        &mut self,
146        entity_id: EntityId,
147        component_type: &Symbol,
148        _env: &Env,
149    ) -> bool {
150        SimpleWorld::remove_component(self, entity_id, component_type)
151    }
152}
153
154impl RuntimeWorld for ArchetypeWorld {
155    fn backend(&self) -> WorldBackend {
156        WorldBackend::Archetype
157    }
158
159    fn entity_count(&self) -> usize {
160        self.entity_archetype.len().try_into().unwrap()
161    }
162
163    fn version(&self) -> u64 {
164        self.version()
165    }
166
167    fn has_component(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
168        self.has_component(entity_id, component_type)
169    }
170
171    fn entities_with_component(
172        &self,
173        component_type: &Symbol,
174        _storage: QueryStorage,
175        env: &Env,
176    ) -> Vec<EntityId> {
177        self.query(core::slice::from_ref(component_type), env)
178    }
179}
180
181impl RuntimeWorldMut for ArchetypeWorld {
182    fn spawn_entity(&mut self) -> EntityId {
183        ArchetypeWorld::spawn_entity(self)
184    }
185
186    fn despawn_entity(&mut self, entity_id: EntityId, env: &Env) {
187        ArchetypeWorld::despawn_entity(self, entity_id, env);
188    }
189
190    fn get_component(
191        &self,
192        entity_id: EntityId,
193        component_type: &Symbol,
194    ) -> Option<soroban_sdk::Bytes> {
195        ArchetypeWorld::get_component(self, entity_id, component_type)
196    }
197
198    fn add_component(
199        &mut self,
200        entity_id: EntityId,
201        component_type: Symbol,
202        data: soroban_sdk::Bytes,
203        env: &Env,
204    ) {
205        ArchetypeWorld::add_component(self, entity_id, component_type, data, env);
206    }
207
208    fn remove_component(
209        &mut self,
210        entity_id: EntityId,
211        component_type: &Symbol,
212        env: &Env,
213    ) -> bool {
214        ArchetypeWorld::remove_component(self, entity_id, component_type, env)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::component::Position;
222
223    fn exercise_runtime_mut<W: RuntimeWorldMut>(world: &mut W, env: &Env) {
224        let entity = world.spawn_entity();
225        world.set_typed(env, entity, &Position::new(4, 5));
226        assert!(world.has_typed::<Position>(entity));
227        let pos: Position = world.get_typed(env, entity).unwrap();
228        assert_eq!(pos.x, 4);
229        assert!(world.remove_typed::<Position>(env, entity));
230        world.despawn_entity(entity, env);
231    }
232
233    #[test]
234    fn runtime_world_mut_supports_simple_world() {
235        let env = Env::default();
236        let mut world = SimpleWorld::new(&env);
237        exercise_runtime_mut(&mut world, &env);
238        assert_eq!(world.backend(), WorldBackend::Simple);
239    }
240
241    #[test]
242    fn runtime_world_mut_supports_archetype_world() {
243        let env = Env::default();
244        let mut world = ArchetypeWorld::new(&env);
245        exercise_runtime_mut(&mut world, &env);
246        assert_eq!(world.backend(), WorldBackend::Archetype);
247    }
248}