use std::any::{Any, TypeId};
use std::collections::{HashMap, HashSet};
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use bevy_ecs::component::Component;
use bevy_ecs::entity::Entity;
use bevy_ecs::event::EntityEvent;
use bevy_ecs::lifecycle::{Add, Remove};
use bevy_ecs::observer::On;
use bevy_ecs::prelude::{Commands, Query, Res, Resource, World};
use bevy_ecs::system::{In, IntoSystem, System};
#[derive(Clone, Debug)]
pub struct Signal<E> {
pub source: Entity,
pub target: Entity,
pub event: E,
}
pub struct Added<T>(PhantomData<fn() -> T>);
pub struct Removed<T>(PhantomData<fn() -> T>);
impl<T> Clone for Added<T> {
fn clone(&self) -> Self {
Self(PhantomData)
}
}
impl<T> Clone for Removed<T> {
fn clone(&self) -> Self {
Self(PhantomData)
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Connection {
pub handler: String,
pub target: Entity,
}
#[derive(
Component,
Clone,
Debug,
Default,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
)]
pub struct Connections {
pub list: Vec<Connection>,
}
impl Connections {
#[must_use]
pub fn with(mut self, handler: impl Into<String>, target: Entity) -> Self {
self.list.push(Connection {
handler: handler.into(),
target,
});
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SignalError {
UnknownHandler(String),
DuplicateHandler(String),
MissingEntity(Entity),
}
impl std::fmt::Display for SignalError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownHandler(name) => {
write!(formatter, "no signal handler is named `{name}`")
}
Self::DuplicateHandler(name) => write!(
formatter,
"a signal handler named `{name}` is already registered"
),
Self::MissingEntity(entity) => {
write!(formatter, "entity {entity:?} does not exist")
}
}
}
}
impl std::error::Error for SignalError {}
impl From<SignalError> for super::AppError {
fn from(error: SignalError) -> Self {
Self::PluginSetup {
plugin: "signals",
message: error.to_string(),
}
}
}
type BoxedHandler<E> = Box<dyn System<In = In<Signal<E>>, Out = ()>>;
type Slot = Arc<Mutex<Option<Box<dyn Any + Send>>>>;
struct Handler {
signal: TypeId,
slot: Slot,
}
#[derive(Resource, Default)]
pub(super) struct SignalHandlers {
handlers: HashMap<String, Handler>,
dispatchers: HashSet<TypeId>,
pub(super) muted: bool,
}
pub trait SignalEvent: Clone + Send + Sync + 'static {
#[doc(hidden)]
fn add_dispatcher(world: &mut World);
}
impl<E: EntityEvent + Clone> SignalEvent for E {
fn add_dispatcher(world: &mut World) {
world.add_observer(
|on: On<E>,
connections: Query<&Connections>,
handlers: Res<SignalHandlers>,
mut commands: Commands| {
let event = on.event();
queue_signals(
event.event_target(),
event,
&connections,
&handlers,
&mut commands,
);
},
);
}
}
impl<T: Component> SignalEvent for Added<T> {
fn add_dispatcher(world: &mut World) {
world.add_observer(
|on: On<Add, T>,
connections: Query<&Connections>,
handlers: Res<SignalHandlers>,
mut commands: Commands| {
queue_signals(
on.entity,
&Self(PhantomData),
&connections,
&handlers,
&mut commands,
);
},
);
}
}
impl<T: Component> SignalEvent for Removed<T> {
fn add_dispatcher(world: &mut World) {
world.add_observer(
|on: On<Remove, T>,
connections: Query<&Connections>,
handlers: Res<SignalHandlers>,
mut commands: Commands| {
queue_signals(
on.entity,
&Self(PhantomData),
&connections,
&handlers,
&mut commands,
);
},
);
}
}
fn queue_signals<E: SignalEvent>(
source: Entity,
event: &E,
connections: &Query<&Connections>,
handlers: &SignalHandlers,
commands: &mut Commands,
) {
if handlers.muted {
return;
}
let Ok(connections) = connections.get(source) else {
return;
};
for connection in &connections.list {
let Some(handler) = handlers.handlers.get(&connection.handler) else {
continue;
};
if handler.signal != TypeId::of::<E>() {
continue;
}
let slot = Arc::clone(&handler.slot);
let signal = Signal {
source,
target: connection.target,
event: event.clone(),
};
commands
.queue(move |world: &mut World| run_handler(&slot, signal, world));
}
}
fn run_handler<E: SignalEvent>(
slot: &Slot,
signal: Signal<E>,
world: &mut World,
) {
let taken = slot.lock().expect("handler slot").take();
let Some(mut system) = taken else { return };
let handler = system
.downcast_mut::<BoxedHandler<E>>()
.expect("signal type checked at dispatch");
let _ = handler.run(signal, world);
*slot.lock().expect("handler slot") = Some(system);
}
pub(super) fn validate_connections(
world: &World,
spawned: &HashMap<uuid::Uuid, Entity>,
) -> Result<(), super::SceneIoError> {
let mut objects: Vec<_> = spawned.iter().collect();
objects.sort_unstable();
for (&object, &entity) in objects {
let Some(connections) = world.get::<Connections>(entity) else {
continue;
};
for connection in &connections.list {
let problem =
if !SignalHandlers::contains(world, &connection.handler) {
SignalError::UnknownHandler(connection.handler.clone())
} else if world.get_entity(connection.target).is_err() {
SignalError::MissingEntity(connection.target)
} else {
continue;
};
return Err(super::SceneIoError::Connection { object, problem });
}
}
Ok(())
}
impl SignalHandlers {
pub(super) fn register<E: SignalEvent, M>(
world: &mut World,
name: String,
handler: impl IntoSystem<In<Signal<E>>, (), M>,
) -> Result<(), SignalError> {
let handlers = world.get_resource_or_init::<Self>().into_inner();
if handlers.handlers.contains_key(&name) {
return Err(SignalError::DuplicateHandler(name));
}
let add_dispatcher = handlers.dispatchers.insert(TypeId::of::<E>());
let mut system: BoxedHandler<E> =
Box::new(IntoSystem::into_system(handler));
system.initialize(world);
world.resource_mut::<Self>().handlers.insert(
name,
Handler {
signal: TypeId::of::<E>(),
slot: Arc::new(Mutex::new(Some(Box::new(system)))),
},
);
if add_dispatcher {
E::add_dispatcher(world);
}
Ok(())
}
pub(super) fn contains(world: &World, name: &str) -> bool {
world
.get_resource::<Self>()
.is_some_and(|handlers| handlers.handlers.contains_key(name))
}
}
#[cfg(test)]
mod tests {
use bevy_ecs::prelude::ResMut;
use super::super::App;
use super::*;
#[derive(EntityEvent, Clone)]
struct Hit {
entity: Entity,
amount: u32,
}
#[derive(Component, Clone)]
struct Health;
#[derive(Resource, Clone, Default)]
struct Log(Vec<(&'static str, Entity, Entity, u32)>);
fn app() -> App {
let mut app = App::new();
app.insert_resource(Log::default())
.ignore_in_snapshots::<Log>()
.register_snapshot_component::<Health>();
app.add_signal_handler(
"hit",
|In(signal): In<Signal<Hit>>, mut log: ResMut<Log>| {
log.0.push((
"hit",
signal.source,
signal.target,
signal.event.amount,
));
},
)
.unwrap()
.add_signal_handler(
"hit_again",
|In(signal): In<Signal<Hit>>, mut log: ResMut<Log>| {
log.0.push(("again", signal.source, signal.target, 0));
},
)
.unwrap()
.add_signal_handler(
"armed",
|In(signal): In<Signal<Added<Health>>>, mut log: ResMut<Log>| {
log.0.push(("armed", signal.source, signal.target, 0));
},
)
.unwrap()
.add_signal_handler(
"disarmed",
|In(signal): In<Signal<Removed<Health>>>, mut log: ResMut<Log>| {
log.0.push(("disarmed", signal.source, signal.target, 0));
},
)
.unwrap();
app
}
fn take_log(app: &mut App) -> Vec<(&'static str, Entity, Entity, u32)> {
app.world_mut().flush();
std::mem::take(&mut app.world_mut().resource_mut::<Log>().0)
}
#[test]
fn connected_handlers_answer_events_and_component_changes() {
let mut app = app();
let coin = app.spawn(());
let player = app.spawn(());
let other = app.spawn(());
for (handler, target) in [
("hit", player),
("hit_again", coin),
("armed", coin),
("disarmed", player),
] {
app.connect(coin, handler, target).unwrap();
}
let mut commands = app.world_mut().commands();
commands.trigger(Hit {
entity: coin,
amount: 3,
});
commands.trigger(Hit {
entity: other,
amount: 9,
});
assert_eq!(
take_log(&mut app),
[("hit", coin, player, 3), ("again", coin, coin, 0)]
);
app.world_mut().entity_mut(coin).insert(Health);
app.world_mut().entity_mut(other).insert(Health);
assert_eq!(take_log(&mut app), [("armed", coin, coin, 0)]);
app.world_mut().despawn(coin);
assert_eq!(take_log(&mut app), [("disarmed", coin, player, 0)]);
}
#[test]
fn snapshots_keep_connections_and_restores_fire_nothing() {
let mut app = app();
let coin = app.spawn(Health);
app.connect(coin, "armed", coin)
.unwrap()
.connect(coin, "disarmed", coin)
.unwrap()
.connect(coin, "hit", coin)
.unwrap();
take_log(&mut app);
let snapshot = app.snapshot().unwrap();
app.world_mut().entity_mut(coin).remove::<Health>();
assert_eq!(take_log(&mut app), [("disarmed", coin, coin, 0)]);
app.restore(&snapshot).unwrap();
assert!(take_log(&mut app).is_empty());
app.world_mut().entity_mut(coin).remove::<Connections>();
app.restore(&snapshot).unwrap();
app.world_mut().trigger(Hit {
entity: coin,
amount: 1,
});
assert_eq!(take_log(&mut app), [("hit", coin, coin, 1)]);
}
#[test]
fn connections_name_registered_handlers_and_live_entities() {
let mut app = app();
let coin = app.spawn(());
let gone = app.spawn(());
app.despawn(gone).unwrap();
assert_eq!(
app.connect(coin, "missing", coin).err(),
Some(SignalError::UnknownHandler("missing".into()))
);
assert_eq!(
app.connect(coin, "hit", gone).err(),
Some(SignalError::MissingEntity(gone))
);
assert_eq!(
app.connect(gone, "hit", coin).err(),
Some(SignalError::MissingEntity(gone))
);
assert_eq!(
app.add_signal_handler("hit", |_: In<Signal<Hit>>| {}).err(),
Some(SignalError::DuplicateHandler("hit".into()))
);
}
#[test]
fn scenes_save_connections_by_object_id_and_check_them_on_load() {
use super::super::{
load_scene_document, scene_document, Name, SceneComponentRegistry,
SceneId, SceneIoError, SceneLoadMode, CONNECTIONS_COMPONENT,
};
let game = || {
let mut app = app();
app.add_plugin(crate::AssetPlugin).unwrap();
app
};
let mut editor = game();
let coin = editor.spawn(Name("Coin".into()));
let player = editor.spawn(Name("Player".into()));
editor.connect(coin, "hit", player).unwrap();
let document = scene_document(editor.world_mut(), "level").unwrap();
let id =
|app: &App, entity| app.world().get::<SceneId>(entity).unwrap().0;
let (coin_id, player_id) = (id(&editor, coin), id(&editor, player));
let saved = document
.entities
.iter()
.find(|entity| entity.id == coin_id)
.unwrap()
.components[CONNECTIONS_COMPONENT]
.clone();
assert!(saved.contains(&player_id.to_string()), "{saved}");
let mut app = game();
load_scene_document(app.world_mut(), &document, SceneLoadMode::Replace)
.unwrap();
let entity = |app: &mut App, wanted| {
let mut query = app.world_mut().query::<(Entity, &SceneId)>();
query
.iter(app.world())
.find(|(_, id)| id.0 == wanted)
.unwrap()
.0
};
let (coin, player) =
(entity(&mut app, coin_id), entity(&mut app, player_id));
app.world_mut().trigger(Hit {
entity: coin,
amount: 2,
});
assert_eq!(take_log(&mut app), [("hit", coin, player, 2)]);
let with_connections = |connections: String| {
let mut document = document.clone();
for entity in &mut document.entities {
if entity.id == coin_id {
entity.components.insert(
CONNECTIONS_COMPONENT.into(),
connections.clone(),
);
}
}
document
};
let unknown = with_connections(saved.replace("\"hit\"", "\"gone\""));
match load_scene_document(
app.world_mut(),
&unknown,
SceneLoadMode::Replace,
) {
Err(SceneIoError::Connection {
object,
problem: SignalError::UnknownHandler(name),
}) if object == coin_id && name == "gone" => {}
other => panic!("{other:?}"),
}
let mut dangling = document.clone();
dangling.entities.retain(|entity| entity.id != player_id);
match load_scene_document(
app.world_mut(),
&dangling,
SceneLoadMode::Replace,
) {
Err(SceneIoError::Connection {
object,
problem: SignalError::MissingEntity(_),
}) if object == coin_id => {}
other => panic!("{other:?}"),
}
assert_eq!(app.world().get::<SceneId>(coin).unwrap().0, coin_id);
app.world_mut()
.resource_mut::<SceneComponentRegistry>()
.keep_unregistered();
load_scene_document(app.world_mut(), &unknown, SceneLoadMode::Replace)
.unwrap();
}
}