Initial implementation of the Bevy Remote Protocol (Adopted) (#14880)

# Objective

Adopted from #13563.

The goal is to implement the Bevy Remote Protocol over HTTP/JSON,
allowing the ECS to be interacted with remotely.

## Solution

At a high level, there are really two separate things that have been
undertaken here:
1. First, `RemotePlugin` has been created, which has the effect of
embedding a [JSON-RPC](https://www.jsonrpc.org/specification) endpoint
into a Bevy application.
2. Second, the [Bevy Remote Protocol
verbs](https://gist.github.com/coreh/1baf6f255d7e86e4be29874d00137d1d#file-bevy-remote-protocol-md)
(excluding `POLL`) have been implemented as remote methods for that
JSON-RPC endpoint under a Bevy-exclusive namespace (e.g. `bevy/get`,
`bevy/list`, etc.).

To avoid some repetition, here is the crate-level documentation, which
explains the request/response structure, built-in-methods, and custom
method configuration:
<details>
  <summary>Click to view crate-level docs</summary>

```rust
//! An implementation of the Bevy Remote Protocol over HTTP and JSON, to allow
//! for remote control of a Bevy app.
//!
//! Adding the [`RemotePlugin`] to your [`App`] causes Bevy to accept
//! connections over HTTP (by default, on port 15702) while your app is running.
//! These *remote clients* can inspect and alter the state of the
//! entity-component system. Clients are expected to `POST` JSON requests to the
//! root URL; see the `client` example for a trivial example of use.
//!
//! The Bevy Remote Protocol is based on the JSON-RPC 2.0 protocol.
//!
//! ## Request objects
//!
//! A typical client request might look like this:
//!
//! ```json
//! {
//!     "method": "bevy/get",
//!     "id": 0,
//!     "params": {
//!         "entity": 4294967298,
//!         "components": [
//!             "bevy_transform::components::transform::Transform"
//!         ]
//!     }
//! }
//! ```
//!
//! The `id` and `method` fields are required. The `param` field may be omitted
//! for certain methods:
//!
//! * `id` is arbitrary JSON data. The server completely ignores its contents,
//!   and the client may use it for any purpose. It will be copied via
//!   serialization and deserialization (so object property order, etc. can't be
//!   relied upon to be identical) and sent back to the client as part of the
//!   response.
//!
//! * `method` is a string that specifies one of the possible [`BrpRequest`]
//!   variants: `bevy/query`, `bevy/get`, `bevy/insert`, etc. It's case-sensitive.
//!
//! * `params` is parameter data specific to the request.
//!
//! For more information, see the documentation for [`BrpRequest`].
//! [`BrpRequest`] is serialized to JSON via `serde`, so [the `serde`
//! documentation] may be useful to clarify the correspondence between the Rust
//! structure and the JSON format.
//!
//! ## Response objects
//!
//! A response from the server to the client might look like this:
//!
//! ```json
//! {
//!     "jsonrpc": "2.0",
//!     "id": 0,
//!     "result": {
//!         "bevy_transform::components::transform::Transform": {
//!             "rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
//!             "scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
//!             "translation": { "x": 0.0, "y": 0.5, "z": 0.0 }
//!         }
//!     }
//! }
//! ```
//!
//! The `id` field will always be present. The `result` field will be present if the
//! request was successful. Otherwise, an `error` field will replace it.
//!
//! * `id` is the arbitrary JSON data that was sent as part of the request. It
//!   will be identical to the `id` data sent during the request, modulo
//!   serialization and deserialization. If there's an error reading the `id` field,
//!   it will be `null`.
//!
//! * `result` will be present if the request succeeded and will contain the response
//!   specific to the request.
//!
//! * `error` will be present if the request failed and will contain an error object
//!   with more information about the cause of failure.
//!
//! ## Error objects
//!
//! An error object might look like this:
//!
//! ```json
//! {
//!     "code": -32602,
//!     "message": "Missing \"entity\" field"
//! }
//! ```
//!
//! The `code` and `message` fields will always be present. There may also be a `data` field.
//!
//! * `code` is an integer representing the kind of an error that happened. Error codes documented
//!   in the [`error_codes`] module.
//!
//! * `message` is a short, one-sentence human-readable description of the error.
//!
//! * `data` is an optional field of arbitrary type containing additional information about the error.
//!
//! ## Built-in methods
//!
//! The Bevy Remote Protocol includes a number of built-in methods for accessing and modifying data
//! in the ECS. Each of these methods uses the `bevy/` prefix, which is a namespace reserved for
//! BRP built-in methods.
//!
//! ### bevy/get
//!
//! Retrieve the values of one or more components from an entity.
//!
//! `params`:
//! - `entity`: The ID of the entity whose components will be fetched.
//! - `components`: An array of fully-qualified type names of components to fetch.
//!
//! `result`: A map associating each type name to its value on the requested entity.
//!
//! ### bevy/query
//!
//! Perform a query over components in the ECS, returning all matching entities and their associated
//! component values.
//!
//! All of the arrays that comprise this request are optional, and when they are not provided, they
//! will be treated as if they were empty.
//!
//! `params`:
//! `params`:
//! - `data`:
//!   - `components` (optional): An array of fully-qualified type names of components to fetch.
//!   - `option` (optional): An array of fully-qualified type names of components to fetch optionally.
//!   - `has` (optional): An array of fully-qualified type names of components whose presence will be
//!      reported as boolean values.
//! - `filter` (optional):
//!   - `with` (optional): An array of fully-qualified type names of components that must be present
//!     on entities in order for them to be included in results.
//!   - `without` (optional): An array of fully-qualified type names of components that must *not* be
//!     present on entities in order for them to be included in results.
//!
//! `result`: An array, each of which is an object containing:
//! - `entity`: The ID of a query-matching entity.
//! - `components`: A map associating each type name from `components`/`option` to its value on the matching
//!   entity if the component is present.
//! - `has`: A map associating each type name from `has` to a boolean value indicating whether or not the
//!   entity has that component. If `has` was empty or omitted, this key will be omitted in the response.
//!
//! ### bevy/spawn
//!
//! Create a new entity with the provided components and return the resulting entity ID.
//!
//! `params`:
//! - `components`: A map associating each component's fully-qualified type name with its value.
//!
//! `result`:
//! - `entity`: The ID of the newly spawned entity.
//!
//! ### bevy/destroy
//!
//! Despawn the entity with the given ID.
//!
//! `params`:
//! - `entity`: The ID of the entity to be despawned.
//!
//! `result`: null.
//!
//! ### bevy/remove
//!
//! Delete one or more components from an entity.
//!
//! `params`:
//! - `entity`: The ID of the entity whose components should be removed.
//! - `components`: An array of fully-qualified type names of components to be removed.
//!
//! `result`: null.
//!
//! ### bevy/insert
//!
//! Insert one or more components into an entity.
//!
//! `params`:
//! - `entity`: The ID of the entity to insert components into.
//! - `components`: A map associating each component's fully-qualified type name with its value.
//!
//! `result`: null.
//!
//! ### bevy/reparent
//!
//! Assign a new parent to one or more entities.
//!
//! `params`:
//! - `entities`: An array of entity IDs of entities that will be made children of the `parent`.
//! - `parent` (optional): The entity ID of the parent to which the child entities will be assigned.
//!   If excluded, the given entities will be removed from their parents.
//!
//! `result`: null.
//!
//! ### bevy/list
//!
//! List all registered components or all components present on an entity.
//!
//! When `params` is not provided, this lists all registered components. If `params` is provided,
//! this lists only those components present on the provided entity.
//!
//! `params` (optional):
//! - `entity`: The ID of the entity whose components will be listed.
//!
//! `result`: An array of fully-qualified type names of components.
//!
//! ## Custom methods
//!
//! In addition to the provided methods, the Bevy Remote Protocol can be extended to include custom
//! methods. This is primarily done during the initialization of [`RemotePlugin`], although the
//! methods may also be extended at runtime using the [`RemoteMethods`] resource.
//!
//! ### Example
//! ```ignore
//! fn main() {
//!     App::new()
//!         .add_plugins(DefaultPlugins)
//!         .add_plugins(
//!             // `default` adds all of the built-in methods, while `with_method` extends them
//!             RemotePlugin::default()
//!                 .with_method("super_user/cool_method".to_owned(), path::to::my:🆒:handler)
//!                 // ... more methods can be added by chaining `with_method`
//!         )
//!         .add_systems(
//!             // ... standard application setup
//!         )
//!         .run();
//! }
//! ```
//!
//! The handler is expected to be a system-convertible function which takes optional JSON parameters
//! as input and returns a [`BrpResult`]. This means that it should have a type signature which looks
//! something like this:
//! ```
//! # use serde_json::Value;
//! # use bevy_ecs::prelude::{In, World};
//! # use bevy_remote::BrpResult;
//! fn handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
//!     todo!()
//! }
//! ```
//!
//! Arbitrary system parameters can be used in conjunction with the optional `Value` input. The
//! handler system will always run with exclusive `World` access.
//!
//! [the `serde` documentation]: https://serde.rs/
```

</details>

### Message lifecycle

At a high level, the lifecycle of client-server interactions is
something like this:
1. The client sends one or more `BrpRequest`s. The deserialized version
of that is just the Rust representation of a JSON-RPC request, and it
looks like this:
```rust
pub struct BrpRequest {
    /// The action to be performed. Parsing is deferred for the sake of error reporting.
    pub method: Option<Value>,

    /// Arbitrary data that will be returned verbatim to the client as part of
    /// the response.
    pub id: Option<Value>,

    /// The parameters, specific to each method.
    ///
    /// These are passed as the first argument to the method handler.
    /// Sometimes params can be omitted.
    pub params: Option<Value>,
}
```
2. These requests are accumulated in a mailbox resource (small lie but
close enough).
3. Each update, the mailbox is drained by a system
`process_remote_requests`, where each request is processed according to
its `method`, which has an associated handler. Each handler is a Bevy
system that runs with exclusive world access and returns a result; e.g.:
```rust
pub fn process_remote_get_request(In(params): In<Option<Value>>, world: &World) -> BrpResult { // ... }
```
4. The result (or an error) is reported back to the client.

## Testing

This can be tested by using the `server` and `client` examples. The
`client` example is not particularly exhaustive at the moment (it only
creates barebones `bevy/query` requests) but is still informative. Other
queries can be made using `curl` with the `server` example running.

For example, to make a `bevy/list` request and list all registered
components:
```bash
curl -X POST -d '{ "jsonrpc": "2.0", "id": 1, "method": "bevy/list" }' 127.0.0.1:15702 | jq .
```

---

## Future direction

There were a couple comments on BRP versioning while this was in draft.
I agree that BRP versioning is a good idea, but I think that it requires
some consensus on a couple fronts:
- First of all, what does the version actually mean? Is it a version for
the protocol itself or for the `bevy/*` methods implemented using it?
Both?
- Where does the version actually live? The most natural place is just
where we have `"jsonrpc"` right now (at least if it's versioning the
protocol itself), but this means we're not actually conforming to
JSON-RPC any more (so, for example, any client library used to construct
JSON-RPC requests would stop working). I'm not really against that, but
it's at least a real decision.
- What do we actually do when we encounter mismatched versions? Adding
handling for this would be actual scope creep instead of just a little
add-on in my opinion.

Another thing that would be nice is making the internal structure of the
implementation less JSON-specific. Right now, for example, component
values that will appear in server responses are quite eagerly converted
to JSON `Value`s, which prevents disentangling the handler logic from
the communication medium, but it can probably be done in principle and I
imagine it would enable more code reuse (e.g. for custom method
handlers) in addition to making the internals more readily usable for
other formats.

---------

Co-authored-by: Patrick Walton <pcwalton@mimiga.net>
Co-authored-by: DragonGamesStudios <margos.michal@gmail.com>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
This commit is contained in:
Matty 2024-09-23 14:36:16 -04:00 committed by GitHub
parent 27bea6abf7
commit 89e98b208f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1809 additions and 0 deletions

View file

@ -75,6 +75,7 @@ default = [
"bevy_sprite", "bevy_sprite",
"bevy_text", "bevy_text",
"bevy_ui", "bevy_ui",
"bevy_remote",
"multi_threaded", "multi_threaded",
"png", "png",
"hdr", "hdr",
@ -174,6 +175,9 @@ bevy_gizmos = ["bevy_internal/bevy_gizmos", "bevy_color"]
# Provides a collection of developer tools # Provides a collection of developer tools
bevy_dev_tools = ["bevy_internal/bevy_dev_tools"] bevy_dev_tools = ["bevy_internal/bevy_dev_tools"]
# Enable the Bevy Remote Protocol
bevy_remote = ["bevy_internal/bevy_remote"]
# Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)
spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"]
@ -376,6 +380,7 @@ rand_chacha = "0.3.1"
ron = "0.8.0" ron = "0.8.0"
flate2 = "1.0" flate2 = "1.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1"
bytemuck = "1.7" bytemuck = "1.7"
bevy_render = { path = "crates/bevy_render", version = "0.15.0-dev", default-features = false } bevy_render = { path = "crates/bevy_render", version = "0.15.0-dev", default-features = false }
# Needed to poll Task examples # Needed to poll Task examples
@ -385,6 +390,16 @@ crossbeam-channel = "0.5.0"
argh = "0.1.12" argh = "0.1.12"
thiserror = "1.0" thiserror = "1.0"
event-listener = "5.3.0" event-listener = "5.3.0"
hyper = { version = "1", features = ["server", "http1"] }
http-body-util = "0.1"
anyhow = "1"
macro_rules_attribute = "0.2"
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
smol = "2"
smol-macros = "0.1"
smol-hyper = "0.1"
ureq = { version = "2.10.1", features = ["json"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies] [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen = { version = "0.2" } wasm-bindgen = { version = "0.2" }
@ -3384,6 +3399,28 @@ description = "Demonstrates volumetric fog and lighting"
category = "3D Rendering" category = "3D Rendering"
wasm = true wasm = true
[[example]]
name = "client"
path = "examples/remote/client.rs"
doc-scrape-examples = true
[package.metadata.example.client]
name = "client"
description = "A simple command line client that can control Bevy apps via the BRP"
category = "Remote Protocol"
wasm = false
[[example]]
name = "server"
path = "examples/remote/server.rs"
doc-scrape-examples = true
[package.metadata.example.server]
name = "server"
description = "A Bevy app that you can connect to with the BRP and edit"
category = "Remote Protocol"
wasm = false
[[example]] [[example]]
name = "anisotropy" name = "anisotropy"
path = "examples/3d/anisotropy.rs" path = "examples/3d/anisotropy.rs"

View file

@ -192,6 +192,9 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
# Provides a collection of developer tools # Provides a collection of developer tools
bevy_dev_tools = ["dep:bevy_dev_tools"] bevy_dev_tools = ["dep:bevy_dev_tools"]
# Enable support for the Bevy Remote Protocol
bevy_remote = ["dep:bevy_remote"]
# Provides a picking functionality # Provides a picking functionality
bevy_picking = [ bevy_picking = [
"dep:bevy_picking", "dep:bevy_picking",
@ -249,6 +252,7 @@ bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.15.0-dev"
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.15.0-dev" } bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.15.0-dev" }
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.15.0-dev" } bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.15.0-dev" }
bevy_picking = { path = "../bevy_picking", optional = true, version = "0.15.0-dev" } bevy_picking = { path = "../bevy_picking", optional = true, version = "0.15.0-dev" }
bevy_remote = { path = "../bevy_remote", optional = true, version = "0.15.0-dev" }
bevy_render = { path = "../bevy_render", optional = true, version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.15.0-dev" }
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.15.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.15.0-dev" }
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.15.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.15.0-dev" }

View file

@ -46,6 +46,8 @@ pub use bevy_pbr as pbr;
pub use bevy_picking as picking; pub use bevy_picking as picking;
pub use bevy_ptr as ptr; pub use bevy_ptr as ptr;
pub use bevy_reflect as reflect; pub use bevy_reflect as reflect;
#[cfg(feature = "bevy_remote")]
pub use bevy_remote as remote;
#[cfg(feature = "bevy_render")] #[cfg(feature = "bevy_render")]
pub use bevy_render as render; pub use bevy_render as render;
#[cfg(feature = "bevy_scene")] #[cfg(feature = "bevy_scene")]

View file

@ -0,0 +1,41 @@
[package]
name = "bevy_remote"
version = "0.15.0-dev"
edition = "2021"
description = "The Bevy Remote Protocol"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
readme = "README.md"
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", features = [
"serialize",
] }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
# other
anyhow = "1"
hyper = { version = "1", features = ["server", "http1"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }
http-body-util = "0.1"
# dependencies that will not compile on wasm
[target.'cfg(not(target_family = "wasm"))'.dependencies]
smol = "2"
smol-hyper = "0.1"
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"]
all-features = true

View file

@ -0,0 +1,721 @@
//! Built-in verbs for the Bevy Remote Protocol.
use std::any::TypeId;
use anyhow::{anyhow, Result as AnyhowResult};
use bevy_ecs::{
component::ComponentId,
entity::Entity,
query::QueryBuilder,
reflect::{AppTypeRegistry, ReflectComponent},
system::In,
world::{EntityRef, EntityWorldMut, FilteredEntityRef, World},
};
use bevy_hierarchy::BuildChildren as _;
use bevy_reflect::{
serde::{ReflectSerializer, TypedReflectDeserializer},
PartialReflect, TypeRegistration, TypeRegistry,
};
use bevy_utils::HashMap;
use serde::de::DeserializeSeed as _;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{error_codes, BrpError, BrpResult};
/// The method path for a `bevy/get` request.
pub const BRP_GET_METHOD: &str = "bevy/get";
/// The method path for a `bevy/query` request.
pub const BRP_QUERY_METHOD: &str = "bevy/query";
/// The method path for a `bevy/spawn` request.
pub const BRP_SPAWN_METHOD: &str = "bevy/spawn";
/// The method path for a `bevy/insert` request.
pub const BRP_INSERT_METHOD: &str = "bevy/insert";
/// The method path for a `bevy/remove` request.
pub const BRP_REMOVE_METHOD: &str = "bevy/remove";
/// The method path for a `bevy/destroy` request.
pub const BRP_DESTROY_METHOD: &str = "bevy/destroy";
/// The method path for a `bevy/reparent` request.
pub const BRP_REPARENT_METHOD: &str = "bevy/reparent";
/// The method path for a `bevy/list` request.
pub const BRP_LIST_METHOD: &str = "bevy/list";
/// `bevy/get`: Retrieves one or more components from the entity with the given
/// ID.
///
/// The server responds with a [`BrpGetResponse`].
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpGetParams {
/// The ID of the entity from which components are to be requested.
pub entity: Entity,
/// The [full paths] of the component types that are to be requested
/// from the entity.
///
/// Note that these strings must consist of the *full* type paths: e.g.
/// `bevy_transform::components::transform::Transform`, not just
/// `Transform`.
///
/// [full paths]: bevy_reflect::TypePath::type_path
pub components: Vec<String>,
}
/// `bevy/query`: Performs a query over components in the ECS, returning entities
/// and component values that match.
///
/// The server responds with a [`BrpQueryResponse`].
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpQueryParams {
/// The components to select.
pub data: BrpQuery,
/// An optional filter that specifies which entities to include or
/// exclude from the results.
#[serde(default)]
pub filter: BrpQueryFilter,
}
/// `bevy/spawn`: Creates a new entity with the given components and responds
/// with its ID.
///
/// The server responds with a [`BrpSpawnResponse`].
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpSpawnParams {
/// A map from each component's full path to its serialized value.
///
/// These components will be added to the entity.
///
/// Note that the keys of the map must be the [full type paths]: e.g.
/// `bevy_transform::components::transform::Transform`, not just
/// `Transform`.
///
/// [full type paths]: bevy_reflect::TypePath::type_path
pub components: HashMap<String, Value>,
}
/// `bevy/destroy`: Given an ID, despawns the entity with that ID.
///
/// The server responds with an okay.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpDestroyParams {
/// The ID of the entity to despawn.
pub entity: Entity,
}
/// `bevy/remove`: Deletes one or more components from an entity.
///
/// The server responds with a null.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpRemoveParams {
/// The ID of the entity from which components are to be removed.
pub entity: Entity,
/// The full paths of the component types that are to be removed from
/// the entity.
///
/// Note that these strings must consist of the [full type paths]: e.g.
/// `bevy_transform::components::transform::Transform`, not just
/// `Transform`.
///
/// [full type paths]: bevy_reflect::TypePath::type_path
pub components: Vec<String>,
}
/// `bevy/insert`: Adds one or more components to an entity.
///
/// The server responds with a null.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpInsertParams {
/// The ID of the entity that components are to be added to.
pub entity: Entity,
/// A map from each component's full path to its serialized value.
///
/// These components will be added to the entity.
///
/// Note that the keys of the map must be the [full type paths]: e.g.
/// `bevy_transform::components::transform::Transform`, not just
/// `Transform`.
///
/// [full type paths]: bevy_reflect::TypePath::type_path
pub components: HashMap<String, Value>,
}
/// `bevy/reparent`: Assign a new parent to one or more entities.
///
/// The server responds with a null.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpReparentParams {
/// The IDs of the entities that are to become the new children of the
/// `parent`.
pub entities: Vec<Entity>,
/// The IDs of the entity that will become the new parent of the
/// `entities`.
///
/// If this is `None`, then the entities are removed from all parents.
#[serde(default)]
pub parent: Option<Entity>,
}
/// `bevy/list`: Returns a list of all type names of registered components in the
/// system (no params provided), or those on an entity (params provided).
///
/// The server responds with a [`BrpListResponse`]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpListParams {
/// The entity to query.
pub entity: Entity,
}
/// Describes the data that is to be fetched in a query.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct BrpQuery {
/// The [full path] of the type name of each component that is to be
/// fetched.
///
/// [full path]: bevy_reflect::TypePath::type_path
#[serde(default)]
pub components: Vec<String>,
/// The [full path] of the type name of each component that is to be
/// optionally fetched.
///
/// [full path]: bevy_reflect::TypePath::type_path
#[serde(default)]
pub option: Vec<String>,
/// The [full path] of the type name of each component that is to be checked
/// for presence.
///
/// [full path]: bevy_reflect::TypePath::type_path
#[serde(default)]
pub has: Vec<String>,
}
/// Additional constraints that can be placed on a query to include or exclude
/// certain entities.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct BrpQueryFilter {
/// The [full path] of the type name of each component that must not be
/// present on the entity for it to be included in the results.
///
/// [full path]: bevy_reflect::TypePath::type_path
#[serde(default)]
pub without: Vec<String>,
/// The [full path] of the type name of each component that must be present
/// on the entity for it to be included in the results.
///
/// [full path]: bevy_reflect::TypePath::type_path
#[serde(default)]
pub with: Vec<String>,
}
/// A response from the world to the client that specifies a single entity.
///
/// This is sent in response to `bevy/spawn`.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpSpawnResponse {
/// The ID of the entity in question.
pub entity: Entity,
}
/// The response to a `bevy/get` request.
pub type BrpGetResponse = HashMap<String, Value>;
/// The response to a `bevy/list` request.
pub type BrpListResponse = Vec<String>;
/// The response to a `bevy/query` request.
pub type BrpQueryResponse = Vec<BrpQueryRow>;
/// One query match result: a single entity paired with the requested components.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpQueryRow {
/// The ID of the entity that matched.
pub entity: Entity,
/// The serialized values of the requested components.
pub components: HashMap<String, Value>,
/// The boolean-only containment query results.
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub has: HashMap<String, Value>,
}
/// A helper function used to parse a `serde_json::Value`.
fn parse<T: for<'de> Deserialize<'de>>(value: Value) -> Result<T, BrpError> {
serde_json::from_value(value).map_err(|err| BrpError {
code: error_codes::INVALID_PARAMS,
message: err.to_string(),
data: None,
})
}
/// A helper function used to parse a `serde_json::Value` wrapped in an `Option`.
fn parse_some<T: for<'de> Deserialize<'de>>(value: Option<Value>) -> Result<T, BrpError> {
match value {
Some(value) => parse(value),
None => Err(BrpError {
code: error_codes::INVALID_PARAMS,
message: String::from("Params not provided"),
data: None,
}),
}
}
/// Handles a `bevy/get` request coming from a client.
pub fn process_remote_get_request(In(params): In<Option<Value>>, world: &World) -> BrpResult {
let BrpGetParams { entity, components } = parse_some(params)?;
let app_type_registry = world.resource::<AppTypeRegistry>();
let type_registry = app_type_registry.read();
let entity_ref = get_entity(world, entity)?;
let mut response = BrpGetResponse::default();
for component_path in components {
let reflect_component = get_reflect_component(&type_registry, &component_path)
.map_err(BrpError::component_error)?;
// Retrieve the reflected value for the given specified component on the given entity.
let Some(reflected) = reflect_component.reflect(entity_ref) else {
return Err(BrpError::component_not_present(&component_path, entity));
};
// 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,
})?
else {
return Err(BrpError {
code: error_codes::COMPONENT_ERROR,
message: format!("Component `{}` could not be serialized", component_path),
data: None,
});
};
response.extend(serialized_object.into_iter());
}
serde_json::to_value(response).map_err(BrpError::internal)
}
/// Handles a `bevy/query` request coming from a client.
pub fn process_remote_query_request(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let BrpQueryParams {
data: BrpQuery {
components,
option,
has,
},
filter: BrpQueryFilter { without, with },
} = parse_some(params)?;
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let type_registry = app_type_registry.read();
let components =
get_component_ids(&type_registry, world, components).map_err(BrpError::component_error)?;
let option =
get_component_ids(&type_registry, world, option).map_err(BrpError::component_error)?;
let has = get_component_ids(&type_registry, world, has).map_err(BrpError::component_error)?;
let without =
get_component_ids(&type_registry, world, without).map_err(BrpError::component_error)?;
let with = get_component_ids(&type_registry, world, with).map_err(BrpError::component_error)?;
let mut query = QueryBuilder::<FilteredEntityRef>::new(world);
for (_, component) in &components {
query.ref_id(*component);
}
for (_, option) in &option {
query.optional(|query| {
query.ref_id(*option);
});
}
for (_, has) in &has {
query.optional(|query| {
query.ref_id(*has);
});
}
for (_, without) in without {
query.without_id(without);
}
for (_, with) in with {
query.with_id(with);
}
// At this point, we can safely unify `components` and `option`, since we only retrieved
// entities that actually have all the `components` already.
//
// We also will just collect the `ReflectComponent` values from the type registry all
// at once so that we can reuse them between components.
let paths_and_reflect_components: Vec<(&str, &ReflectComponent)> = components
.into_iter()
.chain(option)
.map(|(type_id, _)| reflect_component_from_id(type_id, &type_registry))
.collect::<AnyhowResult<Vec<(&str, &ReflectComponent)>>>()
.map_err(BrpError::component_error)?;
// ... and the analogous construction for `has`:
let has_paths_and_reflect_components: Vec<(&str, &ReflectComponent)> = has
.into_iter()
.map(|(type_id, _)| reflect_component_from_id(type_id, &type_registry))
.collect::<AnyhowResult<Vec<(&str, &ReflectComponent)>>>()
.map_err(BrpError::component_error)?;
let mut response = BrpQueryResponse::default();
let mut query = query.build();
for row in query.iter(world) {
// The map of component values:
let components_map = build_components_map(
row.clone(),
paths_and_reflect_components.iter().copied(),
&type_registry,
)
.map_err(BrpError::component_error)?;
// The map of boolean-valued component presences:
let has_map = build_has_map(
row.clone(),
has_paths_and_reflect_components.iter().copied(),
);
response.push(BrpQueryRow {
entity: row.id(),
components: components_map,
has: has_map,
});
}
serde_json::to_value(response).map_err(BrpError::internal)
}
/// Handles a `bevy/spawn` request coming from a client.
pub fn process_remote_spawn_request(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let BrpSpawnParams { components } = parse_some(params)?;
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let type_registry = app_type_registry.read();
let reflect_components =
deserialize_components(&type_registry, components).map_err(BrpError::component_error)?;
let entity = world.spawn_empty();
let entity_id = entity.id();
insert_reflected_components(&type_registry, entity, reflect_components)
.map_err(BrpError::component_error)?;
let response = BrpSpawnResponse { entity: entity_id };
serde_json::to_value(response).map_err(BrpError::internal)
}
/// Handles a `bevy/insert` request (insert components) coming from a client.
pub fn process_remote_insert_request(
In(params): In<Option<Value>>,
world: &mut World,
) -> BrpResult {
let BrpInsertParams { entity, components } = parse_some(params)?;
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let type_registry = app_type_registry.read();
let reflect_components =
deserialize_components(&type_registry, components).map_err(BrpError::component_error)?;
insert_reflected_components(
&type_registry,
get_entity_mut(world, entity)?,
reflect_components,
)
.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>>,
world: &mut World,
) -> BrpResult {
let BrpRemoveParams { entity, components } = parse_some(params)?;
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let type_registry = app_type_registry.read();
let component_ids =
get_component_ids(&type_registry, world, components).map_err(BrpError::component_error)?;
// Remove the components.
let mut entity_world_mut = get_entity_mut(world, entity)?;
for (_, component_id) in component_ids {
entity_world_mut.remove_by_id(component_id);
}
Ok(Value::Null)
}
/// Handles a `bevy/destroy` (despawn entity) request coming from a client.
pub fn process_remote_destroy_request(
In(params): In<Option<Value>>,
world: &mut World,
) -> BrpResult {
let BrpDestroyParams { entity } = parse_some(params)?;
get_entity_mut(world, entity)?.despawn();
Ok(Value::Null)
}
/// Handles a `bevy/reparent` request coming from a client.
pub fn process_remote_reparent_request(
In(params): In<Option<Value>>,
world: &mut World,
) -> BrpResult {
let BrpReparentParams {
entities,
parent: maybe_parent,
} = parse_some(params)?;
// If `Some`, reparent the entities.
if let Some(parent) = maybe_parent {
let mut parent_commands =
get_entity_mut(world, parent).map_err(|_| BrpError::entity_not_found(parent))?;
for entity in entities {
if entity == parent {
return Err(BrpError::self_reparent(entity));
}
parent_commands.add_child(entity);
}
}
// If `None`, remove the entities' parents.
else {
for entity in entities {
get_entity_mut(world, entity)?.remove_parent();
}
}
Ok(Value::Null)
}
/// Handles a `bevy/list` request (list all components) coming from a client.
pub fn process_remote_list_request(In(params): In<Option<Value>>, world: &World) -> BrpResult {
let app_type_registry = world.resource::<AppTypeRegistry>();
let type_registry = app_type_registry.read();
let mut response = BrpListResponse::default();
// If `Some`, return all components of the provided entity.
if let Some(BrpListParams { entity }) = params.map(parse).transpose()? {
let entity = get_entity(world, entity)?;
for component_id in entity.archetype().components() {
let Some(component_info) = world.components().get_info(component_id) else {
continue;
};
response.push(component_info.name().to_owned());
}
}
// If `None`, list all registered components.
else {
for registered_type in type_registry.iter() {
if registered_type.data::<ReflectComponent>().is_some() {
response.push(registered_type.type_info().type_path().to_owned());
}
}
}
// Sort both for cleanliness and to reduce the risk that clients start
// accidentally depending on the order.
response.sort();
serde_json::to_value(response).map_err(BrpError::internal)
}
/// Immutably retrieves an entity from the [`World`], returning an error if the
/// entity isn't present.
fn get_entity(world: &World, entity: Entity) -> Result<EntityRef<'_>, BrpError> {
world
.get_entity(entity)
.ok_or_else(|| BrpError::entity_not_found(entity))
}
/// Mutably retrieves an entity from the [`World`], returning an error if the
/// entity isn't present.
fn get_entity_mut(world: &mut World, entity: Entity) -> Result<EntityWorldMut<'_>, BrpError> {
world
.get_entity_mut(entity)
.ok_or_else(|| BrpError::entity_not_found(entity))
}
/// Returns the [`TypeId`] and [`ComponentId`] of the components with the given
/// full path names.
///
/// Note that the supplied path names must be *full* path names: e.g.
/// `bevy_transform::components::transform::Transform` instead of `Transform`.
fn get_component_ids(
type_registry: &TypeRegistry,
world: &World,
component_paths: Vec<String>,
) -> AnyhowResult<Vec<(TypeId, ComponentId)>> {
let mut component_ids = vec![];
for component_path in component_paths {
let type_id = get_component_type_registration(type_registry, &component_path)?.type_id();
let Some(component_id) = world.components().get_id(type_id) else {
return Err(anyhow!(
"Component `{}` isn't used in the world",
component_path
));
};
component_ids.push((type_id, component_id));
}
Ok(component_ids)
}
/// Given an entity (`entity_ref`) and a list of reflected component information
/// (`paths_and_reflect_components`), return a map which associates each component to
/// its serialized value from the entity.
///
/// This is intended to be used on an entity which has already been filtered; components
/// where the value is not present on an entity are simply skipped.
fn build_components_map<'a>(
entity_ref: FilteredEntityRef,
paths_and_reflect_components: impl Iterator<Item = (&'a str, &'a ReflectComponent)>,
type_registry: &TypeRegistry,
) -> AnyhowResult<HashMap<String, Value>> {
let mut serialized_components_map = HashMap::new();
for (type_path, reflect_component) in paths_and_reflect_components {
let Some(reflected) = reflect_component.reflect(entity_ref.clone()) else {
continue;
};
let reflect_serializer =
ReflectSerializer::new(reflected.as_partial_reflect(), type_registry);
let Value::Object(serialized_object) = serde_json::to_value(&reflect_serializer)? else {
return Err(anyhow!("Component `{}` could not be serialized", type_path));
};
serialized_components_map.extend(serialized_object.into_iter());
}
Ok(serialized_components_map)
}
/// Given an entity (`entity_ref`) and list of reflected component information
/// (`paths_and_reflect_components`), return a map which associates each component to
/// a boolean value indicating whether or not that component is present on the entity.
fn build_has_map<'a>(
entity_ref: FilteredEntityRef,
paths_and_reflect_components: impl Iterator<Item = (&'a str, &'a ReflectComponent)>,
) -> HashMap<String, Value> {
let mut has_map = HashMap::new();
for (type_path, reflect_component) in paths_and_reflect_components {
let has = reflect_component.contains(entity_ref.clone());
has_map.insert(type_path.to_owned(), Value::Bool(has));
}
has_map
}
/// Given a component ID, return the associated [type path] and `ReflectComponent` if possible.
///
/// The `ReflectComponent` part is the meat of this; the type path is only used for error messages.
///
/// [type path]: bevy_reflect::TypePath::type_path
fn reflect_component_from_id(
component_type_id: TypeId,
type_registry: &TypeRegistry,
) -> AnyhowResult<(&str, &ReflectComponent)> {
let Some(type_registration) = type_registry.get(component_type_id) else {
return Err(anyhow!(
"Component `{:?}` isn't registered",
component_type_id
));
};
let type_path = type_registration.type_info().type_path();
let Some(reflect_component) = type_registration.data::<ReflectComponent>() else {
return Err(anyhow!("Component `{}` isn't reflectable", type_path));
};
Ok((type_path, reflect_component))
}
/// Given a collection of component paths and their associated serialized values (`components`),
/// return the associated collection of deserialized reflected values.
fn deserialize_components(
type_registry: &TypeRegistry,
components: HashMap<String, Value>,
) -> AnyhowResult<Vec<Box<dyn PartialReflect>>> {
let mut reflect_components = vec![];
for (component_path, component) in components {
let Some(component_type) = type_registry.get_with_type_path(&component_path) else {
return Err(anyhow!("Unknown component type: `{}`", component_path));
};
let reflected: Box<dyn PartialReflect> =
TypedReflectDeserializer::new(component_type, type_registry)
.deserialize(&component)
.unwrap();
reflect_components.push(reflected);
}
Ok(reflect_components)
}
/// Given a collection `reflect_components` of reflected component values, insert them into
/// the given entity (`entity_world_mut`).
fn insert_reflected_components(
type_registry: &TypeRegistry,
mut entity_world_mut: EntityWorldMut,
reflect_components: Vec<Box<dyn PartialReflect>>,
) -> AnyhowResult<()> {
for reflected in reflect_components {
let reflect_component =
get_reflect_component(type_registry, reflected.reflect_type_path())?;
reflect_component.insert(&mut entity_world_mut, &*reflected, type_registry);
}
Ok(())
}
/// Given a component's type path, return the associated [`ReflectComponent`] from the given
/// `type_registry` if possible.
fn get_reflect_component<'r>(
type_registry: &'r TypeRegistry,
component_path: &str,
) -> AnyhowResult<&'r ReflectComponent> {
let component_registration = get_component_type_registration(type_registry, component_path)?;
component_registration
.data::<ReflectComponent>()
.ok_or_else(|| anyhow!("Component `{}` isn't reflectable", component_path))
}
/// Given a component's type path, return the associated [`TypeRegistration`] from the given
/// `type_registry` if possible.
fn get_component_type_registration<'r>(
type_registry: &'r TypeRegistry,
component_path: &str,
) -> AnyhowResult<&'r TypeRegistration> {
type_registry
.get_with_type_path(component_path)
.ok_or_else(|| anyhow!("Unknown component type: `{}`", component_path))
}

View file

@ -0,0 +1,867 @@
//! An implementation of the Bevy Remote Protocol over HTTP and JSON, to allow
//! for remote control of a Bevy app.
//!
//! Adding the [`RemotePlugin`] to your [`App`] causes Bevy to accept
//! connections over HTTP (by default, on port 15702) while your app is running.
//! These *remote clients* can inspect and alter the state of the
//! entity-component system. Clients are expected to `POST` JSON requests to the
//! root URL; see the `client` example for a trivial example of use.
//!
//! The Bevy Remote Protocol is based on the JSON-RPC 2.0 protocol.
//!
//! ## Request objects
//!
//! A typical client request might look like this:
//!
//! ```json
//! {
//! "method": "bevy/get",
//! "id": 0,
//! "params": {
//! "entity": 4294967298,
//! "components": [
//! "bevy_transform::components::transform::Transform"
//! ]
//! }
//! }
//! ```
//!
//! The `id` and `method` fields are required. The `params` field may be omitted
//! for certain methods:
//!
//! * `id` is arbitrary JSON data. The server completely ignores its contents,
//! and the client may use it for any purpose. It will be copied via
//! serialization and deserialization (so object property order, etc. can't be
//! relied upon to be identical) and sent back to the client as part of the
//! response.
//!
//! * `method` is a string that specifies one of the possible [`BrpRequest`]
//! variants: `bevy/query`, `bevy/get`, `bevy/insert`, etc. It's case-sensitive.
//!
//! * `params` is parameter data specific to the request.
//!
//! For more information, see the documentation for [`BrpRequest`].
//! [`BrpRequest`] is serialized to JSON via `serde`, so [the `serde`
//! documentation] may be useful to clarify the correspondence between the Rust
//! structure and the JSON format.
//!
//! ## Response objects
//!
//! A response from the server to the client might look like this:
//!
//! ```json
//! {
//! "jsonrpc": "2.0",
//! "id": 0,
//! "result": {
//! "bevy_transform::components::transform::Transform": {
//! "rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
//! "scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
//! "translation": { "x": 0.0, "y": 0.5, "z": 0.0 }
//! }
//! }
//! }
//! ```
//!
//! The `id` field will always be present. The `result` field will be present if the
//! request was successful. Otherwise, an `error` field will replace it.
//!
//! * `id` is the arbitrary JSON data that was sent as part of the request. It
//! will be identical to the `id` data sent during the request, modulo
//! serialization and deserialization. If there's an error reading the `id` field,
//! it will be `null`.
//!
//! * `result` will be present if the request succeeded and will contain the response
//! specific to the request.
//!
//! * `error` will be present if the request failed and will contain an error object
//! with more information about the cause of failure.
//!
//! ## Error objects
//!
//! An error object might look like this:
//!
//! ```json
//! {
//! "code": -32602,
//! "message": "Missing \"entity\" field"
//! }
//! ```
//!
//! The `code` and `message` fields will always be present. There may also be a `data` field.
//!
//! * `code` is an integer representing the kind of an error that happened. Error codes documented
//! in the [`error_codes`] module.
//!
//! * `message` is a short, one-sentence human-readable description of the error.
//!
//! * `data` is an optional field of arbitrary type containing additional information about the error.
//!
//! ## Built-in methods
//!
//! The Bevy Remote Protocol includes a number of built-in methods for accessing and modifying data
//! in the ECS. Each of these methods uses the `bevy/` prefix, which is a namespace reserved for
//! BRP built-in methods.
//!
//! ### bevy/get
//!
//! Retrieve the values of one or more components from an entity.
//!
//! `params`:
//! - `entity`: The ID of the entity whose components will be fetched.
//! - `components`: An array of [fully-qualified type names] of components to fetch.
//!
//! `result`: A map associating each type name to its value on the requested entity.
//!
//! ### bevy/query
//!
//! Perform a query over components in the ECS, returning all matching entities and their associated
//! component values.
//!
//! All of the arrays that comprise this request are optional, and when they are not provided, they
//! will be treated as if they were empty.
//!
//! `params`:
//! - `data`:
//! - `components` (optional): An array of [fully-qualified type names] of components to fetch.
//! - `option` (optional): An array of fully-qualified type names of components to fetch optionally.
//! - `has` (optional): An array of fully-qualified type names of components whose presence will be
//! reported as boolean values.
//! - `filter` (optional):
//! - `with` (optional): An array of fully-qualified type names of components that must be present
//! on entities in order for them to be included in results.
//! - `without` (optional): An array of fully-qualified type names of components that must *not* be
//! present on entities in order for them to be included in results.
//!
//! `result`: An array, each of which is an object containing:
//! - `entity`: The ID of a query-matching entity.
//! - `components`: A map associating each type name from `components`/`option` to its value on the matching
//! entity if the component is present.
//! - `has`: A map associating each type name from `has` to a boolean value indicating whether or not the
//! entity has that component. If `has` was empty or omitted, this key will be omitted in the response.
//!
//! ### bevy/spawn
//!
//! Create a new entity with the provided components and return the resulting entity ID.
//!
//! `params`:
//! - `components`: A map associating each component's [fully-qualified type name] with its value.
//!
//! `result`:
//! - `entity`: The ID of the newly spawned entity.
//!
//! ### bevy/destroy
//!
//! Despawn the entity with the given ID.
//!
//! `params`:
//! - `entity`: The ID of the entity to be despawned.
//!
//! `result`: null.
//!
//! ### bevy/remove
//!
//! Delete one or more components from an entity.
//!
//! `params`:
//! - `entity`: The ID of the entity whose components should be removed.
//! - `components`: An array of [fully-qualified type names] of components to be removed.
//!
//! `result`: null.
//!
//! ### bevy/insert
//!
//! Insert one or more components into an entity.
//!
//! `params`:
//! - `entity`: The ID of the entity to insert components into.
//! - `components`: A map associating each component's fully-qualified type name with its value.
//!
//! `result`: null.
//!
//! ### bevy/reparent
//!
//! Assign a new parent to one or more entities.
//!
//! `params`:
//! - `entities`: An array of entity IDs of entities that will be made children of the `parent`.
//! - `parent` (optional): The entity ID of the parent to which the child entities will be assigned.
//! If excluded, the given entities will be removed from their parents.
//!
//! `result`: null.
//!
//! ### bevy/list
//!
//! List all registered components or all components present on an entity.
//!
//! When `params` is not provided, this lists all registered components. If `params` is provided,
//! this lists only those components present on the provided entity.
//!
//! `params` (optional):
//! - `entity`: The ID of the entity whose components will be listed.
//!
//! `result`: An array of fully-qualified type names of components.
//!
//! ## Custom methods
//!
//! In addition to the provided methods, the Bevy Remote Protocol can be extended to include custom
//! methods. This is primarily done during the initialization of [`RemotePlugin`], although the
//! methods may also be extended at runtime using the [`RemoteMethods`] resource.
//!
//! ### Example
//! ```ignore
//! fn main() {
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(
//! // `default` adds all of the built-in methods, while `with_method` extends them
//! RemotePlugin::default()
//! .with_method("super_user/cool_method", path::to::my::cool::handler)
//! // ... more methods can be added by chaining `with_method`
//! )
//! .add_systems(
//! // ... standard application setup
//! )
//! .run();
//! }
//! ```
//!
//! The handler is expected to be a system-convertible function which takes optional JSON parameters
//! as input and returns a [`BrpResult`]. This means that it should have a type signature which looks
//! something like this:
//! ```
//! # use serde_json::Value;
//! # use bevy_ecs::prelude::{In, World};
//! # use bevy_remote::BrpResult;
//! fn handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
//! todo!()
//! }
//! ```
//!
//! Arbitrary system parameters can be used in conjunction with the optional `Value` input. The
//! handler system will always run with exclusive `World` access.
//!
//! [the `serde` documentation]: https://serde.rs/
//! [fully-qualified type names]: bevy_reflect::TypePath::type_path
//! [fully-qualified type name]: bevy_reflect::TypePath::type_path
#![cfg(not(target_family = "wasm"))]
use std::{
net::{IpAddr, Ipv4Addr},
sync::RwLock,
};
use anyhow::Result as AnyhowResult;
use bevy_app::prelude::*;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
entity::Entity,
system::{Commands, In, IntoSystem, Res, Resource, System, SystemId},
world::World,
};
use bevy_reflect::Reflect;
use bevy_tasks::IoTaskPool;
use bevy_utils::{prelude::default, HashMap};
use http_body_util::{BodyExt as _, Full};
use hyper::{
body::{Bytes, Incoming},
server::conn::http1,
service, Request, Response,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::{
channel::{self, Receiver, Sender},
Async,
};
use smol_hyper::rt::{FuturesIo, SmolTimer};
use std::net::{TcpListener, TcpStream};
pub mod builtin_methods;
/// The default port that Bevy will listen on.
///
/// This value was chosen randomly.
pub const DEFAULT_PORT: u16 = 15702;
/// The default host address that Bevy will use for its server.
pub const DEFAULT_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const CHANNEL_SIZE: usize = 16;
/// Add this plugin to your [`App`] to allow remote connections to inspect and modify entities.
/// This the main plugin for `bevy_remote`. See the [crate-level documentation] for details on
/// the protocol and its default methods.
///
/// The defaults are:
/// - [`DEFAULT_ADDR`] : 127.0.0.1.
/// - [`DEFAULT_PORT`] : 15702.
///
/// [crate-level documentation]: crate
pub struct RemotePlugin {
/// The address that Bevy will use.
address: IpAddr,
/// The port that Bevy will listen on.
port: u16,
/// The verbs that the server will recognize and respond to.
methods: RwLock<
Vec<(
String,
Box<dyn System<In = In<Option<Value>>, Out = BrpResult>>,
)>,
>,
}
impl RemotePlugin {
/// Create a [`RemotePlugin`] with the default address and port but without
/// any associated methods.
fn empty() -> Self {
Self {
address: DEFAULT_ADDR,
port: DEFAULT_PORT,
methods: RwLock::new(vec![]),
}
}
/// Set the IP address that the server will use.
#[must_use]
pub fn with_address(mut self, address: impl Into<IpAddr>) -> Self {
self.address = address.into();
self
}
/// Set the remote port that the server will listen on.
#[must_use]
pub fn with_port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// Add a remote method to the plugin using the given `name` and `handler`.
#[must_use]
pub fn with_method<M>(
mut self,
name: impl Into<String>,
handler: impl IntoSystem<In<Option<Value>>, BrpResult, M>,
) -> Self {
self.methods
.get_mut()
.unwrap()
.push((name.into(), Box::new(IntoSystem::into_system(handler))));
self
}
}
impl Default for RemotePlugin {
fn default() -> Self {
Self::empty()
.with_method(
builtin_methods::BRP_GET_METHOD,
builtin_methods::process_remote_get_request,
)
.with_method(
builtin_methods::BRP_QUERY_METHOD,
builtin_methods::process_remote_query_request,
)
.with_method(
builtin_methods::BRP_SPAWN_METHOD,
builtin_methods::process_remote_spawn_request,
)
.with_method(
builtin_methods::BRP_INSERT_METHOD,
builtin_methods::process_remote_insert_request,
)
.with_method(
builtin_methods::BRP_REMOVE_METHOD,
builtin_methods::process_remote_remove_request,
)
.with_method(
builtin_methods::BRP_DESTROY_METHOD,
builtin_methods::process_remote_destroy_request,
)
.with_method(
builtin_methods::BRP_REPARENT_METHOD,
builtin_methods::process_remote_reparent_request,
)
.with_method(
builtin_methods::BRP_LIST_METHOD,
builtin_methods::process_remote_list_request,
)
}
}
impl Plugin for RemotePlugin {
fn build(&self, app: &mut App) {
let mut remote_methods = RemoteMethods::new();
let plugin_methods = &mut *self.methods.write().unwrap();
for (name, system) in plugin_methods.drain(..) {
remote_methods.insert(
name,
app.main_mut().world_mut().register_boxed_system(system),
);
}
app.insert_resource(HostAddress(self.address))
.insert_resource(HostPort(self.port))
.insert_resource(remote_methods)
.add_systems(Startup, start_server)
.add_systems(Update, process_remote_requests);
}
}
/// A resource containing the IP address that Bevy will host on.
///
/// Currently, changing this while the application is running has no effect; this merely
/// reflects the IP address that is set during the setup of the [`RemotePlugin`].
#[derive(Debug, Resource)]
pub struct HostAddress(pub IpAddr);
/// A resource containing the port number that Bevy will listen on.
///
/// Currently, changing this while the application is running has no effect; this merely
/// reflects the host that is set during the setup of the [`RemotePlugin`].
#[derive(Debug, Resource, Reflect)]
pub struct HostPort(pub u16);
/// The type of a function that implements a remote method (`bevy/get`, `bevy/query`, etc.)
///
/// The first parameter is the JSON value of the `params`. Typically, an
/// implementation will deserialize these as the first thing they do.
///
/// The returned JSON value will be returned as the response. Bevy will
/// automatically populate the `id` field before sending.
pub type RemoteMethod = SystemId<In<Option<Value>>, BrpResult>;
/// Holds all implementations of methods known to the server.
///
/// Custom methods can be added to this list using [`RemoteMethods::insert`].
#[derive(Debug, Resource, Default)]
pub struct RemoteMethods(HashMap<String, RemoteMethod>);
impl RemoteMethods {
/// Creates a new [`RemoteMethods`] resource with no methods registered in it.
pub fn new() -> Self {
default()
}
/// Adds a new method, replacing any existing method with that name.
///
/// If there was an existing method with that name, returns its handler.
pub fn insert(
&mut self,
method_name: impl Into<String>,
handler: RemoteMethod,
) -> Option<RemoteMethod> {
self.0.insert(method_name.into(), handler)
}
}
/// A single request from a Bevy Remote Protocol client to the server,
/// serialized in JSON.
///
/// The JSON payload is expected to look like this:
///
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "method": "bevy/get",
/// "id": 0,
/// "params": {
/// "entity": 4294967298,
/// "components": [
/// "bevy_transform::components::transform::Transform"
/// ]
/// }
/// }
/// ```
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpRequest {
/// This field is mandatory and must be set to `"2.0"` for the request to be accepted.
pub jsonrpc: String,
/// The action to be performed.
pub method: String,
/// Arbitrary data that will be returned verbatim to the client as part of
/// the response.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Value>,
/// The parameters, specific to each method.
///
/// These are passed as the first argument to the method handler.
/// Sometimes params can be omitted.
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
/// A response according to BRP.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpResponse {
/// This field is mandatory and must be set to `"2.0"`.
pub jsonrpc: &'static str,
/// The id of the original request.
pub id: Option<Value>,
/// The actual response payload.
#[serde(flatten)]
pub payload: BrpPayload,
}
impl BrpResponse {
/// Generates a [`BrpResponse`] from an id and a `Result`.
#[must_use]
pub fn new(id: Option<Value>, result: BrpResult) -> Self {
Self {
jsonrpc: "2.0",
id,
payload: BrpPayload::from(result),
}
}
}
/// A result/error payload present in every response.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum BrpPayload {
/// `Ok` variant
Result(Value),
/// `Err` variant
Error(BrpError),
}
impl From<BrpResult> for BrpPayload {
fn from(value: BrpResult) -> Self {
match value {
Ok(v) => Self::Result(v),
Err(err) => Self::Error(err),
}
}
}
/// An error a request might return.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrpError {
/// Defines the general type of the error.
pub code: i16,
/// Short, human-readable description of the error.
pub message: String,
/// Optional additional error data.
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl BrpError {
/// Entity wasn't found.
#[must_use]
pub fn entity_not_found(entity: Entity) -> Self {
Self {
code: error_codes::ENTITY_NOT_FOUND,
message: format!("Entity {entity} not found"),
data: None,
}
}
/// Component wasn't found in an entity.
#[must_use]
pub fn component_not_present(component: &str, entity: Entity) -> Self {
Self {
code: error_codes::COMPONENT_NOT_PRESENT,
message: format!("Component `{component}` not present in Entity {entity}"),
data: None,
}
}
/// An arbitrary component error. Possibly related to reflection.
#[must_use]
pub fn component_error<E: ToString>(error: E) -> Self {
Self {
code: error_codes::COMPONENT_ERROR,
message: error.to_string(),
data: None,
}
}
/// An arbitrary internal error.
#[must_use]
pub fn internal<E: ToString>(error: E) -> Self {
Self {
code: error_codes::INTERNAL_ERROR,
message: error.to_string(),
data: None,
}
}
/// Attempt to reparent an entity to itself.
#[must_use]
pub fn self_reparent(entity: Entity) -> Self {
Self {
code: error_codes::SELF_REPARENT,
message: format!("Cannot reparent Entity {entity} to itself"),
data: None,
}
}
}
/// Error codes used by BRP.
pub mod error_codes {
// JSON-RPC errors
// Note that the range -32728 to -32000 (inclusive) is reserved by the JSON-RPC specification.
/// Invalid JSON.
pub const PARSE_ERROR: i16 = -32700;
/// JSON sent is not a valid request object.
pub const INVALID_REQUEST: i16 = -32600;
/// The method does not exist / is not available.
pub const METHOD_NOT_FOUND: i16 = -32601;
/// Invalid method parameter(s).
pub const INVALID_PARAMS: i16 = -32602;
/// Internal error.
pub const INTERNAL_ERROR: i16 = -32603;
// Bevy errors (i.e. application errors)
/// Entity not found.
pub const ENTITY_NOT_FOUND: i16 = -23401;
/// Could not reflect or find component.
pub const COMPONENT_ERROR: i16 = -23402;
/// Could not find component in entity.
pub const COMPONENT_NOT_PRESENT: i16 = -23403;
/// Cannot reparent an entity to itself.
pub const SELF_REPARENT: i16 = -23404;
}
/// The result of a request.
pub type BrpResult = Result<Value, BrpError>;
/// The requests may occur on their own or in batches.
/// Actual parsing is deferred for the sake of proper
/// error reporting.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BrpBatch {
/// Multiple requests with deferred parsing.
Batch(Vec<Value>),
/// A single request with deferred parsing.
Single(Value),
}
/// A message from the Bevy Remote Protocol server thread to the main world.
///
/// This is placed in the [`BrpMailbox`].
#[derive(Debug, Clone)]
pub struct BrpMessage {
/// The request method.
pub method: String,
/// The request params.
pub params: Option<Value>,
/// The channel on which the response is to be sent.
///
/// The value sent here is serialized and sent back to the client.
pub sender: Sender<BrpResult>,
}
/// A resource that receives messages sent by Bevy Remote Protocol clients.
///
/// Every frame, the `process_remote_requests` system drains this mailbox and
/// processes the messages within.
#[derive(Debug, Resource, Deref, DerefMut)]
pub struct BrpMailbox(Receiver<BrpMessage>);
/// A system that starts up the Bevy Remote Protocol server.
fn start_server(mut commands: Commands, address: Res<HostAddress>, remote_port: Res<HostPort>) {
// Create the channel and the mailbox.
let (request_sender, request_receiver) = channel::bounded(CHANNEL_SIZE);
commands.insert_resource(BrpMailbox(request_receiver));
IoTaskPool::get()
.spawn(server_main(address.0, remote_port.0, request_sender))
.detach();
}
/// A system that receives requests placed in the [`BrpMailbox`] and processes
/// them, using the [`RemoteMethods`] resource to map each request to its handler.
///
/// This needs exclusive access to the [`World`] because clients can manipulate
/// anything in the ECS.
fn process_remote_requests(world: &mut World) {
if !world.contains_resource::<BrpMailbox>() {
return;
}
while let Ok(message) = world.resource_mut::<BrpMailbox>().try_recv() {
// Fetch the handler for the method. If there's no such handler
// registered, return an error.
let methods = world.resource::<RemoteMethods>();
let Some(handler) = methods.0.get(&message.method) else {
let _ = message.sender.send_blocking(Err(BrpError {
code: error_codes::METHOD_NOT_FOUND,
message: format!("Method `{}` not found", message.method),
data: None,
}));
continue;
};
// Execute the handler, and send the result back to the client.
let result = match world.run_system_with_input(*handler, message.params) {
Ok(result) => result,
Err(error) => {
let _ = message.sender.send_blocking(Err(BrpError {
code: error_codes::INTERNAL_ERROR,
message: format!("Failed to run method handler: {error}"),
data: None,
}));
continue;
}
};
let _ = message.sender.send_blocking(result);
}
}
/// The Bevy Remote Protocol server main loop.
async fn server_main(
address: IpAddr,
port: u16,
request_sender: Sender<BrpMessage>,
) -> AnyhowResult<()> {
listen(
Async::<TcpListener>::bind((address, port))?,
&request_sender,
)
.await
}
async fn listen(
listener: Async<TcpListener>,
request_sender: &Sender<BrpMessage>,
) -> AnyhowResult<()> {
loop {
let (client, _) = listener.accept().await?;
let request_sender = request_sender.clone();
IoTaskPool::get()
.spawn(async move {
let _ = handle_client(client, request_sender).await;
})
.detach();
}
}
async fn handle_client(
client: Async<TcpStream>,
request_sender: Sender<BrpMessage>,
) -> AnyhowResult<()> {
http1::Builder::new()
.timer(SmolTimer::new())
.serve_connection(
FuturesIo::new(client),
service::service_fn(|request| process_request_batch(request, &request_sender)),
)
.await?;
Ok(())
}
/// A helper function for the Bevy Remote Protocol server that handles a batch
/// of requests coming from a client.
async fn process_request_batch(
request: Request<Incoming>,
request_sender: &Sender<BrpMessage>,
) -> AnyhowResult<Response<Full<Bytes>>> {
let batch_bytes = request.into_body().collect().await?.to_bytes();
let batch: Result<BrpBatch, _> = serde_json::from_slice(&batch_bytes);
let serialized = match batch {
Ok(BrpBatch::Single(request)) => {
serde_json::to_string(&process_single_request(request, request_sender).await?)?
}
Ok(BrpBatch::Batch(requests)) => {
let mut responses = Vec::new();
for request in requests {
responses.push(process_single_request(request, request_sender).await?);
}
serde_json::to_string(&responses)?
}
Err(err) => {
let err = BrpResponse::new(
None,
Err(BrpError {
code: error_codes::INVALID_REQUEST,
message: err.to_string(),
data: None,
}),
);
serde_json::to_string(&err)?
}
};
Ok(Response::new(Full::new(Bytes::from(
serialized.as_bytes().to_owned(),
))))
}
/// A helper function for the Bevy Remote Protocol server that processes a single
/// request coming from a client.
async fn process_single_request(
request: Value,
request_sender: &Sender<BrpMessage>,
) -> AnyhowResult<BrpResponse> {
// Reach in and get the request ID early so that we can report it even when parsing fails.
let id = request.as_object().and_then(|map| map.get("id")).cloned();
let request: BrpRequest = match serde_json::from_value(request) {
Ok(v) => v,
Err(err) => {
return Ok(BrpResponse::new(
id,
Err(BrpError {
code: error_codes::INVALID_REQUEST,
message: err.to_string(),
data: None,
}),
));
}
};
if request.jsonrpc != "2.0" {
return Ok(BrpResponse::new(
id,
Err(BrpError {
code: error_codes::INVALID_REQUEST,
message: String::from("JSON-RPC request requires `\"jsonrpc\": \"2.0\"`"),
data: None,
}),
));
}
let (result_sender, result_receiver) = channel::bounded(1);
let _ = request_sender
.send(BrpMessage {
method: request.method,
params: request.params,
sender: result_sender,
})
.await;
let result = result_receiver.recv().await?;
Ok(BrpResponse::new(request.id, result))
}

View file

@ -23,6 +23,7 @@ The default feature set enables most of the expected features of a game engine,
|bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support|
|bevy_pbr|Adds PBR rendering| |bevy_pbr|Adds PBR rendering|
|bevy_picking|Provides picking functionality| |bevy_picking|Provides picking functionality|
|bevy_remote|Enable the Bevy Remote Protocol|
|bevy_render|Provides rendering functionality| |bevy_render|Provides rendering functionality|
|bevy_scene|Provides scene functionality| |bevy_scene|Provides scene functionality|
|bevy_sprite|Provides sprite functionality| |bevy_sprite|Provides sprite functionality|

View file

@ -56,6 +56,7 @@ git checkout v0.4.0
- [Movement](#movement) - [Movement](#movement)
- [Picking](#picking) - [Picking](#picking)
- [Reflection](#reflection) - [Reflection](#reflection)
- [Remote Protocol](#remote-protocol)
- [Scene](#scene) - [Scene](#scene)
- [Shaders](#shaders) - [Shaders](#shaders)
- [State](#state) - [State](#state)
@ -379,6 +380,13 @@ Example | Description
[Reflection Types](../examples/reflection/reflection_types.rs) | Illustrates the various reflection types available [Reflection Types](../examples/reflection/reflection_types.rs) | Illustrates the various reflection types available
[Type Data](../examples/reflection/type_data.rs) | Demonstrates how to create and use type data [Type Data](../examples/reflection/type_data.rs) | Demonstrates how to create and use type data
## Remote Protocol
Example | Description
--- | ---
[client](../examples/remote/client.rs) | A simple command line client that can control Bevy apps via the BRP
[server](../examples/remote/server.rs) | A Bevy app that you can connect to with the BRP and edit
## Scene ## Scene
Example | Description Example | Description

70
examples/remote/client.rs Normal file
View file

@ -0,0 +1,70 @@
//! A simple command line client that allows issuing queries to a remote Bevy
//! app via the BRP.
use anyhow::Result as AnyhowResult;
use argh::FromArgs;
use bevy::remote::{
builtin_methods::{BrpQuery, BrpQueryFilter, BrpQueryParams, BRP_QUERY_METHOD},
BrpRequest, DEFAULT_ADDR, DEFAULT_PORT,
};
/// Struct containing the command-line arguments that can be passed to this example.
/// The components are passed by their full type names positionally, while `host`
/// and `port` are optional arguments which should correspond to those used on
/// the server.
///
/// When running this example in conjunction with the `server` example, the `host`
/// and `port` can be left as their defaults.
///
/// For example, to connect to port 1337 on the default IP address and query for entities
/// with `Transform` components:
/// ```text
/// cargo run --example client -- --port 1337 bevy_transform::components::transform::Transform
/// ```
#[derive(FromArgs)]
struct Args {
/// the host IP address to connect to
#[argh(option, default = "DEFAULT_ADDR.to_string()")]
host: String,
/// the port to connect to
#[argh(option, default = "DEFAULT_PORT")]
port: u16,
/// the full type names of the components to query for
#[argh(positional, greedy)]
components: Vec<String>,
}
/// The application entry point.
fn main() -> AnyhowResult<()> {
// Parse the arguments.
let args: Args = argh::from_env();
// Create the URL. We're going to need it to issue the HTTP request.
let host_part = format!("{}:{}", args.host, args.port);
let url = format!("http://{}/", host_part);
let req = BrpRequest {
jsonrpc: String::from("2.0"),
method: String::from(BRP_QUERY_METHOD),
id: Some(ureq::json!(1)),
params: Some(
serde_json::to_value(BrpQueryParams {
data: BrpQuery {
components: args.components,
option: Vec::default(),
has: Vec::default(),
},
filter: BrpQueryFilter::default(),
})
.expect("Unable to convert query parameters to a valid JSON value"),
),
};
let res = ureq::post(&url)
.send_json(req)?
.into_json::<serde_json::Value>()?;
println!("{:#}", res);
Ok(())
}

58
examples/remote/server.rs Normal file
View file

@ -0,0 +1,58 @@
//! A Bevy app that you can connect to with the BRP and edit.
use bevy::{prelude::*, remote::RemotePlugin};
use serde::{Deserialize, Serialize};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(RemotePlugin::default())
.add_systems(Startup, setup)
.register_type::<Cube>()
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// circular base
commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(4.0)),
material: materials.add(Color::WHITE),
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
..default()
});
// cube
commands.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::srgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
Cube(1.0),
));
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
#[derive(Component, Reflect, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize)]
struct Cube(f32);