Skip to main content

cougr_core/
observers.rs

1use crate::simple_world::{EntityId, SimpleWorld};
2use alloc::vec::Vec;
3use soroban_sdk::{Bytes, Env, Symbol};
4
5/// The kind of component event that triggered an observer.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ComponentEventKind {
8    Added,
9    Removed,
10}
11
12/// A component event passed to observer functions.
13#[derive(Debug, Clone)]
14pub struct ComponentEvent {
15    pub entity_id: EntityId,
16    pub component_type: Symbol,
17    pub kind: ComponentEventKind,
18}
19
20/// Observer function signature.
21///
22/// Receives the event details plus read-only access to the world
23/// (after the mutation has been applied).
24pub type ObserverFn = fn(event: &ComponentEvent, world: &SimpleWorld, env: &Env);
25
26/// Registry of component observers that fire on component events.
27///
28/// Observers are runtime-only (not persisted) and must be re-registered
29/// each contract invocation. They provide reactive patterns where systems
30/// trigger immediately on component changes rather than polling.
31///
32/// # Example
33/// ```
34/// use cougr_core::runtime::{ComponentEvent, ObserverRegistry};
35/// use cougr_core::SimpleWorld;
36/// use soroban_sdk::{symbol_short, Env};
37///
38/// fn on_position_added(_event: &ComponentEvent, _world: &SimpleWorld, _env: &Env) {}
39///
40/// let mut registry = ObserverRegistry::new();
41/// registry.on_add(symbol_short!("pos"), on_position_added);
42/// assert_eq!(registry.observer_count(), 1);
43/// ```
44pub struct ObserverRegistry {
45    observers: Vec<(Symbol, ComponentEventKind, ObserverFn)>,
46}
47
48impl ObserverRegistry {
49    /// Create an empty observer registry.
50    pub fn new() -> Self {
51        Self {
52            observers: Vec::new(),
53        }
54    }
55
56    /// Register an observer that fires when a component of the given type is added.
57    pub fn on_add(&mut self, component_type: Symbol, observer: ObserverFn) {
58        self.observers
59            .push((component_type, ComponentEventKind::Added, observer));
60    }
61
62    /// Register an observer that fires when a component of the given type is removed.
63    pub fn on_remove(&mut self, component_type: Symbol, observer: ObserverFn) {
64        self.observers
65            .push((component_type, ComponentEventKind::Removed, observer));
66    }
67
68    /// Fire all matching observers for the given event.
69    pub fn fire(&self, event: &ComponentEvent, world: &SimpleWorld, env: &Env) {
70        for (ctype, kind, observer) in &self.observers {
71            if ctype == &event.component_type && kind == &event.kind {
72                observer(event, world, env);
73            }
74        }
75    }
76
77    /// Returns the total number of registered observers.
78    pub fn observer_count(&self) -> usize {
79        self.observers.len()
80    }
81}
82
83impl Default for ObserverRegistry {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89/// A wrapper around `SimpleWorld` that fires observers on component mutations.
90///
91/// Observers receive read-only access to the world **after** the mutation completes,
92/// allowing them to see the updated state.
93///
94/// # Example
95/// ```
96/// use cougr_core::runtime::{ComponentEvent, ObservedWorld};
97/// use cougr_core::SimpleWorld;
98/// use soroban_sdk::{symbol_short, Bytes, Env};
99///
100/// fn my_observer(_event: &ComponentEvent, _world: &SimpleWorld, _env: &Env) {}
101///
102/// let env = Env::default();
103/// let world = SimpleWorld::new(&env);
104/// let mut observed = ObservedWorld::new(world);
105/// let entity_id = observed.spawn_entity();
106/// observed.observers_mut().on_add(symbol_short!("pos"), my_observer);
107/// observed.add_component(entity_id, symbol_short!("pos"), Bytes::new(&env), &env);
108/// assert!(observed.world().has_component(entity_id, &symbol_short!("pos")));
109/// ```
110pub struct ObservedWorld {
111    world: SimpleWorld,
112    observers: ObserverRegistry,
113}
114
115impl ObservedWorld {
116    /// Wrap a `SimpleWorld` with an empty observer registry.
117    pub fn new(world: SimpleWorld) -> Self {
118        Self {
119            world,
120            observers: ObserverRegistry::new(),
121        }
122    }
123
124    /// Wrap a `SimpleWorld` with a pre-configured observer registry.
125    pub fn with_observers(world: SimpleWorld, observers: ObserverRegistry) -> Self {
126        Self { world, observers }
127    }
128
129    /// Access the underlying `SimpleWorld`.
130    pub fn world(&self) -> &SimpleWorld {
131        &self.world
132    }
133
134    /// Mutably access the underlying `SimpleWorld`.
135    pub fn world_mut(&mut self) -> &mut SimpleWorld {
136        &mut self.world
137    }
138
139    /// Access the observer registry.
140    pub fn observers(&self) -> &ObserverRegistry {
141        &self.observers
142    }
143
144    /// Mutably access the observer registry.
145    pub fn observers_mut(&mut self) -> &mut ObserverRegistry {
146        &mut self.observers
147    }
148
149    /// Consume the wrapper and return the inner `SimpleWorld`.
150    pub fn into_inner(self) -> SimpleWorld {
151        self.world
152    }
153
154    /// Spawn a new entity (delegates to `SimpleWorld`).
155    pub fn spawn_entity(&mut self) -> EntityId {
156        self.world.spawn_entity()
157    }
158
159    /// Add a component, firing `on_add` observers after insertion.
160    pub fn add_component(
161        &mut self,
162        entity_id: EntityId,
163        component_type: Symbol,
164        data: Bytes,
165        env: &Env,
166    ) {
167        self.world
168            .add_component(entity_id, component_type.clone(), data);
169        let event = ComponentEvent {
170            entity_id,
171            component_type,
172            kind: ComponentEventKind::Added,
173        };
174        self.observers.fire(&event, &self.world, env);
175    }
176
177    /// Remove a component, firing `on_remove` observers after removal.
178    pub fn remove_component(
179        &mut self,
180        entity_id: EntityId,
181        component_type: &Symbol,
182        env: &Env,
183    ) -> bool {
184        let removed = self.world.remove_component(entity_id, component_type);
185        if removed {
186            let event = ComponentEvent {
187                entity_id,
188                component_type: component_type.clone(),
189                kind: ComponentEventKind::Removed,
190            };
191            self.observers.fire(&event, &self.world, env);
192        }
193        removed
194    }
195
196    /// Get a component (delegates to `SimpleWorld`).
197    pub fn get_component(&self, entity_id: EntityId, component_type: &Symbol) -> Option<Bytes> {
198        self.world.get_component(entity_id, component_type)
199    }
200
201    /// Check if an entity has a component (delegates to `SimpleWorld`).
202    pub fn has_component(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
203        self.world.has_component(entity_id, component_type)
204    }
205
206    /// Despawn an entity, firing `on_remove` observers for each component.
207    pub fn despawn_entity(&mut self, entity_id: EntityId, env: &Env) {
208        if let Some(types) = self.world.entity_components.get(entity_id) {
209            for i in 0..types.len() {
210                if let Some(t) = types.get(i) {
211                    let event = ComponentEvent {
212                        entity_id,
213                        component_type: t,
214                        kind: ComponentEventKind::Removed,
215                    };
216                    self.observers.fire(&event, &self.world, env);
217                }
218            }
219        }
220        self.world.despawn_entity(entity_id);
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use core::sync::atomic::{AtomicU32, Ordering};
228    use soroban_sdk::symbol_short;
229
230    static ADD_COUNT: AtomicU32 = AtomicU32::new(0);
231    static REMOVE_COUNT: AtomicU32 = AtomicU32::new(0);
232
233    fn counting_add_observer(_event: &ComponentEvent, _world: &SimpleWorld, _env: &Env) {
234        ADD_COUNT.fetch_add(1, Ordering::Relaxed);
235    }
236
237    fn counting_remove_observer(_event: &ComponentEvent, _world: &SimpleWorld, _env: &Env) {
238        REMOVE_COUNT.fetch_add(1, Ordering::Relaxed);
239    }
240
241    fn noop_observer(_event: &ComponentEvent, _world: &SimpleWorld, _env: &Env) {}
242
243    #[test]
244    fn test_registry_new() {
245        let registry = ObserverRegistry::new();
246        assert_eq!(registry.observer_count(), 0);
247    }
248
249    #[test]
250    fn test_registry_register() {
251        let _env = Env::default();
252        let mut registry = ObserverRegistry::new();
253        registry.on_add(symbol_short!("pos"), noop_observer);
254        registry.on_remove(symbol_short!("pos"), noop_observer);
255        assert_eq!(registry.observer_count(), 2);
256    }
257
258    #[test]
259    fn test_observer_fires_on_add() {
260        let env = Env::default();
261        ADD_COUNT.store(0, Ordering::Relaxed);
262
263        let world = SimpleWorld::new(&env);
264        let mut observed = ObservedWorld::new(world);
265        observed
266            .observers_mut()
267            .on_add(symbol_short!("pos"), counting_add_observer);
268
269        let e1 = observed.spawn_entity();
270        let data = Bytes::from_array(&env, &[1, 2, 3]);
271        observed.add_component(e1, symbol_short!("pos"), data, &env);
272
273        assert_eq!(ADD_COUNT.load(Ordering::Relaxed), 1);
274        assert!(observed.has_component(e1, &symbol_short!("pos")));
275    }
276
277    #[test]
278    fn test_observer_fires_on_remove() {
279        let env = Env::default();
280        REMOVE_COUNT.store(0, Ordering::Relaxed);
281
282        let world = SimpleWorld::new(&env);
283        let mut observed = ObservedWorld::new(world);
284        observed
285            .observers_mut()
286            .on_remove(symbol_short!("pos"), counting_remove_observer);
287
288        let e1 = observed.spawn_entity();
289        let data = Bytes::from_array(&env, &[1]);
290        observed.add_component(e1, symbol_short!("pos"), data, &env);
291
292        assert!(observed.remove_component(e1, &symbol_short!("pos"), &env));
293        assert_eq!(REMOVE_COUNT.load(Ordering::Relaxed), 1);
294    }
295
296    #[test]
297    fn test_observer_not_fired_for_wrong_type() {
298        let env = Env::default();
299        ADD_COUNT.store(0, Ordering::Relaxed);
300
301        let world = SimpleWorld::new(&env);
302        let mut observed = ObservedWorld::new(world);
303        observed
304            .observers_mut()
305            .on_add(symbol_short!("vel"), counting_add_observer);
306
307        let e1 = observed.spawn_entity();
308        let data = Bytes::from_array(&env, &[1]);
309        // Add "pos" but observer is for "vel"
310        observed.add_component(e1, symbol_short!("pos"), data, &env);
311
312        assert_eq!(ADD_COUNT.load(Ordering::Relaxed), 0);
313    }
314
315    #[test]
316    fn test_observed_world_despawn() {
317        let env = Env::default();
318        let before = REMOVE_COUNT.load(Ordering::Relaxed);
319
320        let world = SimpleWorld::new(&env);
321        let mut observed = ObservedWorld::new(world);
322        observed
323            .observers_mut()
324            .on_remove(symbol_short!("a"), counting_remove_observer);
325        observed
326            .observers_mut()
327            .on_remove(symbol_short!("b"), counting_remove_observer);
328
329        let e1 = observed.spawn_entity();
330        let data = Bytes::from_array(&env, &[1]);
331        observed.add_component(e1, symbol_short!("a"), data.clone(), &env);
332        observed.add_component(e1, symbol_short!("b"), data, &env);
333
334        observed.despawn_entity(e1, &env);
335        // Both components should trigger remove observers
336        let after = REMOVE_COUNT.load(Ordering::Relaxed);
337        assert_eq!(after - before, 2);
338    }
339
340    #[test]
341    fn test_observed_world_into_inner() {
342        let env = Env::default();
343        let world = SimpleWorld::new(&env);
344        let mut observed = ObservedWorld::new(world);
345
346        let e1 = observed.spawn_entity();
347        let data = Bytes::from_array(&env, &[1]);
348        observed.add_component(e1, symbol_short!("test"), data, &env);
349
350        let inner = observed.into_inner();
351        assert!(inner.has_component(e1, &symbol_short!("test")));
352    }
353
354    #[test]
355    fn test_with_observers_constructor() {
356        let env = Env::default();
357        let world = SimpleWorld::new(&env);
358
359        let mut observers = ObserverRegistry::new();
360        observers.on_add(symbol_short!("pos"), noop_observer);
361
362        let observed = ObservedWorld::with_observers(world, observers);
363        assert_eq!(observed.observers().observer_count(), 1);
364    }
365}