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#[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#[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#[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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 let registry = crate::registry::default_scene_registry();
502
503 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, ®istry).expect("save başarısız");
518
519 let mut loaded = World::new();
521 SceneData::load_into(&path, &mut loaded, ®istry).expect("load başarısız");
522 let _ = std::fs::remove_file(&path);
523
524 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 {
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 {
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 #[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 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, ®istry).expect("save_prefab başarısız");
576
577 let mut loaded = World::new();
579 let host = loaded.spawn();
580 let new_root = SceneData::load_prefab(&path, Some(host.id()), &mut loaded, ®istry)
581 .expect("load_prefab başarısız");
582 let _ = std::fs::remove_file(&path);
583
584 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 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()])); 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, ®istry).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, ®istry)
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 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 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 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, ®istry)
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 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, ®istry).expect("save başarısız");
704
705 let mut loaded = World::new();
706 SceneData::load_into(&path, &mut loaded, ®istry).expect("load başarısız");
707 let _ = std::fs::remove_file(&path);
708
709 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 #[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 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, ®istry).expect("save başarısız");
756
757 let mut loaded = World::new();
758 SceneData::load_into(&path, &mut loaded, ®istry).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}