diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 80dc592d9a..f75e4d4da3 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -18,7 +18,7 @@ use bevy_platform_support::collections::HashMap; use bevy_reflect::{ prelude::ReflectDefault, serde::{ReflectSerializer, TypedReflectDeserializer}, - GetPath as _, NamedField, OpaqueInfo, PartialReflect, ReflectDeserialize, ReflectSerialize, + GetPath, NamedField, OpaqueInfo, PartialReflect, ReflectDeserialize, ReflectSerialize, TypeInfo, TypeRegistration, TypeRegistry, VariantInfo, }; use serde::{de::DeserializeSeed as _, Deserialize, Serialize}; @@ -59,6 +59,21 @@ pub const BRP_GET_AND_WATCH_METHOD: &str = "bevy/get+watch"; /// The method path for a `bevy/list+watch` request. pub const BRP_LIST_AND_WATCH_METHOD: &str = "bevy/list+watch"; +/// The method path for a `bevy/get_resource` request. +pub const BRP_GET_RESOURCE_METHOD: &str = "bevy/get_resource"; + +/// The method path for a `bevy/insert_resource` request. +pub const BRP_INSERT_RESOURCE_METHOD: &str = "bevy/insert_resource"; + +/// The method path for a `bevy/remove_resource` request. +pub const BRP_REMOVE_RESOURCE_METHOD: &str = "bevy/remove_resource"; + +/// The method path for a `bevy/mutate_resource` request. +pub const BRP_MUTATE_RESOURCE_METHOD: &str = "bevy/mutate_resource"; + +/// The method path for a `bevy/list_resources` request. +pub const BRP_LIST_RESOURCES_METHOD: &str = "bevy/list_resources"; + /// The method path for a `bevy/registry/schema` request. pub const BRP_REGISTRY_SCHEMA_METHOD: &str = "bevy/registry/schema"; @@ -87,6 +102,15 @@ pub struct BrpGetParams { pub strict: bool, } +/// `bevy/get_resource`: Retrieves the value of a given resource. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct BrpGetResourceParams { + /// The [full path] of the resource type being requested. + /// + /// [full path]: bevy_reflect::TypePath::type_path + pub resource: String, +} + /// `bevy/query`: Performs a query over components in the ECS, returning entities /// and component values that match. /// @@ -153,6 +177,15 @@ pub struct BrpRemoveParams { pub components: Vec, } +/// `bevy/remove_resource`: Removes the given resource from the world. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct BrpRemoveResourceParams { + /// The [full path] of the resource type to remove. + /// + /// [full path]: bevy_reflect::TypePath::type_path + pub resource: String, +} + /// `bevy/insert`: Adds one or more components to an entity. /// /// The server responds with a null. @@ -173,6 +206,19 @@ pub struct BrpInsertParams { pub components: HashMap, } +/// `bevy/insert_resource`: Inserts a resource into the world with a given +/// value. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct BrpInsertResourceParams { + /// The [full path] of the resource type to insert. + /// + /// [full path]: bevy_reflect::TypePath::type_path + pub resource: String, + + /// The serialized value of the resource to be inserted. + pub value: Value, +} + /// `bevy/reparent`: Assign a new parent to one or more entities. /// /// The server responds with a null. @@ -204,7 +250,7 @@ pub struct BrpListParams { /// /// The server responds with a null. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct BrpMutateParams { +pub struct BrpMutateComponentParams { /// The entity of the component to mutate. pub entity: Entity, @@ -222,6 +268,25 @@ pub struct BrpMutateParams { pub value: Value, } +/// `bevy/mutate_resource`: +/// +/// The server responds with a null. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct BrpMutateResourceParams { + /// The [full path] of the resource to mutate. + /// + /// [full path]: bevy_reflect::TypePath::type_path + pub resource: String, + + /// The [path] of the field within the resource. + /// + /// [path]: bevy_reflect::GetPath + pub path: String, + + /// The value to insert at `path`. + pub value: Value, +} + /// Describes the data that is to be fetched in a query. #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] pub struct BrpQuery { @@ -323,6 +388,13 @@ pub enum BrpGetResponse { Strict(HashMap), } +/// The response to a `bevy/get_resource` request. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct BrpGetResourceResponse { + /// The value of the requested resource. + pub value: Value, +} + /// A single response from a `bevy/get+watch` request. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(untagged)] @@ -351,6 +423,9 @@ pub enum BrpGetWatchingResponse { /// The response to a `bevy/list` request. pub type BrpListResponse = Vec; +/// The response to a `bevy/list_resources` request. +pub type BrpListResourcesResponse = Vec; + /// A single response from a `bevy/list+watch` request. #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct BrpListWatchingResponse { @@ -413,6 +488,45 @@ pub fn process_remote_get_request(In(params): In>, world: &World) serde_json::to_value(response).map_err(BrpError::internal) } +/// Handles a `bevy/get_resource` request coming from a client. +pub fn process_remote_get_resource_request( + In(params): In>, + world: &World, +) -> BrpResult { + let BrpGetResourceParams { + resource: resource_path, + } = parse_some(params)?; + + let app_type_registry = world.resource::(); + let type_registry = app_type_registry.read(); + let reflect_resource = + get_reflect_resource(&type_registry, &resource_path).map_err(BrpError::resource_error)?; + + let Some(reflected) = reflect_resource.reflect(world) else { + return Err(BrpError::resource_not_present(&resource_path)); + }; + + // Use the `ReflectSerializer` to serialize the value of the resource; + // this produces a map with a single item. + let reflect_serializer = ReflectSerializer::new(reflected.as_partial_reflect(), &type_registry); + let Value::Object(serialized_object) = + serde_json::to_value(&reflect_serializer).map_err(BrpError::resource_error)? + else { + return Err(BrpError { + code: error_codes::RESOURCE_ERROR, + message: format!("Resource `{}` could not be serialized", resource_path), + data: None, + }); + }; + + // Get the single value out of the map. + let value = serialized_object.into_values().next().ok_or_else(|| { + BrpError::internal(anyhow!("Unexpected format of serialized resource value")) + })?; + let response = BrpGetResourceResponse { value }; + serde_json::to_value(response).map_err(BrpError::internal) +} + /// Handles a `bevy/get+watch` request coming from a client. pub fn process_remote_get_watching_request( In(params): In>, @@ -569,11 +683,7 @@ fn reflect_component( // Each component value serializes to a map with a single entry. let reflect_serializer = ReflectSerializer::new(reflected.as_partial_reflect(), type_registry); let Value::Object(serialized_object) = - serde_json::to_value(&reflect_serializer).map_err(|err| BrpError { - code: error_codes::COMPONENT_ERROR, - message: err.to_string(), - data: None, - })? + serde_json::to_value(&reflect_serializer).map_err(BrpError::component_error)? else { return Err(BrpError { code: error_codes::COMPONENT_ERROR, @@ -719,6 +829,29 @@ pub fn process_remote_insert_request( Ok(Value::Null) } +/// Handles a `bevy/insert_resource` request coming from a client. +pub fn process_remote_insert_resource_request( + In(params): In>, + world: &mut World, +) -> BrpResult { + let BrpInsertResourceParams { + resource: resource_path, + value, + } = parse_some(params)?; + + let app_type_registry = world.resource::().clone(); + let type_registry = app_type_registry.read(); + + let reflected_resource = deserialize_resource(&type_registry, &resource_path, value) + .map_err(BrpError::resource_error)?; + + let reflect_resource = + get_reflect_resource(&type_registry, &resource_path).map_err(BrpError::resource_error)?; + reflect_resource.insert(world, &*reflected_resource, &type_registry); + + Ok(Value::Null) +} + /// Handles a `bevy/mutate_component` request coming from a client. /// /// This method allows you to mutate a single field inside an Entity's @@ -727,7 +860,7 @@ pub fn process_remote_mutate_component_request( In(params): In>, world: &mut World, ) -> BrpResult { - let BrpMutateParams { + let BrpMutateComponentParams { entity, component, path, @@ -747,7 +880,7 @@ pub fn process_remote_mutate_component_request( let mut reflected = component_type .data::() .ok_or_else(|| { - BrpError::component_error(anyhow!("Component `{}` isn't registered.", component)) + BrpError::component_error(anyhow!("Component `{}` isn't registered", component)) })? .reflect_mut(world.entity_mut(entity)) .ok_or_else(|| { @@ -783,6 +916,57 @@ pub fn process_remote_mutate_component_request( Ok(Value::Null) } +/// Handles a `bevy/mutate_resource` request coming from a client. +pub fn process_remote_mutate_resource_request( + In(params): In>, + world: &mut World, +) -> BrpResult { + let BrpMutateResourceParams { + resource: resource_path, + path: field_path, + value, + } = parse_some(params)?; + + let app_type_registry = world.resource::().clone(); + let type_registry = app_type_registry.read(); + + // Get the `ReflectResource` for the given resource path. + let reflect_resource = + get_reflect_resource(&type_registry, &resource_path).map_err(BrpError::resource_error)?; + + // Get the actual resource value from the world as a `dyn Reflect`. + let mut reflected_resource = reflect_resource + .reflect_mut(world) + .ok_or_else(|| BrpError::resource_not_present(&resource_path))?; + + // Get the type registration for the field with the given path. + let value_registration = type_registry + .get_with_type_path( + reflected_resource + .reflect_path(field_path.as_str()) + .map_err(BrpError::resource_error)? + .reflect_type_path(), + ) + .ok_or_else(|| { + BrpError::resource_error(anyhow!("Unknown resource field type: `{}`", resource_path)) + })?; + + // Use the field's type registration to deserialize the given value. + let deserialized_value: Box = + TypedReflectDeserializer::new(value_registration, &type_registry) + .deserialize(&value) + .map_err(BrpError::resource_error)?; + + // Apply the value to the resource. + reflected_resource + .reflect_path_mut(field_path.as_str()) + .map_err(BrpError::resource_error)? + .try_apply(&*deserialized_value) + .map_err(BrpError::resource_error)?; + + Ok(Value::Null) +} + /// Handles a `bevy/remove` request (remove components) coming from a client. pub fn process_remote_remove_request( In(params): In>, @@ -805,6 +989,25 @@ pub fn process_remote_remove_request( Ok(Value::Null) } +/// Handles a `bevy/remove_resource` request coming from a client. +pub fn process_remote_remove_resource_request( + In(params): In>, + world: &mut World, +) -> BrpResult { + let BrpRemoveResourceParams { + resource: resource_path, + } = parse_some(params)?; + + let app_type_registry = world.resource::().clone(); + let type_registry = app_type_registry.read(); + + let reflect_resource = + get_reflect_resource(&type_registry, &resource_path).map_err(BrpError::resource_error)?; + reflect_resource.remove(world); + + Ok(Value::Null) +} + /// Handles a `bevy/destroy` (despawn entity) request coming from a client. pub fn process_remote_destroy_request( In(params): In>, @@ -881,7 +1084,28 @@ pub fn process_remote_list_request(In(params): In>, world: &World) serde_json::to_value(response).map_err(BrpError::internal) } -/// Handles a `bevy/list` request (list all components) coming from a client. +/// Handles a `bevy/list_resources` request coming from a client. +pub fn process_remote_list_resources_request( + In(_params): In>, + world: &World, +) -> BrpResult { + let mut response = BrpListResourcesResponse::default(); + + let app_type_registry = world.resource::(); + let type_registry = app_type_registry.read(); + + for registered_type in type_registry.iter() { + if registered_type.data::().is_some() { + response.push(registered_type.type_info().type_path().to_owned()); + } + } + + response.sort(); + + serde_json::to_value(response).map_err(BrpError::internal) +} + +/// Handles a `bevy/list+watch` request coming from a client. pub fn process_remote_list_watching_request( In(params): In>, world: &World, @@ -1232,6 +1456,7 @@ pub enum SchemaKind { pub enum SchemaType { /// Represents a string value. String, + /// Represents a floating-point number. Float, @@ -1451,6 +1676,23 @@ fn deserialize_components( Ok(reflect_components) } +/// Given a resource path and an associated serialized value (`value`), return the +/// deserialized value. +fn deserialize_resource( + type_registry: &TypeRegistry, + resource_path: &str, + value: Value, +) -> AnyhowResult> { + let Some(resource_type) = type_registry.get_with_type_path(resource_path) else { + return Err(anyhow!("Unknown resource type: `{}`", resource_path)); + }; + let reflected: Box = + TypedReflectDeserializer::new(resource_type, type_registry) + .deserialize(&value) + .map_err(|err| anyhow!("{resource_path} is invalid: {err}"))?; + Ok(reflected) +} + /// Given a collection `reflect_components` of reflected component values, insert them into /// the given entity (`entity_world_mut`). fn insert_reflected_components( @@ -1491,6 +1733,30 @@ fn get_component_type_registration<'r>( .ok_or_else(|| anyhow!("Unknown component type: `{}`", component_path)) } +/// Given a resource's type path, return the associated [`ReflectResource`] from the given +/// `type_registry` if possible. +fn get_reflect_resource<'r>( + type_registry: &'r TypeRegistry, + resource_path: &str, +) -> AnyhowResult<&'r ReflectResource> { + let resource_registration = get_resource_type_registration(type_registry, resource_path)?; + + resource_registration + .data::() + .ok_or_else(|| anyhow!("Resource `{}` isn't reflectable", resource_path)) +} + +/// Given a resource's type path, return the associated [`TypeRegistration`] from the given +/// `type_registry` if possible. +fn get_resource_type_registration<'r>( + type_registry: &'r TypeRegistry, + resource_path: &str, +) -> AnyhowResult<&'r TypeRegistration> { + type_registry + .get_with_type_path(resource_path) + .ok_or_else(|| anyhow!("Unknown resource type: `{}`", resource_path)) +} + #[cfg(test)] mod tests { /// A generic function that tests serialization and deserialization of any type diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index ca3792b7ea..39e1b8c526 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -102,7 +102,7 @@ //! in the ECS. Each of these methods uses the `bevy/` prefix, which is a namespace reserved for //! BRP built-in methods. //! -//! ### bevy/get +//! ### `bevy/get` //! //! Retrieve the values of one or more components from an entity. //! @@ -123,7 +123,7 @@ //! //! `result`: A map associating each type name to its value on the requested entity. //! -//! ### bevy/query +//! ### `bevy/query` //! //! Perform a query over components in the ECS, returning all matching entities and their associated //! component values. @@ -155,7 +155,7 @@ //! //! //! -//! ### bevy/spawn +//! ### `bevy/spawn` //! //! Create a new entity with the provided components and return the resulting entity ID. //! @@ -165,7 +165,7 @@ //! `result`: //! - `entity`: The ID of the newly spawned entity. //! -//! ### bevy/destroy +//! ### `bevy/destroy` //! //! Despawn the entity with the given ID. //! @@ -174,7 +174,7 @@ //! //! `result`: null. //! -//! ### bevy/remove +//! ### `bevy/remove` //! //! Delete one or more components from an entity. //! @@ -184,7 +184,7 @@ //! //! `result`: null. //! -//! ### bevy/insert +//! ### `bevy/insert` //! //! Insert one or more components into an entity. //! @@ -207,7 +207,7 @@ //! //! `result`: null. //! -//! ### bevy/reparent +//! ### `bevy/reparent` //! //! Assign a new parent to one or more entities. //! @@ -218,7 +218,7 @@ //! //! `result`: null. //! -//! ### bevy/list +//! ### `bevy/list` //! //! List all registered components or all components present on an entity. //! @@ -230,7 +230,7 @@ //! //! `result`: An array of fully-qualified type names of components. //! -//! ### bevy/get+watch +//! ### `bevy/get+watch` //! //! Watch the values of one or more components from an entity. //! @@ -258,7 +258,7 @@ //! - `removed`: An array of fully-qualified type names of components removed from the entity //! in the last tick. //! -//! ### bevy/list+watch +//! ### `bevy/list+watch` //! //! Watch all components present on an entity. //! @@ -274,6 +274,52 @@ //! - `removed`: An array of fully-qualified type names of components removed from the entity //! in the last tick. //! +//! ### `bevy/get_resource` +//! +//! Extract the value of a given resource from the world. +//! +//! `params`: +//! - `resource`: The [fully-qualified type name] of the resource to get. +//! +//! `result`: +//! - `value`: The value of the resource in the world. +//! +//! ### `bevy/insert_resource` +//! +//! Insert the given resource into the world with the given value. +//! +//! `params`: +//! - `resource`: The [fully-qualified type name] of the resource to insert. +//! - `value`: The value of the resource to be inserted. +//! +//! `result`: null. +//! +//! ### `bevy/remove_resource` +//! +//! Remove the given resource from the world. +//! +//! `params` +//! - `resource`: The [fully-qualified type name] of the resource to remove. +//! +//! `result`: null. +//! +//! ### `bevy/mutate_resource` +//! +//! Mutate a field in a resource. +//! +//! `params`: +//! - `resource`: The [fully-qualified type name] of the resource to mutate. +//! - `path`: The path of the field within the resource. See +//! [`GetPath`](bevy_reflect::GetPath#syntax) for more information on formatting this string. +//! - `value`: The value to be inserted at `path`. +//! +//! `result`: null. +//! +//! ### `bevy/list_resources` +//! +//! List all reflectable registered resource types. This method has no parameters. +//! +//! `result`: An array of [fully-qualified type names] of registered resource types. //! //! ## Custom methods //! @@ -424,10 +470,6 @@ impl Default for RemotePlugin { builtin_methods::BRP_LIST_METHOD, builtin_methods::process_remote_list_request, ) - .with_method( - builtin_methods::BRP_REGISTRY_SCHEMA_METHOD, - builtin_methods::export_registry_types, - ) .with_method( builtin_methods::BRP_MUTATE_COMPONENT_METHOD, builtin_methods::process_remote_mutate_component_request, @@ -440,6 +482,30 @@ impl Default for RemotePlugin { builtin_methods::BRP_LIST_AND_WATCH_METHOD, builtin_methods::process_remote_list_watching_request, ) + .with_method( + builtin_methods::BRP_GET_RESOURCE_METHOD, + builtin_methods::process_remote_get_resource_request, + ) + .with_method( + builtin_methods::BRP_INSERT_RESOURCE_METHOD, + builtin_methods::process_remote_insert_resource_request, + ) + .with_method( + builtin_methods::BRP_REMOVE_RESOURCE_METHOD, + builtin_methods::process_remote_remove_resource_request, + ) + .with_method( + builtin_methods::BRP_MUTATE_RESOURCE_METHOD, + builtin_methods::process_remote_mutate_resource_request, + ) + .with_method( + builtin_methods::BRP_LIST_RESOURCES_METHOD, + builtin_methods::process_remote_list_resources_request, + ) + .with_method( + builtin_methods::BRP_REGISTRY_SCHEMA_METHOD, + builtin_methods::export_registry_types, + ) } } @@ -718,6 +784,26 @@ impl BrpError { } } + /// Resource was not present in the world. + #[must_use] + pub fn resource_not_present(resource: &str) -> Self { + Self { + code: error_codes::RESOURCE_NOT_PRESENT, + message: format!("Resource `{resource}` not present in the world"), + data: None, + } + } + + /// An arbitrary resource error. Possibly related to reflection. + #[must_use] + pub fn resource_error(error: E) -> Self { + Self { + code: error_codes::RESOURCE_ERROR, + message: error.to_string(), + data: None, + } + } + /// An arbitrary internal error. #[must_use] pub fn internal(error: E) -> Self { @@ -772,6 +858,12 @@ pub mod error_codes { /// Cannot reparent an entity to itself. pub const SELF_REPARENT: i16 = -23404; + + /// Could not reflect or find resource. + pub const RESOURCE_ERROR: i16 = -23501; + + /// Could not find resource in the world. + pub const RESOURCE_NOT_PRESENT: i16 = -23502; } /// The result of a request. diff --git a/examples/remote/server.rs b/examples/remote/server.rs index 39033218a7..e90f5ea44e 100644 --- a/examples/remote/server.rs +++ b/examples/remote/server.rs @@ -16,7 +16,9 @@ fn main() { .add_systems(Startup, setup) .add_systems(Update, remove.run_if(input_just_pressed(KeyCode::Space))) .add_systems(Update, move_cube) + // New types must be registered in order to be usable with reflection. .register_type::() + .register_type::() .run(); } @@ -40,6 +42,12 @@ fn setup( Cube(1.0), )); + // test resource + commands.insert_resource(TestResource { + foo: Vec2::new(1.0, -1.0), + bar: false, + }); + // light commands.spawn(( PointLight { @@ -56,6 +64,17 @@ fn setup( )); } +/// An arbitrary resource that can be inspected and manipulated with remote methods. +#[derive(Resource, Reflect, Serialize, Deserialize)] +#[reflect(Resource, Serialize, Deserialize)] +pub struct TestResource { + /// An arbitrary field of the test resource. + pub foo: Vec2, + + /// Another arbitrary field. + pub bar: bool, +} + fn move_cube(mut query: Query<&mut Transform, With>, time: Res