//! An implementation of the Bevy Remote Protocol, to allow for remote control of a Bevy app.1//!2//! Adding the [`RemotePlugin`] to your [`App`] will setup everything needed without3//! starting any transports. To start accepting remote connections you will need to4//! add a second plugin like the [`RemoteHttpPlugin`](http::RemoteHttpPlugin) to enable communication5//! over HTTP. These *remote clients* can inspect and alter the state of the6//! entity-component system.7//!8//! The Bevy Remote Protocol is based on the JSON-RPC 2.0 protocol.9//!10//! ## Request objects11//!12//! A typical client request might look like this:13//!14//! ```json15//! {16//! "method": "world.get_components",17//! "id": 0,18//! "params": {19//! "entity": 4294967298,20//! "components": [21//! "bevy_transform::components::transform::Transform"22//! ]23//! }24//! }25//! ```26//!27//! The `id` and `method` fields are required. The `params` field may be omitted28//! for certain methods:29//!30//! * `id` is arbitrary JSON data. The server completely ignores its contents,31//! and the client may use it for any purpose. It will be copied via32//! serialization and deserialization (so object property order, etc. can't be33//! relied upon to be identical) and sent back to the client as part of the34//! response.35//!36//! * `method` is a string that specifies one of the possible [`BrpRequest`]37//! variants: `world.query`, `world.get_components`, `world.insert_components`, etc. It's case-sensitive.38//!39//! * `params` is parameter data specific to the request.40//!41//! For more information, see the documentation for [`BrpRequest`].42//! [`BrpRequest`] is serialized to JSON via `serde`, so [the `serde`43//! documentation] may be useful to clarify the correspondence between the Rust44//! structure and the JSON format.45//!46//! ## Response objects47//!48//! A response from the server to the client might look like this:49//!50//! ```json51//! {52//! "jsonrpc": "2.0",53//! "id": 0,54//! "result": {55//! "bevy_transform::components::transform::Transform": {56//! "rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },57//! "scale": { "x": 1.0, "y": 1.0, "z": 1.0 },58//! "translation": { "x": 0.0, "y": 0.5, "z": 0.0 }59//! }60//! }61//! }62//! ```63//!64//! The `id` field will always be present. The `result` field will be present if the65//! request was successful. Otherwise, an `error` field will replace it.66//!67//! * `id` is the arbitrary JSON data that was sent as part of the request. It68//! will be identical to the `id` data sent during the request, modulo69//! serialization and deserialization. If there's an error reading the `id` field,70//! it will be `null`.71//!72//! * `result` will be present if the request succeeded and will contain the response73//! specific to the request.74//!75//! * `error` will be present if the request failed and will contain an error object76//! with more information about the cause of failure.77//!78//! ## Error objects79//!80//! An error object might look like this:81//!82//! ```json83//! {84//! "code": -32602,85//! "message": "Missing \"entity\" field"86//! }87//! ```88//!89//! The `code` and `message` fields will always be present. There may also be a `data` field.90//!91//! * `code` is an integer representing the kind of an error that happened. Error codes documented92//! in the [`error_codes`] module.93//!94//! * `message` is a short, one-sentence human-readable description of the error.95//!96//! * `data` is an optional field of arbitrary type containing additional information about the error.97//!98//! ## Built-in methods99//!100//! The Bevy Remote Protocol includes a number of built-in methods for accessing and modifying data101//! in the ECS.102//!103//! ### `world.get_components`104//!105//! Retrieve the values of one or more components from an entity.106//!107//! `params`:108//! - `entity`: The ID of the entity whose components will be fetched.109//! - `components`: An array of [fully-qualified type names] of components to fetch.110//! - `strict` (optional): A flag to enable strict mode which will fail if any one of the111//! components is not present or can not be reflected. Defaults to false.112//!113//! If `strict` is false:114//!115//! `result`:116//! - `components`: A map associating each type name to its value on the requested entity.117//! - `errors`: A map associating each type name with an error if it was not on the entity118//! or could not be reflected.119//!120//! If `strict` is true:121//!122//! `result`: A map associating each type name to its value on the requested entity.123//!124//! ### `world.query`125//!126//! Perform a query over components in the ECS, returning all matching entities and their associated127//! component values.128//!129//! All of the arrays that comprise this request are optional, and when they are not provided, they130//! will be treated as if they were empty.131//!132//! `params`:133//! - `data`:134//! - `components` (optional): An array of [fully-qualified type names] of components to fetch,135//! see _below_ example for a query to list all the type names in **your** project.136//! - `option` (optional): An array of fully-qualified type names of components to fetch optionally.137//! to fetch all reflectable components, you can pass in the string `"all"`.138//! - `has` (optional): An array of fully-qualified type names of components whose presence will be139//! reported as boolean values.140//! - `filter` (optional):141//! - `with` (optional): An array of fully-qualified type names of components that must be present142//! on entities in order for them to be included in results.143//! - `without` (optional): An array of fully-qualified type names of components that must *not* be144//! present on entities in order for them to be included in results.145//! - `strict` (optional): A flag to enable strict mode which will fail if any one of the components146//! is not present or can not be reflected. Defaults to false.147//!148//! `result`: An array, each of which is an object containing:149//! - `entity`: The ID of a query-matching entity.150//! - `components`: A map associating each type name from `components`/`option` to its value on the matching151//! entity if the component is present.152//! - `has`: A map associating each type name from `has` to a boolean value indicating whether or not the153//! entity has that component. If `has` was empty or omitted, this key will be omitted in the response.154//!155//! #### Example156//! To use the query API and retrieve Transform data for all entities that have a Transform157//! use this query:158//!159//! ```json160//! {161//! "jsonrpc": "2.0",162//! "method": "world.query",163//! "id": 0,164//! "params": {165//! "data": {166//! "components": ["bevy_transform::components::transform::Transform"]167//! "option": [],168//! "has": []169//! },170//! "filter": {171//! "with": [],172//! "without": []173//! },174//! "strict": false175//! }176//! }177//! ```178//!179//!180//! To query all entities and all of their Reflectable components (and retrieve their values), you can pass in "all" for the option field:181//! ```json182//! {183//! "jsonrpc": "2.0",184//! "method": "world.query",185//! "id": 0,186//! "params": {187//! "data": {188//! "components": []189//! "option": "all",190//! "has": []191//! },192//! "filter": {193//! "with": [],194//! "without": []195//! },196//! "strict": false197//! }198//! }199//! ```200//!201//! This should return you something like the below (in a larger list):202//! ```json203//! {204//! "components": {205//! "bevy_camera::Camera3d": {206//! "depth_load_op": {207//! "Clear": 0.0208//! },209//! "depth_texture_usages": 16,210//! },211//! "bevy_core_pipeline::tonemapping::DebandDither": "Enabled",212//! "bevy_core_pipeline::tonemapping::Tonemapping": "TonyMcMapface",213//! "bevy_light::cluster::ClusterConfig": {214//! "FixedZ": {215//! "dynamic_resizing": true,216//! "total": 4096,217//! "z_config": {218//! "far_z_mode": "MaxClusterableObjectRange",219//! "first_slice_depth": 5.0220//! },221//! "z_slices": 24222//! }223//! },224//! "bevy_camera::Camera": {225//! "clear_color": "Default",226//! "is_active": true,227//! "msaa_writeback": true,228//! "order": 0,229//! "sub_camera_view": null,230//! "target": {231//! "Window": "Primary"232//! },233//! "viewport": null234//! },235//! "bevy_camera::Projection": {236//! "Perspective": {237//! "aspect_ratio": 1.7777777910232544,238//! "far": 1000.0,239//! "fov": 0.7853981852531433,240//! "near": 0.10000000149011612241//! }242//! },243//! "bevy_camera::primitives::Frustum": {},244//! "bevy_render::sync_world::RenderEntity": 4294967291,245//! "bevy_render::sync_world::SyncToRenderWorld": {},246//! "bevy_render::view::Msaa": "Sample4",247//! "bevy_camera::visibility::InheritedVisibility": true,248//! "bevy_camera::visibility::ViewVisibility": false,249//! "bevy_camera::visibility::Visibility": "Inherited",250//! "bevy_camera::visibility::VisibleEntities": {},251//! "bevy_transform::components::global_transform::GlobalTransform": [252//! 0.9635179042816162,253//! -3.725290298461914e-9,254//! 0.26764383912086487,255//! 0.11616238951683044,256//! 0.9009039402008056,257//! -0.4181846082210541,258//! -0.24112138152122495,259//! 0.4340185225009918,260//! 0.8680371046066284,261//! -2.5,262//! 4.5,263//! 9.0264//! ],265//! "bevy_transform::components::transform::Transform": {266//! "rotation": [267//! -0.22055435180664065,268//! -0.13167093694210052,269//! -0.03006339818239212,270//! 0.9659786224365234271//! ],272//! "scale": [273//! 1.0,274//! 1.0,275//! 1.0276//! ],277//! "translation": [278//! -2.5,279//! 4.5,280//! 9.0281//! ]282//! },283//! "bevy_transform::components::transform::TransformTreeChanged": null284//! },285//! "entity": 4294967261286//!},287//! ```288//!289//! ### `world.spawn_entity`290//!291//! Create a new entity with the provided components and return the resulting entity ID.292//!293//! `params`:294//! - `components`: A map associating each component's [fully-qualified type name] with its value.295//!296//! `result`:297//! - `entity`: The ID of the newly spawned entity.298//!299//! ### `world.despawn_entity`300//!301//! Despawn the entity with the given ID.302//!303//! `params`:304//! - `entity`: The ID of the entity to be despawned.305//!306//! `result`: null.307//!308//! ### `world.remove_components`309//!310//! Delete one or more components from an entity.311//!312//! `params`:313//! - `entity`: The ID of the entity whose components should be removed.314//! - `components`: An array of [fully-qualified type names] of components to be removed.315//!316//! `result`: null.317//!318//! ### `world.insert_components`319//!320//! Insert one or more components into an entity.321//!322//! `params`:323//! - `entity`: The ID of the entity to insert components into.324//! - `components`: A map associating each component's fully-qualified type name with its value.325//!326//! `result`: null.327//!328//! ### `world.mutate_components`329//!330//! Mutate a field in a component.331//!332//! `params`:333//! - `entity`: The ID of the entity with the component to mutate.334//! - `component`: The component's [fully-qualified type name].335//! - `path`: The path of the field within the component. See336//! [`GetPath`](bevy_reflect::GetPath#syntax) for more information on formatting this string.337//! - `value`: The value to insert at `path`.338//!339//! `result`: null.340//!341//! ### `world.reparent_entities`342//!343//! Assign a new parent to one or more entities.344//!345//! `params`:346//! - `entities`: An array of entity IDs of entities that will be made children of the `parent`.347//! - `parent` (optional): The entity ID of the parent to which the child entities will be assigned.348//! If excluded, the given entities will be removed from their parents.349//!350//! `result`: null.351//!352//! ### `world.list_components`353//!354//! List all registered components or all components present on an entity.355//!356//! When `params` is not provided, this lists all registered components. If `params` is provided,357//! this lists only those components present on the provided entity.358//!359//! `params` (optional):360//! - `entity`: The ID of the entity whose components will be listed.361//!362//! `result`: An array of fully-qualified type names of components.363//!364//! ### `world.get_components+watch`365//!366//! Watch the values of one or more components from an entity.367//!368//! `params`:369//! - `entity`: The ID of the entity whose components will be fetched.370//! - `components`: An array of [fully-qualified type names] of components to fetch.371//! - `strict` (optional): A flag to enable strict mode which will fail if any one of the372//! components is not present or can not be reflected. Defaults to false.373//!374//! If `strict` is false:375//!376//! `result`:377//! - `components`: A map of components added or changed in the last tick associating each type378//! name to its value on the requested entity.379//! - `removed`: An array of fully-qualified type names of components removed from the entity380//! in the last tick.381//! - `errors`: A map associating each type name with an error if it was not on the entity382//! or could not be reflected.383//!384//! If `strict` is true:385//!386//! `result`:387//! - `components`: A map of components added or changed in the last tick associating each type388//! name to its value on the requested entity.389//! - `removed`: An array of fully-qualified type names of components removed from the entity390//! in the last tick.391//!392//! ### `world.list_components+watch`393//!394//! Watch all components present on an entity.395//!396//! When `params` is not provided, this lists all registered components. If `params` is provided,397//! this lists only those components present on the provided entity.398//!399//! `params`:400//! - `entity`: The ID of the entity whose components will be listed.401//!402//! `result`:403//! - `added`: An array of fully-qualified type names of components added to the entity in the404//! last tick.405//! - `removed`: An array of fully-qualified type names of components removed from the entity406//! in the last tick.407//!408//! ### `world.get_resources`409//!410//! Extract the value of a given resource from the world.411//!412//! `params`:413//! - `resource`: The [fully-qualified type name] of the resource to get.414//!415//! `result`:416//! - `value`: The value of the resource in the world.417//!418//! ### `world.insert_resources`419//!420//! Insert the given resource into the world with the given value.421//!422//! `params`:423//! - `resource`: The [fully-qualified type name] of the resource to insert.424//! - `value`: The value of the resource to be inserted.425//!426//! `result`: null.427//!428//! ### `world.remove_resources`429//!430//! Remove the given resource from the world.431//!432//! `params`433//! - `resource`: The [fully-qualified type name] of the resource to remove.434//!435//! `result`: null.436//!437//! ### `world.mutate_resources`438//!439//! Mutate a field in a resource.440//!441//! `params`:442//! - `resource`: The [fully-qualified type name] of the resource to mutate.443//! - `path`: The path of the field within the resource. See444//! [`GetPath`](bevy_reflect::GetPath#syntax) for more information on formatting this string.445//! - `value`: The value to be inserted at `path`.446//!447//! `result`: null.448//!449//! ### `world.list_resources`450//!451//! List all reflectable registered resource types. This method has no parameters.452//!453//! `result`: An array of [fully-qualified type names] of registered resource types.454//!455//! ### `world.trigger_event`456//!457//! Triggers an event.458//!459//! `params`:460//! - `event`: The [fully-qualified type name] of the event to trigger.461//! - `value`: The value of the event to trigger.462//!463//! `result`: null.464//!465//! ### `registry.schema`466//!467//! Retrieve schema information about registered types in the Bevy app's type registry.468//!469//! `params` (optional):470//! - `with_crates`: An array of crate names to include in the results. When empty or omitted, types from all crates will be included.471//! - `without_crates`: An array of crate names to exclude from the results. When empty or omitted, no crates will be excluded.472//! - `type_limit`: Additional type constraints:473//! - `with`: An array of [fully-qualified type names] that must be present for a type to be included474//! - `without`: An array of [fully-qualified type names] that must not be present for a type to be excluded475//!476//! `result`: A map associating each type's [fully-qualified type name] to a [`JsonSchemaBevyType`](crate::schemas::json_schema::JsonSchemaBevyType).477//! This contains schema information about that type, including field definitions, type information, reflect type information, and other metadata478//! helpful for understanding the structure of the type.479//!480//! ### `rpc.discover`481//!482//! Discover available remote methods and server information. This follows the [`OpenRPC` specification for service discovery](https://spec.open-rpc.org/#service-discovery-method).483//!484//! This method takes no parameters.485//!486//! `result`: An `OpenRPC` document containing:487//! - Information about all available remote methods488//! - Server connection information (when using HTTP transport)489//! - `OpenRPC` specification version490//!491//! ## Custom methods492//!493//! In addition to the provided methods, the Bevy Remote Protocol can be extended to include custom494//! methods. This is primarily done during the initialization of [`RemotePlugin`], although the495//! methods may also be extended at runtime using the [`RemoteMethods`] resource.496//!497//! ### Example498//! ```ignore499//! fn main() {500//! App::new()501//! .add_plugins(DefaultPlugins)502//! .add_plugins(503//! // `default` adds all of the built-in methods, while `with_method` extends them504//! RemotePlugin::default()505//! .with_method("super_user/cool_method", path::to::my::cool::handler)506//! // ... more methods can be added by chaining `with_method`507//! )508//! .add_systems(509//! // ... standard application setup510//! )511//! .run();512//! }513//! ```514//!515//! The handler is expected to be a system-convertible function which takes optional JSON parameters516//! as input and returns a [`BrpResult`]. This means that it should have a type signature which looks517//! something like this:518//! ```519//! # use serde_json::Value;520//! # use bevy_ecs::prelude::{In, World};521//! # use bevy_remote::BrpResult;522//! fn handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {523//! todo!()524//! }525//! ```526//!527//! Arbitrary system parameters can be used in conjunction with the optional `Value` input. The528//! handler system will always run with exclusive `World` access.529//!530//! [the `serde` documentation]: https://serde.rs/531//! [fully-qualified type names]: bevy_reflect::TypePath::type_path532//! [fully-qualified type name]: bevy_reflect::TypePath::type_path533534extern crate alloc;535536use async_channel::{Receiver, Sender};537use bevy_app::{prelude::*, MainScheduleOrder};538use bevy_derive::{Deref, DerefMut};539use bevy_ecs::{540entity::Entity,541resource::Resource,542schedule::{IntoScheduleConfigs, ScheduleLabel, SystemSet},543system::{Commands, In, IntoSystem, ResMut, System, SystemId},544world::World,545};546use bevy_platform::collections::HashMap;547use bevy_utils::prelude::default;548use serde::{Deserialize, Serialize};549use serde_json::Value;550use std::sync::RwLock;551552pub mod builtin_methods;553#[cfg(feature = "http")]554pub mod http;555pub mod schemas;556557const CHANNEL_SIZE: usize = 16;558559/// Add this plugin to your [`App`] to allow remote connections to inspect and modify entities.560///561/// This the main plugin for `bevy_remote`. See the [crate-level documentation] for details on562/// the available protocols and its default methods.563///564/// [crate-level documentation]: crate565pub struct RemotePlugin {566/// The verbs that the server will recognize and respond to.567methods: RwLock<Vec<(String, RemoteMethodHandler)>>,568}569570impl RemotePlugin {571/// Create a [`RemotePlugin`] with the default address and port but without572/// any associated methods.573fn empty() -> Self {574Self {575methods: RwLock::new(vec![]),576}577}578579/// Add a remote method to the plugin using the given `name` and `handler`.580#[must_use]581pub fn with_method<M>(582mut self,583name: impl Into<String>,584handler: impl IntoSystem<In<Option<Value>>, BrpResult, M>,585) -> Self {586self.methods.get_mut().unwrap().push((587name.into(),588RemoteMethodHandler::Instant(Box::new(IntoSystem::into_system(handler))),589));590self591}592593/// Add a remote method with a watching handler to the plugin using the given `name`.594#[must_use]595pub fn with_watching_method<M>(596mut self,597name: impl Into<String>,598handler: impl IntoSystem<In<Option<Value>>, BrpResult<Option<Value>>, M>,599) -> Self {600self.methods.get_mut().unwrap().push((601name.into(),602RemoteMethodHandler::Watching(Box::new(IntoSystem::into_system(handler))),603));604self605}606}607608impl Default for RemotePlugin {609fn default() -> Self {610Self::empty()611.with_method(612builtin_methods::BRP_GET_COMPONENTS_METHOD,613builtin_methods::process_remote_get_components_request,614)615.with_method(616builtin_methods::BRP_QUERY_METHOD,617builtin_methods::process_remote_query_request,618)619.with_method(620builtin_methods::BRP_SPAWN_ENTITY_METHOD,621builtin_methods::process_remote_spawn_entity_request,622)623.with_method(624builtin_methods::BRP_INSERT_COMPONENTS_METHOD,625builtin_methods::process_remote_insert_components_request,626)627.with_method(628builtin_methods::BRP_REMOVE_COMPONENTS_METHOD,629builtin_methods::process_remote_remove_components_request,630)631.with_method(632builtin_methods::BRP_DESPAWN_COMPONENTS_METHOD,633builtin_methods::process_remote_despawn_entity_request,634)635.with_method(636builtin_methods::BRP_REPARENT_ENTITIES_METHOD,637builtin_methods::process_remote_reparent_entities_request,638)639.with_method(640builtin_methods::BRP_LIST_COMPONENTS_METHOD,641builtin_methods::process_remote_list_components_request,642)643.with_method(644builtin_methods::BRP_MUTATE_COMPONENTS_METHOD,645builtin_methods::process_remote_mutate_components_request,646)647.with_method(648builtin_methods::RPC_DISCOVER_METHOD,649builtin_methods::process_remote_list_methods_request,650)651.with_watching_method(652builtin_methods::BRP_GET_COMPONENTS_AND_WATCH_METHOD,653builtin_methods::process_remote_get_components_watching_request,654)655.with_watching_method(656builtin_methods::BRP_LIST_COMPONENTS_AND_WATCH_METHOD,657builtin_methods::process_remote_list_components_watching_request,658)659.with_method(660builtin_methods::BRP_GET_RESOURCE_METHOD,661builtin_methods::process_remote_get_resources_request,662)663.with_method(664builtin_methods::BRP_INSERT_RESOURCE_METHOD,665builtin_methods::process_remote_insert_resources_request,666)667.with_method(668builtin_methods::BRP_REMOVE_RESOURCE_METHOD,669builtin_methods::process_remote_remove_resources_request,670)671.with_method(672builtin_methods::BRP_MUTATE_RESOURCE_METHOD,673builtin_methods::process_remote_mutate_resources_request,674)675.with_method(676builtin_methods::BRP_LIST_RESOURCES_METHOD,677builtin_methods::process_remote_list_resources_request,678)679.with_method(680builtin_methods::BRP_TRIGGER_EVENT_METHOD,681builtin_methods::process_remote_trigger_event_request,682)683.with_method(684builtin_methods::BRP_REGISTRY_SCHEMA_METHOD,685builtin_methods::export_registry_types,686)687}688}689690impl Plugin for RemotePlugin {691fn build(&self, app: &mut App) {692let mut remote_methods = RemoteMethods::new();693694let plugin_methods = &mut *self.methods.write().unwrap();695for (name, handler) in plugin_methods.drain(..) {696remote_methods.insert(697name,698match handler {699RemoteMethodHandler::Instant(system) => RemoteMethodSystemId::Instant(700app.main_mut().world_mut().register_boxed_system(system),701),702RemoteMethodHandler::Watching(system) => RemoteMethodSystemId::Watching(703app.main_mut().world_mut().register_boxed_system(system),704),705},706);707}708709app.init_schedule(RemoteLast)710.world_mut()711.resource_mut::<MainScheduleOrder>()712.insert_after(Last, RemoteLast);713714app.insert_resource(remote_methods)715.init_resource::<schemas::SchemaTypesMetadata>()716.init_resource::<RemoteWatchingRequests>()717.add_systems(PreStartup, setup_mailbox_channel)718.configure_sets(719RemoteLast,720(RemoteSystems::ProcessRequests, RemoteSystems::Cleanup).chain(),721)722.add_systems(723RemoteLast,724(725(process_remote_requests, process_ongoing_watching_requests)726.chain()727.in_set(RemoteSystems::ProcessRequests),728remove_closed_watching_requests.in_set(RemoteSystems::Cleanup),729),730);731}732}733734/// Schedule that contains all systems to process Bevy Remote Protocol requests735#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]736pub struct RemoteLast;737738/// The systems sets of the [`RemoteLast`] schedule.739///740/// These can be useful for ordering.741#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]742pub enum RemoteSystems {743/// Processing of remote requests.744ProcessRequests,745/// Cleanup (remove closed watchers etc)746Cleanup,747}748749/// A type to hold the allowed types of systems to be used as method handlers.750#[derive(Debug)]751pub enum RemoteMethodHandler {752/// A handler that only runs once and returns one response.753Instant(Box<dyn System<In = In<Option<Value>>, Out = BrpResult>>),754/// A handler that watches for changes and response when a change is detected.755Watching(Box<dyn System<In = In<Option<Value>>, Out = BrpResult<Option<Value>>>>),756}757758/// The [`SystemId`] of a function that implements a remote instant method (`world.get_components`, `world.query`, etc.)759///760/// The first parameter is the JSON value of the `params`. Typically, an761/// implementation will deserialize these as the first thing they do.762///763/// The returned JSON value will be returned as the response. Bevy will764/// automatically populate the `id` field before sending.765pub type RemoteInstantMethodSystemId = SystemId<In<Option<Value>>, BrpResult>;766767/// The [`SystemId`] of a function that implements a remote watching method (`world.get_components+watch`, `world.list_components+watch`, etc.)768///769/// The first parameter is the JSON value of the `params`. Typically, an770/// implementation will deserialize these as the first thing they do.771///772/// The optional returned JSON value will be sent as a response. If no773/// changes were detected this should be [`None`]. Re-running of this774/// handler is done in the [`RemotePlugin`].775pub type RemoteWatchingMethodSystemId = SystemId<In<Option<Value>>, BrpResult<Option<Value>>>;776777/// The [`SystemId`] of a function that can be used as a remote method.778#[derive(Debug, Clone, Copy)]779pub enum RemoteMethodSystemId {780/// A handler that only runs once and returns one response.781Instant(RemoteInstantMethodSystemId),782/// A handler that watches for changes and response when a change is detected.783Watching(RemoteWatchingMethodSystemId),784}785786/// Holds all implementations of methods known to the server.787///788/// Custom methods can be added to this list using [`RemoteMethods::insert`].789#[derive(Debug, Resource, Default)]790pub struct RemoteMethods(HashMap<String, RemoteMethodSystemId>);791792impl RemoteMethods {793/// Creates a new [`RemoteMethods`] resource with no methods registered in it.794pub fn new() -> Self {795default()796}797798/// Adds a new method, replacing any existing method with that name.799///800/// If there was an existing method with that name, returns its handler.801pub fn insert(802&mut self,803method_name: impl Into<String>,804handler: RemoteMethodSystemId,805) -> Option<RemoteMethodSystemId> {806self.0.insert(method_name.into(), handler)807}808809/// Get a [`RemoteMethodSystemId`] with its method name.810pub fn get(&self, method: &str) -> Option<&RemoteMethodSystemId> {811self.0.get(method)812}813814/// Get a [`Vec<String>`] with method names.815pub fn methods(&self) -> Vec<String> {816self.0.keys().cloned().collect()817}818}819820/// Holds the [`BrpMessage`]'s of all ongoing watching requests along with their handlers.821#[derive(Debug, Resource, Default)]822pub struct RemoteWatchingRequests(Vec<(BrpMessage, RemoteWatchingMethodSystemId)>);823824/// A single request from a Bevy Remote Protocol client to the server,825/// serialized in JSON.826///827/// The JSON payload is expected to look like this:828///829/// ```json830/// {831/// "jsonrpc": "2.0",832/// "method": "world.get_components",833/// "id": 0,834/// "params": {835/// "entity": 4294967298,836/// "components": [837/// "bevy_transform::components::transform::Transform"838/// ]839/// }840/// }841/// ```842/// Or, to list all the fully-qualified type paths in **your** project, pass Null to the843/// `params`.844/// ```json845/// {846/// "jsonrpc": "2.0",847/// "method": "world.list_components",848/// "id": 0,849/// "params": null850///}851///```852///853/// In Rust:854/// ```ignore855/// let req = BrpRequest {856/// jsonrpc: "2.0".to_string(),857/// method: BRP_LIST_METHOD.to_string(), // All the methods have consts858/// id: Some(ureq::json!(0)),859/// params: None,860/// };861/// ```862#[derive(Debug, Serialize, Deserialize, Clone)]863pub struct BrpRequest {864/// This field is mandatory and must be set to `"2.0"` for the request to be accepted.865pub jsonrpc: String,866867/// The action to be performed.868pub method: String,869870/// Arbitrary data that will be returned verbatim to the client as part of871/// the response.872#[serde(skip_serializing_if = "Option::is_none")]873pub id: Option<Value>,874875/// The parameters, specific to each method.876///877/// These are passed as the first argument to the method handler.878/// Sometimes params can be omitted.879#[serde(skip_serializing_if = "Option::is_none")]880pub params: Option<Value>,881}882883/// A response according to BRP.884#[derive(Debug, Serialize, Deserialize, Clone)]885pub struct BrpResponse {886/// This field is mandatory and must be set to `"2.0"`.887pub jsonrpc: &'static str,888889/// The id of the original request.890pub id: Option<Value>,891892/// The actual response payload.893#[serde(flatten)]894pub payload: BrpPayload,895}896897impl BrpResponse {898/// Generates a [`BrpResponse`] from an id and a `Result`.899#[must_use]900pub fn new(id: Option<Value>, result: BrpResult) -> Self {901Self {902jsonrpc: "2.0",903id,904payload: BrpPayload::from(result),905}906}907}908909/// A result/error payload present in every response.910#[derive(Debug, Serialize, Deserialize, Clone)]911#[serde(rename_all = "snake_case")]912pub enum BrpPayload {913/// `Ok` variant914Result(Value),915/// `Err` variant916Error(BrpError),917}918919impl From<BrpResult> for BrpPayload {920fn from(value: BrpResult) -> Self {921match value {922Ok(v) => Self::Result(v),923Err(err) => Self::Error(err),924}925}926}927928/// An error a request might return.929#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]930pub struct BrpError {931/// Defines the general type of the error.932pub code: i16,933/// Short, human-readable description of the error.934pub message: String,935/// Optional additional error data.936#[serde(skip_serializing_if = "Option::is_none")]937pub data: Option<Value>,938}939940impl BrpError {941/// Entity wasn't found.942#[must_use]943pub fn entity_not_found(entity: Entity) -> Self {944Self {945code: error_codes::ENTITY_NOT_FOUND,946message: format!("Entity {entity} not found"),947data: None,948}949}950951/// Component wasn't found in an entity.952#[must_use]953pub fn component_not_present(component: &str, entity: Entity) -> Self {954Self {955code: error_codes::COMPONENT_NOT_PRESENT,956message: format!("Component `{component}` not present in Entity {entity}"),957data: None,958}959}960961/// An arbitrary component error. Possibly related to reflection.962#[must_use]963pub fn component_error<E: ToString>(error: E) -> Self {964Self {965code: error_codes::COMPONENT_ERROR,966message: error.to_string(),967data: None,968}969}970971/// Resource was not present in the world.972#[must_use]973pub fn resource_not_present(resource: &str) -> Self {974Self {975code: error_codes::RESOURCE_NOT_PRESENT,976message: format!("Resource `{resource}` not present in the world"),977data: None,978}979}980981/// An arbitrary resource error. Possibly related to reflection.982#[must_use]983pub fn resource_error<E: ToString>(error: E) -> Self {984Self {985code: error_codes::RESOURCE_ERROR,986message: error.to_string(),987data: None,988}989}990991/// An arbitrary internal error.992#[must_use]993pub fn internal<E: ToString>(error: E) -> Self {994Self {995code: error_codes::INTERNAL_ERROR,996message: error.to_string(),997data: None,998}999}10001001/// Attempt to reparent an entity to itself.1002#[must_use]1003pub fn self_reparent(entity: Entity) -> Self {1004Self {1005code: error_codes::SELF_REPARENT,1006message: format!("Cannot reparent Entity {entity} to itself"),1007data: None,1008}1009}1010}10111012/// Error codes used by BRP.1013pub mod error_codes {1014// JSON-RPC errors1015// Note that the range -32728 to -32000 (inclusive) is reserved by the JSON-RPC specification.10161017/// Invalid JSON.1018pub const PARSE_ERROR: i16 = -32700;10191020/// JSON sent is not a valid request object.1021pub const INVALID_REQUEST: i16 = -32600;10221023/// The method does not exist / is not available.1024pub const METHOD_NOT_FOUND: i16 = -32601;10251026/// Invalid method parameter(s).1027pub const INVALID_PARAMS: i16 = -32602;10281029/// Internal error.1030pub const INTERNAL_ERROR: i16 = -32603;10311032// Bevy errors (i.e. application errors)10331034/// Entity not found.1035pub const ENTITY_NOT_FOUND: i16 = -23401;10361037/// Could not reflect or find component.1038pub const COMPONENT_ERROR: i16 = -23402;10391040/// Could not find component in entity.1041pub const COMPONENT_NOT_PRESENT: i16 = -23403;10421043/// Cannot reparent an entity to itself.1044pub const SELF_REPARENT: i16 = -23404;10451046/// Could not reflect or find resource.1047pub const RESOURCE_ERROR: i16 = -23501;10481049/// Could not find resource in the world.1050pub const RESOURCE_NOT_PRESENT: i16 = -23502;1051}10521053/// The result of a request.1054pub type BrpResult<T = Value> = Result<T, BrpError>;10551056/// The requests may occur on their own or in batches.1057/// Actual parsing is deferred for the sake of proper1058/// error reporting.1059#[derive(Debug, Clone, Serialize, Deserialize)]1060#[serde(untagged)]1061pub enum BrpBatch {1062/// Multiple requests with deferred parsing.1063Batch(Vec<Value>),1064/// A single request with deferred parsing.1065Single(Value),1066}10671068/// A message from the Bevy Remote Protocol server thread to the main world.1069///1070/// This is placed in the [`BrpReceiver`].1071#[derive(Debug, Clone)]1072pub struct BrpMessage {1073/// The request method.1074pub method: String,10751076/// The request params.1077pub params: Option<Value>,10781079/// The channel on which the response is to be sent.1080///1081/// The value sent here is serialized and sent back to the client.1082pub sender: Sender<BrpResult>,1083}10841085/// A resource holding the matching sender for the [`BrpReceiver`]'s receiver.1086#[derive(Debug, Resource, Deref, DerefMut)]1087pub struct BrpSender(Sender<BrpMessage>);10881089/// A resource that receives messages sent by Bevy Remote Protocol clients.1090///1091/// Every frame, the `process_remote_requests` system drains this mailbox and1092/// processes the messages within.1093#[derive(Debug, Resource, Deref, DerefMut)]1094pub struct BrpReceiver(Receiver<BrpMessage>);10951096fn setup_mailbox_channel(mut commands: Commands) {1097// Create the channel and the mailbox.1098let (request_sender, request_receiver) = async_channel::bounded(CHANNEL_SIZE);1099commands.insert_resource(BrpSender(request_sender));1100commands.insert_resource(BrpReceiver(request_receiver));1101}11021103/// A system that receives requests placed in the [`BrpReceiver`] and processes1104/// them, using the [`RemoteMethods`] resource to map each request to its handler.1105///1106/// This needs exclusive access to the [`World`] because clients can manipulate1107/// anything in the ECS.1108fn process_remote_requests(world: &mut World) {1109if !world.contains_resource::<BrpReceiver>() {1110return;1111}11121113while let Ok(message) = world.resource_mut::<BrpReceiver>().try_recv() {1114// Fetch the handler for the method. If there's no such handler1115// registered, return an error.1116let Some(&handler) = world.resource::<RemoteMethods>().get(&message.method) else {1117let _ = message.sender.force_send(Err(BrpError {1118code: error_codes::METHOD_NOT_FOUND,1119message: format!("Method `{}` not found", message.method),1120data: None,1121}));1122return;1123};11241125match handler {1126RemoteMethodSystemId::Instant(id) => {1127let result = match world.run_system_with(id, message.params) {1128Ok(result) => result,1129Err(error) => {1130let _ = message.sender.force_send(Err(BrpError {1131code: error_codes::INTERNAL_ERROR,1132message: format!("Failed to run method handler: {error}"),1133data: None,1134}));1135continue;1136}1137};11381139let _ = message.sender.force_send(result);1140}1141RemoteMethodSystemId::Watching(id) => {1142world1143.resource_mut::<RemoteWatchingRequests>()1144.01145.push((message, id));1146}1147}1148}1149}11501151/// A system that checks all ongoing watching requests for changes that should be sent1152/// and handles it if so.1153fn process_ongoing_watching_requests(world: &mut World) {1154world.resource_scope::<RemoteWatchingRequests, ()>(|world, requests| {1155for (message, system_id) in requests.0.iter() {1156let handler_result = process_single_ongoing_watching_request(world, message, system_id);1157let sender_result = match handler_result {1158Ok(Some(value)) => message.sender.try_send(Ok(value)),1159Err(err) => message.sender.try_send(Err(err)),1160Ok(None) => continue,1161};11621163if sender_result.is_err() {1164// The [`remove_closed_watching_requests`] system will clean this up.1165message.sender.close();1166}1167}1168});1169}11701171fn process_single_ongoing_watching_request(1172world: &mut World,1173message: &BrpMessage,1174system_id: &RemoteWatchingMethodSystemId,1175) -> BrpResult<Option<Value>> {1176world1177.run_system_with(*system_id, message.params.clone())1178.map_err(|error| BrpError {1179code: error_codes::INTERNAL_ERROR,1180message: format!("Failed to run method handler: {error}"),1181data: None,1182})?1183}11841185fn remove_closed_watching_requests(mut requests: ResMut<RemoteWatchingRequests>) {1186for i in (0..requests.0.len()).rev() {1187let Some((message, _)) = requests.0.get(i) else {1188unreachable!()1189};11901191if message.sender.is_closed() {1192requests.0.swap_remove(i);1193}1194}1195}119611971198