From 26ea38e4a671a6507be6c2a188009c5432c7374b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 12 Mar 2025 17:11:02 +0000 Subject: [PATCH 01/74] Remove the entity index from the UI phase's sort key (#18273) # Objective The sort key for the transparent UI phase is a (float32, u32) pair consisting of the stack index and the render entity's index. I guess the render entity index was intended to break ties but it's not needed as the sort is stable. It also assumes the indices of the render entities are generated sequentially, which isn't guaranteed. Fixes the issues with the text wrap example seen in #18266 ## Solution Change the sort key to just use the stack index alone. --- crates/bevy_ui/src/render/box_shadow.rs | 6 ++---- crates/bevy_ui/src/render/mod.rs | 5 +---- crates/bevy_ui/src/render/render_pass.rs | 4 ++-- crates/bevy_ui/src/render/ui_material_pipeline.rs | 5 +---- crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs | 5 ++--- 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 50e602149c..7ed9855038 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -365,10 +365,8 @@ pub fn queue_shadows( draw_function, pipeline, entity: (entity, extracted_shadow.main_entity), - sort_key: ( - FloatOrd(extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW), - entity.index(), - ), + sort_key: FloatOrd(extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW), + batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, index, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 3c0e353f03..b5d24f194d 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -981,10 +981,7 @@ pub fn queue_uinodes( draw_function, pipeline, entity: (entity, extracted_uinode.main_entity), - sort_key: ( - FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::NODE), - entity.index(), - ), + sort_key: FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::NODE), index, // batch_range will be calculated in prepare_uinodes batch_range: 0..0, diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index f89133e31e..e0b3b20fab 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -106,7 +106,7 @@ impl Node for UiPassNode { } pub struct TransparentUi { - pub sort_key: (FloatOrd, u32), + pub sort_key: FloatOrd, pub entity: (Entity, MainEntity), pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, @@ -153,7 +153,7 @@ impl PhaseItem for TransparentUi { } impl SortedPhaseItem for TransparentUi { - type SortKey = (FloatOrd, u32); + type SortKey = FloatOrd; #[inline] fn sort_key(&self) -> Self::SortKey { diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 2162b92373..fb893b390e 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -665,10 +665,7 @@ pub fn queue_ui_material_nodes( draw_function, pipeline, entity: (extracted_uinode.render_entity, extracted_uinode.main_entity), - sort_key: ( - FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::MATERIAL), - extracted_uinode.render_entity.index(), - ), + sort_key: FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::MATERIAL), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, index, diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 68c6a4a3ef..dee19ad867 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -372,9 +372,8 @@ pub fn queue_ui_slices( draw_function, pipeline, entity: (extracted_slicer.render_entity, extracted_slicer.main_entity), - sort_key: ( - FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::TEXTURE_SLICE), - extracted_slicer.render_entity.index(), + sort_key: FloatOrd( + extracted_slicer.stack_index as f32 + stack_z_offsets::TEXTURE_SLICE, ), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, From 5d80ac3ded1e5f1544f15b97a47dd7386e25fa11 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Wed, 12 Mar 2025 18:57:20 +0100 Subject: [PATCH 02/74] Add derive Default to Disabled (#18275) # Objective - `#[require(Disabled)]` doesn't work as you'd expect ## Solution - `#[derive(Default)]` --- crates/bevy_ecs/src/entity_disabling.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index d4b1ad266c..a7da08af57 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -92,7 +92,7 @@ use {crate::reflect::ReflectComponent, bevy_reflect::Reflect}; /// See [the module docs] for more info. /// /// [the module docs]: crate::entity_disabling -#[derive(Component, Clone, Debug)] +#[derive(Component, Clone, Debug, Default)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), From 81c0900cf23218732c013656669dfdeece2937a8 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Wed, 12 Mar 2025 11:03:45 -0700 Subject: [PATCH 03/74] Upgrade to cosmic-text 0.13 (#18239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Upgrade to `cosmic-text` 0.13 https://github.com/pop-os/cosmic-text/releases This should include some performance improvements for layout and system font loading. ## Solution Bump version, fix the one changed API. ## Testing Tested some examples locally, will invoke the example runner. ## Layout Perf ||main fps|cosmic-13 fps| |-|-|-| |many_buttons --recompute-text --no-borders|6.79|9.42 🟩 +38.7%| |many_text2d --no-frustum-culling --recompute|3.19|4.28 🟩 +34.0%| |many_glyphs --recompute-text|7.09|11.17 🟩 +57.6%| |text_pipeline |140.15|139.90 ⬜ -0.2%| ## System Font Loading Perf I tested on macOS somewhat lazily by adding the following system to the `system_fonts` example from #16365.
Expand code ```rust fn exit_on_load( mut reader: EventReader, mut writer: EventWriter, ) { for _evt in reader.read() { writer.write(AppExit::Success); } } ```
And running `hyperfine 'cargo run --release --example system_fonts --features=system_font'`. The results were nearly identical with and without this PR cherry-picked there. Co-authored-by: Alice Cecile --- crates/bevy_text/Cargo.toml | 2 +- crates/bevy_text/src/pipeline.rs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 1e971cfe09..bc939f2daf 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -33,7 +33,7 @@ bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-d ] } # other -cosmic-text = { version = "0.12", features = ["shape-run-cache"] } +cosmic-text = { version = "0.13", features = ["shape-run-cache"] } thiserror = { version = "2", default-features = false } serde = { version = "1", features = ["derive"] } smallvec = "1.13" diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 2b2e013059..4ce2e1bd96 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -185,13 +185,14 @@ impl TextPipeline { }, ); - buffer.set_rich_text(font_system, spans_iter, Attrs::new(), Shaping::Advanced); + buffer.set_rich_text( + font_system, + spans_iter, + Attrs::new(), + Shaping::Advanced, + Some(justify.into()), + ); - // PERF: https://github.com/pop-os/cosmic-text/issues/166: - // Setting alignment afterwards appears to invalidate some layouting performed by `set_text` which is presumably not free? - for buffer_line in buffer.lines.iter_mut() { - buffer_line.set_align(Some(justify.into())); - } buffer.shape_until_scroll(font_system, false); // Workaround for alignment not working for unbounded text. From dc7cb2a3e17be7ff576dad65cf1a1e05722c59e7 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Thu, 13 Mar 2025 07:15:39 +1100 Subject: [PATCH 04/74] Switch to `ImDocument` in `BevyManifest` (#18272) # Objective When reviewing #18263, I noticed that `BevyManifest` internally stores a `DocumentMut`, a mutable TOML document, instead of an `ImDocument`, an immutable one. The process of creating a `DocumentMut` first involves creating a `ImDocument` and then cloning all the referenced spans of text into their own allocations (internally referred to as `despan` in `toml_edit`). As such, using a `DocumentMut` without mutation is strictly additional overhead. In addition, I noticed that the filesystem operations associated with reading a manifest and parsing it were written to be completed _while_ a write-lock was held on `MANIFESTS`. This likely doesn't translate into a performance or deadlock issue as the manifest files are generally small and can be read quickly, but it is generally considered a bad practice. ## Solution - Switched to `ImDocument>` instead of `DocumentMut` - Re-ordered operations in `BevyManifest::shared` to minimise time spent holding open the write-lock on `MANIFESTS` ## Testing - CI --- ## Notes I wasn't able to measure a meaningful performance difference with this PR, so this is purely a code quality change and not one for performance. --------- Co-authored-by: Carter Anderson --- crates/bevy_macro_utils/src/bevy_manifest.rs | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/bevy_macro_utils/src/bevy_manifest.rs b/crates/bevy_macro_utils/src/bevy_manifest.rs index 813f8533c9..b0d321ba22 100644 --- a/crates/bevy_macro_utils/src/bevy_manifest.rs +++ b/crates/bevy_macro_utils/src/bevy_manifest.rs @@ -8,12 +8,12 @@ use std::{ path::{Path, PathBuf}, time::SystemTime, }; -use toml_edit::{DocumentMut, Item}; +use toml_edit::{ImDocument, Item}; /// The path to the `Cargo.toml` file for the Bevy project. #[derive(Debug)] pub struct BevyManifest { - manifest: DocumentMut, + manifest: ImDocument>, modified_time: SystemTime, } @@ -34,14 +34,15 @@ impl BevyManifest { return manifest; } } + + let manifest = BevyManifest { + manifest: Self::read_manifest(&manifest_path), + modified_time, + }; + + let key = manifest_path.clone(); let mut manifests = MANIFESTS.write(); - manifests.insert( - manifest_path.clone(), - BevyManifest { - manifest: Self::read_manifest(&manifest_path), - modified_time, - }, - ); + manifests.insert(key, manifest); RwLockReadGuard::map(RwLockWriteGuard::downgrade(manifests), |manifests| { manifests.get(&manifest_path).unwrap() @@ -69,11 +70,11 @@ impl BevyManifest { std::fs::metadata(cargo_manifest_path).and_then(|metadata| metadata.modified()) } - fn read_manifest(path: &Path) -> DocumentMut { + fn read_manifest(path: &Path) -> ImDocument> { let manifest = std::fs::read_to_string(path) - .unwrap_or_else(|_| panic!("Unable to read cargo manifest: {}", path.display())); - manifest - .parse::() + .unwrap_or_else(|_| panic!("Unable to read cargo manifest: {}", path.display())) + .into_boxed_str(); + ImDocument::parse(manifest) .unwrap_or_else(|_| panic!("Failed to parse cargo manifest: {}", path.display())) } From 84463e60e121b60c993a835303327d8e2054e995 Mon Sep 17 00:00:00 2001 From: Matty Weatherley Date: Wed, 12 Mar 2025 17:38:29 -0400 Subject: [PATCH 05/74] Implement Serialize/Deserialize/PartialEq for bounding primitives (#18281) # Objective Probably just because of an oversight, bounding primitives like `Aabb3d` did not implement `Serialize`/`Deserialize` with the `serialize` feature enabled, so the goal of this PR is to fill the gap. ## Solution Derive it conditionally, just like we do for everything else. Also added in `PartialEq`, just because I touched the files. ## Testing Compiled with different feature combinations. --- .../bevy_math/src/bounding/bounded2d/mod.rs | 18 ++++++++++++++++-- .../bevy_math/src/bounding/bounded3d/mod.rs | 19 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index c5be831a86..4fb2e2e327 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -9,6 +9,10 @@ use crate::{ #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; +#[cfg(all(feature = "bevy_reflect", feature = "serialize"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; /// Computes the geometric center of the given set of points. #[inline(always)] @@ -32,8 +36,13 @@ pub trait Bounded2d { /// A 2D axis-aligned bounding box, or bounding rectangle #[doc(alias = "BoundingRectangle")] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] +#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] pub struct Aabb2d { /// The minimum, conventionally bottom-left, point of the box pub min: Vec2, @@ -450,8 +459,13 @@ mod aabb2d_tests { use crate::primitives::Circle; /// A bounding circle -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] +#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] pub struct BoundingCircle { /// The center of the bounding circle pub center: Vec2, diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index c4f3c979f6..a04fdb3b5b 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -11,6 +11,11 @@ use crate::{ #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; +#[cfg(all(feature = "bevy_reflect", feature = "serialize"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; + pub use extrusion::BoundedExtrusion; /// Computes the geometric center of the given set of points. @@ -36,8 +41,13 @@ pub trait Bounded3d { } /// A 3D axis-aligned bounding box -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] +#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] pub struct Aabb3d { /// The minimum point of the box pub min: Vec3A, @@ -456,8 +466,13 @@ mod aabb3d_tests { use crate::primitives::Sphere; /// A bounding sphere -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] +#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] pub struct BoundingSphere { /// The center of the bounding sphere pub center: Vec3A, From 8f38ea352ecc1fed3bd3e4959b7b2ae14f9dc43f Mon Sep 17 00:00:00 2001 From: MevLyshkin Date: Thu, 13 Mar 2025 00:32:06 +0100 Subject: [PATCH 06/74] RPC Discover endpoint with basic informations (#18068) # Objective It does not resolves issue for full support for OpenRPC for `bevy_remote`, but it is a first step in that direction. Connected to the #16744 issue. ## Solution - Adds `rpc.discover` endpoint to the bevy_remote which follows https://spec.open-rpc.org/#openrpc-document For now in methods array only the name, which is the endpoint address is populated. - Moves json_schema structs into new module inside `bevy_remote`. ## Testing Tested the commands by running the BRP sample( cargo run --example server --features="bevy_remote") and with these curl command: ```sh curl -X POST -d '{ "jsonrpc": "2.0", "id": 1, "method": "rpc.discover"}' 127.0.0.1:15702 | jq . ``` The output is: ```json { "jsonrpc": "2.0", "id": 1, "result": { "info": { "title": "Bevy Remote Protocol", "version": "0.16.0-dev" }, "methods": [ { "name": "bevy/mutate_component", "params": [] }, { "name": "bevy/insert", "params": [] }, { "name": "bevy/get", "params": [] }, { "name": "bevy/spawn", "params": [] }, { "name": "bevy/get+watch", "params": [] }, { "name": "bevy/destroy", "params": [] }, { "name": "bevy/list", "params": [] }, { "name": "bevy/mutate_resource", "params": [] }, { "name": "bevy/reparent", "params": [] }, { "name": "bevy/registry/schema", "params": [] }, { "name": "bevy/get_resource", "params": [] }, { "name": "bevy/query", "params": [] }, { "name": "bevy/remove_resource", "params": [] }, { "name": "rpc.discover", "params": [] }, { "name": "bevy/insert_resource", "params": [] }, { "name": "bevy/list_resources", "params": [] }, { "name": "bevy/remove", "params": [] }, { "name": "bevy/list+watch", "params": [] } ], "openrpc": "1.3.2", "servers": [ { "name": "Server", "url": "127.0.0.1:15702" } ] } } ``` --------- Co-authored-by: Viktor Gustavsson --- crates/bevy_remote/src/builtin_methods.rs | 578 ++---------------- crates/bevy_remote/src/lib.rs | 10 + crates/bevy_remote/src/schemas/json_schema.rs | 543 ++++++++++++++++ crates/bevy_remote/src/schemas/mod.rs | 4 + crates/bevy_remote/src/schemas/open_rpc.rs | 118 ++++ 5 files changed, 722 insertions(+), 531 deletions(-) create mode 100644 crates/bevy_remote/src/schemas/json_schema.rs create mode 100644 crates/bevy_remote/src/schemas/mod.rs create mode 100644 crates/bevy_remote/src/schemas/open_rpc.rs diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 83a0bbe9da..b42cf41055 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -16,15 +16,21 @@ use bevy_ecs::{ }; use bevy_platform_support::collections::HashMap; use bevy_reflect::{ - prelude::ReflectDefault, serde::{ReflectSerializer, TypedReflectDeserializer}, - GetPath, NamedField, OpaqueInfo, PartialReflect, ReflectDeserialize, ReflectSerialize, - TypeInfo, TypeRegistration, TypeRegistry, VariantInfo, + GetPath, PartialReflect, TypeRegistration, TypeRegistry, }; +use bevy_utils::default; use serde::{de::DeserializeSeed as _, Deserialize, Serialize}; -use serde_json::{json, Map, Value}; +use serde_json::{Map, Value}; -use crate::{error_codes, BrpError, BrpResult}; +use crate::{ + error_codes, + schemas::{ + json_schema::JsonSchemaBevyType, + open_rpc::{OpenRpcDocument, ServerObject}, + }, + BrpError, BrpResult, +}; /// The method path for a `bevy/get` request. pub const BRP_GET_METHOD: &str = "bevy/get"; @@ -77,6 +83,9 @@ pub const BRP_LIST_RESOURCES_METHOD: &str = "bevy/list_resources"; /// The method path for a `bevy/registry/schema` request. pub const BRP_REGISTRY_SCHEMA_METHOD: &str = "bevy/registry/schema"; +/// The method path for a `rpc.discover` request. +pub const RPC_DISCOVER_METHOD: &str = "rpc.discover"; + /// `bevy/get`: Retrieves one or more components from the entity with the given /// ID. /// @@ -806,6 +815,38 @@ pub fn process_remote_spawn_request(In(params): In>, world: &mut W serde_json::to_value(response).map_err(BrpError::internal) } +/// Handles a `rpc.discover` request coming from a client. +pub fn process_remote_list_methods_request( + In(_params): In>, + world: &mut World, +) -> BrpResult { + let remote_methods = world.resource::(); + let servers = match ( + world.get_resource::(), + world.get_resource::(), + ) { + (Some(url), Some(port)) => Some(vec![ServerObject { + name: "Server".to_owned(), + url: format!("{}:{}", url.0, port.0), + ..default() + }]), + (Some(url), None) => Some(vec![ServerObject { + name: "Server".to_owned(), + url: url.0.to_string(), + ..default() + }]), + _ => None, + }; + let doc = OpenRpcDocument { + info: Default::default(), + methods: remote_methods.into(), + openrpc: "1.3.2".to_owned(), + servers, + }; + + serde_json::to_value(doc).map_err(BrpError::internal) +} + /// Handles a `bevy/insert` request (insert components) coming from a client. pub fn process_remote_insert_request( In(params): In>, @@ -1162,7 +1203,7 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br let types = types.read(); let schemas = types .iter() - .map(export_type) + .map(crate::schemas::json_schema::export_type) .filter(|(_, schema)| { if let Some(crate_name) = &schema.crate_name { if !filter.with_crates.is_empty() @@ -1202,339 +1243,6 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br serde_json::to_value(schemas).map_err(BrpError::internal) } -/// Exports schema info for a given type -fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { - let t = reg.type_info(); - let binding = t.type_path_table(); - - let short_path = binding.short_path(); - let type_path = binding.path(); - let mut typed_schema = JsonSchemaBevyType { - reflect_types: get_registered_reflect_types(reg), - short_path: short_path.to_owned(), - type_path: type_path.to_owned(), - crate_name: binding.crate_name().map(str::to_owned), - module_path: binding.module_path().map(str::to_owned), - ..Default::default() - }; - match t { - TypeInfo::Struct(info) => { - typed_schema.properties = info - .iter() - .map(|field| (field.name().to_owned(), field.ty().ref_type())) - .collect::>(); - typed_schema.required = info - .iter() - .filter(|field| !field.type_path().starts_with("core::option::Option")) - .map(|f| f.name().to_owned()) - .collect::>(); - typed_schema.additional_properties = Some(false); - typed_schema.schema_type = SchemaType::Object; - typed_schema.kind = SchemaKind::Struct; - } - TypeInfo::Enum(info) => { - typed_schema.kind = SchemaKind::Enum; - - let simple = info - .iter() - .all(|variant| matches!(variant, VariantInfo::Unit(_))); - if simple { - typed_schema.schema_type = SchemaType::String; - typed_schema.one_of = info - .iter() - .map(|variant| match variant { - VariantInfo::Unit(v) => v.name().into(), - _ => unreachable!(), - }) - .collect::>(); - } else { - typed_schema.schema_type = SchemaType::Object; - typed_schema.one_of = info - .iter() - .map(|variant| match variant { - VariantInfo::Struct(v) => json!({ - "type": "object", - "kind": "Struct", - "typePath": format!("{}::{}", type_path, v.name()), - "shortPath": v.name(), - "properties": v - .iter() - .map(|field| (field.name().to_owned(), field.ref_type())) - .collect::>(), - "additionalProperties": false, - "required": v - .iter() - .filter(|field| !field.type_path().starts_with("core::option::Option")) - .map(NamedField::name) - .collect::>(), - }), - VariantInfo::Tuple(v) => json!({ - "type": "array", - "kind": "Tuple", - "typePath": format!("{}::{}", type_path, v.name()), - "shortPath": v.name(), - "prefixItems": v - .iter() - .map(SchemaJsonReference::ref_type) - .collect::>(), - "items": false, - }), - VariantInfo::Unit(v) => json!({ - "typePath": format!("{}::{}", type_path, v.name()), - "shortPath": v.name(), - }), - }) - .collect::>(); - } - } - TypeInfo::TupleStruct(info) => { - typed_schema.schema_type = SchemaType::Array; - typed_schema.kind = SchemaKind::TupleStruct; - typed_schema.prefix_items = info - .iter() - .map(SchemaJsonReference::ref_type) - .collect::>(); - typed_schema.items = Some(false.into()); - } - TypeInfo::List(info) => { - typed_schema.schema_type = SchemaType::Array; - typed_schema.kind = SchemaKind::List; - typed_schema.items = info.item_ty().ref_type().into(); - } - TypeInfo::Array(info) => { - typed_schema.schema_type = SchemaType::Array; - typed_schema.kind = SchemaKind::Array; - typed_schema.items = info.item_ty().ref_type().into(); - } - TypeInfo::Map(info) => { - typed_schema.schema_type = SchemaType::Object; - typed_schema.kind = SchemaKind::Map; - typed_schema.key_type = info.key_ty().ref_type().into(); - typed_schema.value_type = info.value_ty().ref_type().into(); - } - TypeInfo::Tuple(info) => { - typed_schema.schema_type = SchemaType::Array; - typed_schema.kind = SchemaKind::Tuple; - typed_schema.prefix_items = info - .iter() - .map(SchemaJsonReference::ref_type) - .collect::>(); - typed_schema.items = Some(false.into()); - } - TypeInfo::Set(info) => { - typed_schema.schema_type = SchemaType::Set; - typed_schema.kind = SchemaKind::Set; - typed_schema.items = info.value_ty().ref_type().into(); - } - TypeInfo::Opaque(info) => { - typed_schema.schema_type = info.map_json_type(); - typed_schema.kind = SchemaKind::Value; - } - }; - - (t.type_path().to_owned(), typed_schema) -} - -fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { - // Vec could be moved to allow registering more types by game maker. - let registered_reflect_types: [(TypeId, &str); 5] = [ - { (TypeId::of::(), "Component") }, - { (TypeId::of::(), "Resource") }, - { (TypeId::of::(), "Default") }, - { (TypeId::of::(), "Serialize") }, - { (TypeId::of::(), "Deserialize") }, - ]; - let mut result = Vec::new(); - for (id, name) in registered_reflect_types { - if reg.data_by_id(id).is_some() { - result.push(name.to_owned()); - } - } - result -} - -/// JSON Schema type for Bevy Registry Types -/// It tries to follow this standard: -/// -/// To take the full advantage from info provided by Bevy registry it provides extra fields -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -pub struct JsonSchemaBevyType { - /// Bevy specific field, short path of the type. - pub short_path: String, - /// Bevy specific field, full path of the type. - pub type_path: String, - /// Bevy specific field, path of the module that type is part of. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub module_path: Option, - /// Bevy specific field, name of the crate that type is part of. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub crate_name: Option, - /// Bevy specific field, names of the types that type reflects. - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub reflect_types: Vec, - /// Bevy specific field, [`TypeInfo`] type mapping. - pub kind: SchemaKind, - /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. - /// - /// It contains type info of key of the Map. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub key_type: Option, - /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. - /// - /// It contains type info of value of the Map. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub value_type: Option, - /// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema. - #[serde(rename = "type")] - pub schema_type: SchemaType, - /// The behavior of this keyword depends on the presence and annotation results of "properties" - /// and "patternProperties" within the same schema object. - /// Validation with "additionalProperties" applies only to the child - /// values of instance names that do not appear in the annotation results of either "properties" or "patternProperties". - #[serde(skip_serializing_if = "Option::is_none", default)] - pub additional_properties: Option, - /// Validation succeeds if, for each name that appears in both the instance and as a name - /// within this keyword's value, the child instance for that name successfully validates - /// against the corresponding schema. - #[serde(skip_serializing_if = "HashMap::is_empty", default)] - pub properties: HashMap, - /// An object instance is valid against this keyword if every item in the array is the name of a property in the instance. - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub required: Vec, - /// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value. - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub one_of: Vec, - /// Validation succeeds if each element of the instance validates against the schema at the same position, if any. This keyword does not constrain the length of the array. If the array is longer than this keyword's value, this keyword validates only the prefix of matching length. - /// - /// This keyword produces an annotation value which is the largest index to which this keyword - /// applied a subschema. The value MAY be a boolean true if a subschema was applied to every - /// index of the instance, such as is produced by the "items" keyword. - /// This annotation affects the behavior of "items" and "unevaluatedItems". - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub prefix_items: Vec, - /// This keyword applies its subschema to all instance elements at indexes greater - /// than the length of the "prefixItems" array in the same schema object, - /// as reported by the annotation result of that "prefixItems" keyword. - /// If no such annotation result exists, "items" applies its subschema to all - /// instance array elements. - /// - /// If the "items" subschema is applied to any positions within the instance array, - /// it produces an annotation result of boolean true, indicating that all remaining - /// array elements have been evaluated against this keyword's subschema. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub items: Option, -} - -/// Kind of json schema, maps [`TypeInfo`] type -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -pub enum SchemaKind { - /// Struct - #[default] - Struct, - /// Enum type - Enum, - /// A key-value map - Map, - /// Array - Array, - /// List - List, - /// Fixed size collection of items - Tuple, - /// Fixed size collection of items with named fields - TupleStruct, - /// Set of unique values - Set, - /// Single value, eg. primitive types - Value, -} - -/// Type of json schema -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "lowercase")] -pub enum SchemaType { - /// Represents a string value. - String, - - /// Represents a floating-point number. - Float, - - /// Represents an unsigned integer. - Uint, - - /// Represents a signed integer. - Int, - - /// Represents an object with key-value pairs. - Object, - - /// Represents an array of values. - Array, - - /// Represents a boolean value (true or false). - Boolean, - - /// Represents a set of unique values. - Set, - - /// Represents a null value. - #[default] - Null, -} - -/// Helper trait for generating json schema reference -trait SchemaJsonReference { - /// Reference to another type in schema. - /// The value `$ref` is a URI-reference that is resolved against the schema. - fn ref_type(self) -> Value; -} - -/// Helper trait for mapping bevy type path into json schema type -trait SchemaJsonType { - /// Bevy Reflect type path - fn get_type_path(&self) -> &'static str; - - /// JSON Schema type keyword from Bevy reflect type path into - fn map_json_type(&self) -> SchemaType { - match self.get_type_path() { - "bool" => SchemaType::Boolean, - "u8" | "u16" | "u32" | "u64" | "u128" | "usize" => SchemaType::Uint, - "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => SchemaType::Int, - "f32" | "f64" => SchemaType::Float, - "char" | "str" | "alloc::string::String" => SchemaType::String, - _ => SchemaType::Object, - } - } -} - -impl SchemaJsonType for OpaqueInfo { - fn get_type_path(&self) -> &'static str { - self.type_path() - } -} - -impl SchemaJsonReference for &bevy_reflect::Type { - fn ref_type(self) -> Value { - let path = self.path(); - json!({"type": json!({ "$ref": format!("#/$defs/{path}") })}) - } -} - -impl SchemaJsonReference for &bevy_reflect::UnnamedField { - fn ref_type(self) -> Value { - let path = self.type_path(); - json!({"type": json!({ "$ref": format!("#/$defs/{path}") })}) - } -} - -impl SchemaJsonReference for &NamedField { - fn ref_type(self) -> Value { - let type_path = self.type_path(); - json!({"type": json!({ "$ref": format!("#/$defs/{type_path}") }), "typePath": self.name()}) - } -} - /// Immutably retrieves an entity from the [`World`], returning an error if the /// entity isn't present. fn get_entity(world: &World, entity: Entity) -> Result, BrpError> { @@ -1758,7 +1466,6 @@ fn get_resource_type_registration<'r>( } #[cfg(test)] -#[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { /// A generic function that tests serialization and deserialization of any type /// implementing Serialize and Deserialize traits. @@ -1779,8 +1486,6 @@ mod tests { ); } use super::*; - use bevy_ecs::{component::Component, resource::Resource}; - use bevy_reflect::Reflect; #[test] fn serialization_tests() { @@ -1803,193 +1508,4 @@ mod tests { entity: Entity::from_raw(0), }); } - - #[test] - fn reflect_export_struct() { - #[derive(Reflect, Resource, Default, Deserialize, Serialize)] - #[reflect(Resource, Default, Serialize, Deserialize)] - struct Foo { - a: f32, - b: Option, - } - - let atr = AppTypeRegistry::default(); - { - let mut register = atr.write(); - register.register::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); - - assert!( - !schema.reflect_types.contains(&"Component".to_owned()), - "Should not be a component" - ); - assert!( - schema.reflect_types.contains(&"Resource".to_owned()), - "Should be a resource" - ); - let _ = schema.properties.get("a").expect("Missing `a` field"); - let _ = schema.properties.get("b").expect("Missing `b` field"); - assert!( - schema.required.contains(&"a".to_owned()), - "Field a should be required" - ); - assert!( - !schema.required.contains(&"b".to_owned()), - "Field b should not be required" - ); - } - - #[test] - fn reflect_export_enum() { - #[derive(Reflect, Component, Default, Deserialize, Serialize)] - #[reflect(Component, Default, Serialize, Deserialize)] - enum EnumComponent { - ValueOne(i32), - ValueTwo { - test: i32, - }, - #[default] - NoValue, - } - - let atr = AppTypeRegistry::default(); - { - let mut register = atr.write(); - register.register::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - assert!( - schema.reflect_types.contains(&"Component".to_owned()), - "Should be a component" - ); - assert!( - !schema.reflect_types.contains(&"Resource".to_owned()), - "Should not be a resource" - ); - assert!(schema.properties.is_empty(), "Should not have any field"); - assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); - } - - #[test] - fn reflect_export_struct_without_reflect_types() { - #[derive(Reflect, Component, Default, Deserialize, Serialize)] - enum EnumComponent { - ValueOne(i32), - ValueTwo { - test: i32, - }, - #[default] - NoValue, - } - - let atr = AppTypeRegistry::default(); - { - let mut register = atr.write(); - register.register::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - assert!( - !schema.reflect_types.contains(&"Component".to_owned()), - "Should not be a component" - ); - assert!( - !schema.reflect_types.contains(&"Resource".to_owned()), - "Should not be a resource" - ); - assert!(schema.properties.is_empty(), "Should not have any field"); - assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); - } - - #[test] - fn reflect_export_tuple_struct() { - #[derive(Reflect, Component, Default, Deserialize, Serialize)] - #[reflect(Component, Default, Serialize, Deserialize)] - struct TupleStructType(usize, i32); - - let atr = AppTypeRegistry::default(); - { - let mut register = atr.write(); - register.register::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); - assert!( - schema.reflect_types.contains(&"Component".to_owned()), - "Should be a component" - ); - assert!( - !schema.reflect_types.contains(&"Resource".to_owned()), - "Should not be a resource" - ); - assert!(schema.properties.is_empty(), "Should not have any field"); - assert!(schema.prefix_items.len() == 2, "Should have 2 prefix items"); - } - - #[test] - fn reflect_export_serialization_check() { - #[derive(Reflect, Resource, Default, Deserialize, Serialize)] - #[reflect(Resource, Default)] - struct Foo { - a: f32, - } - - let atr = AppTypeRegistry::default(); - { - let mut register = atr.write(); - register.register::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); - let value = json!({ - "shortPath": "Foo", - "typePath": "bevy_remote::builtin_methods::tests::Foo", - "modulePath": "bevy_remote::builtin_methods::tests", - "crateName": "bevy_remote", - "reflectTypes": [ - "Resource", - "Default", - ], - "kind": "Struct", - "type": "object", - "additionalProperties": false, - "properties": { - "a": { - "type": { - "$ref": "#/$defs/f32" - } - }, - }, - "required": [ - "a" - ] - }); - assert_eq!(schema_as_value, value); - } } diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index c8260dae44..86da53b32e 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -383,6 +383,7 @@ use std::sync::RwLock; pub mod builtin_methods; #[cfg(feature = "http")] pub mod http; +pub mod schemas; const CHANNEL_SIZE: usize = 16; @@ -474,6 +475,10 @@ impl Default for RemotePlugin { builtin_methods::BRP_MUTATE_COMPONENT_METHOD, builtin_methods::process_remote_mutate_component_request, ) + .with_method( + builtin_methods::RPC_DISCOVER_METHOD, + builtin_methods::process_remote_list_methods_request, + ) .with_watching_method( builtin_methods::BRP_GET_AND_WATCH_METHOD, builtin_methods::process_remote_get_watching_request, @@ -631,6 +636,11 @@ impl RemoteMethods { pub fn get(&self, method: &str) -> Option<&RemoteMethodSystemId> { self.0.get(method) } + + /// Get a [`Vec`] with method names. + pub fn methods(&self) -> Vec { + self.0.keys().cloned().collect() + } } /// Holds the [`BrpMessage`]'s of all ongoing watching requests along with their handlers. diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs new file mode 100644 index 0000000000..f7a58006a5 --- /dev/null +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -0,0 +1,543 @@ +//! Module with JSON Schema type for Bevy Registry Types. +//! It tries to follow this standard: +use bevy_ecs::reflect::{ReflectComponent, ReflectResource}; +use bevy_platform_support::collections::HashMap; +use bevy_reflect::{ + prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, + TypeInfo, TypeRegistration, VariantInfo, +}; +use core::any::TypeId; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; + +/// Exports schema info for a given type +pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { + (reg.type_info().type_path().to_owned(), reg.into()) +} + +fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { + // Vec could be moved to allow registering more types by game maker. + let registered_reflect_types: [(TypeId, &str); 5] = [ + { (TypeId::of::(), "Component") }, + { (TypeId::of::(), "Resource") }, + { (TypeId::of::(), "Default") }, + { (TypeId::of::(), "Serialize") }, + { (TypeId::of::(), "Deserialize") }, + ]; + let mut result = Vec::new(); + for (id, name) in registered_reflect_types { + if reg.data_by_id(id).is_some() { + result.push(name.to_owned()); + } + } + result +} + +impl From<&TypeRegistration> for JsonSchemaBevyType { + fn from(reg: &TypeRegistration) -> Self { + let t = reg.type_info(); + let binding = t.type_path_table(); + + let short_path = binding.short_path(); + let type_path = binding.path(); + let mut typed_schema = JsonSchemaBevyType { + reflect_types: get_registered_reflect_types(reg), + short_path: short_path.to_owned(), + type_path: type_path.to_owned(), + crate_name: binding.crate_name().map(str::to_owned), + module_path: binding.module_path().map(str::to_owned), + ..Default::default() + }; + match t { + TypeInfo::Struct(info) => { + typed_schema.properties = info + .iter() + .map(|field| (field.name().to_owned(), field.ty().ref_type())) + .collect::>(); + typed_schema.required = info + .iter() + .filter(|field| !field.type_path().starts_with("core::option::Option")) + .map(|f| f.name().to_owned()) + .collect::>(); + typed_schema.additional_properties = Some(false); + typed_schema.schema_type = SchemaType::Object; + typed_schema.kind = SchemaKind::Struct; + } + TypeInfo::Enum(info) => { + typed_schema.kind = SchemaKind::Enum; + + let simple = info + .iter() + .all(|variant| matches!(variant, VariantInfo::Unit(_))); + if simple { + typed_schema.schema_type = SchemaType::String; + typed_schema.one_of = info + .iter() + .map(|variant| match variant { + VariantInfo::Unit(v) => v.name().into(), + _ => unreachable!(), + }) + .collect::>(); + } else { + typed_schema.schema_type = SchemaType::Object; + typed_schema.one_of = info + .iter() + .map(|variant| match variant { + VariantInfo::Struct(v) => json!({ + "type": "object", + "kind": "Struct", + "typePath": format!("{}::{}", type_path, v.name()), + "shortPath": v.name(), + "properties": v + .iter() + .map(|field| (field.name().to_owned(), field.ref_type())) + .collect::>(), + "additionalProperties": false, + "required": v + .iter() + .filter(|field| !field.type_path().starts_with("core::option::Option")) + .map(NamedField::name) + .collect::>(), + }), + VariantInfo::Tuple(v) => json!({ + "type": "array", + "kind": "Tuple", + "typePath": format!("{}::{}", type_path, v.name()), + "shortPath": v.name(), + "prefixItems": v + .iter() + .map(SchemaJsonReference::ref_type) + .collect::>(), + "items": false, + }), + VariantInfo::Unit(v) => json!({ + "typePath": format!("{}::{}", type_path, v.name()), + "shortPath": v.name(), + }), + }) + .collect::>(); + } + } + TypeInfo::TupleStruct(info) => { + typed_schema.schema_type = SchemaType::Array; + typed_schema.kind = SchemaKind::TupleStruct; + typed_schema.prefix_items = info + .iter() + .map(SchemaJsonReference::ref_type) + .collect::>(); + typed_schema.items = Some(false.into()); + } + TypeInfo::List(info) => { + typed_schema.schema_type = SchemaType::Array; + typed_schema.kind = SchemaKind::List; + typed_schema.items = info.item_ty().ref_type().into(); + } + TypeInfo::Array(info) => { + typed_schema.schema_type = SchemaType::Array; + typed_schema.kind = SchemaKind::Array; + typed_schema.items = info.item_ty().ref_type().into(); + } + TypeInfo::Map(info) => { + typed_schema.schema_type = SchemaType::Object; + typed_schema.kind = SchemaKind::Map; + typed_schema.key_type = info.key_ty().ref_type().into(); + typed_schema.value_type = info.value_ty().ref_type().into(); + } + TypeInfo::Tuple(info) => { + typed_schema.schema_type = SchemaType::Array; + typed_schema.kind = SchemaKind::Tuple; + typed_schema.prefix_items = info + .iter() + .map(SchemaJsonReference::ref_type) + .collect::>(); + typed_schema.items = Some(false.into()); + } + TypeInfo::Set(info) => { + typed_schema.schema_type = SchemaType::Set; + typed_schema.kind = SchemaKind::Set; + typed_schema.items = info.value_ty().ref_type().into(); + } + TypeInfo::Opaque(info) => { + typed_schema.schema_type = info.map_json_type(); + typed_schema.kind = SchemaKind::Value; + } + }; + typed_schema + } +} + +/// JSON Schema type for Bevy Registry Types +/// It tries to follow this standard: +/// +/// To take the full advantage from info provided by Bevy registry it provides extra fields +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct JsonSchemaBevyType { + /// Bevy specific field, short path of the type. + pub short_path: String, + /// Bevy specific field, full path of the type. + pub type_path: String, + /// Bevy specific field, path of the module that type is part of. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub module_path: Option, + /// Bevy specific field, name of the crate that type is part of. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub crate_name: Option, + /// Bevy specific field, names of the types that type reflects. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub reflect_types: Vec, + /// Bevy specific field, [`TypeInfo`] type mapping. + pub kind: SchemaKind, + /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. + /// + /// It contains type info of key of the Map. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub key_type: Option, + /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. + /// + /// It contains type info of value of the Map. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub value_type: Option, + /// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema. + #[serde(rename = "type")] + pub schema_type: SchemaType, + /// The behavior of this keyword depends on the presence and annotation results of "properties" + /// and "patternProperties" within the same schema object. + /// Validation with "additionalProperties" applies only to the child + /// values of instance names that do not appear in the annotation results of either "properties" or "patternProperties". + #[serde(skip_serializing_if = "Option::is_none", default)] + pub additional_properties: Option, + /// Validation succeeds if, for each name that appears in both the instance and as a name + /// within this keyword's value, the child instance for that name successfully validates + /// against the corresponding schema. + #[serde(skip_serializing_if = "HashMap::is_empty", default)] + pub properties: HashMap, + /// An object instance is valid against this keyword if every item in the array is the name of a property in the instance. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub required: Vec, + /// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub one_of: Vec, + /// Validation succeeds if each element of the instance validates against the schema at the same position, if any. This keyword does not constrain the length of the array. If the array is longer than this keyword's value, this keyword validates only the prefix of matching length. + /// + /// This keyword produces an annotation value which is the largest index to which this keyword + /// applied a subschema. The value MAY be a boolean true if a subschema was applied to every + /// index of the instance, such as is produced by the "items" keyword. + /// This annotation affects the behavior of "items" and "unevaluatedItems". + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub prefix_items: Vec, + /// This keyword applies its subschema to all instance elements at indexes greater + /// than the length of the "prefixItems" array in the same schema object, + /// as reported by the annotation result of that "prefixItems" keyword. + /// If no such annotation result exists, "items" applies its subschema to all + /// instance array elements. + /// + /// If the "items" subschema is applied to any positions within the instance array, + /// it produces an annotation result of boolean true, indicating that all remaining + /// array elements have been evaluated against this keyword's subschema. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub items: Option, +} + +/// Kind of json schema, maps [`TypeInfo`] type +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +pub enum SchemaKind { + /// Struct + #[default] + Struct, + /// Enum type + Enum, + /// A key-value map + Map, + /// Array + Array, + /// List + List, + /// Fixed size collection of items + Tuple, + /// Fixed size collection of items with named fields + TupleStruct, + /// Set of unique values + Set, + /// Single value, eg. primitive types + Value, +} + +/// Type of json schema +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum SchemaType { + /// Represents a string value. + String, + + /// Represents a floating-point number. + Float, + + /// Represents an unsigned integer. + Uint, + + /// Represents a signed integer. + Int, + + /// Represents an object with key-value pairs. + Object, + + /// Represents an array of values. + Array, + + /// Represents a boolean value (true or false). + Boolean, + + /// Represents a set of unique values. + Set, + + /// Represents a null value. + #[default] + Null, +} + +/// Helper trait for generating json schema reference +trait SchemaJsonReference { + /// Reference to another type in schema. + /// The value `$ref` is a URI-reference that is resolved against the schema. + fn ref_type(self) -> Value; +} + +/// Helper trait for mapping bevy type path into json schema type +pub trait SchemaJsonType { + /// Bevy Reflect type path + fn get_type_path(&self) -> &'static str; + + /// JSON Schema type keyword from Bevy reflect type path into + fn map_json_type(&self) -> SchemaType { + match self.get_type_path() { + "bool" => SchemaType::Boolean, + "u8" | "u16" | "u32" | "u64" | "u128" | "usize" => SchemaType::Uint, + "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => SchemaType::Int, + "f32" | "f64" => SchemaType::Float, + "char" | "str" | "alloc::string::String" => SchemaType::String, + _ => SchemaType::Object, + } + } +} + +impl SchemaJsonType for OpaqueInfo { + fn get_type_path(&self) -> &'static str { + self.type_path() + } +} + +impl SchemaJsonReference for &bevy_reflect::Type { + fn ref_type(self) -> Value { + let path = self.path(); + json!({"type": json!({ "$ref": format!("#/$defs/{path}") })}) + } +} + +impl SchemaJsonReference for &bevy_reflect::UnnamedField { + fn ref_type(self) -> Value { + let path = self.type_path(); + json!({"type": json!({ "$ref": format!("#/$defs/{path}") })}) + } +} + +impl SchemaJsonReference for &NamedField { + fn ref_type(self) -> Value { + let type_path = self.type_path(); + json!({"type": json!({ "$ref": format!("#/$defs/{type_path}") }), "typePath": self.name()}) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; + use bevy_reflect::Reflect; + + #[test] + fn reflect_export_struct() { + #[derive(Reflect, Resource, Default, Deserialize, Serialize)] + #[reflect(Resource, Default, Serialize, Deserialize)] + struct Foo { + a: f32, + b: Option, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + + assert!( + !schema.reflect_types.contains(&"Component".to_owned()), + "Should not be a component" + ); + assert!( + schema.reflect_types.contains(&"Resource".to_owned()), + "Should be a resource" + ); + let _ = schema.properties.get("a").expect("Missing `a` field"); + let _ = schema.properties.get("b").expect("Missing `b` field"); + assert!( + schema.required.contains(&"a".to_owned()), + "Field a should be required" + ); + assert!( + !schema.required.contains(&"b".to_owned()), + "Field b should not be required" + ); + } + + #[test] + fn reflect_export_enum() { + #[derive(Reflect, Component, Default, Deserialize, Serialize)] + #[reflect(Component, Default, Serialize, Deserialize)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + assert!( + schema.reflect_types.contains(&"Component".to_owned()), + "Should be a component" + ); + assert!( + !schema.reflect_types.contains(&"Resource".to_owned()), + "Should not be a resource" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); + } + + #[test] + fn reflect_export_struct_without_reflect_types() { + #[derive(Reflect, Component, Default, Deserialize, Serialize)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + assert!( + !schema.reflect_types.contains(&"Component".to_owned()), + "Should not be a component" + ); + assert!( + !schema.reflect_types.contains(&"Resource".to_owned()), + "Should not be a resource" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); + } + + #[test] + fn reflect_export_tuple_struct() { + #[derive(Reflect, Component, Default, Deserialize, Serialize)] + #[reflect(Component, Default, Serialize, Deserialize)] + struct TupleStructType(usize, i32); + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + assert!( + schema.reflect_types.contains(&"Component".to_owned()), + "Should be a component" + ); + assert!( + !schema.reflect_types.contains(&"Resource".to_owned()), + "Should not be a resource" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.prefix_items.len() == 2, "Should have 2 prefix items"); + } + + #[test] + fn reflect_export_serialization_check() { + #[derive(Reflect, Resource, Default, Deserialize, Serialize)] + #[reflect(Resource, Default)] + struct Foo { + a: f32, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); + let value = json!({ + "shortPath": "Foo", + "typePath": "bevy_remote::schemas::json_schema::tests::Foo", + "modulePath": "bevy_remote::schemas::json_schema::tests", + "crateName": "bevy_remote", + "reflectTypes": [ + "Resource", + "Default", + ], + "kind": "Struct", + "type": "object", + "additionalProperties": false, + "properties": { + "a": { + "type": { + "$ref": "#/$defs/f32" + } + }, + }, + "required": [ + "a" + ] + }); + assert_eq!(schema_as_value, value); + } +} diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs new file mode 100644 index 0000000000..7104fd5547 --- /dev/null +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -0,0 +1,4 @@ +//! Module with schemas used for various BRP endpoints + +pub mod json_schema; +pub mod open_rpc; diff --git a/crates/bevy_remote/src/schemas/open_rpc.rs b/crates/bevy_remote/src/schemas/open_rpc.rs new file mode 100644 index 0000000000..90a0aee70b --- /dev/null +++ b/crates/bevy_remote/src/schemas/open_rpc.rs @@ -0,0 +1,118 @@ +//! Module with trimmed down `OpenRPC` document structs. +//! It tries to follow this standard: +use bevy_platform_support::collections::HashMap; +use bevy_utils::default; +use serde::{Deserialize, Serialize}; + +use crate::RemoteMethods; + +use super::json_schema::JsonSchemaBevyType; + +/// Represents an `OpenRPC` document as defined by the `OpenRPC` specification. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenRpcDocument { + /// The version of the `OpenRPC` specification being used. + pub openrpc: String, + /// Informational metadata about the document. + pub info: InfoObject, + /// List of RPC methods defined in the document. + pub methods: Vec, + /// Optional list of server objects that provide the API endpoint details. + pub servers: Option>, +} + +/// Contains metadata information about the `OpenRPC` document. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InfoObject { + /// The title of the API or document. + pub title: String, + /// The version of the API. + pub version: String, + /// An optional description providing additional details about the API. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A collection of custom extension fields. + #[serde(flatten)] + pub extensions: HashMap, +} + +impl Default for InfoObject { + fn default() -> Self { + Self { + title: "Bevy Remote Protocol".to_owned(), + version: env!("CARGO_PKG_VERSION").to_owned(), + description: None, + extensions: Default::default(), + } + } +} + +/// Describes a server hosting the API as specified in the `OpenRPC` document. +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct ServerObject { + /// The name of the server. + pub name: String, + /// The URL endpoint of the server. + pub url: String, + /// An optional description of the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Additional custom extension fields. + #[serde(flatten)] + pub extensions: HashMap, +} + +/// Represents an RPC method in the `OpenRPC` document. +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct MethodObject { + /// The method name (e.g., "/bevy/get") + pub name: String, + /// An optional short summary of the method. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// An optional detailed description of the method. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Parameters for the RPC method + #[serde(default)] + pub params: Vec, + // /// The expected result of the method + // #[serde(skip_serializing_if = "Option::is_none")] + // pub result: Option, + /// Additional custom extension fields. + #[serde(flatten)] + pub extensions: HashMap, +} + +/// Represents an RPC method parameter in the `OpenRPC` document. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Parameter { + /// Parameter name + pub name: String, + /// Parameter description + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// JSON schema describing the parameter + pub schema: JsonSchemaBevyType, + /// Additional custom extension fields. + #[serde(flatten)] + pub extensions: HashMap, +} + +impl From<&RemoteMethods> for Vec { + fn from(value: &RemoteMethods) -> Self { + value + .methods() + .iter() + .map(|e| MethodObject { + name: e.to_owned(), + ..default() + }) + .collect() + } +} From 25103df30133f30076f9ef1852e4873ad4c6448f Mon Sep 17 00:00:00 2001 From: krunchington Date: Wed, 12 Mar 2025 16:52:39 -0700 Subject: [PATCH 07/74] Update gamepad_viewer to use children macro (#18282) # Objective Contributes to #18238 Updates the `gamepad_viewer`, example to use the `children!` macro. ## Solution Updates examples to use the Improved Spawning API merged in https://github.com/bevyengine/bevy/pull/17521 ## Testing - Did you test these changes? If so, how? - Opened the examples before and after and verified the same behavior was observed. I did this on Ubuntu 24.04.2 LTS using `--features wayland`. - Are there any parts that need more testing? - Other OS's and features can't hurt, but this is such a small change it shouldn't be a problem. - How can other people (reviewers) test your changes? Is there anything specific they need to know? - Run the examples yourself with and without these changes. - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? - see above --- ## Showcase n/a ## Migration Guide n/a --- examples/tools/gamepad_viewer.rs | 213 ++++++++++++++----------------- 1 file changed, 98 insertions(+), 115 deletions(-) diff --git a/examples/tools/gamepad_viewer.rs b/examples/tools/gamepad_viewer.rs index 32549893c6..c8092c2ebe 100644 --- a/examples/tools/gamepad_viewer.rs +++ b/examples/tools/gamepad_viewer.rs @@ -132,41 +132,40 @@ fn setup(mut commands: Commands, meshes: Res, materials: Res, materials: Res, ) { let mut spawn_trigger = |x, y, button_type| { - commands - .spawn(GamepadButtonBundle::new( + commands.spawn(( + GamepadButtonBundle::new( button_type, meshes.trigger.clone(), materials.normal.clone(), x, y, - )) - .with_children(|parent| { - parent.spawn(( - Transform::from_xyz(0., 0., 1.), - Text(format!("{:.3}", 0.)), - TextFont { - font_size: 13., - ..default() - }, - TextWithButtonValue(button_type), - )); - }); + ), + children![( + Transform::from_xyz(0., 0., 1.), + Text(format!("{:.3}", 0.)), + TextFont { + font_size: 13., + ..default() + }, + TextWithButtonValue(button_type), + )], + )); }; spawn_trigger(-BUTTONS_X, BUTTONS_Y + 145., GamepadButton::LeftTrigger2); @@ -374,18 +358,17 @@ fn setup_triggers( fn setup_connected(mut commands: Commands) { // This is UI text, unlike other text in this example which is 2d. - commands - .spawn(( - Text::new("Connected Gamepads:\n"), - Node { - position_type: PositionType::Absolute, - top: Val::Px(12.), - left: Val::Px(12.), - ..default() - }, - ConnectedGamepadsText, - )) - .with_child(TextSpan::new("None")); + commands.spawn(( + Text::new("Connected Gamepads:\n"), + Node { + position_type: PositionType::Absolute, + top: Val::Px(12.), + left: Val::Px(12.), + ..default() + }, + ConnectedGamepadsText, + children![TextSpan::new("None")], + )); } fn update_buttons( From ab0e3f871452f6be5c99f18175a8949b8a19e2dd Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 12 Mar 2025 20:13:02 -0400 Subject: [PATCH 08/74] Small cleanup for ECS error handling (#18280) # Objective While poking at https://github.com/bevyengine/bevy/issues/17272, I noticed a few small things to clean up. ## Solution - Improve the docs - ~~move `SystemErrorContext` out of the `handler.rs` module: it's not an error handler~~ --- crates/bevy_app/src/app.rs | 2 +- crates/bevy_app/src/sub_app.rs | 2 +- crates/bevy_ecs/src/error/mod.rs | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 20f15bb098..9ce162d217 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1276,7 +1276,7 @@ impl App { /// Set the global system error handler to use for systems that return a [`Result`]. /// - /// See the [`bevy_ecs::result` module-level documentation](../../bevy_ecs/result/index.html) + /// See the [`bevy_ecs::error` module-level documentation](bevy_ecs::error) /// for more information. pub fn set_system_error_handler( &mut self, diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 0cb50ac887..7d0dfd3106 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -338,7 +338,7 @@ impl SubApp { /// Set the global error handler to use for systems that return a [`Result`]. /// - /// See the [`bevy_ecs::result` module-level documentation](../../bevy_ecs/result/index.html) + /// See the [`bevy_ecs::error` module-level documentation](bevy_ecs::error) /// for more information. pub fn set_system_error_handler( &mut self, diff --git a/crates/bevy_ecs/src/error/mod.rs b/crates/bevy_ecs/src/error/mod.rs index 307d93158f..4c8ad10d8e 100644 --- a/crates/bevy_ecs/src/error/mod.rs +++ b/crates/bevy_ecs/src/error/mod.rs @@ -8,10 +8,10 @@ //! [`panic`] error handler function is used, resulting in a panic with the error message attached. //! //! You can change the default behavior by registering a custom error handler, either globally or -//! per `Schedule`: +//! per [`Schedule`]: //! -//! - [`App::set_system_error_handler`] sets the global error handler for all systems of the -//! current [`World`]. +//! - `App::set_system_error_handler` (via `bevy_app`) sets the global error handler for all systems of the +//! current [`World`] by modifying the [`DefaultSystemErrorHandler`]. //! - [`Schedule::set_error_handler`] sets the error handler for all systems of that schedule. //! //! Bevy provides a number of pre-built error-handlers for you to use: @@ -76,5 +76,7 @@ mod handler; pub use bevy_error::*; pub use handler::*; -/// A result type for use in fallible systems. +/// A result type for use in fallible systems, commands and observers. +/// +/// The [`BevyError`] type is a type-erased error type with optional Bevy-specific diagnostics. pub type Result = core::result::Result; From 4d47de8ad832e82c6e3fd98058763c1835d6e233 Mon Sep 17 00:00:00 2001 From: krunchington Date: Wed, 12 Mar 2025 20:11:13 -0700 Subject: [PATCH 09/74] Update custom_transitions and sub_states examples to use children macro (#18292) # Objective Contributes to #18238 Updates the `custom_transitions` and `sub_states` examples to use the `children!` macro. ## Solution Updates examples to use the Improved Spawning API merged in https://github.com/bevyengine/bevy/pull/17521 ## Testing - Did you test these changes? If so, how? - Opened the examples before and after and verified the same behavior was observed. I did this on Ubuntu 24.04.2 LTS using `--features wayland`. - Are there any parts that need more testing? - Other OS's and features can't hurt, but this is such a small change it shouldn't be a problem. - How can other people (reviewers) test your changes? Is there anything specific they need to know? - Run the examples yourself with and without these changes. - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? - see above --- ## Showcase n/a ## Migration Guide n/a --- examples/state/custom_transitions.rs | 61 +++++++------ examples/state/sub_states.rs | 123 +++++++++++++-------------- 2 files changed, 86 insertions(+), 98 deletions(-) diff --git a/examples/state/custom_transitions.rs b/examples/state/custom_transitions.rs index f5b1415d3e..f7d43f4327 100644 --- a/examples/state/custom_transitions.rs +++ b/examples/state/custom_transitions.rs @@ -243,40 +243,37 @@ const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); fn setup_menu(mut commands: Commands) { let button_entity = commands - .spawn(Node { - // center button - width: Val::Percent(100.), - height: Val::Percent(100.), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }) - .with_children(|parent| { - parent - .spawn(( - Button, - Node { - width: Val::Px(150.), - height: Val::Px(65.), - // horizontally center child text - justify_content: JustifyContent::Center, - // vertically center child text - align_items: AlignItems::Center, + .spawn(( + Node { + // center button + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + children![( + Button, + Node { + width: Val::Px(150.), + height: Val::Px(65.), + // horizontally center child text + justify_content: JustifyContent::Center, + // vertically center child text + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Play"), + TextFont { + font_size: 33.0, ..default() }, - BackgroundColor(NORMAL_BUTTON), - )) - .with_children(|parent| { - parent.spawn(( - Text::new("Play"), - TextFont { - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - )); - }); - }) + TextColor(Color::srgb(0.9, 0.9, 0.9)), + )] + )], + )) .id(); commands.insert_resource(MenuData { button_entity }); } diff --git a/examples/state/sub_states.rs b/examples/state/sub_states.rs index 767e3fb05d..abf2492aba 100644 --- a/examples/state/sub_states.rs +++ b/examples/state/sub_states.rs @@ -156,40 +156,37 @@ mod ui { pub fn setup_menu(mut commands: Commands) { let button_entity = commands - .spawn(Node { - // center button - width: Val::Percent(100.), - height: Val::Percent(100.), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }) - .with_children(|parent| { - parent - .spawn(( - Button, - Node { - width: Val::Px(150.), - height: Val::Px(65.), - // horizontally center child text - justify_content: JustifyContent::Center, - // vertically center child text - align_items: AlignItems::Center, + .spawn(( + Node { + // center button + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + children![( + Button, + Node { + width: Val::Px(150.), + height: Val::Px(65.), + // horizontally center child text + justify_content: JustifyContent::Center, + // vertically center child text + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Play"), + TextFont { + font_size: 33.0, ..default() }, - BackgroundColor(NORMAL_BUTTON), - )) - .with_children(|parent| { - parent.spawn(( - Text::new("Play"), - TextFont { - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - )); - }); - }) + TextColor(Color::srgb(0.9, 0.9, 0.9)), + )] + )], + )) .id(); commands.insert_resource(MenuData { button_entity }); } @@ -199,44 +196,38 @@ mod ui { } pub fn setup_paused_screen(mut commands: Commands) { - commands - .spawn(( - StateScoped(IsPaused::Paused), + commands.spawn(( + StateScoped(IsPaused::Paused), + Node { + // center button + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.), + ..default() + }, + children![( Node { - // center button - width: Val::Percent(100.), - height: Val::Percent(100.), + width: Val::Px(400.), + height: Val::Px(400.), + // horizontally center child text justify_content: JustifyContent::Center, + // vertically center child text align_items: AlignItems::Center, - flex_direction: FlexDirection::Column, - row_gap: Val::Px(10.), ..default() }, - )) - .with_children(|parent| { - parent - .spawn(( - Node { - width: Val::Px(400.), - height: Val::Px(400.), - // horizontally center child text - justify_content: JustifyContent::Center, - // vertically center child text - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(NORMAL_BUTTON), - )) - .with_children(|parent| { - parent.spawn(( - Text::new("Paused"), - TextFont { - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - )); - }); - }); + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Paused"), + TextFont { + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + )] + )], + )); } } From 6299e3de3bc72881f16d86384f1a4e6468fec544 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Fri, 14 Mar 2025 03:34:16 +1100 Subject: [PATCH 10/74] Add `examples/helpers/*` as library examples (#18288) # Objective Some of Bevy's examples contain boilerplate which is split out into the `helpers` folder. This allows examples to have access to common functionality without building into Bevy directly. However, these helpers are themselves quite high-quality code, and we do intend for users to read them and even use them. But, we don't list them in the examples document, and they aren't explicitly checked in CI, only transitively through examples which import them. ## Solution - Added `camera_controller` and `widgets` as library examples. ## Testing - CI --- ## Notes - Library examples are identical to any other example, just with `crate-type = ["lib"]` in the `Cargo.toml`. Since they are marked as libraries, they don't require a `main` function but do require public items to be documented. - Library examples opens the possibility of creating examples which don't need to be actual runnable applications. This may be more appropriate for certain ECS examples, and allows for adding helpers which (currently) don't have an example that needs them without them going stale. - I learned about this as a concept during research for `no_std` examples, but believe it has value for Bevy outside that specific niche. --------- Co-authored-by: mgi388 <135186256+mgi388@users.noreply.github.com> Co-authored-by: Carter Weinberg --- Cargo.toml | 24 ++++++++++++++++++++++++ examples/README.md | 8 ++++++++ examples/helpers/camera_controller.rs | 25 +++++++++++++++++++++++++ examples/helpers/widgets.rs | 2 ++ 4 files changed, 59 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 130eafda2c..6da9dd703d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4263,3 +4263,27 @@ name = "Occlusion Culling" description = "Demonstration of Occlusion Culling" category = "3D Rendering" wasm = false + +[[example]] +name = "camera_controller" +path = "examples/helpers/camera_controller.rs" +doc-scrape-examples = true +crate-type = ["lib"] + +[package.metadata.example.camera_controller] +name = "Camera Controller" +description = "Example Free-Cam Styled Camera Controller" +category = "Helpers" +wasm = true + +[[example]] +name = "widgets" +path = "examples/helpers/widgets.rs" +doc-scrape-examples = true +crate-type = ["lib"] + +[package.metadata.example.widgets] +name = "Widgets" +description = "Example UI Widgets" +category = "Helpers" +wasm = true diff --git a/examples/README.md b/examples/README.md index 1d18be05ce..b8cd2c891b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -51,6 +51,7 @@ git checkout v0.4.0 - [ECS (Entity Component System)](#ecs-entity-component-system) - [Games](#games) - [Gizmos](#gizmos) + - [Helpers](#helpers) - [Input](#input) - [Math](#math) - [Movement](#movement) @@ -352,6 +353,13 @@ Example | Description [Axes](../examples/gizmos/axes.rs) | Demonstrates the function of axes gizmos [Light Gizmos](../examples/gizmos/light_gizmos.rs) | A scene showcasing light gizmos +## Helpers + +Example | Description +--- | --- +[Camera Controller](../examples/helpers/camera_controller.rs) | Example Free-Cam Styled Camera Controller +[Widgets](../examples/helpers/widgets.rs) | Example UI Widgets + ## Input Example | Description diff --git a/examples/helpers/camera_controller.rs b/examples/helpers/camera_controller.rs index d061def030..07f0f31b11 100644 --- a/examples/helpers/camera_controller.rs +++ b/examples/helpers/camera_controller.rs @@ -2,6 +2,8 @@ //! To use in your own application: //! - Copy the code for the [`CameraControllerPlugin`] and add the plugin to your App. //! - Attach the [`CameraController`] component to an entity with a [`Camera3d`]. +//! +//! Unlike other examples, which demonstrate an application, this demonstrates a plugin library. use bevy::{ input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseScrollUnit}, @@ -10,6 +12,7 @@ use bevy::{ }; use std::{f32::consts::*, fmt}; +/// A freecam-style camera controller plugin. pub struct CameraControllerPlugin; impl Plugin for CameraControllerPlugin { @@ -23,26 +26,48 @@ impl Plugin for CameraControllerPlugin { /// it because it felt nice. pub const RADIANS_PER_DOT: f32 = 1.0 / 180.0; +/// Camera controller [`Component`]. #[derive(Component)] pub struct CameraController { + /// Enables this [`CameraController`] when `true`. pub enabled: bool, + /// Indicates if this controller has been initialized by the [`CameraControllerPlugin`]. pub initialized: bool, + /// Multiplier for pitch and yaw rotation speed. pub sensitivity: f32, + /// [`KeyCode`] for forward translation. pub key_forward: KeyCode, + /// [`KeyCode`] for backward translation. pub key_back: KeyCode, + /// [`KeyCode`] for left translation. pub key_left: KeyCode, + /// [`KeyCode`] for right translation. pub key_right: KeyCode, + /// [`KeyCode`] for up translation. pub key_up: KeyCode, + /// [`KeyCode`] for down translation. pub key_down: KeyCode, + /// [`KeyCode`] to use [`run_speed`](CameraController::run_speed) instead of + /// [`walk_speed`](CameraController::walk_speed) for translation. pub key_run: KeyCode, + /// [`MouseButton`] for grabbing the mouse focus. pub mouse_key_cursor_grab: MouseButton, + /// [`KeyCode`] for grabbing the keyboard focus. pub keyboard_key_toggle_cursor_grab: KeyCode, + /// Multiplier for unmodified translation speed. pub walk_speed: f32, + /// Multiplier for running translation speed. pub run_speed: f32, + /// Multiplier for how the mouse scroll wheel modifies [`walk_speed`](CameraController::walk_speed) + /// and [`run_speed`](CameraController::run_speed). pub scroll_factor: f32, + /// Friction factor used to exponentially decay [`velocity`](CameraController::velocity) over time. pub friction: f32, + /// This [`CameraController`]'s pitch rotation. pub pitch: f32, + /// This [`CameraController`]'s yaw rotation. pub yaw: f32, + /// This [`CameraController`]'s translation velocity. pub velocity: Vec3, } diff --git a/examples/helpers/widgets.rs b/examples/helpers/widgets.rs index ce3168e55d..5d83c18c12 100644 --- a/examples/helpers/widgets.rs +++ b/examples/helpers/widgets.rs @@ -1,4 +1,6 @@ //! Simple widgets for example UI. +//! +//! Unlike other examples, which demonstrate an application, this demonstrates a plugin library. use bevy::{ecs::system::EntityCommands, prelude::*}; From d70c469483b0d4ba201cecc6d2925de28df7ffae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Maita?= <47983254+mnmaita@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:34:33 +0100 Subject: [PATCH 11/74] Update ui_test requirement from 0.23.0 to 0.29.1 (#18289) # Objective - Fixes #18223. ## Solution - Updated ui_test requirement from 0.23.0 to 0.29.1. - Updated code to use the new APIs. ## Testing - Ran CI locally. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/compile_fail_utils/Cargo.toml | 2 +- tools/compile_fail_utils/src/lib.rs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/compile_fail_utils/Cargo.toml b/tools/compile_fail_utils/Cargo.toml index 7fa33d7962..215c82d419 100644 --- a/tools/compile_fail_utils/Cargo.toml +++ b/tools/compile_fail_utils/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -ui_test = "0.23.0" +ui_test = "0.29.1" [[test]] name = "example" diff --git a/tools/compile_fail_utils/src/lib.rs b/tools/compile_fail_utils/src/lib.rs index 816e81d062..46e975a29f 100644 --- a/tools/compile_fail_utils/src/lib.rs +++ b/tools/compile_fail_utils/src/lib.rs @@ -7,13 +7,14 @@ use std::{ pub use ui_test; use ui_test::{ + bless_output_files, color_eyre::eyre::eyre, default_file_filter, default_per_file_config, dependencies::DependencyBuilder, - run_tests_generic, + ignore_output_conflict, run_tests_generic, spanned::Spanned, status_emitter::{Gha, StatusEmitter, Text}, - Args, Config, OutputConflictHandling, + Args, Config, }; /// Use this instead of hand rolling configs. @@ -44,10 +45,10 @@ fn basic_config(root_dir: impl Into, args: &Args) -> ui_test::Result Date: Thu, 13 Mar 2025 09:35:32 -0700 Subject: [PATCH 12/74] Update computed_states example to use children macro (#18290) # Objective Contributes to #18238 Updates the `computed_states`, example to use the `children!` macro. Note that this example requires `--features bevy_dev_tools` to run ## Solution Updates examples to use the Improved Spawning API merged in https://github.com/bevyengine/bevy/pull/17521 ## Testing - Did you test these changes? If so, how? - Opened the examples before and after and verified the same behavior was observed. I did this on Ubuntu 24.04.2 LTS using `--features wayland`. - Are there any parts that need more testing? - Other OS's and features can't hurt, but this is such a small change it shouldn't be a problem. - How can other people (reviewers) test your changes? Is there anything specific they need to know? - Run the examples yourself with and without these changes. - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? - see above --- ## Showcase n/a ## Migration Guide n/a --- examples/state/computed_states.rs | 253 ++++++++++++++---------------- 1 file changed, 117 insertions(+), 136 deletions(-) diff --git a/examples/state/computed_states.rs b/examples/state/computed_states.rs index 4c5ea224e3..048a96eda3 100644 --- a/examples/state/computed_states.rs +++ b/examples/state/computed_states.rs @@ -336,19 +336,19 @@ mod ui { pub fn setup_menu(mut commands: Commands, tutorial_state: Res>) { let button_entity = commands - .spawn(Node { - // center button - width: Val::Percent(100.), - height: Val::Percent(100.), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - flex_direction: FlexDirection::Column, - row_gap: Val::Px(10.), - ..default() - }) - .with_children(|parent| { - parent - .spawn(( + .spawn(( + Node { + // center button + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.), + ..default() + }, + children![ + ( Button, Node { width: Val::Px(200.), @@ -361,20 +361,16 @@ mod ui { }, BackgroundColor(NORMAL_BUTTON), MenuButton::Play, - )) - .with_children(|parent| { - parent.spawn(( + children![( Text::new("Play"), TextFont { font_size: 33.0, ..default() }, TextColor(Color::srgb(0.9, 0.9, 0.9)), - )); - }); - - parent - .spawn(( + )], + ), + ( Button, Node { width: Val::Px(200.), @@ -390,18 +386,17 @@ mod ui { TutorialState::Inactive => NORMAL_BUTTON, }), MenuButton::Tutorial, - )) - .with_children(|parent| { - parent.spawn(( + children![( Text::new("Tutorial"), TextFont { font_size: 33.0, ..default() }, TextColor(Color::srgb(0.9, 0.9, 0.9)), - )); - }); - }) + )] + ), + ], + )) .id(); commands.insert_resource(MenuData { root_entity: button_entity, @@ -453,75 +448,66 @@ mod ui { pub fn setup_paused_screen(mut commands: Commands) { info!("Printing Pause"); - commands - .spawn(( - StateScoped(IsPaused::Paused), + commands.spawn(( + StateScoped(IsPaused::Paused), + Node { + // center button + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.), + position_type: PositionType::Absolute, + ..default() + }, + children![( Node { - // center button - width: Val::Percent(100.), - height: Val::Percent(100.), + width: Val::Px(400.), + height: Val::Px(400.), + // horizontally center child text justify_content: JustifyContent::Center, + // vertically center child text align_items: AlignItems::Center, - flex_direction: FlexDirection::Column, - row_gap: Val::Px(10.), - position_type: PositionType::Absolute, ..default() }, - )) - .with_children(|parent| { - parent - .spawn(( - Node { - width: Val::Px(400.), - height: Val::Px(400.), - // horizontally center child text - justify_content: JustifyContent::Center, - // vertically center child text - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(NORMAL_BUTTON), - MenuButton::Play, - )) - .with_children(|parent| { - parent.spawn(( - Text::new("Paused"), - TextFont { - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - )); - }); - }); - } - - pub fn setup_turbo_text(mut commands: Commands) { - commands - .spawn(( - StateScoped(TurboMode), - Node { - // center button - width: Val::Percent(100.), - height: Val::Percent(100.), - justify_content: JustifyContent::Start, - align_items: AlignItems::Center, - flex_direction: FlexDirection::Column, - row_gap: Val::Px(10.), - position_type: PositionType::Absolute, - ..default() - }, - )) - .with_children(|parent| { - parent.spawn(( - Text::new("TURBO MODE"), + BackgroundColor(NORMAL_BUTTON), + MenuButton::Play, + children![( + Text::new("Paused"), TextFont { font_size: 33.0, ..default() }, - TextColor(Color::srgb(0.9, 0.3, 0.1)), - )); - }); + TextColor(Color::srgb(0.9, 0.9, 0.9)), + )], + ),], + )); + } + + pub fn setup_turbo_text(mut commands: Commands) { + commands.spawn(( + StateScoped(TurboMode), + Node { + // center button + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Start, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.), + position_type: PositionType::Absolute, + ..default() + }, + children![( + Text::new("TURBO MODE"), + TextFont { + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.3, 0.1)), + )], + )); } pub fn change_color(time: Res