use std::any::TypeId;
use std::collections::HashMap;
use std::path::Path;
use bevy::ecs::system::IntoObserverSystem;
use bevy::prelude::*;
use serde::de::DeserializeOwned;
use ts_rs::TS;
use crate::event::{ReactEvent, ReactEventRegistry};
use crate::registry::{NamedEntry, register_entry};
use crate::request::{ReactRequest, ReactRequestRegistry, RequestEvent};
use crate::ts_codegen::TsCollector;
#[derive(Clone, Debug)]
pub struct ReactMessage {
pub name: String,
pub value: serde_json::Value,
}
pub trait ReactPayload: Event + DeserializeOwned + TS + Send + Sync + 'static {
const NAME: &'static str;
}
#[derive(Resource, Default)]
pub(crate) struct ReactRegistry {
pub(crate) handlers: HashMap<&'static str, Registration>,
}
pub(crate) struct Registration {
type_id: TypeId,
handler: Handler,
pub(crate) ts_name: fn() -> String,
pub(crate) ts_collect: fn(&mut TsCollector),
}
type Handler =
Box<dyn Fn(serde_json::Value, &mut Commands) -> Result<(), serde_json::Error> + Send + Sync>;
impl NamedEntry for Registration {
fn type_id(&self) -> TypeId {
self.type_id
}
}
impl ReactRegistry {
pub(crate) fn register<T>(&mut self)
where
T: ReactPayload,
for<'a> <T as Event>::Trigger<'a>: Default,
{
register_entry(
&mut self.handlers,
T::NAME,
"message",
Registration {
type_id: TypeId::of::<T>(),
handler: Box::new(|value, commands| {
let payload: T = serde_json::from_value(value)?;
commands.trigger(payload);
Ok(())
}),
ts_name: T::name,
ts_collect: |c| c.add::<T>(),
},
);
}
pub(crate) fn dispatch(&self, msg: ReactMessage, commands: &mut Commands) {
match self.handlers.get(msg.name.as_str()) {
None => warn!("no handler registered for react message {:?}", msg.name),
Some(reg) => {
if let Err(e) = (reg.handler)(msg.value, commands) {
error!("malformed react message {:?}: {e}", msg.name);
}
}
}
}
}
pub trait ReactAppExt {
fn add_react_message<T>(&mut self) -> &mut Self
where
T: ReactPayload,
for<'a> <T as Event>::Trigger<'a>: Default;
fn add_react_handler<E, B, M, S>(&mut self, observer: S) -> &mut Self
where
E: ReactPayload,
for<'a> <E as Event>::Trigger<'a>: Default,
B: Bundle,
S: IntoObserverSystem<E, B, M>;
fn add_react_request<T>(&mut self) -> &mut Self
where
T: ReactRequest;
fn add_react_request_handler<E, B, M, S>(&mut self, observer: S) -> &mut Self
where
E: Event + RequestEvent,
for<'a> <E as Event>::Trigger<'a>: Default,
B: Bundle,
S: IntoObserverSystem<E, B, M>;
fn add_react_event<E>(&mut self) -> &mut Self
where
E: ReactEvent;
fn export_react_typescript(&self, path: impl AsRef<Path>) -> std::io::Result<()>;
}
impl ReactAppExt for App {
fn add_react_message<T>(&mut self) -> &mut Self
where
T: ReactPayload,
for<'a> <T as Event>::Trigger<'a>: Default,
{
self.world_mut()
.get_resource_or_init::<ReactRegistry>()
.register::<T>();
self
}
fn add_react_handler<E, B, M, S>(&mut self, observer: S) -> &mut Self
where
E: ReactPayload,
for<'a> <E as Event>::Trigger<'a>: Default,
B: Bundle,
S: IntoObserverSystem<E, B, M>,
{
self.add_react_message::<E>();
self.add_observer(observer);
self
}
fn add_react_request<T>(&mut self) -> &mut Self
where
T: ReactRequest,
{
self.world_mut()
.get_resource_or_init::<ReactRequestRegistry>()
.register::<T>();
self
}
fn add_react_request_handler<E, B, M, S>(&mut self, observer: S) -> &mut Self
where
E: Event + RequestEvent,
for<'a> <E as Event>::Trigger<'a>: Default,
B: Bundle,
S: IntoObserverSystem<E, B, M>,
{
self.add_react_request::<E::Req>();
self.add_observer(observer);
self
}
fn add_react_event<E>(&mut self) -> &mut Self
where
E: ReactEvent,
{
self.world_mut()
.get_resource_or_init::<ReactEventRegistry>()
.register::<E>();
self
}
fn export_react_typescript(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
crate::ts_codegen::export(self.world(), path.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::react_message;
use bevy::ecs::world::CommandQueue;
#[react_message]
struct Count(usize);
#[react_message(name = "hp")]
#[allow(dead_code)]
struct Health(u32);
#[react_message]
#[allow(dead_code)]
struct PlayerScore(i64);
#[derive(Resource, Default)]
struct LastCount(usize);
#[test]
fn derives_emit_name() {
assert_eq!(Count::NAME, "count");
assert_eq!(PlayerScore::NAME, "playerScore");
assert_eq!(Health::NAME, "hp");
}
fn test_app() -> App {
let mut app = App::new();
app.init_resource::<LastCount>();
app.add_react_handler(|on: On<Count>, mut last: ResMut<LastCount>| last.0 = on.event().0);
app
}
fn dispatch(app: &mut App, msg: ReactMessage) {
app.world_mut()
.resource_scope(|world, registry: Mut<ReactRegistry>| {
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, world);
registry.dispatch(msg, &mut commands);
queue.apply(world);
});
}
#[test]
fn dispatches_to_observer() {
let mut app = test_app();
dispatch(
&mut app,
ReactMessage {
name: "count".into(),
value: serde_json::json!(3),
},
);
assert_eq!(app.world().resource::<LastCount>().0, 3);
}
#[test]
fn tolerates_unknown_and_malformed() {
let mut app = test_app();
dispatch(
&mut app,
ReactMessage {
name: "nope".into(),
value: serde_json::json!(1),
},
);
dispatch(
&mut app,
ReactMessage {
name: "count".into(),
value: serde_json::json!("not a number"),
},
);
assert_eq!(app.world().resource::<LastCount>().0, 0);
}
}