Commit Graph

7 Commits

Author SHA1 Message Date
Joe Buehler
d16d216083
Add support for returning all Component and values to query method in the Bevy Remote Protocol (#19857)
# Objective

We should have an API with filtering to allow BRP clients to retrieve
all relevant data from the world state. Currently working on adding
examples - but reviews are appreciated! Still semi-WIP while I get my
head around bevy’s reflection and implementation :)

## Solution

This change adds support to query all entities in the world, and returns
all of their Reflected Components with corresponding values. For custom
`Components` it's important to still implement `Reflect` so that this
endpoint returns these. This will be useful for the
`bevy_entity_inspector` so that we can easily get the current world
state. We have modified the existing query API
so that clients can now pass in an empty `components[]` on the JSON
request.

## Testing

Updated example to showcase how to use the new endpoint to get all data:
```rust
/// Create a query_all request to send to the remote Bevy app.
/// This request will return all entities in the app, their components, and their
/// component values.
fn run_query_all_components_and_entities(url: String) -> Result<(), anyhow::Error> {
    let query_all_req = BrpRequest {
        jsonrpc: String::from("2.0"),
        method: String::from(BRP_QUERY_METHOD),
        id: Some(serde_json::to_value(1)?),
        params: None,
    };
    println!("query_all req: {:#?}", query_all_req);
    let query_all_res = ureq::post(&url)
        .send_json(query_all_req)?
        .body_mut()
        .read_json::<serde_json::Value>()?;
    println!("{query_all_res:#}");
    Ok(())
}
```

---

## Showcase
In the `client.rs` example, we can clearly see (assuming the `server.rs`
is running) a query hit for all entities and components:
```text
query_all req: BrpRequest {
    jsonrpc: "2.0",
    method: "bevy/query",
    id: Some(
        Number(1),
    ),
    params: Some(
        Object {
            "data": Object {
                "components": Array [],
                "has": Array [],
                "option": Array [],
            },
            "filter": Object {
                "with": Array [],
                "without": Array [],
            },
            "strict": Bool(false),
        },
    ),
}
```

And in the massive response:
```text
.....
{
      "components": {
        "bevy_window::monitor::Monitor": {
          "name": "\\\\.\\DISPLAY1",
          "physical_height": 1080,
          "physical_position": [
            -1920,
            0
          ],
          "physical_width": 1920,
          "refresh_rate_millihertz": 240000,
          "scale_factor": 1.25,
          "video_modes": [
            {
              "bit_depth": 32,
              "physical_size": [
                1920,
                1080
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1680,
                1050
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1600,
                900
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1440,
                900
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1400,
                1050
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1366,
                768
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1360,
                768
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1280,
                1024
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1280,
                960
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1280,
                800
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1280,
                768
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1280,
                720
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1280,
                600
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1152,
                864
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                1024,
                768
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                800,
                600
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                640,
                480
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                640,
                400
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                512,
                384
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                400,
                300
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                320,
                240
              ],
              "refresh_rate_millihertz": 240000
            },
            {
              "bit_depth": 32,
              "physical_size": [
                320,
                200
              ],
              "refresh_rate_millihertz": 240000
            }
          ]
        }
      },
      "entity": 4294967267
    },
....
```

What's also really cool about this and `bevy_reflect` is that we also
get custom components returned as well (see below for `"server::Cube":
1.0` as the custom reflected struct specified in `server.rs`:

```text
{
      "components": {
        "bevy_render::primitives::Aabb": {
          "center": [
            0.0,
            0.0,
            0.0
          ],
          "half_extents": [
            0.5,
            0.5,
            0.5
          ]
        },
        "bevy_render::view::visibility::InheritedVisibility": true,
        "bevy_render::view::visibility::ViewVisibility": true,
        "bevy_render::view::visibility::Visibility": "Inherited",
        "bevy_transform::components::global_transform::GlobalTransform": [
          1.0,
          0.0,
          0.0,
          0.0,
          1.0,
          0.0,
          0.0,
          0.0,
          1.0,
          0.0,
          2.4572744369506836,
          0.0
        ],
        "bevy_transform::components::transform::Transform": {
          "rotation": [
            0.0,
            0.0,
            0.0,
            1.0
          ],
          "scale": [
            1.0,
            1.0,
            1.0
          ],
          "translation": [
            0.0,
            2.4572744369506836,
            0.0
          ]
        },
        "bevy_transform::components::transform::TransformTreeChanged": null,
        "server::Cube": 1.0
      },
```

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
2025-07-03 18:51:32 +00:00
charlotte 🌸
92e65d5eb1
Upgrade to Rust 1.88 (#19825) 2025-06-26 19:38:19 +00:00
Martín Maita
f3db44b635
Update ureq requirement from 2.10.1 to 3.0.8 (#18146)
# Objective

- Closes #18131

## Solution

- Update ureq requirement from 2.10.1 to 3.0.8 and migrate breaking
code.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-04 01:11:28 +00:00
MevLyshkin
897ffad8af
BRP strict field in query (#16725)
# Objective

- Allow skiping components that don't have ComponentId yet instead of
failing `bevy/query` request.

## Solution

- Describe the solution used to achieve the objective above.

## Testing

My naive approach boils down to:
- bevy/list to get list of all components.
- bevy/query with empty components and has fields and a option that
contains result of the bevy/list.

Before that change I end up with bunch of `Component xxx isn't used in
the world` because some of the components wasn't spawned at any moment
yet in the game. Now it should work.

## Migration Guide

- `BrpQueryParams` now has `strict` boolean field. It serfs as a flag to
fail when encountering an invalid component rather than skipping it.
Defaults to false.
2024-12-14 05:22:19 +00:00
Liam Gallagher
60cf7ca025
Refactor BRP to allow for 3rd-party transports (#15438)
## Objective

Closes #15408 (somewhat)

## Solution

- Moved the existing HTTP transport to its own module with its own
plugin (`RemoteHttpPlugin`) (disabled on WASM)
- Swapped out the `smol` crate for the smaller crates it re-exports to
make it easier to keep out non-wasm code (HTTP transport needs
`async-io` which can't build on WASM)
- Added a new public `BrpSender` resource holding the matching sender
for the `BrpReceiver`' (formally `BrpMailbox`). This allows other crates
to send `BrpMessage`'s to the "mailbox".

## Testing

TODO

---------

Co-authored-by: Matty <weatherleymatthew@gmail.com>
2024-09-27 20:09:46 +00:00
Clar Fon
efda7f3f9c
Simpler lint fixes: makes ci lints work but disables a lint for now (#15376)
Takes the first two commits from #15375 and adds suggestions from this
comment:
https://github.com/bevyengine/bevy/pull/15375#issuecomment-2366968300

See #15375 for more reasoning/motivation.

## Rebasing (rerunning)

```rust
git switch simpler-lint-fixes
git reset --hard main
cargo fmt --all -- --unstable-features --config normalize_comments=true,imports_granularity=Crate
cargo fmt --all
git add --update
git commit --message "rustfmt"
cargo clippy --workspace --all-targets --all-features --fix
cargo fmt --all -- --unstable-features --config normalize_comments=true,imports_granularity=Crate
cargo fmt --all
git add --update
git commit --message "clippy"
git cherry-pick e6c0b94f6795222310fb812fa5c4512661fc7887
```
2024-09-24 11:42:59 +00:00
Matty
89e98b208f
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>
2024-09-23 18:36:16 +00:00