mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +00:00
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:
parent
27bea6abf7
commit
89e98b208f
10 changed files with 1809 additions and 0 deletions
37
Cargo.toml
37
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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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")]
|
||||
|
|
41
crates/bevy_remote/Cargo.toml
Normal file
41
crates/bevy_remote/Cargo.toml
Normal 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
|
721
crates/bevy_remote/src/builtin_methods.rs
Normal file
721
crates/bevy_remote/src/builtin_methods.rs
Normal 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))
|
||||
}
|
867
crates/bevy_remote/src/lib.rs
Normal file
867
crates/bevy_remote/src/lib.rs
Normal 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))
|
||||
}
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
70
examples/remote/client.rs
Normal file
70
examples/remote/client.rs
Normal 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
58
examples/remote/server.rs
Normal 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);
|
Loading…
Reference in a new issue