Skip to content

Commit 0af6fc1

Browse files
Implement world.trigger_event remote method (#21798)
# Objective Tools using `bevy_remote` will be able to identify (via the schema) and trigger events. - [x] Document `bevy_ecs/src/reflect/event.rs` ## Solution I've added a method `world.trigger_event`, added `Event` to the schema's reflection metadata and `ReflectEvent` to allow this. ## Testing I have copied the (tested) code from my game but have NOT tested this branch yet. I am new to Rust/Cargo and need to go to sleep now, I'll figure this out and test it tomorrow. --- ## Showcase Here's what I needed to add to my game in order to allow my editor to access and trigger an event: ```rust #[derive(Event, Reflect)] #[reflect(Event)] pub struct AssignToRoute { pub vehicle: Entity, pub route: Entity, pub origin: Entity, } ``` Here's a screenshot of my editor using this feature: <img width="1463" height="967" alt="Screenshot 2025-11-10 at 1 40 42 AM" src="https://github.com/user-attachments/assets/7696ddb8-3552-4e0d-a78f-6178c0c66cbb" />
1 parent 0d13444 commit 0af6fc1

File tree

6 files changed

+190
-7
lines changed

6 files changed

+190
-7
lines changed

crates/bevy_ecs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ pub mod prelude {
109109
#[doc(hidden)]
110110
#[cfg(feature = "bevy_reflect")]
111111
pub use crate::reflect::{
112-
AppTypeRegistry, ReflectComponent, ReflectFromWorld, ReflectResource,
112+
AppTypeRegistry, ReflectComponent, ReflectEvent, ReflectFromWorld, ReflectResource,
113113
};
114114

115115
#[doc(hidden)]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Definitions for [`Event`] reflection.
2+
//! This allows triggering events whose type is only known at runtime.
3+
//!
4+
//! This module exports two types: [`ReflectEventFns`] and [`ReflectEvent`].
5+
//!
6+
//! Same as [`component`](`super::component`), but for events.
7+
8+
use crate::{event::Event, reflect::from_reflect_with_fallback, world::World};
9+
use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect, TypePath, TypeRegistry};
10+
11+
/// A struct used to operate on reflected [`Event`] trait of a type.
12+
///
13+
/// A [`ReflectEvent`] for type `T` can be obtained via
14+
/// [`bevy_reflect::TypeRegistration::data`].
15+
#[derive(Clone)]
16+
pub struct ReflectEvent(ReflectEventFns);
17+
18+
/// The raw function pointers needed to make up a [`ReflectEvent`].
19+
///
20+
/// This is used when creating custom implementations of [`ReflectEvent`] with
21+
/// [`ReflectEventFns::new()`].
22+
///
23+
/// > **Note:**
24+
/// > Creating custom implementations of [`ReflectEvent`] is an advanced feature that most users
25+
/// > will not need.
26+
/// > Usually a [`ReflectEvent`] is created for a type by deriving [`Reflect`]
27+
/// > and adding the `#[reflect(Event)]` attribute.
28+
/// > After adding the component to the [`TypeRegistry`],
29+
/// > its [`ReflectEvent`] can then be retrieved when needed.
30+
///
31+
/// Creating a custom [`ReflectEvent`] may be useful if you need to create new component types
32+
/// at runtime, for example, for scripting implementations.
33+
///
34+
/// By creating a custom [`ReflectEvent`] and inserting it into a type's
35+
/// [`TypeRegistration`][bevy_reflect::TypeRegistration],
36+
/// you can modify the way that reflected event of that type will be triggered in the Bevy
37+
/// world.
38+
#[derive(Clone)]
39+
pub struct ReflectEventFns {
40+
trigger: fn(&mut World, &dyn PartialReflect, &TypeRegistry),
41+
}
42+
43+
impl ReflectEventFns {
44+
/// Get the default set of [`ReflectEventFns`] for a specific event type using its
45+
/// [`FromType`] implementation.
46+
///
47+
/// This is useful if you want to start with the default implementation before overriding some
48+
/// of the functions to create a custom implementation.
49+
pub fn new<'a, T: Event + FromReflect + TypePath>() -> Self
50+
where
51+
T::Trigger<'a>: Default,
52+
{
53+
<ReflectEvent as FromType<T>>::from_type().0
54+
}
55+
}
56+
57+
impl ReflectEvent {
58+
/// Triggers a reflected [`Event`] like [`trigger()`](World::trigger).
59+
pub fn trigger(&self, world: &mut World, event: &dyn PartialReflect, registry: &TypeRegistry) {
60+
(self.0.trigger)(world, event, registry);
61+
}
62+
}
63+
64+
impl<'a, E: Event + Reflect + TypePath> FromType<E> for ReflectEvent
65+
where
66+
<E as Event>::Trigger<'a>: Default,
67+
{
68+
fn from_type() -> Self {
69+
ReflectEvent(ReflectEventFns {
70+
trigger: |world, reflected_event, registry| {
71+
let event = from_reflect_with_fallback::<E>(reflected_event, world, registry);
72+
world.trigger(event);
73+
},
74+
})
75+
}
76+
}

crates/bevy_ecs/src/reflect/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use bevy_reflect::{
1414
mod bundle;
1515
mod component;
1616
mod entity_commands;
17+
mod event;
1718
mod from_world;
1819
mod map_entities;
1920
mod resource;
@@ -22,6 +23,7 @@ use bevy_utils::prelude::DebugName;
2223
pub use bundle::{ReflectBundle, ReflectBundleFns};
2324
pub use component::{ReflectComponent, ReflectComponentFns};
2425
pub use entity_commands::ReflectCommandExt;
26+
pub use event::{ReflectEvent, ReflectEventFns};
2527
pub use from_world::{ReflectFromWorld, ReflectFromWorldFns};
2628
pub use map_entities::ReflectMapEntities;
2729
pub use resource::{ReflectResource, ReflectResourceFns};

crates/bevy_remote/src/builtin_methods.rs

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ use bevy_ecs::{
1010
lifecycle::RemovedComponentEntity,
1111
message::MessageCursor,
1212
query::QueryBuilder,
13-
reflect::{AppTypeRegistry, ReflectComponent, ReflectResource},
13+
reflect::{AppTypeRegistry, ReflectComponent, ReflectEvent, ReflectResource},
1414
system::{In, Local},
15-
world::{EntityRef, EntityWorldMut, FilteredEntityRef, World},
15+
world::{EntityRef, EntityWorldMut, FilteredEntityRef, Mut, World},
1616
};
1717
use bevy_log::warn_once;
1818
use bevy_platform::collections::HashMap;
1919
use bevy_reflect::{
2020
serde::{ReflectSerializer, TypedReflectDeserializer},
21-
GetPath, PartialReflect, TypeRegistration, TypeRegistry,
21+
DynamicStruct, GetPath, PartialReflect, TypeRegistration, TypeRegistry,
2222
};
23-
use serde::{de::DeserializeSeed as _, Deserialize, Serialize};
23+
use serde::{de::DeserializeSeed as _, de::IntoDeserializer, Deserialize, Serialize};
2424
use serde_json::{Map, Value};
2525

2626
use crate::{
@@ -83,6 +83,9 @@ pub const BRP_MUTATE_RESOURCE_METHOD: &str = "world.mutate_resources";
8383
/// The method path for a `world.list_resources` request.
8484
pub const BRP_LIST_RESOURCES_METHOD: &str = "world.list_resources";
8585

86+
/// The method path for a `world.trigger_event` request.
87+
pub const BRP_TRIGGER_EVENT_METHOD: &str = "world.trigger_event";
88+
8689
/// The method path for a `registry.schema` request.
8790
pub const BRP_REGISTRY_SCHEMA_METHOD: &str = "registry.schema";
8891

@@ -299,6 +302,19 @@ pub struct BrpMutateResourcesParams {
299302
pub value: Value,
300303
}
301304

305+
/// `world.trigger_event`:
306+
///
307+
/// The server responds with a null.
308+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
309+
struct BrpTriggerEventParams {
310+
/// The [full path] of the event to trigger.
311+
///
312+
/// [full path]: bevy_reflect::TypePath::type_path
313+
pub event: String,
314+
/// The serialized value of the event to be triggered, if any.
315+
pub value: Option<Value>,
316+
}
317+
302318
/// Describes the data that is to be fetched in a query.
303319
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
304320
pub struct BrpQuery {
@@ -1348,6 +1364,44 @@ pub fn process_remote_list_components_watching_request(
13481364
}
13491365
}
13501366

1367+
/// Handles a `world.trigger_event` request coming from a client.
1368+
pub fn process_remote_trigger_event_request(
1369+
In(params): In<Option<Value>>,
1370+
world: &mut World,
1371+
) -> BrpResult {
1372+
let BrpTriggerEventParams { event, value } = parse_some(params)?;
1373+
1374+
world.resource_scope(|world, registry: Mut<AppTypeRegistry>| {
1375+
let registry = registry.read();
1376+
1377+
let Some(registration) = registry.get_with_type_path(&event) else {
1378+
return Err(BrpError::resource_error(format!(
1379+
"Unknown event type: `{event}`"
1380+
)));
1381+
};
1382+
let Some(reflect_event) = registration.data::<ReflectEvent>() else {
1383+
return Err(BrpError::resource_error(format!(
1384+
"Event `{event}` is not reflectable"
1385+
)));
1386+
};
1387+
1388+
if let Some(payload) = value {
1389+
let payload: Box<dyn PartialReflect> =
1390+
TypedReflectDeserializer::new(registration, &registry)
1391+
.deserialize(payload.into_deserializer())
1392+
.map_err(|err| {
1393+
BrpError::resource_error(format!("{event} is invalid: {err}"))
1394+
})?;
1395+
reflect_event.trigger(world, &*payload, &registry);
1396+
} else {
1397+
let payload = DynamicStruct::default();
1398+
reflect_event.trigger(world, &payload, &registry);
1399+
}
1400+
1401+
Ok(Value::Null)
1402+
})
1403+
}
1404+
13511405
/// Handles a `registry.schema` request (list all registry types in form of schema) coming from a client.
13521406
pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> BrpResult {
13531407
let filter: BrpJsonSchemaQueryFilter = match params {
@@ -1628,6 +1682,11 @@ mod tests {
16281682
}
16291683

16301684
use super::*;
1685+
use bevy_ecs::{
1686+
component::Component, event::Event, observer::On, resource::Resource, system::ResMut,
1687+
};
1688+
use bevy_reflect::Reflect;
1689+
use serde_json::Value::Null;
16311690

16321691
#[test]
16331692
fn insert_reflect_only_component() {
@@ -1659,6 +1718,37 @@ mod tests {
16591718
insert_reflected_components(e, deserialized_components).expect("FAIL");
16601719
}
16611720

1721+
#[test]
1722+
fn trigger_reflect_only_event() {
1723+
#[derive(Event, Reflect)]
1724+
#[reflect(Event)]
1725+
struct Pass;
1726+
1727+
#[derive(Resource)]
1728+
struct TestResult(pub bool);
1729+
1730+
let atr = AppTypeRegistry::default();
1731+
{
1732+
let mut register = atr.write();
1733+
register.register::<Pass>();
1734+
}
1735+
let mut world = World::new();
1736+
world.add_observer(move |_event: On<Pass>, mut result: ResMut<TestResult>| result.0 = true);
1737+
world.insert_resource(TestResult(false));
1738+
world.insert_resource(atr);
1739+
1740+
let params = serde_json::to_value(&BrpTriggerEventParams {
1741+
event: "bevy_remote::builtin_methods::tests::Pass".to_owned(),
1742+
value: None,
1743+
})
1744+
.expect("FAIL");
1745+
assert_eq!(
1746+
process_remote_trigger_event_request(In(Some(params)), &mut world),
1747+
Ok(Null)
1748+
);
1749+
assert!(world.resource::<TestResult>().0);
1750+
}
1751+
16621752
#[test]
16631753
fn serialization_tests() {
16641754
test_serialize_deserialize(BrpQueryRow {

crates/bevy_remote/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,16 @@
455455
//!
456456
//! `result`: An array of [fully-qualified type names] of registered resource types.
457457
//!
458+
//! ### `world.trigger_event`
459+
//!
460+
//! Triggers an event.
461+
//!
462+
//! `params`:
463+
//! - `event`: The [fully-qualified type name] of the event to trigger.
464+
//! - `value`: The value of the event to trigger.
465+
//!
466+
//! `result`: null.
467+
//!
458468
//! ### `registry.schema`
459469
//!
460470
//! Retrieve schema information about registered types in the Bevy app's type registry.
@@ -669,6 +679,10 @@ impl Default for RemotePlugin {
669679
builtin_methods::BRP_LIST_RESOURCES_METHOD,
670680
builtin_methods::process_remote_list_resources_request,
671681
)
682+
.with_method(
683+
builtin_methods::BRP_TRIGGER_EVENT_METHOD,
684+
builtin_methods::process_remote_trigger_event_request,
685+
)
672686
.with_method(
673687
builtin_methods::BRP_REGISTRY_SCHEMA_METHOD,
674688
builtin_methods::export_registry_types,
@@ -915,7 +929,7 @@ impl From<BrpResult> for BrpPayload {
915929
}
916930

917931
/// An error a request might return.
918-
#[derive(Debug, Serialize, Deserialize, Clone)]
932+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
919933
pub struct BrpError {
920934
/// Defines the general type of the error.
921935
pub code: i16,

crates/bevy_remote/src/schemas/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Module with schemas used for various BRP endpoints
22
use bevy_ecs::{
3-
reflect::{ReflectComponent, ReflectResource},
3+
reflect::{ReflectComponent, ReflectEvent, ReflectResource},
44
resource::Resource,
55
};
66
use bevy_platform::collections::HashMap;
@@ -29,6 +29,7 @@ impl Default for SchemaTypesMetadata {
2929
};
3030
data_types.map_type_data::<ReflectComponent>("Component");
3131
data_types.map_type_data::<ReflectResource>("Resource");
32+
data_types.map_type_data::<ReflectEvent>("Event");
3233
data_types.map_type_data::<ReflectDefault>("Default");
3334
#[cfg(feature = "bevy_asset")]
3435
data_types.map_type_data::<bevy_asset::ReflectAsset>("Asset");

0 commit comments

Comments
 (0)