Skip to main content

godot_bevy/plugins/scene_tree/
plugin.rs

1use super::node_type_checking::{
2    add_node_type_markers_from_string, remove_comprehensive_node_type_markers,
3};
4use crate::plugins::core::SceneTreeComponentRegistry;
5use crate::prelude::GodotScene;
6use crate::{
7    interop::{GodotAccess, GodotNodeHandle},
8    plugins::collisions::{
9        AREA_ENTERED, AREA_EXITED, BODY_ENTERED, BODY_EXITED, CollisionMessageType,
10    },
11};
12use bevy_app::{App, First, Plugin, PreStartup};
13use bevy_ecs::{
14    component::Component,
15    entity::Entity,
16    message::{Message, MessageReader, MessageWriter, message_update_system},
17    prelude::{Name, ReflectComponent, ReflectResource, Resource},
18    schedule::IntoScheduleConfigs,
19    system::{Commands, NonSendMut, Query, Res, ResMut, SystemParam},
20};
21use bevy_reflect::Reflect;
22use godot::classes::ClassDb;
23use godot::{
24    builtin::{GString, StringName},
25    classes::{Engine, Node, SceneTree},
26    meta::ToGodot,
27    obj::{Gd, Inherits, InstanceId, Singleton},
28    prelude::GodotConvert,
29};
30use parking_lot::Mutex;
31use std::collections::HashMap;
32use std::marker::PhantomData;
33use tracing::{debug, trace, warn};
34
35/// A resource that maintains an O(1) lookup from Godot `InstanceId` to Bevy `Entity`.
36///
37/// This index is automatically maintained by the scene tree plugin as entities are
38/// added and removed. Use this for efficient collision handling, signal routing,
39/// and any scenario where you need to find the Bevy entity for a Godot node.
40///
41/// # Example
42///
43/// ```ignore
44/// fn handle_collision(
45///     index: Res<NodeEntityIndex>,
46///     // ... other params
47/// ) {
48///     let colliding_instance_id = /* from collision event */;
49///     if let Some(entity) = index.get(colliding_instance_id) {
50///         // Do something with the entity
51///     }
52/// }
53/// ```
54#[derive(Resource, Default, Debug, Reflect)]
55#[reflect(Resource)]
56pub struct NodeEntityIndex {
57    #[reflect(ignore)]
58    index: HashMap<InstanceId, Entity>,
59}
60
61impl NodeEntityIndex {
62    /// Look up the Bevy `Entity` for a Godot `InstanceId`.
63    ///
64    /// Returns `None` if no entity is registered for this instance ID.
65    #[inline]
66    pub fn get(&self, instance_id: InstanceId) -> Option<Entity> {
67        self.index.get(&instance_id).copied()
68    }
69
70    /// Check if an entity exists for the given `InstanceId`.
71    #[inline]
72    pub fn contains(&self, instance_id: InstanceId) -> bool {
73        self.index.contains_key(&instance_id)
74    }
75
76    /// Look up the Bevy `Entity` for a Godot `GodotNodeHandle`.
77    #[inline]
78    pub fn get_handle(&self, handle: GodotNodeHandle) -> Option<Entity> {
79        self.get(handle.instance_id())
80    }
81
82    /// Check if an entity exists for the given `GodotNodeHandle`.
83    #[inline]
84    pub fn contains_handle(&self, handle: GodotNodeHandle) -> bool {
85        self.contains(handle.instance_id())
86    }
87
88    /// Returns the number of entries in the index.
89    #[inline]
90    pub fn len(&self) -> usize {
91        self.index.len()
92    }
93
94    /// Returns true if the index is empty.
95    #[inline]
96    pub fn is_empty(&self) -> bool {
97        self.index.is_empty()
98    }
99
100    /// Insert a mapping from `InstanceId` to `Entity`.
101    ///
102    /// This is called internally by the scene tree plugin.
103    #[inline]
104    pub(crate) fn insert(&mut self, instance_id: InstanceId, entity: Entity) {
105        self.index.insert(instance_id, entity);
106    }
107
108    /// Remove a mapping by `InstanceId`.
109    ///
110    /// This is called internally by the scene tree plugin.
111    #[inline]
112    pub(crate) fn remove(&mut self, instance_id: InstanceId) -> Option<Entity> {
113        self.index.remove(&instance_id)
114    }
115}
116
117/// Unified scene tree plugin that provides:
118/// - SceneTreeRef for accessing the Godot scene tree
119/// - Scene tree messages (NodeAdded, NodeRemoved, NodeRenamed)
120/// - Automatic entity creation and mirroring for scene tree nodes
121/// - Custom GodotChildOf/GodotChildren relationship for scene tree hierarchy
122///
123/// This plugin is always included in the core plugins and provides
124/// complete scene tree integration out of the box.
125pub struct GodotSceneTreePlugin {
126    /// When true, despawning a parent entity will automatically despawn all children
127    /// via the GodotChildren on_despawn hook.
128    ///
129    /// Set to false if you want to manually manage entity lifetimes independently
130    /// of the Godot scene tree (e.g., for object pooling or entities that outlive their nodes).
131    ///
132    /// `ProtectedNodeEntity` children are never despawned automatically.
133    pub auto_despawn_children: bool,
134}
135
136impl Default for GodotSceneTreePlugin {
137    fn default() -> Self {
138        Self {
139            auto_despawn_children: true,
140        }
141    }
142}
143
144/// Configuration resource for scene tree behavior
145#[derive(Resource, Reflect)]
146#[reflect(Resource)]
147pub struct SceneTreeConfig {
148    /// When true, despawning a parent entity will automatically despawn all children
149    /// via the GodotChildren on_despawn hook.
150    ///
151    /// Set to false if you want to manually manage entity lifetimes independently
152    /// of the Godot scene tree (e.g., for object pooling or entities that outlive their nodes).
153    ///
154    /// `ProtectedNodeEntity` children are never despawned automatically.
155    pub auto_despawn_children: bool,
156}
157
158impl Plugin for GodotSceneTreePlugin {
159    fn build(&self, app: &mut App) {
160        // Auto-register all discovered AutoSyncBundle plugins
161        super::autosync::register_all_autosync_bundles(app);
162
163        app.init_non_send_resource::<SceneTreeRefImpl>()
164            .init_resource::<NodeEntityIndex>()
165            .insert_resource(SceneTreeConfig {
166                auto_despawn_children: self.auto_despawn_children,
167            })
168            .add_message::<SceneTreeMessage>()
169            .add_systems(
170                PreStartup,
171                (connect_scene_tree, initialize_scene_tree).chain(),
172            )
173            .add_systems(
174                First,
175                (
176                    write_scene_tree_messages.before(message_update_system),
177                    read_scene_tree_messages.before(message_update_system),
178                ),
179            );
180    }
181}
182
183#[derive(SystemParam)]
184pub struct SceneTreeRef<'w, 's> {
185    gd: NonSendMut<'w, SceneTreeRefImpl>,
186    phantom: PhantomData<&'s ()>,
187}
188
189impl<'w, 's> SceneTreeRef<'w, 's> {
190    pub fn get(&mut self) -> Gd<SceneTree> {
191        self.gd.0.clone()
192    }
193}
194
195#[doc(hidden)]
196#[derive(Debug)]
197pub(crate) struct SceneTreeRefImpl(Gd<SceneTree>);
198
199impl SceneTreeRefImpl {
200    fn get_ref() -> Gd<SceneTree> {
201        Engine::singleton()
202            .get_main_loop()
203            .unwrap()
204            .cast::<SceneTree>()
205    }
206}
207
208impl Default for SceneTreeRefImpl {
209    fn default() -> Self {
210        Self(Self::get_ref())
211    }
212}
213
214fn initialize_scene_tree(
215    mut commands: Commands,
216    mut scene_tree: SceneTreeRef,
217    mut entities: Query<(&GodotNodeHandle, Entity, Option<&ProtectedNodeEntity>)>,
218    component_registry: Res<SceneTreeComponentRegistry>,
219    mut node_index: ResMut<NodeEntityIndex>,
220    mut godot: GodotAccess,
221) {
222    let root = scene_tree.get().get_root().unwrap();
223
224    // Check if we have the optimized GDScript watcher for type pre-analysis
225    let optimized_watcher = root
226        .try_get_node_as::<Node>("/root/BevyAppSingleton/OptimizedSceneTreeWatcher")
227        .or_else(|| root.try_get_node_as::<Node>("BevyAppSingleton/OptimizedSceneTreeWatcher"));
228
229    let messages = if let Some(mut watcher) = optimized_watcher {
230        // Use optimized GDScript watcher to analyze the initial tree with type information
231        tracing::info!("Using optimized initial tree analysis with type pre-analysis");
232
233        let analysis_result = watcher.call("analyze_initial_tree", &[]);
234        let result_dict = analysis_result.to::<godot::builtin::VarDictionary>();
235        let instance_ids = result_dict
236            .get("instance_ids")
237            .unwrap()
238            .to::<godot::builtin::PackedInt64Array>();
239        let node_types = result_dict
240            .get("node_types")
241            .unwrap()
242            .to::<godot::builtin::PackedStringArray>();
243        let node_names = result_dict
244            .get("node_names")
245            .map(|value| value.to::<godot::builtin::PackedStringArray>());
246        let parent_ids = result_dict
247            .get("parent_ids")
248            .map(|value| value.to::<godot::builtin::PackedInt64Array>());
249        let collision_masks = result_dict
250            .get("collision_masks")
251            .map(|value| value.to::<godot::builtin::PackedInt64Array>());
252        // Groups is optional - only present in v2+ of the addon
253        let groups_array = result_dict
254            .get("groups")
255            .map(|value| value.to::<godot::builtin::VarArray>());
256
257        let mut messages = Vec::new();
258        let len = instance_ids.len().min(node_types.len());
259        for i in 0..len {
260            if let (Some(id), Some(type_gstring)) = (instance_ids.get(i), node_types.get(i)) {
261                let type_str = type_gstring.to_string();
262                let node_name = node_names
263                    .as_ref()
264                    .and_then(|names| names.get(i))
265                    .map(|name| name.to_string());
266                let parent_id =
267                    parent_ids
268                        .as_ref()
269                        .and_then(|ids| ids.get(i))
270                        .and_then(|parent_id| {
271                            if parent_id > 0 {
272                                Some(InstanceId::from_i64(parent_id))
273                            } else {
274                                None
275                            }
276                        });
277                let collision_mask = collision_masks
278                    .as_ref()
279                    .and_then(|masks| masks.get(i))
280                    .and_then(|mask| u8::try_from(mask).ok());
281                // Parse groups if available (v2+ addon)
282                let groups = groups_array.as_ref().and_then(|arr| {
283                    arr.get(i).map(|variant| {
284                        let packed = variant.to::<godot::builtin::PackedStringArray>();
285                        packed
286                            .as_slice()
287                            .iter()
288                            .map(|s| s.to_string())
289                            .collect::<Vec<_>>()
290                    })
291                });
292
293                messages.push(SceneTreeMessage {
294                    node_id: GodotNodeHandle::from(godot::prelude::InstanceId::from_i64(id)),
295                    message_type: SceneTreeMessageType::NodeAdded,
296                    node_type: Some(type_str),
297                    node_name,
298                    parent_id,
299                    collision_mask,
300                    groups,
301                });
302            }
303        }
304
305        messages
306    } else {
307        // Use fallback traversal without type optimization
308        tracing::info!("Using fallback initial tree analysis (no type optimization)");
309        traverse_fallback(root.upcast())
310    };
311
312    create_scene_tree_entity(
313        &mut commands,
314        messages,
315        &mut scene_tree,
316        &mut entities,
317        &component_registry,
318        &mut node_index,
319        &mut godot,
320    );
321}
322
323fn traverse_fallback(node: Gd<Node>) -> Vec<SceneTreeMessage> {
324    fn traverse_recursive(node: Gd<Node>, messages: &mut Vec<SceneTreeMessage>) {
325        messages.push(SceneTreeMessage {
326            node_id: GodotNodeHandle::from(node.instance_id()),
327            message_type: SceneTreeMessageType::NodeAdded,
328            node_type: None, // No type optimization available
329            node_name: None,
330            parent_id: None,
331            collision_mask: None,
332            groups: None, // No groups optimization available
333        });
334
335        for child in node.get_children().iter_shared() {
336            traverse_recursive(child, messages);
337        }
338    }
339
340    let mut messages = Vec::new();
341    traverse_recursive(node, &mut messages);
342    messages
343}
344
345#[derive(Debug, Clone, Message)]
346pub struct SceneTreeMessage {
347    pub node_id: GodotNodeHandle,
348    pub message_type: SceneTreeMessageType,
349    pub node_type: Option<String>, // Pre-analyzed node type from GDScript watcher
350    pub node_name: Option<String>,
351    pub parent_id: Option<InstanceId>,
352    pub collision_mask: Option<u8>,
353    pub groups: Option<Vec<String>>, // Pre-analyzed groups from GDScript watcher (v2+)
354}
355
356#[derive(Copy, Clone, Debug, GodotConvert)]
357#[godot(via = GString)]
358pub enum SceneTreeMessageType {
359    NodeAdded,
360    NodeRemoved,
361    NodeRenamed,
362}
363
364const COLLISION_MASK_BODY_ENTERED: u8 = 1 << 0;
365const COLLISION_MASK_BODY_EXITED: u8 = 1 << 1;
366const COLLISION_MASK_AREA_ENTERED: u8 = 1 << 2;
367const COLLISION_MASK_AREA_EXITED: u8 = 1 << 3;
368
369fn collision_mask_from_node(node: &mut Node) -> u8 {
370    let mut mask = 0;
371    if node.has_signal(BODY_ENTERED) {
372        mask |= COLLISION_MASK_BODY_ENTERED;
373    }
374    if node.has_signal(BODY_EXITED) {
375        mask |= COLLISION_MASK_BODY_EXITED;
376    }
377    if node.has_signal(AREA_ENTERED) {
378        mask |= COLLISION_MASK_AREA_ENTERED;
379    }
380    if node.has_signal(AREA_EXITED) {
381        mask |= COLLISION_MASK_AREA_EXITED;
382    }
383    mask
384}
385
386fn collision_mask_has(mask: u8, flag: u8) -> bool {
387    mask & flag != 0
388}
389
390/// Helper function to recursively search for a node by name
391fn find_node_by_name(parent: &Gd<Node>, name: &StringName) -> Option<Gd<Node>> {
392    // Check if this node matches - compare StringName directly to avoid allocation
393    if &parent.get_name() == name {
394        return Some(parent.clone());
395    }
396
397    // Search children recursively
398    for i in 0..parent.get_child_count() {
399        if let Some(child) = parent.get_child(i) {
400            let child_node = child.cast::<Node>();
401            if let Some(found) = find_node_by_name(&child_node, name) {
402                return Some(found);
403            }
404        }
405    }
406
407    None
408}
409
410fn connect_scene_tree(mut scene_tree: SceneTreeRef) {
411    let mut scene_tree_gd = scene_tree.get();
412    let root = scene_tree_gd.get_root().unwrap();
413
414    // Try multiple paths to find the SceneTreeWatcher - support both production and test environments
415    let watcher = root
416        .try_get_node_as::<Node>("/root/BevyAppSingleton/SceneTreeWatcher")
417        .or_else(|| {
418            // Try without the full path for test environments
419            root.try_get_node_as::<Node>("BevyAppSingleton/SceneTreeWatcher")
420        })
421        .or_else(|| {
422            // Fallback: search entire tree for any SceneTreeWatcher (for test environments)
423            tracing::debug!("Searching entire scene tree for SceneTreeWatcher");
424            find_node_by_name(&root.clone().upcast(), &StringName::from("SceneTreeWatcher"))
425        })
426        .unwrap_or_else(|| {
427            panic!("SceneTreeWatcher not found. Searched /root/BevyAppSingleton/SceneTreeWatcher, BevyAppSingleton/SceneTreeWatcher, and entire tree.");
428        });
429
430    // Check if we have the optimized GDScript watcher
431    let optimized_watcher = root
432        .try_get_node_as::<Node>("/root/BevyAppSingleton/OptimizedSceneTreeWatcher")
433        .or_else(|| root.try_get_node_as::<Node>("BevyAppSingleton/OptimizedSceneTreeWatcher"))
434        .or_else(|| {
435            // Fallback: search entire tree
436            find_node_by_name(
437                &root.clone().upcast(),
438                &StringName::from("OptimizedSceneTreeWatcher"),
439            )
440        });
441
442    if optimized_watcher.is_some() {
443        // The optimized GDScript watcher handles scene tree connections and forwards
444        // pre-analyzed messages to the Rust watcher (which has the MPSC sender)
445        // No need to connect here - it connects automatically in its _ready()
446        tracing::info!("Using optimized GDScript scene tree watcher with type pre-analysis");
447    } else {
448        // Fallback to direct connection without type optimization
449        tracing::info!("Using fallback scene tree connection (no type optimization)");
450
451        scene_tree_gd.connect(
452            "node_added",
453            &watcher
454                .callable("scene_tree_event")
455                .bind(&[SceneTreeMessageType::NodeAdded.to_variant()]),
456        );
457
458        scene_tree_gd.connect(
459            "node_removed",
460            &watcher
461                .callable("scene_tree_event")
462                .bind(&[SceneTreeMessageType::NodeRemoved.to_variant()]),
463        );
464
465        scene_tree_gd.connect(
466            "node_renamed",
467            &watcher
468                .callable("scene_tree_event")
469                .bind(&[SceneTreeMessageType::NodeRenamed.to_variant()]),
470        );
471    }
472}
473
474#[derive(Component, Debug, Reflect)]
475#[reflect(Component)]
476pub struct Groups {
477    groups: Vec<String>,
478}
479
480impl Groups {
481    pub fn is(&self, group_name: &str) -> bool {
482        self.groups.iter().any(|name| name == group_name)
483    }
484}
485
486impl<T: Inherits<Node>> From<&Gd<T>> for Groups {
487    fn from(node: &Gd<T>) -> Self {
488        Groups {
489            groups: node
490                .clone()
491                .upcast::<Node>()
492                .get_groups()
493                .iter_shared()
494                .map(|variant| variant.to_string())
495                .collect(),
496        }
497    }
498}
499
500impl From<Vec<String>> for Groups {
501    fn from(groups: Vec<String>) -> Self {
502        Groups { groups }
503    }
504}
505
506/// Resource for receiving scene tree messages from Godot.
507/// Wrapped in Mutex to be Send+Sync, allowing it to be a regular Bevy Resource.
508#[derive(Resource)]
509pub struct SceneTreeMessageReader(pub Mutex<crossbeam_channel::Receiver<SceneTreeMessage>>);
510
511impl SceneTreeMessageReader {
512    pub fn new(receiver: crossbeam_channel::Receiver<SceneTreeMessage>) -> Self {
513        Self(Mutex::new(receiver))
514    }
515}
516
517fn write_scene_tree_messages(
518    message_reader: Res<SceneTreeMessageReader>,
519    mut message_writer: MessageWriter<SceneTreeMessage>,
520) {
521    let receiver = message_reader.0.lock();
522    let messages: Vec<_> = receiver.try_iter().collect();
523    message_writer.write_batch(messages);
524}
525
526/// Marks an entity so it is not despawned when its corresponding Godot Node is freed, breaking
527/// the usual 1-to-1 lifetime between them. This allows game logic to keep running on entities
528/// that have no Node, such as simulating off-screen factory machines or NPCs in inactive scenes.
529/// A Godot Node can be re-associated later by adding a `GodotScene` component to the **entity.**
530#[derive(Component)]
531pub struct ProtectedNodeEntity;
532
533fn create_scene_tree_entity(
534    commands: &mut Commands,
535    messages: impl IntoIterator<Item = SceneTreeMessage>,
536    scene_tree: &mut SceneTreeRef,
537    entities: &mut Query<(&GodotNodeHandle, Entity, Option<&ProtectedNodeEntity>)>,
538    component_registry: &SceneTreeComponentRegistry,
539    node_index: &mut NodeEntityIndex,
540    godot: &mut GodotAccess,
541) {
542    // Make InstanceId to entity mapping for efficient random access (only needed during this function)
543    let mut godot_entity_map = entities
544        .iter()
545        .map(|(reference, ent, protected)| (reference.instance_id(), (ent, protected)))
546        .collect::<HashMap<_, _>>();
547    let scene_root = scene_tree.get().get_root().unwrap();
548
549    // CollisionWatcher is optional - only required if GodotCollisionsPlugin is added
550    let collision_watcher = scene_root
551        .try_get_node_as::<Node>("/root/BevyAppSingleton/CollisionWatcher")
552        .or_else(|| {
553            // Try without the full path for test environments
554            scene_root.try_get_node_as::<Node>("BevyAppSingleton/CollisionWatcher")
555        })
556        .or_else(|| {
557            // Fallback: search entire tree for any CollisionWatcher (for test environments)
558            tracing::debug!("Searching entire scene tree for CollisionWatcher");
559            find_node_by_name(
560                &scene_root.clone().upcast(),
561                &StringName::from("CollisionWatcher"),
562            )
563        });
564
565    // Collect collision bodies for batched signal connection
566    // Tuple: (instance_id as i64, collision_mask as u8)
567    let mut pending_collision_bodies: Vec<(i64, u8)> = Vec::new();
568
569    for message in messages.into_iter() {
570        trace!(target: "godot_scene_tree_messages", message = ?message);
571
572        let SceneTreeMessage {
573            node_id,
574            message_type,
575            node_type,
576            node_name,
577            parent_id: parent_id_from_gdscript,
578            collision_mask,
579            groups,
580        } = message;
581        let instance_id = node_id.instance_id();
582        let node_handle = node_id;
583        let entity_info = godot_entity_map.get(&instance_id).cloned();
584
585        match message_type {
586            SceneTreeMessageType::NodeAdded => {
587                // Skip nodes that have been freed before we process them (can happen in tests)
588                if !instance_id.lookup_validity() {
589                    continue;
590                }
591
592                let mut new_entity_commands = if let Some((ent, _)) = entity_info {
593                    commands.entity(ent)
594                } else {
595                    commands.spawn_empty()
596                };
597
598                let mut node_accessor = godot.node(node_handle);
599                let mut node = node_accessor.get::<Node>();
600
601                let node_name = node_name.unwrap_or_else(|| node.get_name().to_string());
602                new_entity_commands
603                    .insert(node_id)
604                    .insert(Name::from(node_name));
605
606                // Add node type marker components
607                for class_name in get_inheritance_hierarchy(
608                    node_type
609                        // Fall back to getting node-type from node if not provided by message
610                        .unwrap_or_else(|| node.get_class().to_string())
611                        .as_str(),
612                ) {
613                    add_node_type_markers_from_string(
614                        &mut new_entity_commands,
615                        class_name.as_str(),
616                    );
617                }
618
619                // Check if the node is a collision body (Area2D, Area3D, RigidBody2D, RigidBody3D, etc.)
620                // These nodes typically have collision detection capabilities
621                // Only connect if CollisionWatcher exists (i.e., GodotCollisionsPlugin was added)
622                let collision_mask = collision_mask.or_else(|| {
623                    collision_watcher
624                        .as_ref()
625                        .map(|_| collision_mask_from_node(&mut node))
626                });
627
628                // Check if the node is a collision body and collect for batched signal connection
629                if collision_watcher.is_some()
630                    && let Some(mask) = collision_mask
631                {
632                    let is_collision_body = collision_mask_has(mask, COLLISION_MASK_BODY_ENTERED)
633                        || collision_mask_has(mask, COLLISION_MASK_AREA_ENTERED);
634
635                    if is_collision_body {
636                        debug!(target: "godot_scene_tree_collisions",
637                               node_id = instance_id.to_string(),
638                               "is collision body");
639
640                        // Collect for batched connection
641                        pending_collision_bodies.push((instance_id.to_i64(), mask));
642                    }
643                }
644                // Use pre-analyzed groups from GDScript watcher if available, otherwise fallback to FFI
645                if let Some(groups_vec) = groups {
646                    new_entity_commands.insert(Groups::from(groups_vec));
647                } else {
648                    new_entity_commands.insert(Groups::from(&node));
649                }
650
651                // Add all components registered by plugins
652                component_registry.add_to_entity(&mut new_entity_commands, &mut node_accessor);
653
654                let new_entity = new_entity_commands.id();
655                godot_entity_map.insert(
656                    instance_id,
657                    (new_entity, entity_info.and_then(|(_, protected)| protected)),
658                );
659                node_index.insert(instance_id, new_entity);
660
661                // Try to add any registered bundles for this node type
662                super::autosync::try_add_bundles_for_node(commands, new_entity, godot, node_handle);
663
664                // Add GodotChildOf relationship to mirror Godot's scene tree hierarchy
665                let parent_id = parent_id_from_gdscript
666                    .or_else(|| node.get_parent().map(|parent| parent.instance_id()));
667                if let Some(parent_id) = parent_id
668                    && parent_id != scene_root.instance_id()
669                {
670                    if let Some((parent_entity, _)) = godot_entity_map.get(&parent_id) {
671                        commands
672                            .entity(new_entity)
673                            .insert(super::relationship::GodotChildOf(*parent_entity));
674                    } else {
675                        warn!(target: "godot_scene_tree_messages",
676                            "Parent entity with ID {} not found in godot_entity_map. This might indicate a missing or incorrect mapping. Path={}",
677                            parent_id, node.get_path());
678                    }
679                }
680            }
681            SceneTreeMessageType::NodeRemoved => {
682                if let Some((ent, prot_opt)) = entity_info {
683                    // Check if node is being reparented vs truly removed
684                    // During reparenting, the node is temporarily removed from old parent
685                    // but still exists in the scene tree (has a parent)
686                    // We need to try_get because the node handle might be invalid if freed
687                    let is_reparenting = godot
688                        .try_get::<Node>(node_handle)
689                        .map(|godot_node| godot_node.get_parent().is_some())
690                        .unwrap_or(false);
691
692                    if is_reparenting {
693                        // Node is being reparented - don't despawn entity, it will be re-added
694                        trace!(target: "godot_scene_tree_events",
695                            "Node is being reparented, preserving entity");
696                        // Don't remove from ent_mapping - entity still valid
697                    } else {
698                        // Node is truly being removed (freed or despawned)
699                        let protected = prot_opt.is_some();
700                        if !protected {
701                            commands.entity(ent).despawn();
702                        } else {
703                            _strip_godot_components(commands, ent);
704                        }
705                        godot_entity_map.remove(&instance_id);
706                        node_index.remove(instance_id);
707                    }
708                } else {
709                    // Entity was already despawned (common when using queue_free)
710                    trace!(target: "godot_scene_tree_messages", "Entity for removed node was already despawned");
711                }
712            }
713            SceneTreeMessageType::NodeRenamed => {
714                if let Some((ent, _)) = entity_info {
715                    let name = node_name
716                        .unwrap_or_else(|| godot.get::<Node>(node_handle).get_name().to_string());
717                    commands.entity(ent).insert(Name::from(name));
718                } else {
719                    trace!(target: "godot_scene_tree_messages", "Entity for renamed node was already despawned");
720                }
721            }
722        }
723    }
724
725    // Batch connect collision signals if there are any pending
726    if !pending_collision_bodies.is_empty()
727        && let Some(ref collision_watcher) = collision_watcher
728    {
729        batch_connect_collision_signals(&scene_root, collision_watcher, &pending_collision_bodies);
730    }
731}
732
733fn get_inheritance_hierarchy(class_name: &str) -> Vec<String> {
734    let class_db = ClassDb::singleton();
735    let mut hierarchy = Vec::new();
736
737    // Initialize a local mutable variable to track the "current" class name
738    let mut current_class = StringName::from(class_name);
739
740    while !current_class.is_empty() {
741        // Convert to String for the return vector
742        hierarchy.push(current_class.to_string());
743
744        // Update current_class to its parent
745        current_class = class_db.get_parent_class(&current_class);
746    }
747
748    hierarchy
749}
750
751/// Batch connect collision signals using GDScript bulk operations.
752/// Falls back to individual connections if bulk operations node is not available.
753fn batch_connect_collision_signals(
754    scene_root: &Gd<godot::classes::Window>,
755    collision_watcher: &Gd<Node>,
756    pending_bodies: &[(i64, u8)],
757) {
758    use godot::builtin::PackedInt64Array;
759
760    // Try to find OptimizedBulkOperations node with the required method
761    let bulk_ops = scene_root
762        .get_node_or_null("BevyAppSingleton/OptimizedBulkOperations")
763        .or_else(|| scene_root.get_node_or_null("/root/BevyAppSingleton/OptimizedBulkOperations"))
764        .filter(|node| node.has_method("bulk_connect_collision_signals"));
765
766    if let Some(mut bulk_ops) = bulk_ops {
767        // Use batched GDScript call
768        let instance_ids: Vec<i64> = pending_bodies.iter().map(|(id, _)| *id).collect();
769        let collision_masks: Vec<i64> = pending_bodies
770            .iter()
771            .map(|(_, mask)| i64::from(*mask))
772            .collect();
773
774        let ids_packed = PackedInt64Array::from(instance_ids.as_slice());
775        let masks_packed = PackedInt64Array::from(collision_masks.as_slice());
776
777        bulk_ops.call(
778            "bulk_connect_collision_signals",
779            &[
780                ids_packed.to_variant(),
781                masks_packed.to_variant(),
782                collision_watcher.to_variant(),
783            ],
784        );
785    } else {
786        // Fallback: connect signals individually
787        for (instance_id, mask) in pending_bodies {
788            let instance_id = InstanceId::from_i64(*instance_id);
789            if !instance_id.lookup_validity() {
790                continue;
791            }
792
793            // Get the node from instance ID
794            let Some(mut node) = Gd::<Node>::try_from_instance_id(instance_id).ok() else {
795                continue;
796            };
797
798            let node_clone = node.clone();
799
800            if collision_mask_has(*mask, COLLISION_MASK_BODY_ENTERED) {
801                node.connect(
802                    BODY_ENTERED,
803                    &collision_watcher.callable("collision_event").bind(&[
804                        node_clone.to_variant(),
805                        CollisionMessageType::Started.to_variant(),
806                    ]),
807                );
808            }
809
810            if collision_mask_has(*mask, COLLISION_MASK_BODY_EXITED) {
811                node.connect(
812                    BODY_EXITED,
813                    &collision_watcher.callable("collision_event").bind(&[
814                        node_clone.to_variant(),
815                        CollisionMessageType::Ended.to_variant(),
816                    ]),
817                );
818            }
819
820            if collision_mask_has(*mask, COLLISION_MASK_AREA_ENTERED) {
821                node.connect(
822                    AREA_ENTERED,
823                    &collision_watcher.callable("collision_event").bind(&[
824                        node_clone.to_variant(),
825                        CollisionMessageType::Started.to_variant(),
826                    ]),
827                );
828            }
829
830            if collision_mask_has(*mask, COLLISION_MASK_AREA_EXITED) {
831                node.connect(
832                    AREA_EXITED,
833                    &collision_watcher.callable("collision_event").bind(&[
834                        node_clone.to_variant(),
835                        CollisionMessageType::Ended.to_variant(),
836                    ]),
837                );
838            }
839        }
840
841        debug!(target: "godot_scene_tree_collisions",
842               count = pending_bodies.len(),
843               "Individually connected collision signals (bulk ops not available)");
844    }
845}
846
847fn _strip_godot_components(commands: &mut Commands, ent: Entity) {
848    let mut entity_commands = commands.entity(ent);
849
850    entity_commands.remove::<GodotNodeHandle>();
851    entity_commands.remove::<GodotScene>();
852    entity_commands.remove::<Name>();
853    entity_commands.remove::<Groups>();
854
855    remove_comprehensive_node_type_markers(&mut entity_commands);
856}
857
858fn try_process_node_renamed_messages_fast_path(
859    commands: &mut Commands,
860    messages: &[SceneTreeMessage],
861    node_index: &NodeEntityIndex,
862    godot: &mut GodotAccess,
863) -> bool {
864    if !messages
865        .iter()
866        .all(|message| matches!(message.message_type, SceneTreeMessageType::NodeRenamed))
867    {
868        return false;
869    }
870
871    for message in messages {
872        let node_handle = message.node_id;
873        let Some(entity) = node_index.get(node_handle.instance_id()) else {
874            trace!(target: "godot_scene_tree_messages", "Entity for renamed node was already despawned");
875            continue;
876        };
877
878        let name = message
879            .node_name
880            .clone()
881            .unwrap_or_else(|| godot.get::<Node>(node_handle).get_name().to_string());
882        commands.entity(entity).insert(Name::from(name));
883    }
884
885    true
886}
887
888#[allow(clippy::too_many_arguments)]
889fn read_scene_tree_messages(
890    mut commands: Commands,
891    mut scene_tree: SceneTreeRef,
892    mut message_reader: MessageReader<SceneTreeMessage>,
893    mut entities: Query<(&GodotNodeHandle, Entity, Option<&ProtectedNodeEntity>)>,
894    component_registry: Res<SceneTreeComponentRegistry>,
895    mut node_index: ResMut<NodeEntityIndex>,
896    mut godot: GodotAccess,
897) {
898    let messages: Vec<_> = message_reader.read().cloned().collect();
899    if messages.is_empty() {
900        return;
901    }
902
903    if try_process_node_renamed_messages_fast_path(
904        &mut commands,
905        &messages,
906        &node_index,
907        &mut godot,
908    ) {
909        return;
910    }
911
912    create_scene_tree_entity(
913        &mut commands,
914        messages,
915        &mut scene_tree,
916        &mut entities,
917        &component_registry,
918        &mut node_index,
919        &mut godot,
920    );
921}