Skip to main content

cougr_core/
hooks.rs

1use crate::simple_world::{EntityId, SimpleWorld};
2use alloc::vec::Vec;
3use soroban_sdk::{Bytes, Symbol};
4
5/// Callback invoked when a component is added to an entity.
6pub type OnAddHook = fn(entity_id: EntityId, component_type: &Symbol, data: &Bytes);
7
8/// Callback invoked when a component is removed from an entity.
9pub type OnRemoveHook = fn(entity_id: EntityId, component_type: &Symbol);
10
11/// Registry of component lifecycle hooks.
12///
13/// Hooks are runtime-only (not persisted to on-chain storage) and must be
14/// re-registered each contract invocation. They allow reactive patterns
15/// such as updating indexes or cleaning up related state.
16///
17/// # Example
18/// ```
19/// use cougr_core::runtime::HookRegistry;
20/// use soroban_sdk::{symbol_short, Bytes, Symbol};
21///
22/// fn on_add(_entity_id: u32, _ctype: &Symbol, _data: &Bytes) {}
23///
24/// let mut hooks = HookRegistry::new();
25/// hooks.on_add(symbol_short!("pos"), on_add);
26/// assert_eq!(hooks.add_hook_count(), 1);
27/// ```
28pub struct HookRegistry {
29    add_hooks: Vec<(Symbol, OnAddHook)>,
30    remove_hooks: Vec<(Symbol, OnRemoveHook)>,
31}
32
33impl HookRegistry {
34    /// Create an empty hook registry.
35    pub fn new() -> Self {
36        Self {
37            add_hooks: Vec::new(),
38            remove_hooks: Vec::new(),
39        }
40    }
41
42    /// Register a hook that fires when a component of the given type is added.
43    pub fn on_add(&mut self, component_type: Symbol, hook: OnAddHook) {
44        self.add_hooks.push((component_type, hook));
45    }
46
47    /// Register a hook that fires when a component of the given type is removed.
48    pub fn on_remove(&mut self, component_type: Symbol, hook: OnRemoveHook) {
49        self.remove_hooks.push((component_type, hook));
50    }
51
52    /// Fire all registered `on_add` hooks for the given component type.
53    pub fn fire_on_add(&self, entity_id: EntityId, component_type: &Symbol, data: &Bytes) {
54        for (ctype, hook) in &self.add_hooks {
55            if ctype == component_type {
56                hook(entity_id, component_type, data);
57            }
58        }
59    }
60
61    /// Fire all registered `on_remove` hooks for the given component type.
62    pub fn fire_on_remove(&self, entity_id: EntityId, component_type: &Symbol) {
63        for (ctype, hook) in &self.remove_hooks {
64            if ctype == component_type {
65                hook(entity_id, component_type);
66            }
67        }
68    }
69
70    /// Returns the number of registered add hooks.
71    pub fn add_hook_count(&self) -> usize {
72        self.add_hooks.len()
73    }
74
75    /// Returns the number of registered remove hooks.
76    pub fn remove_hook_count(&self) -> usize {
77        self.remove_hooks.len()
78    }
79}
80
81impl Default for HookRegistry {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87/// A wrapper around `SimpleWorld` that fires lifecycle hooks on component mutations.
88///
89/// Since `SimpleWorld` is a `#[contracttype]` struct and cannot hold function pointers,
90/// this wrapper carries a separate `HookRegistry` alongside the world.
91///
92/// # Example
93/// ```
94/// use cougr_core::runtime::HookedWorld;
95/// use cougr_core::SimpleWorld;
96/// use soroban_sdk::{symbol_short, Bytes, Env, Symbol};
97///
98/// fn on_add(_entity_id: u32, _ctype: &Symbol, _data: &Bytes) {}
99///
100/// let env = Env::default();
101/// let world = SimpleWorld::new(&env);
102/// let mut hooked = HookedWorld::new(world);
103/// let entity_id = hooked.spawn_entity();
104/// hooked.hooks_mut().on_add(symbol_short!("pos"), on_add);
105/// hooked.add_component(entity_id, symbol_short!("pos"), Bytes::new(&env));
106/// assert!(hooked.has_component(entity_id, &symbol_short!("pos")));
107/// ```
108pub struct HookedWorld {
109    world: SimpleWorld,
110    hooks: HookRegistry,
111}
112
113impl HookedWorld {
114    /// Wrap a `SimpleWorld` with an empty hook registry.
115    pub fn new(world: SimpleWorld) -> Self {
116        Self {
117            world,
118            hooks: HookRegistry::new(),
119        }
120    }
121
122    /// Wrap a `SimpleWorld` with a pre-configured hook registry.
123    pub fn with_hooks(world: SimpleWorld, hooks: HookRegistry) -> Self {
124        Self { world, hooks }
125    }
126
127    /// Access the underlying `SimpleWorld`.
128    pub fn world(&self) -> &SimpleWorld {
129        &self.world
130    }
131
132    /// Mutably access the underlying `SimpleWorld`.
133    pub fn world_mut(&mut self) -> &mut SimpleWorld {
134        &mut self.world
135    }
136
137    /// Access the hook registry.
138    pub fn hooks(&self) -> &HookRegistry {
139        &self.hooks
140    }
141
142    /// Mutably access the hook registry.
143    pub fn hooks_mut(&mut self) -> &mut HookRegistry {
144        &mut self.hooks
145    }
146
147    /// Consume the wrapper and return the inner `SimpleWorld`.
148    pub fn into_inner(self) -> SimpleWorld {
149        self.world
150    }
151
152    /// Spawn a new entity (delegates to `SimpleWorld`).
153    pub fn spawn_entity(&mut self) -> EntityId {
154        self.world.spawn_entity()
155    }
156
157    /// Add a component, firing `on_add` hooks after insertion.
158    pub fn add_component(&mut self, entity_id: EntityId, component_type: Symbol, data: Bytes) {
159        self.world
160            .add_component(entity_id, component_type.clone(), data.clone());
161        self.hooks.fire_on_add(entity_id, &component_type, &data);
162    }
163
164    /// Remove a component, firing `on_remove` hooks before removal.
165    pub fn remove_component(&mut self, entity_id: EntityId, component_type: &Symbol) -> bool {
166        self.hooks.fire_on_remove(entity_id, component_type);
167        self.world.remove_component(entity_id, component_type)
168    }
169
170    /// Get a component (delegates to `SimpleWorld`).
171    pub fn get_component(&self, entity_id: EntityId, component_type: &Symbol) -> Option<Bytes> {
172        self.world.get_component(entity_id, component_type)
173    }
174
175    /// Check if an entity has a component (delegates to `SimpleWorld`).
176    pub fn has_component(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
177        self.world.has_component(entity_id, component_type)
178    }
179
180    /// Despawn an entity, firing `on_remove` hooks for each component.
181    pub fn despawn_entity(&mut self, entity_id: EntityId) {
182        // Fire on_remove for each component before despawning
183        if let Some(types) = self.world.entity_components.get(entity_id) {
184            for i in 0..types.len() {
185                if let Some(t) = types.get(i) {
186                    self.hooks.fire_on_remove(entity_id, &t);
187                }
188            }
189        }
190        self.world.despawn_entity(entity_id);
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use soroban_sdk::{symbol_short, Env};
198
199    fn noop_add_hook(_entity_id: EntityId, _component_type: &Symbol, _data: &Bytes) {}
200    fn noop_remove_hook(_entity_id: EntityId, _component_type: &Symbol) {}
201
202    #[test]
203    fn test_hook_registry_new() {
204        let registry = HookRegistry::new();
205        assert_eq!(registry.add_hook_count(), 0);
206        assert_eq!(registry.remove_hook_count(), 0);
207    }
208
209    #[test]
210    fn test_hook_registry_register() {
211        let mut registry = HookRegistry::new();
212        registry.on_add(symbol_short!("pos"), noop_add_hook);
213        registry.on_remove(symbol_short!("pos"), noop_remove_hook);
214        assert_eq!(registry.add_hook_count(), 1);
215        assert_eq!(registry.remove_hook_count(), 1);
216    }
217
218    #[test]
219    fn test_hook_registry_multiple_hooks() {
220        let mut registry = HookRegistry::new();
221        registry.on_add(symbol_short!("pos"), noop_add_hook);
222        registry.on_add(symbol_short!("vel"), noop_add_hook);
223        registry.on_remove(symbol_short!("pos"), noop_remove_hook);
224        assert_eq!(registry.add_hook_count(), 2);
225        assert_eq!(registry.remove_hook_count(), 1);
226    }
227
228    #[test]
229    fn test_hooked_world_add_component() {
230        let env = Env::default();
231        let world = SimpleWorld::new(&env);
232        let mut hooked = HookedWorld::new(world);
233        hooked
234            .hooks_mut()
235            .on_add(symbol_short!("pos"), noop_add_hook);
236
237        let e1 = hooked.spawn_entity();
238        let data = Bytes::from_array(&env, &[1, 2, 3, 4]);
239        hooked.add_component(e1, symbol_short!("pos"), data.clone());
240
241        // Verify the component was actually added
242        assert!(hooked.has_component(e1, &symbol_short!("pos")));
243        assert_eq!(hooked.get_component(e1, &symbol_short!("pos")), Some(data));
244    }
245
246    #[test]
247    fn test_hooked_world_remove_component() {
248        let env = Env::default();
249        let world = SimpleWorld::new(&env);
250        let mut hooked = HookedWorld::new(world);
251        hooked
252            .hooks_mut()
253            .on_remove(symbol_short!("pos"), noop_remove_hook);
254
255        let e1 = hooked.spawn_entity();
256        let data = Bytes::from_array(&env, &[1, 2, 3, 4]);
257        hooked.add_component(e1, symbol_short!("pos"), data);
258
259        assert!(hooked.remove_component(e1, &symbol_short!("pos")));
260        assert!(!hooked.has_component(e1, &symbol_short!("pos")));
261    }
262
263    #[test]
264    fn test_hooked_world_despawn() {
265        let env = Env::default();
266        let world = SimpleWorld::new(&env);
267        let mut hooked = HookedWorld::new(world);
268        hooked
269            .hooks_mut()
270            .on_remove(symbol_short!("a"), noop_remove_hook);
271
272        let e1 = hooked.spawn_entity();
273        let data = Bytes::from_array(&env, &[1]);
274        hooked.add_component(e1, symbol_short!("a"), data.clone());
275        hooked.add_component(e1, symbol_short!("b"), data);
276
277        hooked.despawn_entity(e1);
278
279        assert!(!hooked.has_component(e1, &symbol_short!("a")));
280        assert!(!hooked.has_component(e1, &symbol_short!("b")));
281    }
282
283    #[test]
284    fn test_hooked_world_into_inner() {
285        let env = Env::default();
286        let world = SimpleWorld::new(&env);
287        let mut hooked = HookedWorld::new(world);
288
289        let e1 = hooked.spawn_entity();
290        let data = Bytes::from_array(&env, &[1]);
291        hooked.add_component(e1, symbol_short!("test"), data);
292
293        let inner = hooked.into_inner();
294        assert!(inner.has_component(e1, &symbol_short!("test")));
295    }
296
297    #[test]
298    fn test_hooked_world_with_hooks() {
299        let env = Env::default();
300        let world = SimpleWorld::new(&env);
301
302        let mut hooks = HookRegistry::new();
303        hooks.on_add(symbol_short!("pos"), noop_add_hook);
304
305        let hooked = HookedWorld::with_hooks(world, hooks);
306        assert_eq!(hooked.hooks().add_hook_count(), 1);
307    }
308}