From 89e98b208fbba43f48aa211b8951b265cb03997d Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 23 Sep 2024 14:36:16 -0400 Subject: [PATCH] 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:
Click to view crate-level docs ```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::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>, 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/ ```
### 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, /// Arbitrary data that will be returned verbatim to the client as part of /// the response. pub id: Option, /// 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, } ``` 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>, 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 Co-authored-by: DragonGamesStudios Co-authored-by: Christopher Biscardi Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com> --- Cargo.toml | 37 + crates/bevy_internal/Cargo.toml | 4 + crates/bevy_internal/src/lib.rs | 2 + crates/bevy_remote/Cargo.toml | 41 + crates/bevy_remote/src/builtin_methods.rs | 721 ++++++++++++++++++ crates/bevy_remote/src/lib.rs | 867 ++++++++++++++++++++++ docs/cargo_features.md | 1 + examples/README.md | 8 + examples/remote/client.rs | 70 ++ examples/remote/server.rs | 58 ++ 10 files changed, 1809 insertions(+) create mode 100644 crates/bevy_remote/Cargo.toml create mode 100644 crates/bevy_remote/src/builtin_methods.rs create mode 100644 crates/bevy_remote/src/lib.rs create mode 100644 examples/remote/client.rs create mode 100644 examples/remote/server.rs diff --git a/Cargo.toml b/Cargo.toml index b53b4109a3..1c95c166e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ default = [ "bevy_sprite", "bevy_text", "bevy_ui", + "bevy_remote", "multi_threaded", "png", "hdr", @@ -174,6 +175,9 @@ bevy_gizmos = ["bevy_internal/bevy_gizmos", "bevy_color"] # Provides a collection of developer 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) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -376,6 +380,7 @@ rand_chacha = "0.3.1" ron = "0.8.0" flate2 = "1.0" serde = { version = "1", features = ["derive"] } +serde_json = "1" bytemuck = "1.7" bevy_render = { path = "crates/bevy_render", version = "0.15.0-dev", default-features = false } # Needed to poll Task examples @@ -385,6 +390,16 @@ crossbeam-channel = "0.5.0" argh = "0.1.12" thiserror = "1.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] wasm-bindgen = { version = "0.2" } @@ -3384,6 +3399,28 @@ description = "Demonstrates volumetric fog and lighting" category = "3D Rendering" 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]] name = "anisotropy" path = "examples/3d/anisotropy.rs" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 9550379682..9dba6deabd 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -192,6 +192,9 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"] # Provides a collection of developer tools bevy_dev_tools = ["dep:bevy_dev_tools"] +# Enable support for the Bevy Remote Protocol +bevy_remote = ["dep:bevy_remote"] + # Provides a picking functionality 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_pbr = { path = "../bevy_pbr", 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_scene = { path = "../bevy_scene", optional = true, version = "0.15.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.15.0-dev" } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index bc71e57efa..bc553af972 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -46,6 +46,8 @@ pub use bevy_pbr as pbr; pub use bevy_picking as picking; pub use bevy_ptr as ptr; pub use bevy_reflect as reflect; +#[cfg(feature = "bevy_remote")] +pub use bevy_remote as remote; #[cfg(feature = "bevy_render")] pub use bevy_render as render; #[cfg(feature = "bevy_scene")] diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml new file mode 100644 index 0000000000..7b6a199dad --- /dev/null +++ b/crates/bevy_remote/Cargo.toml @@ -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 diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs new file mode 100644 index 0000000000..d6ca798dad --- /dev/null +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -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, +} + +/// `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, +} + +/// `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, +} + +/// `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, +} + +/// `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, + + /// 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, +} + +/// `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, + + /// 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, + + /// 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, +} + +/// 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, + + /// 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, +} + +/// 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; + +/// The response to a `bevy/list` request. +pub type BrpListResponse = Vec; + +/// The response to a `bevy/query` request. +pub type BrpQueryResponse = Vec; + +/// 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, + + /// The boolean-only containment query results. + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub has: HashMap, +} + +/// A helper function used to parse a `serde_json::Value`. +fn parse Deserialize<'de>>(value: Value) -> Result { + 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 Deserialize<'de>>(value: Option) -> Result { + 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>, world: &World) -> BrpResult { + let BrpGetParams { entity, components } = parse_some(params)?; + + let app_type_registry = world.resource::(); + 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>, world: &mut World) -> BrpResult { + let BrpQueryParams { + data: BrpQuery { + components, + option, + has, + }, + filter: BrpQueryFilter { without, with }, + } = parse_some(params)?; + + let app_type_registry = world.resource::().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::::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::>>() + .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::>>() + .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>, world: &mut World) -> BrpResult { + let BrpSpawnParams { components } = parse_some(params)?; + + let app_type_registry = world.resource::().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>, + world: &mut World, +) -> BrpResult { + let BrpInsertParams { entity, components } = parse_some(params)?; + + let app_type_registry = world.resource::().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>, + world: &mut World, +) -> BrpResult { + let BrpRemoveParams { entity, components } = parse_some(params)?; + + let app_type_registry = world.resource::().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>, + 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>, + 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>, world: &World) -> BrpResult { + let app_type_registry = world.resource::(); + 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::().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, 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, 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, +) -> AnyhowResult> { + 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, + type_registry: &TypeRegistry, +) -> AnyhowResult> { + 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, +) -> HashMap { + 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::() 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, +) -> AnyhowResult>> { + 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 = + 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>, +) -> 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::() + .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)) +} diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs new file mode 100644 index 0000000000..1f9b8d0695 --- /dev/null +++ b/crates/bevy_remote/src/lib.rs @@ -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>, 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>, 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) -> 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( + mut self, + name: impl Into, + handler: impl IntoSystem>, 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>, 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); + +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, + handler: RemoteMethod, + ) -> Option { + 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, + + /// 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, +} + +/// 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, + + /// 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, 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 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, +} + +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(error: E) -> Self { + Self { + code: error_codes::COMPONENT_ERROR, + message: error.to_string(), + data: None, + } + } + + /// An arbitrary internal error. + #[must_use] + pub fn internal(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; + +/// 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), + /// 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, + + /// 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, +} + +/// 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); + +/// A system that starts up the Bevy Remote Protocol server. +fn start_server(mut commands: Commands, address: Res, remote_port: Res) { + // 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::() { + return; + } + + while let Ok(message) = world.resource_mut::().try_recv() { + // Fetch the handler for the method. If there's no such handler + // registered, return an error. + let methods = world.resource::(); + + 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, +) -> AnyhowResult<()> { + listen( + Async::::bind((address, port))?, + &request_sender, + ) + .await +} + +async fn listen( + listener: Async, + request_sender: &Sender, +) -> 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, + request_sender: Sender, +) -> 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, + request_sender: &Sender, +) -> AnyhowResult>> { + let batch_bytes = request.into_body().collect().await?.to_bytes(); + let batch: Result = 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, +) -> AnyhowResult { + // 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)) +} diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 0fc97d48c4..417e209a5a 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -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_pbr|Adds PBR rendering| |bevy_picking|Provides picking functionality| +|bevy_remote|Enable the Bevy Remote Protocol| |bevy_render|Provides rendering functionality| |bevy_scene|Provides scene functionality| |bevy_sprite|Provides sprite functionality| diff --git a/examples/README.md b/examples/README.md index 70f0d9b652..616e37648a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -56,6 +56,7 @@ git checkout v0.4.0 - [Movement](#movement) - [Picking](#picking) - [Reflection](#reflection) + - [Remote Protocol](#remote-protocol) - [Scene](#scene) - [Shaders](#shaders) - [State](#state) @@ -379,6 +380,13 @@ Example | Description [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 +## 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 Example | Description diff --git a/examples/remote/client.rs b/examples/remote/client.rs new file mode 100644 index 0000000000..07f620461e --- /dev/null +++ b/examples/remote/client.rs @@ -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, +} + +/// 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::()?; + + println!("{:#}", res); + + Ok(()) +} diff --git a/examples/remote/server.rs b/examples/remote/server.rs new file mode 100644 index 0000000000..c829de9472 --- /dev/null +++ b/examples/remote/server.rs @@ -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::() + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // 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);