Skip to main content

cougr_core/
change_tracker.rs

1use crate::component::ComponentStorage;
2use crate::simple_world::{EntityId, SimpleWorld};
3use alloc::vec::Vec;
4use soroban_sdk::{Bytes, Symbol};
5
6/// Tracks which components were added, removed, or modified within a tick.
7///
8/// This is a runtime-only structure (not `#[contracttype]`) that records
9/// component mutations as they happen. Systems can query the tracker to
10/// skip unchanged entities, improving performance.
11///
12/// # Example
13/// ```
14/// use cougr_core::runtime::ChangeTracker;
15/// use soroban_sdk::symbol_short;
16///
17/// let entity_id = 1;
18/// let mut tracker = ChangeTracker::new();
19/// tracker.record_add(entity_id, symbol_short!("pos"));
20///
21/// if tracker.was_added(entity_id, &symbol_short!("pos")) {
22///     assert_eq!(tracker.change_count(), 1);
23/// }
24///
25/// tracker.clear();
26/// tracker.advance_tick();
27/// assert_eq!(tracker.tick(), 1);
28/// ```
29pub struct ChangeTracker {
30    added: Vec<(EntityId, Symbol)>,
31    removed: Vec<(EntityId, Symbol)>,
32    modified: Vec<(EntityId, Symbol)>,
33    tick: u64,
34}
35
36impl ChangeTracker {
37    /// Create a new empty change tracker at tick 0.
38    pub fn new() -> Self {
39        Self {
40            added: Vec::new(),
41            removed: Vec::new(),
42            modified: Vec::new(),
43            tick: 0,
44        }
45    }
46
47    /// Record that a component was added to an entity.
48    pub fn record_add(&mut self, entity_id: EntityId, component_type: Symbol) {
49        self.added.push((entity_id, component_type));
50    }
51
52    /// Record that a component was removed from an entity.
53    pub fn record_remove(&mut self, entity_id: EntityId, component_type: Symbol) {
54        self.removed.push((entity_id, component_type));
55    }
56
57    /// Record that a component was modified on an entity.
58    pub fn record_modify(&mut self, entity_id: EntityId, component_type: Symbol) {
59        self.modified.push((entity_id, component_type));
60    }
61
62    /// Check if a specific component was added to an entity this tick.
63    pub fn was_added(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
64        self.added
65            .iter()
66            .any(|(eid, ct)| *eid == entity_id && ct == component_type)
67    }
68
69    /// Check if a specific component was removed from an entity this tick.
70    pub fn was_removed(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
71        self.removed
72            .iter()
73            .any(|(eid, ct)| *eid == entity_id && ct == component_type)
74    }
75
76    /// Check if a specific component was modified on an entity this tick.
77    pub fn was_modified(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
78        self.modified
79            .iter()
80            .any(|(eid, ct)| *eid == entity_id && ct == component_type)
81    }
82
83    /// Get all entity IDs that had the given component added this tick.
84    pub fn added_entities_with(&self, component_type: &Symbol) -> Vec<EntityId> {
85        self.added
86            .iter()
87            .filter(|(_, ct)| ct == component_type)
88            .map(|(eid, _)| *eid)
89            .collect()
90    }
91
92    /// Get all entity IDs that had the given component modified this tick.
93    pub fn modified_entities_with(&self, component_type: &Symbol) -> Vec<EntityId> {
94        self.modified
95            .iter()
96            .filter(|(_, ct)| ct == component_type)
97            .map(|(eid, _)| *eid)
98            .collect()
99    }
100
101    /// Get all entity IDs that had the given component removed this tick.
102    pub fn removed_entities_with(&self, component_type: &Symbol) -> Vec<EntityId> {
103        self.removed
104            .iter()
105            .filter(|(_, ct)| ct == component_type)
106            .map(|(eid, _)| *eid)
107            .collect()
108    }
109
110    /// Clear all recorded changes. Call this between ticks.
111    pub fn clear(&mut self) {
112        self.added.clear();
113        self.removed.clear();
114        self.modified.clear();
115    }
116
117    /// Returns the current tick number.
118    pub fn tick(&self) -> u64 {
119        self.tick
120    }
121
122    /// Advance the tick counter by one.
123    pub fn advance_tick(&mut self) {
124        self.tick += 1;
125    }
126
127    /// Returns the total number of recorded changes.
128    pub fn change_count(&self) -> usize {
129        self.added.len() + self.removed.len() + self.modified.len()
130    }
131}
132
133impl Default for ChangeTracker {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139/// A wrapper around `SimpleWorld` that automatically records component changes.
140///
141/// Similar to `HookedWorld`, but instead of firing callbacks, it records changes
142/// in a `ChangeTracker` for later querying by systems.
143///
144/// # Example
145/// ```
146/// use cougr_core::{SimpleWorld, runtime::TrackedWorld};
147/// use soroban_sdk::{symbol_short, Bytes, Env};
148///
149/// let env = Env::default();
150/// let world = SimpleWorld::new(&env);
151/// let mut tracked = TrackedWorld::new(world);
152///
153/// let e1 = tracked.spawn_entity();
154/// tracked.add_component(e1, symbol_short!("pos"), Bytes::new(&env));
155///
156/// assert!(tracked.tracker().was_added(e1, &symbol_short!("pos")));
157/// ```
158pub struct TrackedWorld {
159    world: SimpleWorld,
160    tracker: ChangeTracker,
161}
162
163impl TrackedWorld {
164    /// Wrap a `SimpleWorld` with a new change tracker.
165    pub fn new(world: SimpleWorld) -> Self {
166        Self {
167            world,
168            tracker: ChangeTracker::new(),
169        }
170    }
171
172    /// Access the underlying `SimpleWorld`.
173    pub fn world(&self) -> &SimpleWorld {
174        &self.world
175    }
176
177    /// Mutably access the underlying `SimpleWorld`.
178    pub fn world_mut(&mut self) -> &mut SimpleWorld {
179        &mut self.world
180    }
181
182    /// Access the change tracker.
183    pub fn tracker(&self) -> &ChangeTracker {
184        &self.tracker
185    }
186
187    /// Mutably access the change tracker (e.g. to clear it).
188    pub fn tracker_mut(&mut self) -> &mut ChangeTracker {
189        &mut self.tracker
190    }
191
192    /// Consume the wrapper and return the inner `SimpleWorld`.
193    pub fn into_inner(self) -> SimpleWorld {
194        self.world
195    }
196
197    /// Spawn a new entity (delegates to `SimpleWorld`).
198    pub fn spawn_entity(&mut self) -> EntityId {
199        self.world.spawn_entity()
200    }
201
202    /// Add a component, recording the change.
203    pub fn add_component(&mut self, entity_id: EntityId, component_type: Symbol, data: Bytes) {
204        let existed = self.world.has_component(entity_id, &component_type);
205        self.world
206            .add_component(entity_id, component_type.clone(), data);
207        if existed {
208            self.tracker.record_modify(entity_id, component_type);
209        } else {
210            self.tracker.record_add(entity_id, component_type);
211        }
212    }
213
214    /// Add a component with explicit storage kind, recording the change.
215    pub fn add_component_with_storage(
216        &mut self,
217        entity_id: EntityId,
218        component_type: Symbol,
219        data: Bytes,
220        storage: ComponentStorage,
221    ) {
222        let existed = self.world.has_component(entity_id, &component_type);
223        self.world
224            .add_component_with_storage(entity_id, component_type.clone(), data, storage);
225        if existed {
226            self.tracker.record_modify(entity_id, component_type);
227        } else {
228            self.tracker.record_add(entity_id, component_type);
229        }
230    }
231
232    /// Remove a component, recording the change.
233    pub fn remove_component(&mut self, entity_id: EntityId, component_type: &Symbol) -> bool {
234        let removed = self.world.remove_component(entity_id, component_type);
235        if removed {
236            self.tracker
237                .record_remove(entity_id, component_type.clone());
238        }
239        removed
240    }
241
242    /// Get a component (delegates to `SimpleWorld`).
243    pub fn get_component(&self, entity_id: EntityId, component_type: &Symbol) -> Option<Bytes> {
244        self.world.get_component(entity_id, component_type)
245    }
246
247    /// Check if an entity has a component (delegates to `SimpleWorld`).
248    pub fn has_component(&self, entity_id: EntityId, component_type: &Symbol) -> bool {
249        self.world.has_component(entity_id, component_type)
250    }
251
252    /// Despawn an entity, recording removals for each component.
253    pub fn despawn_entity(&mut self, entity_id: EntityId) {
254        if let Some(types) = self.world.entity_components.get(entity_id) {
255            for i in 0..types.len() {
256                if let Some(t) = types.get(i) {
257                    self.tracker.record_remove(entity_id, t);
258                }
259            }
260        }
261        self.world.despawn_entity(entity_id);
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use soroban_sdk::{symbol_short, Env};
269
270    #[test]
271    fn test_tracker_new() {
272        let tracker = ChangeTracker::new();
273        assert_eq!(tracker.tick(), 0);
274        assert_eq!(tracker.change_count(), 0);
275    }
276
277    #[test]
278    fn test_record_and_query_add() {
279        let _env = Env::default();
280        let mut tracker = ChangeTracker::new();
281        tracker.record_add(1, symbol_short!("pos"));
282
283        assert!(tracker.was_added(1, &symbol_short!("pos")));
284        assert!(!tracker.was_added(1, &symbol_short!("vel")));
285        assert!(!tracker.was_added(2, &symbol_short!("pos")));
286    }
287
288    #[test]
289    fn test_record_and_query_remove() {
290        let _env = Env::default();
291        let mut tracker = ChangeTracker::new();
292        tracker.record_remove(1, symbol_short!("pos"));
293
294        assert!(tracker.was_removed(1, &symbol_short!("pos")));
295        assert!(!tracker.was_removed(1, &symbol_short!("vel")));
296    }
297
298    #[test]
299    fn test_record_and_query_modify() {
300        let _env = Env::default();
301        let mut tracker = ChangeTracker::new();
302        tracker.record_modify(1, symbol_short!("pos"));
303
304        assert!(tracker.was_modified(1, &symbol_short!("pos")));
305        assert!(!tracker.was_modified(2, &symbol_short!("pos")));
306    }
307
308    #[test]
309    fn test_entities_with_queries() {
310        let _env = Env::default();
311        let mut tracker = ChangeTracker::new();
312        tracker.record_add(1, symbol_short!("pos"));
313        tracker.record_add(2, symbol_short!("pos"));
314        tracker.record_add(3, symbol_short!("vel"));
315
316        let added = tracker.added_entities_with(&symbol_short!("pos"));
317        assert_eq!(added.len(), 2);
318        assert!(added.contains(&1));
319        assert!(added.contains(&2));
320    }
321
322    #[test]
323    fn test_clear_resets() {
324        let _env = Env::default();
325        let mut tracker = ChangeTracker::new();
326        tracker.record_add(1, symbol_short!("pos"));
327        tracker.record_remove(2, symbol_short!("vel"));
328        tracker.record_modify(3, symbol_short!("hp"));
329        assert_eq!(tracker.change_count(), 3);
330
331        tracker.clear();
332        assert_eq!(tracker.change_count(), 0);
333        assert!(!tracker.was_added(1, &symbol_short!("pos")));
334    }
335
336    #[test]
337    fn test_advance_tick() {
338        let mut tracker = ChangeTracker::new();
339        assert_eq!(tracker.tick(), 0);
340        tracker.advance_tick();
341        assert_eq!(tracker.tick(), 1);
342        tracker.advance_tick();
343        assert_eq!(tracker.tick(), 2);
344    }
345
346    #[test]
347    fn test_tracked_world_add() {
348        let env = Env::default();
349        let world = SimpleWorld::new(&env);
350        let mut tracked = TrackedWorld::new(world);
351
352        let e1 = tracked.spawn_entity();
353        let data = Bytes::from_array(&env, &[1, 2, 3]);
354        tracked.add_component(e1, symbol_short!("pos"), data.clone());
355
356        assert!(tracked.has_component(e1, &symbol_short!("pos")));
357        assert!(tracked.tracker().was_added(e1, &symbol_short!("pos")));
358        assert!(!tracked.tracker().was_modified(e1, &symbol_short!("pos")));
359    }
360
361    #[test]
362    fn test_tracked_world_modify() {
363        let env = Env::default();
364        let world = SimpleWorld::new(&env);
365        let mut tracked = TrackedWorld::new(world);
366
367        let e1 = tracked.spawn_entity();
368        let data1 = Bytes::from_array(&env, &[1]);
369        tracked.add_component(e1, symbol_short!("pos"), data1);
370
371        // Overwrite existing component → should record modify
372        let data2 = Bytes::from_array(&env, &[2]);
373        tracked.add_component(e1, symbol_short!("pos"), data2.clone());
374
375        assert!(tracked.tracker().was_added(e1, &symbol_short!("pos")));
376        assert!(tracked.tracker().was_modified(e1, &symbol_short!("pos")));
377        assert_eq!(
378            tracked.get_component(e1, &symbol_short!("pos")),
379            Some(data2)
380        );
381    }
382
383    #[test]
384    fn test_tracked_world_remove() {
385        let env = Env::default();
386        let world = SimpleWorld::new(&env);
387        let mut tracked = TrackedWorld::new(world);
388
389        let e1 = tracked.spawn_entity();
390        let data = Bytes::from_array(&env, &[1]);
391        tracked.add_component(e1, symbol_short!("pos"), data);
392
393        assert!(tracked.remove_component(e1, &symbol_short!("pos")));
394        assert!(tracked.tracker().was_removed(e1, &symbol_short!("pos")));
395        assert!(!tracked.has_component(e1, &symbol_short!("pos")));
396    }
397
398    #[test]
399    fn test_tracked_world_despawn_records_removals() {
400        let env = Env::default();
401        let world = SimpleWorld::new(&env);
402        let mut tracked = TrackedWorld::new(world);
403
404        let e1 = tracked.spawn_entity();
405        let data = Bytes::from_array(&env, &[1]);
406        tracked.add_component(e1, symbol_short!("a"), data.clone());
407        tracked.add_component(e1, symbol_short!("b"), data);
408
409        tracked.tracker_mut().clear(); // clear add records
410        tracked.despawn_entity(e1);
411
412        assert!(tracked.tracker().was_removed(e1, &symbol_short!("a")));
413        assert!(tracked.tracker().was_removed(e1, &symbol_short!("b")));
414    }
415
416    #[test]
417    fn test_tracked_world_into_inner() {
418        let env = Env::default();
419        let world = SimpleWorld::new(&env);
420        let mut tracked = TrackedWorld::new(world);
421
422        let e1 = tracked.spawn_entity();
423        let data = Bytes::from_array(&env, &[1]);
424        tracked.add_component(e1, symbol_short!("test"), data);
425
426        let inner = tracked.into_inner();
427        assert!(inner.has_component(e1, &symbol_short!("test")));
428    }
429}