use crate::interop::{GodotAccess, GodotNodeHandle};
use bevy_app::{App, First, Last, Plugin};
use bevy_ecs::{
component::Component,
entity::Entity,
event::Event,
prelude::Resource,
system::{Commands, Query, Res, SystemParam},
};
use crossbeam_channel::Sender;
use godot::{
classes::Node,
obj::Gd,
prelude::{Callable, Variant},
};
use parking_lot::Mutex;
use std::fmt::Debug;
use std::sync::Arc;
use tracing::error;
pub(crate) trait SignalDispatch: Send {
fn trigger_in_world(self: Box<Self>, world: &mut bevy_ecs::world::World);
}
struct SignalEnvelope<T: Event + Clone + Send + 'static> {
event: T,
}
impl<T: Event + Clone + Send + 'static> SignalDispatch for SignalEnvelope<T>
where
for<'a> T::Trigger<'a>: Default,
{
fn trigger_in_world(self: Box<Self>, world: &mut bevy_ecs::world::World) {
world.trigger(self.event);
}
}
#[derive(Resource)]
pub(crate) struct SignalReceiver(pub Mutex<crossbeam_channel::Receiver<Box<dyn SignalDispatch>>>);
impl SignalReceiver {
pub fn new(receiver: crossbeam_channel::Receiver<Box<dyn SignalDispatch>>) -> Self {
Self(Mutex::new(receiver))
}
}
#[doc(hidden)]
#[derive(Resource)]
pub(crate) struct SignalSender(pub crossbeam_channel::Sender<Box<dyn SignalDispatch>>);
#[derive(Resource, Default)]
struct PendingSignalConnections {
queue: Mutex<Vec<Box<dyn PendingSignalConnection>>>,
}
trait PendingSignalConnection: Send {
fn connect(self: Box<Self>, godot: &mut GodotAccess);
}
impl PendingSignalConnections {
fn push(&self, connection: Box<dyn PendingSignalConnection>) {
self.queue.lock().push(connection);
}
fn drain(&self) -> Vec<Box<dyn PendingSignalConnection>> {
self.queue.lock().drain(..).collect()
}
}
fn ensure_signal_connection_queue(app: &mut App) {
if !app.world().contains_resource::<PendingSignalConnections>() {
app.init_resource::<PendingSignalConnections>()
.add_systems(Last, process_pending_signal_connections);
}
}
fn process_pending_signal_connections(
pending: Res<PendingSignalConnections>,
mut godot: GodotAccess,
) {
for connection in pending.drain() {
connection.connect(&mut godot);
}
}
fn connect_signal<T>(
godot: &mut GodotAccess,
node: GodotNodeHandle,
signal_name: &str,
source_entity: Option<Entity>,
mapper: Box<
dyn FnMut(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + 'static,
>,
sender: Sender<Box<dyn SignalDispatch>>,
) where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
let mut node_ref = godot.get::<Node>(node);
let signal_name_copy = signal_name.to_string();
let source_node_handle = node;
let mut mapper = mapper;
let closure = move |args: &[&Variant]| -> Variant {
let owned: Vec<Variant> = args.iter().map(|&v| v.clone()).collect();
let event = mapper(&owned, source_node_handle, source_entity);
if let Some(event) = event {
let _ = sender.send(Box::new(SignalEnvelope { event }));
}
Variant::nil()
};
let callable = Callable::from_fn(&format!("signal_handler_{signal_name_copy}"), closure);
node_ref.connect(signal_name, &callable);
}
pub struct GodotSignalsPlugin<T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
_phantom: std::marker::PhantomData<T>,
}
impl<T> Default for GodotSignalsPlugin<T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
fn default() -> Self {
Self {
_phantom: Default::default(),
}
}
}
impl<T> Plugin for GodotSignalsPlugin<T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
fn build(&self, app: &mut App) {
ensure_signal_connection_queue(app);
if !app.world().contains_resource::<SignalSender>() {
let (sender, receiver) = crossbeam_channel::unbounded::<Box<dyn SignalDispatch>>();
app.world_mut().insert_resource(SignalSender(sender));
app.world_mut()
.insert_resource(SignalReceiver::new(receiver));
app.add_systems(First, drain_and_trigger_signals);
}
app.add_systems(First, process_deferred_signal_connections::<T>);
}
}
fn drain_and_trigger_signals(world: &mut bevy_ecs::world::World) {
let mut pending: Vec<Box<dyn SignalDispatch>> = Vec::new();
if let Some(receiver) = world.get_resource::<SignalReceiver>() {
let guard = receiver.0.lock();
pending.extend(guard.try_iter());
}
for dispatch in pending.drain(..) {
dispatch.trigger_in_world(world);
}
}
#[derive(SystemParam)]
pub struct GodotSignals<'w, T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
sender: Res<'w, SignalSender>,
pending: Res<'w, PendingSignalConnections>,
_marker: std::marker::PhantomData<T>,
}
impl<'w, T> GodotSignals<'w, T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
pub fn connect<F>(
&self,
node: GodotNodeHandle,
signal_name: &str,
source_entity: Option<Entity>,
mapper: F,
) where
F: FnMut(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + 'static,
{
self.pending.push(Box::new(PendingSignalConnectionImpl {
node,
signal_name: signal_name.to_string(),
source_entity,
mapper: Box::new(mapper),
sender: self.sender.0.clone(),
_marker: std::marker::PhantomData,
}));
}
pub fn connect_object<O, F>(&self, object: Gd<O>, signal_name: &str, mapper: F)
where
O: godot::obj::Inherits<godot::classes::Object> + godot::obj::GodotClass,
F: FnMut(&[Variant]) -> Option<T> + Send + 'static,
{
self.pending.push(Box::new(PendingDirectNodeConnection {
instance_id: object.instance_id(),
signal_name: signal_name.to_string(),
mapper: Box::new(mapper),
sender: self.sender.0.clone(),
_marker: std::marker::PhantomData,
}));
}
}
#[deprecated(note = "Use GodotSignals instead")]
pub type TypedGodotSignals<'w, T> = GodotSignals<'w, T>;
struct PendingSignalConnectionImpl<T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
node: GodotNodeHandle,
signal_name: String,
source_entity: Option<Entity>,
mapper:
Box<dyn FnMut(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + 'static>,
sender: Sender<Box<dyn SignalDispatch>>,
_marker: std::marker::PhantomData<T>,
}
impl<T> PendingSignalConnection for PendingSignalConnectionImpl<T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
fn connect(self: Box<Self>, godot: &mut GodotAccess) {
let PendingSignalConnectionImpl {
node,
signal_name,
source_entity,
mapper,
sender,
_marker: _,
} = *self;
connect_signal(godot, node, &signal_name, source_entity, mapper, sender);
}
}
struct PendingDirectNodeConnection<T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
instance_id: godot::obj::InstanceId,
signal_name: String,
mapper: Box<dyn FnMut(&[Variant]) -> Option<T> + Send + 'static>,
sender: Sender<Box<dyn SignalDispatch>>,
_marker: std::marker::PhantomData<T>,
}
impl<T> PendingSignalConnection for PendingDirectNodeConnection<T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
fn connect(self: Box<Self>, _godot: &mut GodotAccess) {
let PendingDirectNodeConnection {
instance_id,
signal_name,
mut mapper,
sender,
_marker: _,
} = *self;
let Ok(mut node) = Gd::<godot::classes::Object>::try_from_instance_id(instance_id) else {
error!(
"Failed to get object with instance_id {:?} for signal connection",
instance_id
);
return;
};
let signal_name_copy = signal_name.clone();
let closure = move |args: &[&Variant]| -> Variant {
let owned: Vec<Variant> = args.iter().map(|&v| v.clone()).collect();
if let Some(event) = mapper(&owned) {
let _ = sender.send(Box::new(SignalEnvelope { event }));
}
Variant::nil()
};
let callable = Callable::from_fn(&format!("signal_handler_{signal_name_copy}"), closure);
node.connect(&signal_name, &callable);
}
}
fn process_deferred_signal_connections<T>(
mut commands: Commands,
mut query: Query<(Entity, &GodotNodeHandle, &mut DeferredSignalConnections<T>)>,
signals: GodotSignals<T>,
) where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
for (entity, handle, mut deferred) in query.iter_mut() {
for conn in deferred.connections.drain(..) {
let signal = conn.signal_name;
let mapper = conn.mapper;
signals.connect(
*handle,
&signal,
Some(entity),
move |args, node_handle, ent| (mapper)(args, node_handle, ent),
);
}
commands
.entity(entity)
.remove::<DeferredSignalConnections<T>>();
}
}
pub struct DeferredConnection<T: Event + Clone + Send + 'static> {
pub signal_name: String,
pub mapper: Arc<
dyn Fn(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
>,
}
impl<T: Event + Clone + Send + 'static> Debug for DeferredConnection<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DeferredConnection {{ signal_name: {:?} }}",
self.signal_name
)
}
}
#[derive(Component, Debug)]
pub struct DeferredSignalConnections<T: Event + Clone + Send + 'static> {
pub connections: Vec<DeferredConnection<T>>,
}
impl<T: Event + Clone + Send + 'static> Default for DeferredSignalConnections<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Event + Clone + Send + 'static> DeferredSignalConnections<T> {
pub fn new() -> Self {
Self {
connections: Vec::new(),
}
}
pub fn with_connection<F>(signal_name: impl Into<String>, mapper: F) -> Self
where
F: Fn(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
{
Self {
connections: vec![DeferredConnection {
signal_name: signal_name.into(),
mapper: Arc::new(mapper),
}],
}
}
pub fn push<F>(&mut self, signal_name: impl Into<String>, mapper: F)
where
F: Fn(&[Variant], GodotNodeHandle, Option<Entity>) -> Option<T> + Send + Sync + 'static,
{
self.connections.push(DeferredConnection {
signal_name: signal_name.into(),
mapper: Arc::new(mapper),
});
}
}
#[deprecated(note = "Use DeferredSignalConnections instead")]
pub type TypedDeferredSignalConnections<T> = DeferredSignalConnections<T>;
#[deprecated(note = "Use DeferredConnection instead")]
pub type TypedDeferredConnection<T> = DeferredConnection<T>;
#[doc(hidden)]
pub(crate) trait DeferredSignalConnectionTrait: Send + Sync + Debug {
fn connect(&self, root_node: &Gd<Node>, entity: Entity, sender: &SignalSender);
}
#[doc(hidden)]
#[derive(Debug)]
pub(crate) struct SignalConnectionSpec<T>
where
T: Event + Clone + Send + 'static,
for<'a> T::Trigger<'a>: Default,
{
pub(crate) node_path: String,
pub(crate) signal_name: String,
pub(crate) connections: DeferredSignalConnections<T>,
}
#[doc(hidden)]
impl<T> DeferredSignalConnectionTrait for SignalConnectionSpec<T>
where
T: Event + Clone + Send + Debug + 'static,
for<'a> T::Trigger<'a>: Default,
{
fn connect(&self, root_node: &Gd<Node>, source_entity: Entity, sender: &SignalSender) {
let Some(mut target_node) = root_node.get_node_or_null(self.node_path.as_str()) else {
error!(
"Failed to find node at path '{}' for signal connection",
self.node_path
);
return;
};
for connection in self.connections.connections.iter() {
let source_node_id = GodotNodeHandle::from(target_node.instance_id());
let sender_copy = sender.0.clone();
let mapper = connection.mapper.clone();
let signal_name = self.signal_name.clone();
let closure = move |args: &[&Variant]| -> Variant {
let owned: Vec<Variant> = args.iter().map(|&v| v.clone()).collect();
if let Some(event) = mapper(&owned, source_node_id, Some(source_entity)) {
let _ = sender_copy.send(Box::new(SignalEnvelope { event }));
}
Variant::nil()
};
target_node.connect(
&signal_name,
&Callable::from_fn(&format!("signal_handler_{signal_name}"), closure),
);
}
}
}