intuicio_framework_ecs/
observer.rs

1use crate::{commands::CommandBuffer, entity::Entity, world::World, Component};
2use intuicio_data::type_hash::TypeHash;
3use std::collections::HashMap;
4
5#[derive(Default)]
6pub struct ChangeObserver {
7    pub commands: CommandBuffer,
8    #[allow(clippy::type_complexity)]
9    on_added:
10        HashMap<TypeHash, Vec<Box<dyn FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync>>>,
11    #[allow(clippy::type_complexity)]
12    on_removed:
13        HashMap<TypeHash, Vec<Box<dyn FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync>>>,
14    #[allow(clippy::type_complexity)]
15    on_updated:
16        HashMap<TypeHash, Vec<Box<dyn FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync>>>,
17}
18
19impl ChangeObserver {
20    pub fn on_added<T: Component>(
21        &mut self,
22        callback: impl FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync + 'static,
23    ) {
24        self.on_added_raw(TypeHash::of::<T>(), callback);
25    }
26
27    pub fn on_added_raw(
28        &mut self,
29        type_hash: TypeHash,
30        callback: impl FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync + 'static,
31    ) {
32        self.on_added
33            .entry(type_hash)
34            .or_default()
35            .push(Box::new(callback));
36    }
37
38    pub fn on_removed<T: Component>(
39        &mut self,
40        callback: impl FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync + 'static,
41    ) {
42        self.on_removed_raw(TypeHash::of::<T>(), callback);
43    }
44
45    pub fn on_removed_raw(
46        &mut self,
47        type_hash: TypeHash,
48        callback: impl FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync + 'static,
49    ) {
50        self.on_removed
51            .entry(type_hash)
52            .or_default()
53            .push(Box::new(callback));
54    }
55
56    pub fn on_updated<T: Component>(
57        &mut self,
58        callback: impl FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync + 'static,
59    ) {
60        self.on_updated_raw(TypeHash::of::<T>(), callback);
61    }
62
63    pub fn on_updated_raw(
64        &mut self,
65        type_hash: TypeHash,
66        callback: impl FnMut(&World, &mut CommandBuffer, Entity) + Send + Sync + 'static,
67    ) {
68        self.on_updated
69            .entry(type_hash)
70            .or_default()
71            .push(Box::new(callback));
72    }
73
74    pub fn process(&mut self, world: &mut World) {
75        for (entity, types) in world.added().iter() {
76            for type_hash in types {
77                if let Some(listeners) = self.on_added.get_mut(type_hash) {
78                    for listener in listeners {
79                        listener(world, &mut self.commands, entity);
80                    }
81                }
82            }
83        }
84        if let Some(updated) = world.updated() {
85            for (entity, types) in updated.iter() {
86                for type_hash in types {
87                    if let Some(listeners) = self.on_updated.get_mut(type_hash) {
88                        for listener in listeners {
89                            listener(world, &mut self.commands, entity);
90                        }
91                    }
92                }
93            }
94        }
95        for (entity, types) in world.removed().iter() {
96            for type_hash in types {
97                if let Some(listeners) = self.on_removed.get_mut(type_hash) {
98                    for listener in listeners {
99                        listener(world, &mut self.commands, entity);
100                    }
101                }
102            }
103        }
104    }
105
106    pub fn process_execute(&mut self, world: &mut World) {
107        self.process(world);
108        self.commands.execute(world);
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::{commands::DespawnCommand, world::World};
116    use std::sync::{Arc, RwLock};
117
118    #[test]
119    fn test_async() {
120        fn is_async<T: Send + Sync>() {}
121
122        is_async::<ChangeObserver>();
123    }
124
125    #[test]
126    fn test_change_observer() {
127        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
128        enum Phase {
129            None,
130            Added,
131            Updated,
132            Removed,
133        }
134
135        let phase = Arc::new(RwLock::new(Phase::None));
136        let phase1 = phase.clone();
137        let phase2 = phase.clone();
138        let phase3 = phase.clone();
139
140        let mut observer = ChangeObserver::default();
141        observer.on_added::<bool>(move |_, commands, entity| {
142            let phase1 = phase1.clone();
143            // normally you don't need to schedule this code, but it helps here in tests
144            // to test for separate phases. Without it you go from None to Updated phase.
145            commands.schedule(move |world| {
146                let mut access = world.get::<true, bool>(entity, true).unwrap();
147                let data = access.write().unwrap();
148                *data = !*data;
149                world.update::<bool>(entity);
150                *phase1.write().unwrap() = Phase::Added;
151            });
152        });
153        observer.on_updated::<bool>(move |_, commands, entity| {
154            commands.command(DespawnCommand::new(entity));
155            *phase2.write().unwrap() = Phase::Updated;
156        });
157        observer.on_removed::<bool>(move |_, _, _| {
158            *phase3.write().unwrap() = Phase::Removed;
159        });
160
161        let mut world = World::default();
162        let entity = world.spawn((false,)).unwrap();
163        assert!(!*world
164            .get::<true, bool>(entity, false)
165            .unwrap()
166            .read()
167            .unwrap());
168        assert_eq!(*phase.read().unwrap(), Phase::None);
169
170        observer.process(&mut world);
171        world.clear_changes();
172        observer.commands.execute(&mut world);
173        assert_eq!(*phase.read().unwrap(), Phase::Added);
174
175        observer.process(&mut world);
176        world.clear_changes();
177        observer.commands.execute(&mut world);
178        assert_eq!(*phase.read().unwrap(), Phase::Updated);
179
180        observer.process(&mut world);
181        world.clear_changes();
182        observer.commands.execute(&mut world);
183        assert_eq!(*phase.read().unwrap(), Phase::Removed);
184    }
185}