Skip to main content

goud_engine/context_registry/scene/
manager.rs

1//! [`SceneManager`] implementation -- manages multiple isolated ECS worlds.
2
3use std::collections::HashMap;
4
5use crate::core::error::GoudError;
6use crate::ecs::World;
7
8/// Unique identifier for a scene within a [`SceneManager`].
9pub type SceneId = u32;
10
11/// Name assigned to the auto-created default scene.
12pub const DEFAULT_SCENE_NAME: &str = "default";
13
14// =============================================================================
15// SceneSlot
16// =============================================================================
17
18/// Internal storage for a scene slot (occupied or free).
19///
20/// `World` is boxed to avoid a large size difference between variants.
21#[derive(Debug)]
22enum SceneSlot {
23    /// Slot contains a live scene.
24    Occupied { name: String, world: Box<World> },
25    /// Slot has been freed and can be reused.
26    Free,
27}
28
29// =============================================================================
30// SceneManager
31// =============================================================================
32
33/// Manages multiple isolated ECS worlds (scenes).
34///
35/// On construction a "default" scene is created and set as the sole active
36/// scene. The default scene cannot be destroyed.
37///
38/// # Entity Isolation
39///
40/// Entities spawned in one scene are completely invisible to other scenes.
41/// Each scene owns its own [`World`] with independent entity allocators,
42/// component storage, and archetypes.
43#[derive(Debug)]
44pub struct SceneManager {
45    /// Scene storage slots (indexed by `SceneId`).
46    scenes: Vec<SceneSlot>,
47    /// Maps scene names to their IDs for lookup-by-name.
48    name_to_id: HashMap<String, SceneId>,
49    /// List of currently active scene IDs (order-preserving).
50    active_scenes: Vec<SceneId>,
51    /// ID of the default scene (always 0).
52    default_scene: SceneId,
53}
54
55impl SceneManager {
56    /// Creates a new `SceneManager` with a "default" scene already created
57    /// and marked active.
58    pub fn new() -> Self {
59        let mut manager = Self {
60            scenes: Vec::new(),
61            name_to_id: HashMap::new(),
62            active_scenes: Vec::new(),
63            default_scene: 0,
64        };
65        // Create the default scene. This cannot fail because no scenes exist yet.
66        let id = manager
67            .create_scene(DEFAULT_SCENE_NAME)
68            .expect("creating the default scene must not fail");
69        manager.default_scene = id;
70        // The default scene is active from the start.
71        manager.active_scenes.push(id);
72        manager
73    }
74
75    // =========================================================================
76    // Scene Lifecycle
77    // =========================================================================
78
79    /// Creates a new scene with the given name.
80    ///
81    /// Returns the [`SceneId`] on success. Returns an error if a scene with
82    /// the same name already exists.
83    pub fn create_scene(&mut self, name: &str) -> Result<SceneId, GoudError> {
84        if self.name_to_id.contains_key(name) {
85            return Err(GoudError::ResourceAlreadyExists(format!(
86                "Scene '{}' already exists",
87                name
88            )));
89        }
90
91        // Find a free slot or append.
92        let id = self.find_free_slot().unwrap_or_else(|| {
93            let id = self.scenes.len() as SceneId;
94            self.scenes.push(SceneSlot::Free);
95            id
96        });
97
98        self.scenes[id as usize] = SceneSlot::Occupied {
99            name: name.to_string(),
100            world: Box::new(World::new()),
101        };
102        self.name_to_id.insert(name.to_string(), id);
103
104        Ok(id)
105    }
106
107    /// Destroys a scene and frees its resources.
108    ///
109    /// Returns an error if the scene does not exist or if attempting to
110    /// destroy the default scene.
111    pub fn destroy_scene(&mut self, id: SceneId) -> Result<(), GoudError> {
112        if id == self.default_scene {
113            return Err(GoudError::InvalidState(
114                "Cannot destroy the default scene".to_string(),
115            ));
116        }
117
118        let index = id as usize;
119        if index >= self.scenes.len() {
120            return Err(GoudError::ResourceNotFound(format!(
121                "Scene id {} not found",
122                id
123            )));
124        }
125
126        match &self.scenes[index] {
127            SceneSlot::Occupied { name, .. } => {
128                self.name_to_id.remove(name);
129            }
130            SceneSlot::Free => {
131                return Err(GoudError::ResourceNotFound(format!(
132                    "Scene id {} not found",
133                    id
134                )));
135            }
136        }
137
138        self.scenes[index] = SceneSlot::Free;
139        self.active_scenes.retain(|&s| s != id);
140
141        Ok(())
142    }
143
144    // =========================================================================
145    // Scene Access
146    // =========================================================================
147
148    /// Returns a reference to the [`World`] for the given scene.
149    pub fn get_scene(&self, id: SceneId) -> Option<&World> {
150        self.scenes.get(id as usize).and_then(|slot| match slot {
151            SceneSlot::Occupied { world, .. } => Some(world.as_ref()),
152            SceneSlot::Free => None,
153        })
154    }
155
156    /// Returns a mutable reference to the [`World`] for the given scene.
157    pub fn get_scene_mut(&mut self, id: SceneId) -> Option<&mut World> {
158        self.scenes
159            .get_mut(id as usize)
160            .and_then(|slot| match slot {
161                SceneSlot::Occupied { world, .. } => Some(world.as_mut()),
162                SceneSlot::Free => None,
163            })
164    }
165
166    /// Looks up a scene by name, returning its ID if found.
167    pub fn get_scene_by_name(&self, name: &str) -> Option<SceneId> {
168        self.name_to_id.get(name).copied()
169    }
170
171    /// Returns the name of the scene with the given ID, if it exists.
172    pub fn get_scene_name(&self, id: SceneId) -> Option<&str> {
173        self.scenes.get(id as usize).and_then(|slot| match slot {
174            SceneSlot::Occupied { name, .. } => Some(name.as_str()),
175            SceneSlot::Free => None,
176        })
177    }
178
179    // =========================================================================
180    // Active Scene Management
181    // =========================================================================
182
183    /// Sets whether a scene is active.
184    ///
185    /// Active scenes are the ones that participate in the game loop (update,
186    /// render, etc.). Returns an error if the scene does not exist.
187    pub fn set_active(&mut self, id: SceneId, active: bool) -> Result<(), GoudError> {
188        if !self.scene_exists(id) {
189            return Err(GoudError::ResourceNotFound(format!(
190                "Scene id {} not found",
191                id
192            )));
193        }
194
195        if active {
196            if !self.active_scenes.contains(&id) {
197                self.active_scenes.push(id);
198            }
199        } else {
200            self.active_scenes.retain(|&s| s != id);
201        }
202
203        Ok(())
204    }
205
206    /// Returns `true` if the given scene is currently active.
207    pub fn is_active(&self, id: SceneId) -> bool {
208        self.active_scenes.contains(&id)
209    }
210
211    /// Returns a slice of all currently active scene IDs.
212    pub fn active_scenes(&self) -> &[SceneId] {
213        &self.active_scenes
214    }
215
216    // =========================================================================
217    // Queries
218    // =========================================================================
219
220    /// Returns the number of occupied scenes (including the default).
221    pub fn scene_count(&self) -> usize {
222        self.scenes
223            .iter()
224            .filter(|s| matches!(s, SceneSlot::Occupied { .. }))
225            .count()
226    }
227
228    /// Returns the ID of the default scene.
229    #[inline]
230    pub fn default_scene(&self) -> SceneId {
231        self.default_scene
232    }
233
234    // =========================================================================
235    // Internal Helpers
236    // =========================================================================
237
238    /// Finds the first free slot, if any.
239    fn find_free_slot(&self) -> Option<SceneId> {
240        self.scenes
241            .iter()
242            .position(|s| matches!(s, SceneSlot::Free))
243            .map(|i| i as SceneId)
244    }
245
246    /// Returns `true` if the given scene ID refers to an occupied slot.
247    fn scene_exists(&self, id: SceneId) -> bool {
248        self.scenes
249            .get(id as usize)
250            .is_some_and(|s| matches!(s, SceneSlot::Occupied { .. }))
251    }
252}
253
254impl Default for SceneManager {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260// =============================================================================
261// Tests
262// =============================================================================
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::ecs::Component;
268
269    /// Trivial component used in isolation tests.
270    #[derive(Debug, Clone, PartialEq)]
271    struct Health(u32);
272    impl Component for Health {}
273
274    // ----- construction ------------------------------------------------------
275
276    #[test]
277    fn test_default_scene_on_new() {
278        let mgr = SceneManager::new();
279
280        assert_eq!(mgr.scene_count(), 1);
281        assert_eq!(mgr.default_scene(), 0);
282        assert!(mgr.is_active(mgr.default_scene()));
283        assert_eq!(mgr.active_scenes(), &[0]);
284
285        // The default scene world is accessible.
286        assert!(mgr.get_scene(mgr.default_scene()).is_some());
287    }
288
289    // ----- create / destroy --------------------------------------------------
290
291    #[test]
292    fn test_create_scene() {
293        let mut mgr = SceneManager::new();
294        let id = mgr.create_scene("level_2").unwrap();
295
296        assert_ne!(id, mgr.default_scene());
297        assert_eq!(mgr.scene_count(), 2);
298        assert!(mgr.get_scene(id).is_some());
299    }
300
301    #[test]
302    fn test_create_duplicate_fails() {
303        let mut mgr = SceneManager::new();
304        mgr.create_scene("level_2").unwrap();
305
306        let result = mgr.create_scene("level_2");
307        assert!(result.is_err());
308    }
309
310    #[test]
311    fn test_destroy_scene() {
312        let mut mgr = SceneManager::new();
313        let id = mgr.create_scene("temp").unwrap();
314
315        mgr.destroy_scene(id).unwrap();
316
317        assert_eq!(mgr.scene_count(), 1);
318        assert!(mgr.get_scene(id).is_none());
319        assert!(mgr.get_scene_by_name("temp").is_none());
320    }
321
322    #[test]
323    fn test_destroy_default_fails() {
324        let mut mgr = SceneManager::new();
325        let result = mgr.destroy_scene(mgr.default_scene());
326        assert!(result.is_err());
327    }
328
329    #[test]
330    fn test_destroy_nonexistent_fails() {
331        let mut mgr = SceneManager::new();
332        let result = mgr.destroy_scene(999);
333        assert!(result.is_err());
334    }
335
336    // ----- name lookup -------------------------------------------------------
337
338    #[test]
339    fn test_get_scene_by_name() {
340        let mut mgr = SceneManager::new();
341        let id = mgr.create_scene("my_scene").unwrap();
342
343        assert_eq!(mgr.get_scene_by_name("my_scene"), Some(id));
344        assert_eq!(
345            mgr.get_scene_by_name(DEFAULT_SCENE_NAME),
346            Some(mgr.default_scene())
347        );
348        assert_eq!(mgr.get_scene_by_name("nope"), None);
349    }
350
351    // ----- entity isolation --------------------------------------------------
352
353    #[test]
354    fn test_entity_isolation() {
355        let mut mgr = SceneManager::new();
356        let scene_a = mgr.default_scene();
357        let scene_b = mgr.create_scene("b").unwrap();
358
359        // Spawn an entity in scene A.
360        let entity_a = mgr.get_scene_mut(scene_a).unwrap().spawn_empty();
361        mgr.get_scene_mut(scene_a)
362            .unwrap()
363            .insert(entity_a, Health(100));
364
365        // Scene A has the entity; scene B does not.
366        assert_eq!(mgr.get_scene(scene_a).unwrap().entity_count(), 1);
367        assert_eq!(mgr.get_scene(scene_b).unwrap().entity_count(), 0);
368
369        // The entity ID is not alive in scene B.
370        assert!(!mgr.get_scene(scene_b).unwrap().is_alive(entity_a));
371
372        // Spawn in B -- independent.
373        let entity_b = mgr.get_scene_mut(scene_b).unwrap().spawn_empty();
374        mgr.get_scene_mut(scene_b)
375            .unwrap()
376            .insert(entity_b, Health(50));
377
378        assert_eq!(mgr.get_scene(scene_a).unwrap().entity_count(), 1);
379        assert_eq!(mgr.get_scene(scene_b).unwrap().entity_count(), 1);
380
381        // Components are independent.
382        assert_eq!(
383            mgr.get_scene(scene_a).unwrap().get::<Health>(entity_a),
384            Some(&Health(100))
385        );
386        assert_eq!(
387            mgr.get_scene(scene_b).unwrap().get::<Health>(entity_b),
388            Some(&Health(50))
389        );
390    }
391
392    // ----- active scene management -------------------------------------------
393
394    #[test]
395    fn test_multiple_active_scenes() {
396        let mut mgr = SceneManager::new();
397        let b = mgr.create_scene("b").unwrap();
398        let c = mgr.create_scene("c").unwrap();
399
400        mgr.set_active(b, true).unwrap();
401        mgr.set_active(c, true).unwrap();
402
403        assert_eq!(mgr.active_scenes().len(), 3);
404        assert!(mgr.is_active(mgr.default_scene()));
405        assert!(mgr.is_active(b));
406        assert!(mgr.is_active(c));
407    }
408
409    #[test]
410    fn test_set_inactive() {
411        let mut mgr = SceneManager::new();
412        let b = mgr.create_scene("b").unwrap();
413        mgr.set_active(b, true).unwrap();
414        assert!(mgr.is_active(b));
415
416        mgr.set_active(b, false).unwrap();
417        assert!(!mgr.is_active(b));
418    }
419
420    #[test]
421    fn test_set_active_nonexistent_fails() {
422        let mut mgr = SceneManager::new();
423        let result = mgr.set_active(999, true);
424        assert!(result.is_err());
425    }
426
427    #[test]
428    fn test_set_active_idempotent() {
429        let mut mgr = SceneManager::new();
430        let b = mgr.create_scene("b").unwrap();
431
432        mgr.set_active(b, true).unwrap();
433        mgr.set_active(b, true).unwrap();
434
435        // Should only appear once.
436        assert_eq!(mgr.active_scenes().iter().filter(|&&s| s == b).count(), 1);
437    }
438
439    // ----- slot reuse --------------------------------------------------------
440
441    #[test]
442    fn test_slot_reuse_after_destroy() {
443        let mut mgr = SceneManager::new();
444        let a = mgr.create_scene("a").unwrap();
445        mgr.destroy_scene(a).unwrap();
446
447        // Creating a new scene should reuse the freed slot.
448        let b = mgr.create_scene("b").unwrap();
449        assert_eq!(b, a);
450    }
451
452    // ----- destroy removes from active list ----------------------------------
453
454    #[test]
455    fn test_destroy_removes_from_active() {
456        let mut mgr = SceneManager::new();
457        let b = mgr.create_scene("b").unwrap();
458        mgr.set_active(b, true).unwrap();
459        assert!(mgr.is_active(b));
460
461        mgr.destroy_scene(b).unwrap();
462        assert!(!mgr.is_active(b));
463    }
464}