use super::node_type_checking::{
add_node_type_markers_from_string, remove_comprehensive_node_type_markers,
};
use crate::plugins::core::SceneTreeComponentRegistry;
use crate::prelude::GodotScene;
use crate::{
interop::{GodotAccess, GodotNodeHandle},
plugins::collisions::{
AREA_ENTERED, AREA_EXITED, BODY_ENTERED, BODY_EXITED, CollisionMessageType,
},
};
use bevy_app::{App, First, Plugin, PreStartup};
use bevy_ecs::{
component::Component,
entity::Entity,
message::{Message, MessageReader, MessageWriter, message_update_system},
prelude::{Name, ReflectComponent, ReflectResource, Resource},
schedule::IntoScheduleConfigs,
system::{Commands, NonSendMut, Query, Res, ResMut, SystemParam},
};
use bevy_reflect::Reflect;
use godot::classes::ClassDb;
use godot::{
builtin::{GString, StringName},
classes::{Engine, Node, SceneTree},
meta::ToGodot,
obj::{Gd, Inherits, InstanceId, Singleton},
prelude::GodotConvert,
};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::marker::PhantomData;
use tracing::{debug, trace, warn};
#[derive(Resource, Default, Debug, Reflect)]
#[reflect(Resource)]
pub struct NodeEntityIndex {
#[reflect(ignore)]
index: HashMap<InstanceId, Entity>,
}
impl NodeEntityIndex {
#[inline]
pub fn get(&self, instance_id: InstanceId) -> Option<Entity> {
self.index.get(&instance_id).copied()
}
#[inline]
pub fn contains(&self, instance_id: InstanceId) -> bool {
self.index.contains_key(&instance_id)
}
#[inline]
pub fn get_handle(&self, handle: GodotNodeHandle) -> Option<Entity> {
self.get(handle.instance_id())
}
#[inline]
pub fn contains_handle(&self, handle: GodotNodeHandle) -> bool {
self.contains(handle.instance_id())
}
#[inline]
pub fn len(&self) -> usize {
self.index.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.index.is_empty()
}
#[inline]
pub(crate) fn insert(&mut self, instance_id: InstanceId, entity: Entity) {
self.index.insert(instance_id, entity);
}
#[inline]
pub(crate) fn remove(&mut self, instance_id: InstanceId) -> Option<Entity> {
self.index.remove(&instance_id)
}
}
pub struct GodotSceneTreePlugin {
pub auto_despawn_children: bool,
}
impl Default for GodotSceneTreePlugin {
fn default() -> Self {
Self {
auto_despawn_children: true,
}
}
}
#[derive(Resource, Reflect)]
#[reflect(Resource)]
pub struct SceneTreeConfig {
pub auto_despawn_children: bool,
}
impl Plugin for GodotSceneTreePlugin {
fn build(&self, app: &mut App) {
super::autosync::register_all_autosync_bundles(app);
app.init_non_send_resource::<SceneTreeRefImpl>()
.init_resource::<NodeEntityIndex>()
.insert_resource(SceneTreeConfig {
auto_despawn_children: self.auto_despawn_children,
})
.add_message::<SceneTreeMessage>()
.add_systems(
PreStartup,
(connect_scene_tree, initialize_scene_tree).chain(),
)
.add_systems(
First,
(
write_scene_tree_messages.before(message_update_system),
read_scene_tree_messages.before(message_update_system),
),
);
}
}
#[derive(SystemParam)]
pub struct SceneTreeRef<'w, 's> {
gd: NonSendMut<'w, SceneTreeRefImpl>,
phantom: PhantomData<&'s ()>,
}
impl<'w, 's> SceneTreeRef<'w, 's> {
pub fn get(&mut self) -> Gd<SceneTree> {
self.gd.0.clone()
}
}
#[doc(hidden)]
#[derive(Debug)]
pub(crate) struct SceneTreeRefImpl(Gd<SceneTree>);
impl SceneTreeRefImpl {
fn get_ref() -> Gd<SceneTree> {
Engine::singleton()
.get_main_loop()
.unwrap()
.cast::<SceneTree>()
}
}
impl Default for SceneTreeRefImpl {
fn default() -> Self {
Self(Self::get_ref())
}
}
fn initialize_scene_tree(
mut commands: Commands,
mut scene_tree: SceneTreeRef,
mut entities: Query<(&GodotNodeHandle, Entity, Option<&ProtectedNodeEntity>)>,
component_registry: Res<SceneTreeComponentRegistry>,
mut node_index: ResMut<NodeEntityIndex>,
mut godot: GodotAccess,
) {
let root = scene_tree.get().get_root().unwrap();
let optimized_watcher = root
.try_get_node_as::<Node>("/root/BevyAppSingleton/OptimizedSceneTreeWatcher")
.or_else(|| root.try_get_node_as::<Node>("BevyAppSingleton/OptimizedSceneTreeWatcher"));
let messages = if let Some(mut watcher) = optimized_watcher {
tracing::info!("Using optimized initial tree analysis with type pre-analysis");
let analysis_result = watcher.call("analyze_initial_tree", &[]);
let result_dict = analysis_result.to::<godot::builtin::VarDictionary>();
let instance_ids = result_dict
.get("instance_ids")
.unwrap()
.to::<godot::builtin::PackedInt64Array>();
let node_types = result_dict
.get("node_types")
.unwrap()
.to::<godot::builtin::PackedStringArray>();
let node_names = result_dict
.get("node_names")
.map(|value| value.to::<godot::builtin::PackedStringArray>());
let parent_ids = result_dict
.get("parent_ids")
.map(|value| value.to::<godot::builtin::PackedInt64Array>());
let collision_masks = result_dict
.get("collision_masks")
.map(|value| value.to::<godot::builtin::PackedInt64Array>());
let groups_array = result_dict
.get("groups")
.map(|value| value.to::<godot::builtin::VarArray>());
let mut messages = Vec::new();
let len = instance_ids.len().min(node_types.len());
for i in 0..len {
if let (Some(id), Some(type_gstring)) = (instance_ids.get(i), node_types.get(i)) {
let type_str = type_gstring.to_string();
let node_name = node_names
.as_ref()
.and_then(|names| names.get(i))
.map(|name| name.to_string());
let parent_id =
parent_ids
.as_ref()
.and_then(|ids| ids.get(i))
.and_then(|parent_id| {
if parent_id > 0 {
Some(InstanceId::from_i64(parent_id))
} else {
None
}
});
let collision_mask = collision_masks
.as_ref()
.and_then(|masks| masks.get(i))
.and_then(|mask| u8::try_from(mask).ok());
let groups = groups_array.as_ref().and_then(|arr| {
arr.get(i).map(|variant| {
let packed = variant.to::<godot::builtin::PackedStringArray>();
packed
.as_slice()
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
})
});
messages.push(SceneTreeMessage {
node_id: GodotNodeHandle::from(godot::prelude::InstanceId::from_i64(id)),
message_type: SceneTreeMessageType::NodeAdded,
node_type: Some(type_str),
node_name,
parent_id,
collision_mask,
groups,
});
}
}
messages
} else {
tracing::info!("Using fallback initial tree analysis (no type optimization)");
traverse_fallback(root.upcast())
};
create_scene_tree_entity(
&mut commands,
messages,
&mut scene_tree,
&mut entities,
&component_registry,
&mut node_index,
&mut godot,
);
}
fn traverse_fallback(node: Gd<Node>) -> Vec<SceneTreeMessage> {
fn traverse_recursive(node: Gd<Node>, messages: &mut Vec<SceneTreeMessage>) {
messages.push(SceneTreeMessage {
node_id: GodotNodeHandle::from(node.instance_id()),
message_type: SceneTreeMessageType::NodeAdded,
node_type: None, node_name: None,
parent_id: None,
collision_mask: None,
groups: None, });
for child in node.get_children().iter_shared() {
traverse_recursive(child, messages);
}
}
let mut messages = Vec::new();
traverse_recursive(node, &mut messages);
messages
}
#[derive(Debug, Clone, Message)]
pub struct SceneTreeMessage {
pub node_id: GodotNodeHandle,
pub message_type: SceneTreeMessageType,
pub node_type: Option<String>, pub node_name: Option<String>,
pub parent_id: Option<InstanceId>,
pub collision_mask: Option<u8>,
pub groups: Option<Vec<String>>, }
#[derive(Copy, Clone, Debug, GodotConvert)]
#[godot(via = GString)]
pub enum SceneTreeMessageType {
NodeAdded,
NodeRemoved,
NodeRenamed,
}
const COLLISION_MASK_BODY_ENTERED: u8 = 1 << 0;
const COLLISION_MASK_BODY_EXITED: u8 = 1 << 1;
const COLLISION_MASK_AREA_ENTERED: u8 = 1 << 2;
const COLLISION_MASK_AREA_EXITED: u8 = 1 << 3;
fn collision_mask_from_node(node: &mut Node) -> u8 {
let mut mask = 0;
if node.has_signal(BODY_ENTERED) {
mask |= COLLISION_MASK_BODY_ENTERED;
}
if node.has_signal(BODY_EXITED) {
mask |= COLLISION_MASK_BODY_EXITED;
}
if node.has_signal(AREA_ENTERED) {
mask |= COLLISION_MASK_AREA_ENTERED;
}
if node.has_signal(AREA_EXITED) {
mask |= COLLISION_MASK_AREA_EXITED;
}
mask
}
fn collision_mask_has(mask: u8, flag: u8) -> bool {
mask & flag != 0
}
fn find_node_by_name(parent: &Gd<Node>, name: &StringName) -> Option<Gd<Node>> {
if &parent.get_name() == name {
return Some(parent.clone());
}
for i in 0..parent.get_child_count() {
if let Some(child) = parent.get_child(i) {
let child_node = child.cast::<Node>();
if let Some(found) = find_node_by_name(&child_node, name) {
return Some(found);
}
}
}
None
}
fn connect_scene_tree(mut scene_tree: SceneTreeRef) {
let mut scene_tree_gd = scene_tree.get();
let root = scene_tree_gd.get_root().unwrap();
let watcher = root
.try_get_node_as::<Node>("/root/BevyAppSingleton/SceneTreeWatcher")
.or_else(|| {
root.try_get_node_as::<Node>("BevyAppSingleton/SceneTreeWatcher")
})
.or_else(|| {
tracing::debug!("Searching entire scene tree for SceneTreeWatcher");
find_node_by_name(&root.clone().upcast(), &StringName::from("SceneTreeWatcher"))
})
.unwrap_or_else(|| {
panic!("SceneTreeWatcher not found. Searched /root/BevyAppSingleton/SceneTreeWatcher, BevyAppSingleton/SceneTreeWatcher, and entire tree.");
});
let optimized_watcher = root
.try_get_node_as::<Node>("/root/BevyAppSingleton/OptimizedSceneTreeWatcher")
.or_else(|| root.try_get_node_as::<Node>("BevyAppSingleton/OptimizedSceneTreeWatcher"))
.or_else(|| {
find_node_by_name(
&root.clone().upcast(),
&StringName::from("OptimizedSceneTreeWatcher"),
)
});
if optimized_watcher.is_some() {
tracing::info!("Using optimized GDScript scene tree watcher with type pre-analysis");
} else {
tracing::info!("Using fallback scene tree connection (no type optimization)");
scene_tree_gd.connect(
"node_added",
&watcher
.callable("scene_tree_event")
.bind(&[SceneTreeMessageType::NodeAdded.to_variant()]),
);
scene_tree_gd.connect(
"node_removed",
&watcher
.callable("scene_tree_event")
.bind(&[SceneTreeMessageType::NodeRemoved.to_variant()]),
);
scene_tree_gd.connect(
"node_renamed",
&watcher
.callable("scene_tree_event")
.bind(&[SceneTreeMessageType::NodeRenamed.to_variant()]),
);
}
}
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
pub struct Groups {
groups: Vec<String>,
}
impl Groups {
pub fn is(&self, group_name: &str) -> bool {
self.groups.iter().any(|name| name == group_name)
}
}
impl<T: Inherits<Node>> From<&Gd<T>> for Groups {
fn from(node: &Gd<T>) -> Self {
Groups {
groups: node
.clone()
.upcast::<Node>()
.get_groups()
.iter_shared()
.map(|variant| variant.to_string())
.collect(),
}
}
}
impl From<Vec<String>> for Groups {
fn from(groups: Vec<String>) -> Self {
Groups { groups }
}
}
#[derive(Resource)]
pub struct SceneTreeMessageReader(pub Mutex<crossbeam_channel::Receiver<SceneTreeMessage>>);
impl SceneTreeMessageReader {
pub fn new(receiver: crossbeam_channel::Receiver<SceneTreeMessage>) -> Self {
Self(Mutex::new(receiver))
}
}
fn write_scene_tree_messages(
message_reader: Res<SceneTreeMessageReader>,
mut message_writer: MessageWriter<SceneTreeMessage>,
) {
let receiver = message_reader.0.lock();
let messages: Vec<_> = receiver.try_iter().collect();
message_writer.write_batch(messages);
}
#[derive(Component)]
pub struct ProtectedNodeEntity;
fn create_scene_tree_entity(
commands: &mut Commands,
messages: impl IntoIterator<Item = SceneTreeMessage>,
scene_tree: &mut SceneTreeRef,
entities: &mut Query<(&GodotNodeHandle, Entity, Option<&ProtectedNodeEntity>)>,
component_registry: &SceneTreeComponentRegistry,
node_index: &mut NodeEntityIndex,
godot: &mut GodotAccess,
) {
let mut godot_entity_map = entities
.iter()
.map(|(reference, ent, protected)| (reference.instance_id(), (ent, protected)))
.collect::<HashMap<_, _>>();
let scene_root = scene_tree.get().get_root().unwrap();
let collision_watcher = scene_root
.try_get_node_as::<Node>("/root/BevyAppSingleton/CollisionWatcher")
.or_else(|| {
scene_root.try_get_node_as::<Node>("BevyAppSingleton/CollisionWatcher")
})
.or_else(|| {
tracing::debug!("Searching entire scene tree for CollisionWatcher");
find_node_by_name(
&scene_root.clone().upcast(),
&StringName::from("CollisionWatcher"),
)
});
let mut pending_collision_bodies: Vec<(i64, u8)> = Vec::new();
for message in messages.into_iter() {
trace!(target: "godot_scene_tree_messages", message = ?message);
let SceneTreeMessage {
node_id,
message_type,
node_type,
node_name,
parent_id: parent_id_from_gdscript,
collision_mask,
groups,
} = message;
let instance_id = node_id.instance_id();
let node_handle = node_id;
let entity_info = godot_entity_map.get(&instance_id).cloned();
match message_type {
SceneTreeMessageType::NodeAdded => {
if !instance_id.lookup_validity() {
continue;
}
let mut new_entity_commands = if let Some((ent, _)) = entity_info {
commands.entity(ent)
} else {
commands.spawn_empty()
};
let mut node_accessor = godot.node(node_handle);
let mut node = node_accessor.get::<Node>();
let node_name = node_name.unwrap_or_else(|| node.get_name().to_string());
new_entity_commands
.insert(node_id)
.insert(Name::from(node_name));
for class_name in get_inheritance_hierarchy(
node_type
.unwrap_or_else(|| node.get_class().to_string())
.as_str(),
) {
add_node_type_markers_from_string(
&mut new_entity_commands,
class_name.as_str(),
);
}
let collision_mask = collision_mask.or_else(|| {
collision_watcher
.as_ref()
.map(|_| collision_mask_from_node(&mut node))
});
if collision_watcher.is_some()
&& let Some(mask) = collision_mask
{
let is_collision_body = collision_mask_has(mask, COLLISION_MASK_BODY_ENTERED)
|| collision_mask_has(mask, COLLISION_MASK_AREA_ENTERED);
if is_collision_body {
debug!(target: "godot_scene_tree_collisions",
node_id = instance_id.to_string(),
"is collision body");
pending_collision_bodies.push((instance_id.to_i64(), mask));
}
}
if let Some(groups_vec) = groups {
new_entity_commands.insert(Groups::from(groups_vec));
} else {
new_entity_commands.insert(Groups::from(&node));
}
component_registry.add_to_entity(&mut new_entity_commands, &mut node_accessor);
let new_entity = new_entity_commands.id();
godot_entity_map.insert(
instance_id,
(new_entity, entity_info.and_then(|(_, protected)| protected)),
);
node_index.insert(instance_id, new_entity);
super::autosync::try_add_bundles_for_node(commands, new_entity, godot, node_handle);
let parent_id = parent_id_from_gdscript
.or_else(|| node.get_parent().map(|parent| parent.instance_id()));
if let Some(parent_id) = parent_id
&& parent_id != scene_root.instance_id()
{
if let Some((parent_entity, _)) = godot_entity_map.get(&parent_id) {
commands
.entity(new_entity)
.insert(super::relationship::GodotChildOf(*parent_entity));
} else {
warn!(target: "godot_scene_tree_messages",
"Parent entity with ID {} not found in godot_entity_map. This might indicate a missing or incorrect mapping. Path={}",
parent_id, node.get_path());
}
}
}
SceneTreeMessageType::NodeRemoved => {
if let Some((ent, prot_opt)) = entity_info {
let is_reparenting = godot
.try_get::<Node>(node_handle)
.map(|godot_node| godot_node.get_parent().is_some())
.unwrap_or(false);
if is_reparenting {
trace!(target: "godot_scene_tree_events",
"Node is being reparented, preserving entity");
} else {
let protected = prot_opt.is_some();
if !protected {
commands.entity(ent).despawn();
} else {
_strip_godot_components(commands, ent);
}
godot_entity_map.remove(&instance_id);
node_index.remove(instance_id);
}
} else {
trace!(target: "godot_scene_tree_messages", "Entity for removed node was already despawned");
}
}
SceneTreeMessageType::NodeRenamed => {
if let Some((ent, _)) = entity_info {
let name = node_name
.unwrap_or_else(|| godot.get::<Node>(node_handle).get_name().to_string());
commands.entity(ent).insert(Name::from(name));
} else {
trace!(target: "godot_scene_tree_messages", "Entity for renamed node was already despawned");
}
}
}
}
if !pending_collision_bodies.is_empty()
&& let Some(ref collision_watcher) = collision_watcher
{
batch_connect_collision_signals(&scene_root, collision_watcher, &pending_collision_bodies);
}
}
fn get_inheritance_hierarchy(class_name: &str) -> Vec<String> {
let class_db = ClassDb::singleton();
let mut hierarchy = Vec::new();
let mut current_class = StringName::from(class_name);
while !current_class.is_empty() {
hierarchy.push(current_class.to_string());
current_class = class_db.get_parent_class(¤t_class);
}
hierarchy
}
fn batch_connect_collision_signals(
scene_root: &Gd<godot::classes::Window>,
collision_watcher: &Gd<Node>,
pending_bodies: &[(i64, u8)],
) {
use godot::builtin::PackedInt64Array;
let bulk_ops = scene_root
.get_node_or_null("BevyAppSingleton/OptimizedBulkOperations")
.or_else(|| scene_root.get_node_or_null("/root/BevyAppSingleton/OptimizedBulkOperations"))
.filter(|node| node.has_method("bulk_connect_collision_signals"));
if let Some(mut bulk_ops) = bulk_ops {
let instance_ids: Vec<i64> = pending_bodies.iter().map(|(id, _)| *id).collect();
let collision_masks: Vec<i64> = pending_bodies
.iter()
.map(|(_, mask)| i64::from(*mask))
.collect();
let ids_packed = PackedInt64Array::from(instance_ids.as_slice());
let masks_packed = PackedInt64Array::from(collision_masks.as_slice());
bulk_ops.call(
"bulk_connect_collision_signals",
&[
ids_packed.to_variant(),
masks_packed.to_variant(),
collision_watcher.to_variant(),
],
);
} else {
for (instance_id, mask) in pending_bodies {
let instance_id = InstanceId::from_i64(*instance_id);
if !instance_id.lookup_validity() {
continue;
}
let Some(mut node) = Gd::<Node>::try_from_instance_id(instance_id).ok() else {
continue;
};
let node_clone = node.clone();
if collision_mask_has(*mask, COLLISION_MASK_BODY_ENTERED) {
node.connect(
BODY_ENTERED,
&collision_watcher.callable("collision_event").bind(&[
node_clone.to_variant(),
CollisionMessageType::Started.to_variant(),
]),
);
}
if collision_mask_has(*mask, COLLISION_MASK_BODY_EXITED) {
node.connect(
BODY_EXITED,
&collision_watcher.callable("collision_event").bind(&[
node_clone.to_variant(),
CollisionMessageType::Ended.to_variant(),
]),
);
}
if collision_mask_has(*mask, COLLISION_MASK_AREA_ENTERED) {
node.connect(
AREA_ENTERED,
&collision_watcher.callable("collision_event").bind(&[
node_clone.to_variant(),
CollisionMessageType::Started.to_variant(),
]),
);
}
if collision_mask_has(*mask, COLLISION_MASK_AREA_EXITED) {
node.connect(
AREA_EXITED,
&collision_watcher.callable("collision_event").bind(&[
node_clone.to_variant(),
CollisionMessageType::Ended.to_variant(),
]),
);
}
}
debug!(target: "godot_scene_tree_collisions",
count = pending_bodies.len(),
"Individually connected collision signals (bulk ops not available)");
}
}
fn _strip_godot_components(commands: &mut Commands, ent: Entity) {
let mut entity_commands = commands.entity(ent);
entity_commands.remove::<GodotNodeHandle>();
entity_commands.remove::<GodotScene>();
entity_commands.remove::<Name>();
entity_commands.remove::<Groups>();
remove_comprehensive_node_type_markers(&mut entity_commands);
}
fn try_process_node_renamed_messages_fast_path(
commands: &mut Commands,
messages: &[SceneTreeMessage],
node_index: &NodeEntityIndex,
godot: &mut GodotAccess,
) -> bool {
if !messages
.iter()
.all(|message| matches!(message.message_type, SceneTreeMessageType::NodeRenamed))
{
return false;
}
for message in messages {
let node_handle = message.node_id;
let Some(entity) = node_index.get(node_handle.instance_id()) else {
trace!(target: "godot_scene_tree_messages", "Entity for renamed node was already despawned");
continue;
};
let name = message
.node_name
.clone()
.unwrap_or_else(|| godot.get::<Node>(node_handle).get_name().to_string());
commands.entity(entity).insert(Name::from(name));
}
true
}
#[allow(clippy::too_many_arguments)]
fn read_scene_tree_messages(
mut commands: Commands,
mut scene_tree: SceneTreeRef,
mut message_reader: MessageReader<SceneTreeMessage>,
mut entities: Query<(&GodotNodeHandle, Entity, Option<&ProtectedNodeEntity>)>,
component_registry: Res<SceneTreeComponentRegistry>,
mut node_index: ResMut<NodeEntityIndex>,
mut godot: GodotAccess,
) {
let messages: Vec<_> = message_reader.read().cloned().collect();
if messages.is_empty() {
return;
}
if try_process_node_renamed_messages_fast_path(
&mut commands,
&messages,
&node_index,
&mut godot,
) {
return;
}
create_scene_tree_entity(
&mut commands,
messages,
&mut scene_tree,
&mut entities,
&component_registry,
&mut node_index,
&mut godot,
);
}