Skip to main content

gizmo_scene/
scene.rs

1use crate::error::SceneError;
2use gizmo_core::{EntityName, World};
3use gizmo_core::component::{MeshSource, MaterialSource};
4use serde::{Deserialize, Serialize};
5use std::fs;
6
7use gizmo_core::component::{Children, Parent};
8use std::collections::HashMap;
9
10/// Full scene data — all entities together with their components.
11#[derive(Debug, Serialize, Deserialize, Clone, Default)]
12#[non_exhaustive]
13pub struct SceneData {
14    pub entities: Vec<EntityData>,
15    #[serde(default)]
16    pub joints: Vec<gizmo_physics_rigid::joints::Joint>,
17}
18
19/// Prefab data — like [`SceneData`] but anchored to a root entity.
20#[derive(Debug, Serialize, Deserialize, Clone, Default)]
21#[non_exhaustive]
22pub struct PrefabData {
23    pub root_id: u32,
24    pub entities: Vec<EntityData>,
25    #[serde(default)]
26    pub joints: Vec<gizmo_physics_rigid::joints::Joint>,
27}
28
29/// Serializable data for a single entity.
30#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
31#[non_exhaustive]
32pub struct EntityData {
33    pub original_id: u32,
34    pub name: Option<String>,
35    pub mesh_source: Option<String>,
36    pub material_source: Option<MaterialData>,
37    #[serde(default)]
38    pub parent_id: Option<u32>,
39    #[serde(default)]
40    pub components: std::collections::BTreeMap<String, String>,
41}
42
43/// Serializable material data.
44#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
45#[non_exhaustive]
46pub struct MaterialData {
47    pub albedo: [f32; 4],
48    pub roughness: f32,
49    pub metallic: f32,
50    pub unlit: f32,
51    pub texture_source: Option<String>,
52}
53
54impl SceneData {
55    /// Mevcut World durumunu JSON dosyası olarak diske kaydeder
56    pub fn save(
57        world: &World,
58        file_path: &str,
59        registry: &gizmo_core::registry::ComponentRegistry,
60    ) -> Result<(), SceneError> {
61        if let Some(parent) = std::path::Path::new(file_path).parent() {
62            let _ = fs::create_dir_all(parent);
63        }
64        let entities_data = Self::serialize_entities(
65            world,
66            world
67                .iter_alive_entities()
68                .into_iter()
69                .map(|e| e.id())
70                .collect(),
71            registry,
72        );
73
74        let mut joints = Vec::new();
75        if let Ok(physics_world) = world.try_get_resource::<gizmo_physics_rigid::world::PhysicsWorld>() {
76            joints = physics_world.joints.clone();
77        }
78
79        let scene = SceneData {
80            entities: entities_data,
81            joints,
82        };
83
84        let string_data = ron::ser::to_string_pretty(&scene, ron::ser::PrettyConfig::default())?;
85
86        fs::write(file_path, string_data)?;
87
88        tracing::info!("✅ Sahne kaydedildi → {}", file_path);
89        Ok(())
90    }
91
92    pub fn serialize_entities(
93        world: &World,
94        entity_ids: Vec<u32>,
95        registry: &gizmo_core::registry::ComponentRegistry,
96    ) -> Vec<EntityData> {
97        let mut entities_data = Vec::new();
98        let names = world.borrow::<EntityName>();
99        let meshes = world.borrow::<MeshSource>();
100        let materials = world.borrow::<MaterialSource>();
101        let parents = world.borrow::<Parent>();
102        let children_store = world.borrow::<Children>();
103
104        for &id in &entity_ids {
105            let name = names.get(id).map(|n| n.0.clone());
106
107            // Gizmo Studio'nun içsel araçlarını kaydetme
108            if let Some(ref n) = name {
109                if n.starts_with("Editor ") || n == "Highlight Box" {
110                    continue;
111                }
112            }
113
114            let mesh_source = meshes.get(id).map(|m| m.0.clone());
115            let material_source = materials.get(id).map(|m| MaterialData {
116                albedo: m.albedo,
117                roughness: m.roughness,
118                metallic: m.metallic,
119                unlit: m.unlit,
120                texture_source: m.texture_source.clone(),
121            });
122            let parent_id = parents.get(id).map(|p| p.0);
123
124            let mut dynamic_components = std::collections::BTreeMap::new();
125
126            // Entity'nin GERÇEK generation'ı ile lookup yap. Sabit `Entity::new(id, 0)`,
127            // id slotu yeniden kullanılmış (despawn→spawn, generation ≥ 1) entity'lerde
128            // `entity_component_types`'ın `is_alive` generation kontrolünü geçemez →
129            // dinamik bileşenler (Transform, RigidBody, Collider…) diske kaydedilirken
130            // sessizce kaybolurdu. Name/Mesh/Material/Parent raw-id ile okunduğu için
131            // etkilenmez.
132            if let Some(entity) = world.entity(id) {
133                let types = world.entity_component_types(entity);
134                for type_id in types {
135                    if let Some(reg) = registry.get_registration(type_id) {
136                        if let Some(ptr) = world.get_component_ptr(entity, type_id) {
137                            if let Some(string_repr) =
138                                crate::serde_bridge::serialize_component(registry, reg, type_id, ptr)
139                            {
140                                dynamic_components.insert(reg.name.clone(), string_repr);
141                            }
142                        }
143                    }
144                }
145            }
146
147            // Keep a "bare" node that only carries a non-empty `Children` list: it is
148            // a structural group/pivot whose presence keeps its subtree attached on
149            // reload (children resolve their `parent_id` back to it). Dropping it left
150            // the children pointing at a missing id → detached subtree.
151            let is_group_node = children_store
152                .get(id)
153                .is_some_and(|c| !c.0.is_empty());
154
155            if name.is_some()
156                || mesh_source.is_some()
157                || material_source.is_some()
158                || parent_id.is_some()
159                || !dynamic_components.is_empty()
160                || is_group_node
161            {
162                entities_data.push(EntityData {
163                    original_id: id,
164                    name,
165                    mesh_source,
166                    material_source,
167                    parent_id,
168                    components: dynamic_components,
169                });
170            }
171        }
172        
173        tracing::info!(">>> serialize_entities: total entities checked: {}, serialized: {}", entity_ids.len(), entities_data.len());
174        entities_data
175    }
176
177    /// JSON sahne dosyasını okuyup World'e entity olarak yükler
178    pub fn load_into(
179        file_path: &str,
180        world: &mut World,
181        registry: &gizmo_core::registry::ComponentRegistry,
182    ) -> Result<(), SceneError> {
183        let string_data = fs::read_to_string(file_path)?;
184
185        let scene: SceneData = ron::from_str(&string_data)?;
186
187        let entities = scene.entities;
188        tracing::info!(">>> load_into: Read {} entities from file", entities.len());
189
190        let id_map = Self::instantiate_entities(
191            entities,
192            None,
193            world,
194            registry,
195        );
196
197        if let Ok(mut physics_world) = world.try_get_resource_mut::<gizmo_physics_rigid::world::PhysicsWorld>() {
198            for mut joint in scene.joints {
199                if let (Some(&new_a), Some(&new_b)) = (id_map.get(&joint.entity_a.id()), id_map.get(&joint.entity_b.id())) {
200                    joint.entity_a = gizmo_physics_rigid::BodyHandle::from_id(new_a);
201                    joint.entity_b = gizmo_physics_rigid::BodyHandle::from_id(new_b);
202                    physics_world.joints.push(joint);
203                }
204            }
205        }
206
207        tracing::info!("✅ Sahne yüklendi ← {}", file_path);
208        Ok(())
209    }
210
211    pub fn instantiate_entities(
212        entities: Vec<EntityData>,
213        root_parent: Option<u32>,
214        world: &mut World,
215        registry: &gizmo_core::registry::ComponentRegistry,
216    ) -> HashMap<u32, u32> {
217        let mut id_map = HashMap::new(); 
218        let mut entity_structs = HashMap::new();
219
220        for data in &entities {
221            let root_ent = world.spawn();
222            id_map.insert(data.original_id, root_ent.id());
223            entity_structs.insert(root_ent.id(), root_ent);
224        }
225
226        let mut children_map: HashMap<u32, Vec<u32>> = HashMap::new();
227
228        for data in entities {
229            let new_id = id_map[&data.original_id];
230            let entity = entity_structs[&new_id];
231
232            if let Some(n) = data.name {
233                world.add_component(entity, EntityName::new(&n));
234            }
235            
236            for (comp_name, comp_val) in &data.components {
237                if let Some(type_id) = registry.get_type_id(comp_name) {
238                    if let Some(reg) = registry.get_registration(type_id) {
239                        if let Err(e) = crate::serde_bridge::deserialize_component(
240                            world, entity, registry, reg, type_id, comp_val,
241                        ) {
242                            tracing::error!("Failed to deserialize component {}: {}", comp_name, e);
243                        }
244                    }
245                }
246            }
247
248            if let Some(mesh_src) = data.mesh_source {
249                world.add_component(entity, MeshSource(mesh_src));
250            }
251
252            if let Some(mat_data) = data.material_source {
253                world.add_component(entity, MaterialSource {
254                    albedo: mat_data.albedo,
255                    roughness: mat_data.roughness,
256                    metallic: mat_data.metallic,
257                    unlit: mat_data.unlit,
258                    texture_source: mat_data.texture_source,
259                });
260            }
261
262            // Resolve the parent to a freshly-spawned id. If `parent_id` names an
263            // entity that isn't in the saved set (e.g. a prefab whose bare root was
264            // filtered out, or a scene child of an editor-only parent), fall back to
265            // `root_parent` instead of silently orphaning the entity — previously the
266            // `else` fallback was skipped whenever `parent_id` was `Some` but unresolved.
267            let resolved_parent = data
268                .parent_id
269                .and_then(|orig_parent| id_map.get(&orig_parent).copied())
270                .or(root_parent);
271
272            if let Some(p_id) = resolved_parent {
273                world.add_component(entity, Parent(p_id));
274                children_map.entry(p_id).or_default().push(new_id);
275            }
276        }
277
278        for (p_id, mut c_list) in children_map {
279            if let Some(&p_ent) = entity_structs.get(&p_id) {
280                world.add_component(p_ent, Children(c_list));
281            } else if let Some(p_ent) = world.get_entity(p_id) {
282                let existing: Vec<u32> = world
283                    .borrow::<Children>()
284                    .get(p_id)
285                    .map(|c| c.0.clone())
286                    .unwrap_or_default();
287                let mut merged = existing;
288                merged.append(&mut c_list);
289                world.add_component(p_ent, Children(merged));
290            }
291        }
292
293        id_map
294    }
295
296    /// Prefab kaydet
297    pub fn save_prefab(
298        world: &World,
299        root_entity_id: u32,
300        file_path: &str,
301        registry: &gizmo_core::registry::ComponentRegistry,
302    ) -> Result<(), SceneError> {
303        if let Some(parent) = std::path::Path::new(file_path).parent() {
304            let _ = fs::create_dir_all(parent);
305        }
306
307        let mut ids_to_save = vec![root_entity_id];
308        let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
309        visited.insert(root_entity_id);
310        let children_storage = world.borrow::<Children>();
311
312        let mut i = 0;
313        while i < ids_to_save.len() {
314            let current = ids_to_save[i];
315            if let Some(children_comp) = children_storage.get(current) {
316                for &child_id in &children_comp.0 {
317                    // Guard the BFS with a visited set: a `Children` CYCLE (e.g. the
318                    // studio lets you drag an entity onto its own descendant) would
319                    // otherwise grow `ids_to_save` forever (hang/OOM), and a SHARED
320                    // child on a diamond hierarchy would be emitted twice → duplicate
321                    // `EntityData { original_id }` → clobbered/leaked entities on reload.
322                    if visited.insert(child_id) {
323                        ids_to_save.push(child_id);
324                    }
325                }
326            }
327            i += 1;
328        }
329
330        let mut entities_data = Self::serialize_entities(world, ids_to_save.clone(), registry);
331
332        // A "bare" root (e.g. an empty group/pivot node carrying only a `Children`
333        // component, with no name/mesh/material/dynamic components) is dropped by the
334        // skip-filter in `serialize_entities`. Force it back in so the prefab root
335        // always exists on reload — otherwise `load_prefab` can't map `root_id`, the
336        // root is never spawned, and its whole subtree detaches.
337        if !entities_data
338            .iter()
339            .any(|d| d.original_id == root_entity_id)
340        {
341            entities_data.push(EntityData {
342                original_id: root_entity_id,
343                name: None,
344                mesh_source: None,
345                material_source: None,
346                parent_id: None,
347                components: std::collections::BTreeMap::new(),
348            });
349        }
350
351        if let Some(root_data) = entities_data
352            .iter_mut()
353            .find(|d| d.original_id == root_entity_id)
354        {
355            root_data.parent_id = None;
356        }
357
358        let mut joints = Vec::new();
359        if let Ok(physics_world) = world.try_get_resource::<gizmo_physics_rigid::world::PhysicsWorld>() {
360            for joint in &physics_world.joints {
361                if ids_to_save.contains(&joint.entity_a.id()) && ids_to_save.contains(&joint.entity_b.id()) {
362                    joints.push(joint.clone());
363                }
364            }
365        }
366
367        let prefab = PrefabData {
368            root_id: root_entity_id,
369            entities: entities_data,
370            joints,
371        };
372
373        let string_data = ron::ser::to_string_pretty(&prefab, ron::ser::PrettyConfig::default())?;
374
375        fs::write(file_path, string_data)?;
376
377        tracing::info!("✅ Prefab kaydedildi → {}", file_path);
378        Ok(())
379    }
380
381    /// Prefab yükle
382    pub fn load_prefab(
383        file_path: &str,
384        parent_entity: Option<u32>,
385        world: &mut World,
386        registry: &gizmo_core::registry::ComponentRegistry,
387    ) -> Result<Option<u32>, SceneError> {
388        let string_data = fs::read_to_string(file_path)?;
389
390        let prefab: PrefabData = ron::from_str(&string_data)?;
391
392        let id_map = Self::instantiate_entities(
393            prefab.entities,
394            parent_entity,
395            world,
396            registry,
397        );
398
399        let new_root_id = id_map.get(&prefab.root_id).copied();
400
401        if let (Some(new_r), Some(p_id)) = (new_root_id, parent_entity) {
402            if let Some(p_ent) = world.get_entity(p_id) {
403                let mut children_list = world
404                    .borrow::<Children>()
405                    .get(p_id)
406                    .map(|c| c.0.clone())
407                    .unwrap_or_default();
408                // Idempotent: `instantiate_entities` may already have linked the root into
409                // the parent's Children when it set the root's Parent(p_id). Pushing again
410                // duplicated the child. Only add if not already present.
411                if !children_list.contains(&new_r) {
412                    children_list.push(new_r);
413                    world.add_component(p_ent, Children(children_list));
414                }
415            }
416        }
417
418        if let Ok(mut physics_world) = world.try_get_resource_mut::<gizmo_physics_rigid::world::PhysicsWorld>() {
419            for mut joint in prefab.joints {
420                if let (Some(&new_a), Some(&new_b)) = (id_map.get(&joint.entity_a.id()), id_map.get(&joint.entity_b.id())) {
421                    joint.entity_a = gizmo_physics_rigid::BodyHandle::from_id(new_a);
422                    joint.entity_b = gizmo_physics_rigid::BodyHandle::from_id(new_b);
423                    physics_world.joints.push(joint);
424                }
425            }
426        }
427
428        tracing::info!("✅ Prefab yüklendi ← {}", file_path);
429        Ok(new_root_id)
430    }
431
432    /// Entity listesini döndürür (Lua API'si için)
433    pub fn entity_names(world: &World) -> Vec<(u32, String)> {
434        let mut result = Vec::new();
435        let names = world.borrow::<EntityName>();
436        for (entity_id, _) in names.iter() {
437            if let Some(name) = names.get(entity_id) {
438                result.push((entity_id, name.0.clone()));
439            }
440        }
441        result
442    }
443
444    /// İsme göre entity bul
445    pub fn find_entity_by_name(world: &World, target_name: &str) -> Option<u32> {
446        let names = world.borrow::<EntityName>();
447        for (entity_id, _) in names.iter() {
448            if let Some(name) = names.get(entity_id) {
449                if name.0 == target_name {
450                    return Some(entity_id);
451                }
452            }
453        }
454        None
455    }
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461    use gizmo_core::World;
462    use gizmo_physics_rigid::joints::data::Joint;
463
464    #[test]
465    fn test_prefab_joint_serialization() {
466        let mut world = World::new();
467        let ent1 = world.spawn();
468        let ent2 = world.spawn();
469
470        let joint = Joint::fixed(
471            gizmo_physics_rigid::BodyHandle::from_id(ent1.id()),
472            gizmo_physics_rigid::BodyHandle::from_id(ent2.id()),
473            gizmo_math::Vec3::ZERO,
474            gizmo_math::Vec3::ZERO,
475        )
476        .with_break_force(1000.0, 1000.0);
477
478        let prefab_data = PrefabData {
479            root_id: ent1.id(),
480            entities: vec![],
481            joints: vec![joint.clone()],
482        };
483
484        let serialized = ron::ser::to_string(&prefab_data).unwrap();
485        assert!(serialized.contains("Fixed"));
486
487        let deserialized: PrefabData = ron::from_str(&serialized).unwrap();
488        assert_eq!(deserialized.joints.len(), 1);
489        assert!(matches!(deserialized.joints[0].data, gizmo_physics_rigid::joints::data::JointData::Fixed));
490    }
491
492    // Faz 5 — sahne kaydet/yükle GÜVENİLİRLİĞİ: bir dünya kaydedilip TAZE bir dünyaya
493    // yüklendiğinde isim + bileşen değerleri + hiyerarşi KORUNMALI (round-trip sadakati).
494    #[test]
495    fn scene_save_load_roundtrip_preserves_components_and_hierarchy() {
496        use gizmo_physics_core::components::Transform;
497        use gizmo_math::Vec3;
498
499        // Gerçek registry ile (reflect AÇIK ise reflect serileştirmesi, KAPALI ise serde
500        // fallback — her iki yolda da Transform değerleri round-trip'te korunmalı).
501        let registry = crate::registry::default_scene_registry();
502
503        // Kaynak dünya: isimli ebeveyn + çocuk, bilinen Transform değerleriyle.
504        let mut world = World::new();
505        let parent = world.spawn();
506        world.add_component(parent, EntityName::new("Parent"));
507        world.add_component(parent, Transform::new(Vec3::new(1.0, 2.0, 3.0)));
508        let child = world.spawn();
509        world.add_component(child, EntityName::new("Child"));
510        world.add_component(child, Transform::new(Vec3::new(-4.0, 5.0, -6.0)));
511        world.add_component(child, Parent(parent.id()));
512
513        let path = std::env::temp_dir()
514            .join("gizmo_scene_roundtrip_test.ron")
515            .to_string_lossy()
516            .into_owned();
517        SceneData::save(&world, &path, &registry).expect("save başarısız");
518
519        // TAZE dünyaya yükle.
520        let mut loaded = World::new();
521        SceneData::load_into(&path, &mut loaded, &registry).expect("load başarısız");
522        let _ = std::fs::remove_file(&path);
523
524        // İsme göre entity bul.
525        let find = |w: &World, want: &str| -> Option<u32> {
526            let names = w.borrow::<EntityName>();
527            w.iter_alive_entities()
528                .into_iter()
529                .map(|e| e.id())
530                .find(|&id| names.get(id).map(|n| n.0.as_str()) == Some(want))
531        };
532
533        let p = find(&loaded, "Parent").expect("Parent yüklenmedi");
534        let c = find(&loaded, "Child").expect("Child yüklenmedi");
535
536        // Transform değerleri korunmalı.
537        {
538            let ts = loaded.borrow::<Transform>();
539            let pt = ts.get(p).expect("Parent Transform yok (reflect round-trip bozuk)");
540            let ct = ts.get(c).expect("Child Transform yok");
541            assert_eq!(pt.position, Vec3::new(1.0, 2.0, 3.0), "Parent pozisyonu round-trip'te bozuldu");
542            assert_eq!(ct.position, Vec3::new(-4.0, 5.0, -6.0), "Child pozisyonu round-trip'te bozuldu");
543        }
544
545        // Hiyerarşi korunmalı: Child'ın Parent'ı yeni p id'sine çözülmeli.
546        {
547            let parents = loaded.borrow::<Parent>();
548            assert_eq!(parents.get(c).map(|x| x.0), Some(p), "ebeveyn-çocuk ilişkisi round-trip'te bozuldu");
549        }
550    }
551
552    // REGRESYON: "çıplak" bir grup/pivot kök (yalnız `Children`, isim/mesh/dinamik
553    // bileşen yok) prefab olarak kaydedilip yüklendiğinde kök + tüm alt-ağaç KORUNMALI.
554    // Eski kodda serialize_entities çıplak kökü atlıyor, load_prefab `root_id`'yi
555    // haritalayamıyor → kök hiç spawn edilmiyor, çocuklar da öksüz kalıyordu.
556    #[test]
557    fn prefab_roundtrip_keeps_bare_group_root_and_children() {
558        let registry = crate::registry::default_scene_registry();
559
560        let mut world = World::new();
561        // Çıplak grup kökü: yalnızca Children (isim/Transform/mesh YOK → skip-filter'e takılır).
562        let root = world.spawn();
563        let a = world.spawn();
564        let b = world.spawn();
565        world.add_component(a, EntityName::new("A"));
566        world.add_component(a, Parent(root.id()));
567        world.add_component(b, EntityName::new("B"));
568        world.add_component(b, Parent(root.id()));
569        world.add_component(root, Children(vec![a.id(), b.id()]));
570
571        let path = std::env::temp_dir()
572            .join("gizmo_prefab_bare_root_test.ron")
573            .to_string_lossy()
574            .into_owned();
575        SceneData::save_prefab(&world, root.id(), &path, &registry).expect("save_prefab başarısız");
576
577        // Taze dünyaya bir host altına yükle.
578        let mut loaded = World::new();
579        let host = loaded.spawn();
580        let new_root = SceneData::load_prefab(&path, Some(host.id()), &mut loaded, &registry)
581            .expect("load_prefab başarısız");
582        let _ = std::fs::remove_file(&path);
583
584        // Kök round-trip'te var olmalı (fix'ten önce düşüyordu → None).
585        let new_root = new_root.expect("prefab kökü reload sonrası var olmalı");
586
587        let find = |w: &World, want: &str| -> Option<u32> {
588            let names = w.borrow::<EntityName>();
589            w.iter_alive_entities()
590                .into_iter()
591                .map(|e| e.id())
592                .find(|&id| names.get(id).map(|n| n.0.as_str()) == Some(want))
593        };
594        let a2 = find(&loaded, "A").expect("çocuk A yüklenmedi");
595        let b2 = find(&loaded, "B").expect("çocuk B yüklenmedi");
596
597        let parents = loaded.borrow::<Parent>();
598        assert_eq!(
599            parents.get(a2).map(|x| x.0),
600            Some(new_root),
601            "A prefab köküne bağlı kalmalı (öksüz kalmamalı)"
602        );
603        assert_eq!(
604            parents.get(b2).map(|x| x.0),
605            Some(new_root),
606            "B prefab köküne bağlı kalmalı"
607        );
608        assert_eq!(
609            parents.get(new_root).map(|x| x.0),
610            Some(host.id()),
611            "prefab kökü istenen host'a bağlanmalı"
612        );
613    }
614
615    #[test]
616    fn save_prefab_dedups_shared_child_on_diamond_hierarchy() {
617        let registry = crate::registry::default_scene_registry();
618        let mut world = World::new();
619        // Diamond: root -> {A, B}, and BOTH A and B list C as a child.
620        let root = world.spawn();
621        let a = world.spawn();
622        let b = world.spawn();
623        let c = world.spawn();
624        world.add_component(root, EntityName::new("ROOT"));
625        world.add_component(a, EntityName::new("A"));
626        world.add_component(b, EntityName::new("B"));
627        world.add_component(c, EntityName::new("C"));
628        world.add_component(a, Parent(root.id()));
629        world.add_component(b, Parent(root.id()));
630        world.add_component(c, Parent(a.id()));
631        world.add_component(root, Children(vec![a.id(), b.id()]));
632        world.add_component(a, Children(vec![c.id()]));
633        world.add_component(b, Children(vec![c.id()])); // shared child C (diamond)
634
635        let path = std::env::temp_dir()
636            .join("gizmo_prefab_diamond_test.ron")
637            .to_string_lossy()
638            .into_owned();
639        SceneData::save_prefab(&world, root.id(), &path, &registry).expect("save_prefab başarısız");
640
641        let mut loaded = World::new();
642        let host = loaded.spawn();
643        SceneData::load_prefab(&path, Some(host.id()), &mut loaded, &registry)
644            .expect("load_prefab başarısız");
645        let _ = std::fs::remove_file(&path);
646
647        let alive = loaded.iter_alive_entities();
648        let names = loaded.borrow::<EntityName>();
649        // Shared child C must be emitted exactly once — the old BFS pushed it twice
650        // (via A and via B) → two EntityData with the same original_id → one leaked
651        // empty entity on reload.
652        let c_count = alive
653            .iter()
654            .filter(|e| names.get(e.id()).map(|n| n.0.as_str()) == Some("C"))
655            .count();
656        assert_eq!(c_count, 1, "paylaşılan çocuk C tam bir kez görünmeli");
657        // host + ROOT + A + B + C = 5 (eski kod 6. bir boş entity sızdırırdı).
658        assert_eq!(alive.len(), 5, "diamond'da sızan yinelenmiş entity olmamalı");
659    }
660
661    #[test]
662    fn save_prefab_terminates_on_children_cycle() {
663        let registry = crate::registry::default_scene_registry();
664        let mut world = World::new();
665        let root = world.spawn();
666        let x = world.spawn();
667        world.add_component(root, EntityName::new("ROOT"));
668        world.add_component(x, EntityName::new("X"));
669        // `Children` cycle root -> x -> root. The old BFS had no visited set → grew
670        // `ids_to_save` forever (hang/OOM). Completing at all is the assertion.
671        world.add_component(root, Children(vec![x.id()]));
672        world.add_component(x, Children(vec![root.id()]));
673
674        let path = std::env::temp_dir()
675            .join("gizmo_prefab_cycle_test.ron")
676            .to_string_lossy()
677            .into_owned();
678        SceneData::save_prefab(&world, root.id(), &path, &registry)
679            .expect("save_prefab bir döngüde sonlanmalı");
680        let _ = std::fs::remove_file(&path);
681    }
682
683    #[test]
684    fn save_keeps_bare_group_node_so_subtree_stays_attached() {
685        use gizmo_math::Vec3;
686        use gizmo_physics_core::components::Transform;
687
688        let registry = crate::registry::default_scene_registry();
689        let mut world = World::new();
690        // Bare group/pivot node: ONLY a `Children` list (no name/mesh/material/
691        // dynamic component). Its child carries real state + a Parent back to it.
692        let group = world.spawn();
693        let child = world.spawn();
694        world.add_component(child, EntityName::new("CHILD"));
695        world.add_component(child, Transform::new(Vec3::new(1.0, 2.0, 3.0)));
696        world.add_component(child, Parent(group.id()));
697        world.add_component(group, Children(vec![child.id()]));
698
699        let path = std::env::temp_dir()
700            .join("gizmo_scene_bare_group_test.ron")
701            .to_string_lossy()
702            .into_owned();
703        SceneData::save(&world, &path, &registry).expect("save başarısız");
704
705        let mut loaded = World::new();
706        SceneData::load_into(&path, &mut loaded, &registry).expect("load başarısız");
707        let _ = std::fs::remove_file(&path);
708
709        // The child must remain parented — the bare group node was kept, so the
710        // subtree isn't detached. Old skip-filter dropped it → child.parent_id
711        // referenced a missing id → child became a detached root.
712        let names = loaded.borrow::<EntityName>();
713        let child2 = loaded
714            .iter_alive_entities()
715            .into_iter()
716            .find(|e| names.get(e.id()).map(|n| n.0.as_str()) == Some("CHILD"))
717            .expect("çocuk yüklenmedi");
718        drop(names);
719        let parents = loaded.borrow::<Parent>();
720        assert!(
721            parents.get(child2.id()).is_some(),
722            "çocuk, korunan grup node'una bağlı kalmalı (öksüz kalmamalı)"
723        );
724    }
725
726    // REGRESYON (audit 2026-06-29): id slotu yeniden kullanılmış (despawn→spawn,
727    // generation ≥ 1) bir entity'nin dinamik bileşenleri sahne diske kaydedilirken
728    // kaybolmamalı. Eski kod `Entity::new(id, 0)` ile lookup yaptığından
729    // `entity_component_types`'ın `is_alive` generation kontrolü başarısız olur,
730    // boş döner ve Transform sessizce düşerdi. Name/Mesh/Material/Parent raw-id ile
731    // okunduğu için hayatta kalırdı → kısmi, sinsi bozulma.
732    #[test]
733    fn save_preserves_components_for_recycled_id_entity() {
734        use gizmo_physics_core::components::Transform;
735        use gizmo_math::Vec3;
736
737        let registry = crate::registry::default_scene_registry();
738        let mut world = World::new();
739
740        // id 0'ı yak → sonraki spawn onu generation 1 ile geri kullanır.
741        let burned = world.spawn();
742        let burned_id = burned.id();
743        world.despawn(burned);
744
745        let e = world.spawn();
746        assert_eq!(e.id(), burned_id, "ön koşul: id yeniden kullanılmalı");
747        assert_ne!(e.generation(), 0, "ön koşul: recycled entity generation ≥ 1");
748        world.add_component(e, EntityName::new("Recycled"));
749        world.add_component(e, Transform::new(Vec3::new(7.0, 8.0, 9.0)));
750
751        let path = std::env::temp_dir()
752            .join("gizmo_scene_recycled_id_test.ron")
753            .to_string_lossy()
754            .into_owned();
755        SceneData::save(&world, &path, &registry).expect("save başarısız");
756
757        let mut loaded = World::new();
758        SceneData::load_into(&path, &mut loaded, &registry).expect("load başarısız");
759        let _ = std::fs::remove_file(&path);
760
761        let id = {
762            let names = loaded.borrow::<EntityName>();
763            loaded
764                .iter_alive_entities()
765                .into_iter()
766                .map(|x| x.id())
767                .find(|&id| names.get(id).map(|n| n.0.as_str()) == Some("Recycled"))
768                .expect("Recycled entity yüklenmedi")
769        };
770        let ts = loaded.borrow::<Transform>();
771        let t = ts
772            .get(id)
773            .expect("recycled-id entity'nin Transform'ı kaydedilirken DÜŞTÜ (generation-0 lookup bug)");
774        assert_eq!(t.position, Vec3::new(7.0, 8.0, 9.0), "Transform değeri korunmadı");
775    }
776}