Add BRP method to mutate a component (#16940)

# Objective

Add a method to mutate components with BRP.

Currently the only way to modify a component on an entity with BRP is to
insert a new one with the new values. This isn't ideal for several
reasons, one reason being that the client has to know what all the
fields are of the component and stay in sync with the server.

## Solution

Add a new BRP method called `bevy/mutate_component` to mutate a single
field in a component on an entity.

## Testing

Tested on a simple scene on all `Transform`, `Name`, and a custom
component.

---

## Showcase

Example JSON-RPC request to change the `Name` of an entity to "New
name!"

```json
{
    "jsonrpc": "2.0",
    "id": 0,
    "method": "bevy/mutate_component",
    "params": {
        "entity": 4294967308,
        "component": "bevy_ecs::name::Name",
        "path": ".name",
        "value": "New name!"
    }
}
```

Or setting the X translation to 10.0 on a Transform:

```json
{
    "jsonrpc": "2.0",
    "id": 0,
    "method": "bevy/mutate_component",
    "params": {
        "entity": 4294967308,
        "component": "bevy_transform::components::transform::Transform",
        "path": ".translation.x",
        "value": 10.0
    }
}
```

Clip of my Emacs BRP package using this method:


https://github.com/user-attachments/assets/a786b245-5c20-4189-859f-2261c5086a68

---------

Co-authored-by: François Mockers <mockersf@gmail.com>
This commit is contained in:
Ben Whitley 2025-01-14 02:55:40 -05:00 committed by GitHub
parent 41fd280596
commit b0beeab41a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 108 additions and 2 deletions

View File

@ -17,8 +17,8 @@ use bevy_hierarchy::BuildChildren as _;
use bevy_reflect::{
prelude::ReflectDefault,
serde::{ReflectSerializer, TypedReflectDeserializer},
NamedField, OpaqueInfo, PartialReflect, ReflectDeserialize, ReflectSerialize, TypeInfo,
TypeRegistration, TypeRegistry, VariantInfo,
GetPath as _, NamedField, OpaqueInfo, PartialReflect, ReflectDeserialize, ReflectSerialize,
TypeInfo, TypeRegistration, TypeRegistry, VariantInfo,
};
use bevy_utils::HashMap;
use serde::{de::DeserializeSeed as _, Deserialize, Serialize};
@ -50,6 +50,9 @@ pub const BRP_REPARENT_METHOD: &str = "bevy/reparent";
/// The method path for a `bevy/list` request.
pub const BRP_LIST_METHOD: &str = "bevy/list";
/// The method path for a `bevy/reparent` request.
pub const BRP_MUTATE_COMPONENT_METHOD: &str = "bevy/mutate_component";
/// The method path for a `bevy/get+watch` request.
pub const BRP_GET_AND_WATCH_METHOD: &str = "bevy/get+watch";
@ -197,6 +200,28 @@ pub struct BrpListParams {
pub entity: Entity,
}
/// `bevy/mutate`:
///
/// The server responds with a null.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BrpMutateParams {
/// The entity of the component to mutate.
pub entity: Entity,
/// The [full path] of component to mutate.
///
/// [full path]: bevy_reflect::TypePath::type_path
pub component: String,
/// The [path] of the field within the component.
///
/// [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 {
@ -694,6 +719,70 @@ pub fn process_remote_insert_request(
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
/// component.
pub fn process_remote_mutate_component_request(
In(params): In<Option<Value>>,
world: &mut World,
) -> BrpResult {
let BrpMutateParams {
entity,
component,
path,
value,
} = parse_some(params)?;
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let type_registry = app_type_registry.read();
// Get the fully-qualified type names of the component to be mutated.
let component_type: &TypeRegistration = type_registry
.get_with_type_path(&component)
.ok_or_else(|| {
BrpError::component_error(anyhow!("Unknown component type: `{}`", component))
})?;
// Get the reflected representation of the component.
let mut reflected = component_type
.data::<ReflectComponent>()
.ok_or_else(|| {
BrpError::component_error(anyhow!("Component `{}` isn't registered.", component))
})?
.reflect_mut(world.entity_mut(entity))
.ok_or_else(|| {
BrpError::component_error(anyhow!("Cannot reflect component `{}`", component))
})?;
// Get the type of the field in the component that is to be
// mutated.
let value_type: &TypeRegistration = type_registry
.get_with_type_path(
reflected
.reflect_path(path.as_str())
.map_err(BrpError::component_error)?
.reflect_type_path(),
)
.ok_or_else(|| {
BrpError::component_error(anyhow!("Unknown component field type: `{}`", component))
})?;
// Get the reflected representation of the value to be inserted
// into the component.
let value: Box<dyn PartialReflect> = TypedReflectDeserializer::new(value_type, &type_registry)
.deserialize(&value)
.map_err(BrpError::component_error)?;
// Apply the mutation.
reflected
.reflect_path_mut(path.as_str())
.map_err(BrpError::component_error)?
.try_apply(value.as_ref())
.map_err(BrpError::component_error)?;
Ok(Value::Null)
}
/// Handles a `bevy/remove` request (remove components) coming from a client.
pub fn process_remote_remove_request(
In(params): In<Option<Value>>,

View File

@ -194,6 +194,19 @@
//!
//! `result`: null.
//!
//! ### `bevy/mutate_component`
//!
//! Mutate a field in a component.
//!
//! `params`:
//! - `entity`: The ID of the entity to with the component to mutate.
//! - `component`: The component's [fully-qualified type name].
//! - `path`: The path of the field within the component. See
//! [`GetPath`](bevy_reflect::GetPath#syntax) for more information on formatting this string.
//! - `value`: The value to insert at `path`.
//!
//! `result`: null.
//!
//! ### bevy/reparent
//!
//! Assign a new parent to one or more entities.
@ -419,6 +432,10 @@ impl Default for RemotePlugin {
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,
)
.with_watching_method(
builtin_methods::BRP_GET_AND_WATCH_METHOD,
builtin_methods::process_remote_get_watching_request,