From 042337f787e99612085e606e4ffdb20bb5f64db9 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 10 Jul 2025 22:23:13 +0000 Subject: [PATCH 01/25] add back all changes --- crates/bevy_ecs/src/component.rs | 3 + crates/bevy_ecs/src/entity_disabling.rs | 4 ++ crates/bevy_ecs/src/query/builder.rs | 3 + crates/bevy_ecs/src/query/iter.rs | 4 ++ crates/bevy_ecs/src/query/state.rs | 6 +- crates/bevy_ecs/src/resource.rs | 91 +++++++++++++++++++++++++ crates/bevy_ecs/src/world/mod.rs | 8 ++- crates/bevy_scene/src/lib.rs | 10 ++- crates/bevy_scene/src/scene_spawner.rs | 2 +- crates/bevy_scene/src/serde.rs | 61 +++++++++++++---- 10 files changed, 172 insertions(+), 20 deletions(-) diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index cfcde29ab2ff1..ef1e4a4c5e559 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -1734,6 +1734,9 @@ pub struct Components { components: Vec>, indices: TypeIdMap, resource_indices: TypeIdMap, + /// A lookup for the entities on which resources are stored. + /// It uses ComponentIds instead of TypeIds for untyped APIs + pub(crate) resource_entities: HashMap, // This is kept internal and local to verify that no deadlocks can occor. queued: bevy_platform::sync::RwLock, } diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 5d62011174dac..3c5523e4fd2b9 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -207,6 +207,7 @@ mod tests { use crate::{ prelude::World, query::{Has, With}, + resource::IsResource, }; use alloc::{vec, vec::Vec}; @@ -278,6 +279,9 @@ mod tests { let mut world = World::new(); world.register_disabling_component::(); + // We don't want to query resources for this test. + world.register_disabling_component::(); + world.spawn_empty(); world.spawn(Disabled); world.spawn(CustomDisabled); diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index b545caad8f92c..cbef579fbcf80 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -332,6 +332,9 @@ mod tests { #[test] fn builder_or() { let mut world = World::new(); + // We don't want to query resources for this test. + world.register_disabling_component::(); + world.spawn((A(0), B(0))); world.spawn(B(0)); world.spawn(C(0)); diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index eb49204434b6f..0b8a8b9a91745 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -2659,6 +2659,7 @@ mod tests { use crate::component::Component; use crate::entity::Entity; use crate::prelude::World; + use crate::resource::IsResource; #[derive(Component, Debug, PartialEq, PartialOrd, Clone, Copy)] struct A(f32); @@ -2669,6 +2670,9 @@ mod tests { #[test] fn query_iter_sorts() { let mut world = World::new(); + // We don't want to query resources for this test. + world.register_disabling_component::(); + for i in 0..100 { world.spawn(A(i as f32)); world.spawn((A(i as f32), Sparse(i))); diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 00d8b6f97085b..e9188ba0bd740 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1850,6 +1850,9 @@ mod tests { #[test] fn can_transmute_empty_tuple() { let mut world = World::new(); + // We don't want to query resources for this test. + world.register_disabling_component::(); + world.register_component::(); let entity = world.spawn(A(10)).id(); @@ -2207,6 +2210,7 @@ mod tests { #[test] fn query_default_filters_updates_is_dense() { let mut world = World::new(); + let num_resources = world.components().num_resources(); world.spawn((Table, Sparse)); world.spawn(Table); world.spawn(Sparse); @@ -2214,7 +2218,7 @@ mod tests { let mut query = QueryState::<()>::new(&mut world); // There are no sparse components involved thus the query is dense assert!(query.is_dense); - assert_eq!(3, query.iter(&world).count()); + assert_eq!(3, query.iter(&world).count() - num_resources); let mut df = DefaultQueryFilters::empty(); df.register_disabling_component(world.register_component::()); diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 7da4f31113f4f..27ce87cffaa48 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -1,5 +1,10 @@ //! Resources are unique, singleton-like data types that can be accessed from systems and stored in the [`World`](crate::world::World). +use crate::prelude::Component; +use crate::prelude::ReflectComponent; +use bevy_reflect::prelude::ReflectDefault; +use bevy_reflect::Reflect; +use core::marker::PhantomData; // The derive macro for the `Resource` trait pub use bevy_ecs_macros::Resource; @@ -73,3 +78,89 @@ pub use bevy_ecs_macros::Resource; note = "consider annotating `{Self}` with `#[derive(Resource)]`" )] pub trait Resource: Send + Sync + 'static {} + +/// A marker component for the entity that stores the resource of type `T`. +/// +/// This component is automatically inserted when a resource of type `T` is inserted into the world, +/// and can be used to find the entity that stores a particular resource. +/// +/// By contrast, the [`IsResource`] component is used to find all entities that store resources, +/// regardless of the type of resource they store. +/// +/// This component comes with a hook that ensures that at most one entity has this component for any given `R`: +/// adding this component to an entity (or spawning an entity with this component) will despawn any other entity with this component. +#[derive(Component, Debug)] +#[require(IsResource)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Default))] +pub struct ResourceEntity(#[reflect(ignore)] PhantomData); + +impl Default for ResourceEntity { + fn default() -> Self { + ResourceEntity(PhantomData) + } +} + +/// A marker component for entities which store resources. +/// +/// By contrast, the [`ResourceEntity`] component is used to find the entity that stores a particular resource. +/// This component is required by the [`ResourceEntity`] component, and will automatically be added. +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Component, Default, Debug) +)] +#[derive(Component, Default, Debug)] +pub struct IsResource; + +#[cfg(test)] +#[expect(clippy::print_stdout, reason = "Allowed in tests.")] +mod tests { + use crate::change_detection::MaybeLocation; + use crate::ptr::PtrMut; + use crate::resource::Resource; + use crate::world::World; + use bevy_platform::prelude::String; + use core::mem::ManuallyDrop; + + #[test] + fn unique_resource_entities() { + #[derive(Default, Resource)] + struct TestResource1; + + #[derive(Resource)] + #[expect(dead_code, reason = "field needed for testing")] + struct TestResource2(String); + + #[derive(Resource)] + #[expect(dead_code, reason = "field needed for testing")] + struct TestResource3(u8); + + let mut world = World::new(); + let start = world.entities().len(); + world.init_resource::(); + assert_eq!(world.entities().len(), start + 1); + world.insert_resource(TestResource2(String::from("Foo"))); + assert_eq!(world.entities().len(), start + 2); + // like component registration, which just makes it known to the world that a component exists, + // registering a resource should not spawn an entity. + let id = world.register_resource::(); + assert_eq!(world.entities().len(), start + 2); + unsafe { + // SAFETY + // * + world.insert_resource_by_id( + id, + PtrMut::from(&mut ManuallyDrop::new(20 as u8)).promote(), + MaybeLocation::caller(), + ); + } + assert_eq!(world.entities().len(), start + 3); + assert!(world.remove_resource_by_id(id).is_some()); + assert_eq!(world.entities().len(), start + 2); + world.remove_resource::(); + assert_eq!(world.entities().len(), start + 1); + // make sure that trying to add a resource twice results, doesn't change the entity count + world.insert_resource(TestResource2(String::from("Bar"))); + assert_eq!(world.entities().len(), start + 1); + } +} diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 63e5d8543584d..9b27c32eab658 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -216,12 +216,14 @@ impl World { &mut self.entities } - /// Retrieves the number of [`Entities`] in the world. + /// Retrieves the number of [`Entities`] in the world. This count does not include resource entities. /// /// This is helpful as a diagnostic, but it can also be used effectively in tests. #[inline] - pub fn num_entities(&self) -> u32 { - self.entities.len() + pub fn entity_count(&self) -> u32 { + self.entities + .len() + .saturating_sub(self.components.resource_entities.len() as u32) } /// Retrieves this world's [`Archetypes`] collection. diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 9b0845f80f373..b2936aae39997 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -49,7 +49,13 @@ pub mod prelude { use bevy_app::prelude::*; #[cfg(feature = "serialize")] -use {bevy_asset::AssetApp, bevy_ecs::schedule::IntoScheduleConfigs}; +use { + bevy_asset::AssetApp, + bevy_ecs::schedule::IntoScheduleConfigs, + bevy_ecs::{ + entity_disabling::DefaultQueryFilters, resource::IsResource, resource::ResourceEntity, + }, +}; /// Plugin that provides scene functionality to an [`App`]. #[derive(Default)] @@ -64,6 +70,8 @@ impl Plugin for ScenePlugin { .init_resource::() .register_type::() .register_type::() + .register_type::() + .register_type::>() .add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain()); // Register component hooks for DynamicSceneRoot diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 71cd848751894..bbd967527c363 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -609,7 +609,7 @@ mod tests { assert_eq!(scene_component_a.y, 4.0); assert_eq!( app.world().entity(entity).get::().unwrap().len(), - 1 + 3 // two resources-as-entities are also counted ); // let's try to delete the scene diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index cb8206d3dd33f..734f44498654b 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -516,9 +516,11 @@ mod tests { }; use bevy_ecs::{ entity::{Entity, EntityHashMap}, + entity_disabling::DefaultQueryFilters, prelude::{Component, ReflectComponent, ReflectResource, Resource, World}, query::{With, Without}, reflect::AppTypeRegistry, + resource::{IsResource, ResourceEntity}, world::FromWorld, }; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; @@ -611,6 +613,8 @@ mod tests { registry.register::(); registry.register::(); registry.register::(); + registry.register::(); + registry.register::>(); } world.insert_resource(registry); world @@ -638,20 +642,20 @@ mod tests { ), }, entities: { - 4294967293: ( + 4294967291: ( components: { "bevy_scene::serde::tests::Bar": (345), "bevy_scene::serde::tests::Baz": (789), "bevy_scene::serde::tests::Foo": (123), }, ), - 4294967294: ( + 4294967292: ( components: { "bevy_scene::serde::tests::Bar": (345), "bevy_scene::serde::tests::Foo": (123), }, ), - 4294967295: ( + 4294967293: ( components: { "bevy_scene::serde::tests::Foo": (123), }, @@ -757,7 +761,7 @@ mod tests { .write_to_world(&mut dst_world, &mut map) .unwrap(); - assert_eq!(2, deserialized_scene.entities.len()); + assert_eq!(4, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); let bar_to_foo = dst_world @@ -785,7 +789,7 @@ mod tests { let (scene, deserialized_scene) = roundtrip_ron(&world); - assert_eq!(1, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); let mut world = create_world(); @@ -815,10 +819,19 @@ mod tests { assert_eq!( vec![ - 0, 1, 255, 255, 255, 255, 15, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, + 0, 3, 253, 255, 255, 255, 15, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 2, 3, 102, 102, 166, 63, 205, 204, - 108, 64, 1, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 108, 64, 1, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 254, 255, + 255, 255, 15, 1, 30, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, 101, 115, + 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, 101, 115, 111, 117, 114, 99, 101, 255, + 255, 255, 255, 15, 2, 30, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, 101, + 115, 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, 101, 115, 111, 117, 114, 99, 101, + 83, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, 101, 115, 111, 117, 114, 99, + 101, 58, 58, 82, 101, 115, 111, 117, 114, 99, 101, 69, 110, 116, 105, 116, 121, 60, + 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 101, 110, 116, 105, 116, 121, 95, 100, + 105, 115, 97, 98, 108, 105, 110, 103, 58, 58, 68, 101, 102, 97, 117, 108, 116, 81, + 117, 101, 114, 121, 70, 105, 108, 116, 101, 114, 115, 62 ], serialized_scene ); @@ -830,7 +843,7 @@ mod tests { .deserialize(&mut postcard::Deserializer::from_bytes(&serialized_scene)) .unwrap(); - assert_eq!(1, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); } @@ -856,11 +869,21 @@ mod tests { assert_eq!( vec![ - 146, 128, 129, 206, 255, 255, 255, 255, 145, 129, 217, 37, 98, 101, 118, 121, 95, + 146, 128, 131, 206, 255, 255, 255, 253, 145, 129, 217, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 147, 147, 1, 2, 3, 146, 202, 63, 166, 102, 102, 202, 64, 108, 204, 205, 129, 165, 84, 117, 112, - 108, 101, 172, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 108, 101, 172, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 206, 255, + 255, 255, 254, 145, 129, 190, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, + 101, 115, 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, 101, 115, 111, 117, 114, 99, + 101, 144, 206, 255, 255, 255, 255, 145, 130, 190, 98, 101, 118, 121, 95, 101, 99, + 115, 58, 58, 114, 101, 115, 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, 101, 115, + 111, 117, 114, 99, 101, 144, 217, 83, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, + 114, 101, 115, 111, 117, 114, 99, 101, 58, 58, 82, 101, 115, 111, 117, 114, 99, + 101, 69, 110, 116, 105, 116, 121, 60, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, + 101, 110, 116, 105, 116, 121, 95, 100, 105, 115, 97, 98, 108, 105, 110, 103, 58, + 58, 68, 101, 102, 97, 117, 108, 116, 81, 117, 101, 114, 121, 70, 105, 108, 116, + 101, 114, 115, 62, 144 ], buf ); @@ -874,7 +897,7 @@ mod tests { .deserialize(&mut rmp_serde::Deserializer::new(&mut reader)) .unwrap(); - assert_eq!(1, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); } @@ -899,13 +922,23 @@ mod tests { assert_eq!( vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 253, 255, 255, 255, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 37, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 102, 102, 166, 63, 205, 204, 108, 64, 1, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, - 100, 33 + 100, 33, 254, 255, 255, 255, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, + 0, 0, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, 101, 115, 111, 117, 114, + 99, 101, 58, 58, 73, 115, 82, 101, 115, 111, 117, 114, 99, 101, 255, 255, 255, 255, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, + 101, 99, 115, 58, 58, 114, 101, 115, 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, + 101, 115, 111, 117, 114, 99, 101, 83, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, + 101, 99, 115, 58, 58, 114, 101, 115, 111, 117, 114, 99, 101, 58, 58, 82, 101, 115, + 111, 117, 114, 99, 101, 69, 110, 116, 105, 116, 121, 60, 98, 101, 118, 121, 95, + 101, 99, 115, 58, 58, 101, 110, 116, 105, 116, 121, 95, 100, 105, 115, 97, 98, 108, + 105, 110, 103, 58, 58, 68, 101, 102, 97, 117, 108, 116, 81, 117, 101, 114, 121, 70, + 105, 108, 116, 101, 114, 115, 62 ], serialized_scene ); @@ -918,7 +951,7 @@ mod tests { bincode::serde::seed_decode_from_slice(scene_deserializer, &serialized_scene, config) .unwrap(); - assert_eq!(1, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); } From 64eac9a9a563c4477d5e5cd20d4db135d16a9cec Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 10 Jul 2025 22:39:22 +0000 Subject: [PATCH 02/25] cargo fmt --- crates/bevy_ecs/src/component/info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 9f122dbd82f73..0ccd681964338 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -1,5 +1,5 @@ use alloc::{borrow::Cow, vec::Vec}; -use bevy_platform::{collections::HashSet, sync::PoisonError, collections::HashMap}; +use bevy_platform::{collections::HashMap, collections::HashSet, sync::PoisonError}; use bevy_ptr::OwningPtr; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; From 956c1a669aaf483c7101cbccf40f0d779dd9c76b Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 10 Jul 2025 22:48:11 +0000 Subject: [PATCH 03/25] cargo clippy --- crates/bevy_ecs/src/component/info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 0ccd681964338..c9aa707a74dfa 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -348,7 +348,7 @@ pub struct Components { pub(super) indices: TypeIdMap, pub(super) resource_indices: TypeIdMap, /// A lookup for the entities on which resources are stored. - /// It uses ComponentIds instead of TypeIds for untyped APIs + /// It uses `ComponentId`s instead of `TypeId`s for untyped APIs pub(crate) resource_entities: HashMap, // This is kept internal and local to verify that no deadlocks can occor. pub(super) queued: bevy_platform::sync::RwLock, From 073df4bf9df054318b304469093fb1f47ff13602 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 09:12:20 +0000 Subject: [PATCH 04/25] add entities with resources, auto disabled IsResource --- crates/bevy_ecs/src/entity_disabling.rs | 4 -- crates/bevy_ecs/src/query/builder.rs | 2 - crates/bevy_ecs/src/query/iter.rs | 3 -- crates/bevy_ecs/src/query/state.rs | 4 +- crates/bevy_ecs/src/resource.rs | 5 +++ crates/bevy_ecs/src/world/mod.rs | 53 +++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 3c5523e4fd2b9..5d62011174dac 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -207,7 +207,6 @@ mod tests { use crate::{ prelude::World, query::{Has, With}, - resource::IsResource, }; use alloc::{vec, vec::Vec}; @@ -279,9 +278,6 @@ mod tests { let mut world = World::new(); world.register_disabling_component::(); - // We don't want to query resources for this test. - world.register_disabling_component::(); - world.spawn_empty(); world.spawn(Disabled); world.spawn(CustomDisabled); diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index cbef579fbcf80..b7705694c993c 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -332,8 +332,6 @@ mod tests { #[test] fn builder_or() { let mut world = World::new(); - // We don't want to query resources for this test. - world.register_disabling_component::(); world.spawn((A(0), B(0))); world.spawn(B(0)); diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 0b8a8b9a91745..1771a464f9e4a 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -2659,7 +2659,6 @@ mod tests { use crate::component::Component; use crate::entity::Entity; use crate::prelude::World; - use crate::resource::IsResource; #[derive(Component, Debug, PartialEq, PartialOrd, Clone, Copy)] struct A(f32); @@ -2670,8 +2669,6 @@ mod tests { #[test] fn query_iter_sorts() { let mut world = World::new(); - // We don't want to query resources for this test. - world.register_disabling_component::(); for i in 0..100 { world.spawn(A(i as f32)); diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index e9188ba0bd740..fe37a76fe89d6 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1850,8 +1850,6 @@ mod tests { #[test] fn can_transmute_empty_tuple() { let mut world = World::new(); - // We don't want to query resources for this test. - world.register_disabling_component::(); world.register_component::(); let entity = world.spawn(A(10)).id(); @@ -2210,7 +2208,7 @@ mod tests { #[test] fn query_default_filters_updates_is_dense() { let mut world = World::new(); - let num_resources = world.components().num_resources(); + let num_resources = world.resource_count() as usize; world.spawn((Table, Sparse)); world.spawn(Table); world.spawn(Sparse); diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 27ce87cffaa48..de3233429830d 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -112,6 +112,11 @@ impl Default for ResourceEntity { #[derive(Component, Default, Debug)] pub struct IsResource; +/// Used in conjunction with [`ResourceEntity`], when no type information is available. +/// This is used by [`insert_resource_by_id`]. +#[derive(Resource)] +pub(crate) struct TypeErasedResource; + #[cfg(test)] #[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index e6c505462d36c..170a0157ad892 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -22,6 +22,7 @@ use crate::{ event::BufferedEvent, lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, prelude::{Add, Despawn, Insert, Remove, Replace}, + resource::{IsResource, ResourceEntity, TypeErasedResource}, }; pub use bevy_ecs_macros::FromWorld; use bevy_utils::prelude::DebugName; @@ -169,6 +170,7 @@ impl World { // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); + self.register_disabling_component::(); } /// Creates a new empty [`World`]. /// @@ -226,6 +228,12 @@ impl World { .saturating_sub(self.components.resource_entities.len() as u32) } + /// Retrieves the number of [`Resource`]s in the world. + #[inline] + pub fn resource_count(&self) -> u32 { + self.components.resource_entities.len() as u32 + } + /// Retrieves this world's [`Archetypes`] collection. #[inline] pub fn archetypes(&self) -> &Archetypes { @@ -1702,6 +1710,18 @@ impl World { pub fn init_resource(&mut self) -> ComponentId { let caller = MaybeLocation::caller(); let component_id = self.components_registrator().register_resource::(); + + if !self + .components + .resource_entities + .contains_key(&component_id) + { + let entity = self.spawn(ResourceEntity::::default()).id(); + self.components + .resource_entities + .insert(component_id, entity); + } + if self .storages .resources @@ -1739,6 +1759,17 @@ impl World { caller: MaybeLocation, ) { let component_id = self.components_registrator().register_resource::(); + if !self + .components + .resource_entities + .contains_key(&component_id) + { + let entity = self.spawn(ResourceEntity::::default()).id(); + self.components + .resource_entities + .insert(component_id, entity); + } + OwningPtr::make(value, |ptr| { // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { @@ -1806,6 +1837,10 @@ impl World { #[inline] pub fn remove_resource(&mut self) -> Option { let component_id = self.components.get_valid_resource_id(TypeId::of::())?; + if let Some(entity) = self.components.resource_entities.remove(&component_id) { + self.despawn(entity); + } + let (ptr, _, _) = self.storages.resources.get_mut(component_id)?.remove()?; // SAFETY: `component_id` was gotten via looking up the `R` type unsafe { Some(ptr.read::()) } @@ -2709,6 +2744,20 @@ impl World { ) { let change_tick = self.change_tick(); + if !self + .components + .resource_entities + .contains_key(&component_id) + { + // Since we don't know the type, we use a placeholder type. + let entity = self + .spawn(ResourceEntity::::default()) + .id(); + self.components + .resource_entities + .insert(component_id, entity); + } + let resource = self.initialize_resource_internal(component_id); // SAFETY: `value` is valid for `component_id`, ensured by caller unsafe { @@ -3401,6 +3450,10 @@ impl World { /// **You should prefer to use the typed API [`World::remove_resource`] where possible and only /// use this in cases where the actual types are not known at compile time.** pub fn remove_resource_by_id(&mut self, component_id: ComponentId) -> Option<()> { + if let Some(entity) = self.components.resource_entities.remove(&component_id) { + self.despawn(entity); + } + self.storages .resources .get_mut(component_id)? From 6c8281d568c8b51cfad8c540b49a6c39f2a396d2 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 09:48:53 +0000 Subject: [PATCH 05/25] fixed and tests --- crates/bevy_ecs/src/world/entity_ref.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 90438b8d6e1e6..40e5944532aa0 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -4867,6 +4867,7 @@ mod tests { change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, prelude::*, + resource::IsResource, system::{assert_is_system, RunSystemOnce as _}, world::{error::EntityComponentError, DeferredWorld, FilteredEntityMut, FilteredEntityRef}, }; @@ -5253,7 +5254,7 @@ mod tests { world.spawn(TestComponent(0)).insert(TestComponent2(0)); - let mut query = world.query::>(); + let mut query = world.query::>(); let mut found = false; for entity_ref in query.iter_mut(&mut world) { @@ -5311,7 +5312,10 @@ mod tests { world.run_system_once(system).unwrap(); - fn system(_: Query<&mut TestComponent>, query: Query>) { + fn system( + _: Query<&mut TestComponent>, + query: Query>, + ) { for entity_ref in query.iter() { assert!(matches!( entity_ref.get::(), @@ -5328,7 +5332,7 @@ mod tests { let mut world = World::new(); world.spawn(TestComponent(0)).insert(TestComponent2(0)); - let mut query = world.query::>(); + let mut query = world.query::>(); let mut found = false; for mut entity_mut in query.iter_mut(&mut world) { @@ -5393,7 +5397,10 @@ mod tests { world.run_system_once(system).unwrap(); - fn system(_: Query<&mut TestComponent>, mut query: Query>) { + fn system( + _: Query<&mut TestComponent>, + mut query: Query>, + ) { for mut entity_mut in query.iter_mut() { assert!(entity_mut .get_mut::() From f9dc9f1619eb76e3f9e62d705fa8c5ad09af8f88 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 10:10:34 +0000 Subject: [PATCH 06/25] fixed more tests --- crates/bevy_ecs/src/name.rs | 2 +- crates/bevy_ecs/src/query/builder.rs | 3 +++ crates/bevy_ecs/src/query/state.rs | 7 +++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 317c8f5017bb5..2887475cc648f 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -278,7 +278,7 @@ mod tests { let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} - assert_eq!(d1.to_string(), "0v0"); + assert_eq!(d1.to_string(), "1v0"); let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index b7705694c993c..33266f0c1c874 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -275,6 +275,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { mod tests { use crate::{ prelude::*, + resource::IsResource, world::{EntityMutExcept, EntityRefExcept, FilteredEntityMut, FilteredEntityRef}, }; use std::dbg; @@ -486,6 +487,7 @@ mod tests { let mut query = QueryBuilder::<(FilteredEntityMut, EntityMutExcept)>::new(&mut world) .data::() + .filter::>() .build(); // Removing `EntityMutExcept` just leaves A @@ -497,6 +499,7 @@ mod tests { let mut query = QueryBuilder::<(FilteredEntityMut, EntityRefExcept)>::new(&mut world) .data::() + .filter::>() .build(); // Removing `EntityRefExcept` just leaves A, plus read access diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index fe37a76fe89d6..a89c4c77cafb2 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1777,6 +1777,7 @@ mod tests { component::Component, entity_disabling::DefaultQueryFilters, prelude::*, + resource::IsResource, system::{QueryLens, RunSystemOnce}, world::{FilteredEntityMut, FilteredEntityRef}, }; @@ -2176,6 +2177,7 @@ mod tests { let mut df = DefaultQueryFilters::empty(); df.register_disabling_component(world.register_component::()); world.insert_resource(df); + world.register_disabling_component::(); // Without only matches the first entity let mut query = QueryState::<()>::new(&mut world); @@ -2208,7 +2210,6 @@ mod tests { #[test] fn query_default_filters_updates_is_dense() { let mut world = World::new(); - let num_resources = world.resource_count() as usize; world.spawn((Table, Sparse)); world.spawn(Table); world.spawn(Sparse); @@ -2216,11 +2217,12 @@ mod tests { let mut query = QueryState::<()>::new(&mut world); // There are no sparse components involved thus the query is dense assert!(query.is_dense); - assert_eq!(3, query.iter(&world).count() - num_resources); + assert_eq!(3, query.iter(&world).count()); let mut df = DefaultQueryFilters::empty(); df.register_disabling_component(world.register_component::()); world.insert_resource(df); + world.register_disabling_component::(); let mut query = QueryState::<()>::new(&mut world); // The query doesn't ask for sparse components, but the default filters adds @@ -2231,6 +2233,7 @@ mod tests { let mut df = DefaultQueryFilters::empty(); df.register_disabling_component(world.register_component::()); world.insert_resource(df); + world.register_disabling_component::(); let mut query = QueryState::<()>::new(&mut world); // If the filter is instead a table components, the query can still be dense From 07723acf392df8ce903be065f365bbb1857016d7 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 11:03:09 +0000 Subject: [PATCH 07/25] fixed moore tests --- crates/bevy_ecs/src/lib.rs | 12 ++++++------ crates/bevy_ecs/src/system/system.rs | 4 ++-- crates/bevy_ecs/src/world/mod.rs | 12 +++++++----- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 8a07cdc8e1b92..ce566e96d6de2 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -378,9 +378,9 @@ mod tests { let mut world = World::new(); let e = world.spawn((TableStored("abc"), A(123))).id(); let f = world.spawn((TableStored("def"), A(456))).id(); - assert_eq!(world.entities.len(), 2); + assert_eq!(world.entity_count(), 2); assert!(world.despawn(e)); - assert_eq!(world.entities.len(), 1); + assert_eq!(world.entity_count(), 1); assert!(world.get::(e).is_none()); assert!(world.get::(e).is_none()); assert_eq!(world.get::(f).unwrap().0, "def"); @@ -393,9 +393,9 @@ mod tests { let e = world.spawn((TableStored("abc"), SparseStored(123))).id(); let f = world.spawn((TableStored("def"), SparseStored(456))).id(); - assert_eq!(world.entities.len(), 2); + assert_eq!(world.entity_count(), 2); assert!(world.despawn(e)); - assert_eq!(world.entities.len(), 1); + assert_eq!(world.entity_count(), 1); assert!(world.get::(e).is_none()); assert!(world.get::(e).is_none()); assert_eq!(world.get::(f).unwrap().0, "def"); @@ -1786,7 +1786,7 @@ mod tests { fn try_insert_batch() { let mut world = World::default(); let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw_u32(1).unwrap(); + let e1 = Entity::from_raw_u32(2).unwrap(); let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; @@ -1810,7 +1810,7 @@ mod tests { fn try_insert_batch_if_new() { let mut world = World::default(); let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw_u32(1).unwrap(); + let e1 = Entity::from_raw_u32(2).unwrap(); let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index aad37c09d01f4..0e90bf2201868 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -494,9 +494,9 @@ mod tests { #[test] fn command_processing() { let mut world = World::new(); - assert_eq!(world.entities.len(), 0); + assert_eq!(world.entity_count(), 0); world.run_system_once(spawn_entity).unwrap(); - assert_eq!(world.entities.len(), 1); + assert_eq!(world.entity_count(), 1); } #[test] diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 170a0157ad892..1cd215e15790c 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -3727,7 +3727,7 @@ mod tests { entity::EntityHashSet, entity_disabling::{DefaultQueryFilters, Disabled}, ptr::OwningPtr, - resource::Resource, + resource::{IsResource, Resource}, world::{error::EntityMutableFetchError, DeferredWorld}, }; use alloc::{ @@ -4159,8 +4159,10 @@ mod tests { let iterate_and_count_entities = |world: &World, entity_counters: &mut HashMap<_, _>| { entity_counters.clear(); for entity in world.iter_entities() { - let counter = entity_counters.entry(entity.id()).or_insert(0); - *counter += 1; + if !entity.contains::() { + let counter = entity_counters.entry(entity.id()).or_insert(0); + *counter += 1; + } } }; @@ -4257,9 +4259,9 @@ mod tests { let mut entities = world.iter_entities_mut().collect::>(); entities.sort_by_key(|e| e.get::().map(|a| a.0).or(e.get::().map(|b| b.0))); - let (a, b) = entities.split_at_mut(2); + let (a, b) = entities.split_at_mut(3); core::mem::swap( - &mut a[1].get_mut::().unwrap().0, + &mut a[2].get_mut::().unwrap().0, &mut b[0].get_mut::().unwrap().0, ); assert_eq!(world.entity(a1).get(), Some(&A(0))); From 1ba7af2b28baf3c3901d643c33aa8b0952460fc2 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 11:16:11 +0000 Subject: [PATCH 08/25] fix ci --- crates/bevy_ecs/src/resource.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index de3233429830d..fda419c43401c 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -118,14 +118,12 @@ pub struct IsResource; pub(crate) struct TypeErasedResource; #[cfg(test)] -#[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { use crate::change_detection::MaybeLocation; - use crate::ptr::PtrMut; + use crate::ptr::OwningPtr; use crate::resource::Resource; use crate::world::World; use bevy_platform::prelude::String; - use core::mem::ManuallyDrop; #[test] fn unique_resource_entities() { @@ -150,15 +148,12 @@ mod tests { // registering a resource should not spawn an entity. let id = world.register_resource::(); assert_eq!(world.entities().len(), start + 2); - unsafe { - // SAFETY - // * - world.insert_resource_by_id( - id, - PtrMut::from(&mut ManuallyDrop::new(20 as u8)).promote(), - MaybeLocation::caller(), - ); - } + OwningPtr::make(20_u8, |ptr| { + // SAFETY: id was just initialized and corresponds to a resource. + unsafe { + world.insert_resource_by_id(id, ptr, MaybeLocation::caller()); + } + }); assert_eq!(world.entities().len(), start + 3); assert!(world.remove_resource_by_id(id).is_some()); assert_eq!(world.entities().len(), start + 2); From b89c0420faa25350735a11ab8d0120d5bb724a2c Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 11:32:54 +0000 Subject: [PATCH 09/25] fix mooore tests (benches) --- benches/benches/bevy_ecs/world/world_get.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index 81e0bf2b0f511..8efe626a4b542 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -51,9 +51,10 @@ pub fn world_entity(criterion: &mut Criterion) { for entity_count in RANGE.map(|i| i * 10_000) { group.bench_function(format!("{entity_count}_entities"), |bencher| { let world = setup::
(entity_count); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -74,9 +75,10 @@ pub fn world_get(criterion: &mut Criterion) { for entity_count in RANGE.map(|i| i * 10_000) { group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let world = setup::
(entity_count); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -86,9 +88,10 @@ pub fn world_get(criterion: &mut Criterion) { }); group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let world = setup::(entity_count); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -109,10 +112,11 @@ pub fn world_query_get(criterion: &mut Criterion) { for entity_count in RANGE.map(|i| i * 10_000) { group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let mut world = setup::
(entity_count); + let offset = world.resource_count(); let mut query = world.query::<&Table>(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -137,9 +141,10 @@ pub fn world_query_get(criterion: &mut Criterion) { &WideTable<4>, &WideTable<5>, )>(); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -149,10 +154,11 @@ pub fn world_query_get(criterion: &mut Criterion) { }); group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let mut world = setup::(entity_count); + let offset = world.resource_count(); let mut query = world.query::<&Sparse>(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -177,9 +183,10 @@ pub fn world_query_get(criterion: &mut Criterion) { &WideSparse<4>, &WideSparse<5>, )>(); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { // SAFETY: Range is exclusive. let entity = Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); From 9b33933fc5d60d3bf7236e11e8aaf5708de3e323 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 11:48:51 +0000 Subject: [PATCH 10/25] fix docs --- crates/bevy_ecs/src/resource.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index fda419c43401c..06c5a05c30269 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -113,7 +113,7 @@ impl Default for ResourceEntity { pub struct IsResource; /// Used in conjunction with [`ResourceEntity`], when no type information is available. -/// This is used by [`insert_resource_by_id`]. +/// This is used by [`World::insert_resource_by_id`](crate::world::World). #[derive(Resource)] pub(crate) struct TypeErasedResource; From c7b4f1d0b1f3e650daec76736136c83dff870938 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 12:28:51 +0000 Subject: [PATCH 11/25] add migration guide --- .../resources_as_components.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 release-content/migration-guides/resources_as_components.md diff --git a/release-content/migration-guides/resources_as_components.md b/release-content/migration-guides/resources_as_components.md new file mode 100644 index 0000000000000..109a48d809bc8 --- /dev/null +++ b/release-content/migration-guides/resources_as_components.md @@ -0,0 +1,25 @@ +--- +title: Resources as Components +pull_requests: [19711] +--- + +Resources are very similair to Components: they are both data that can be stored in the ECS and queried. +The only real difference between them is that querying a resource will return either one or zero resources, whereas querying for a component can return any number of entities that match it. + +Even so, resources and components have always been seperate concepts within the ECS. +This leads to some annoying restrictions. +While components have [`ComponentHooks`](https://docs.rs/bevy/latest/bevy/ecs/component/struct.ComponentHooks.html), it's not possible to add lifecycle hooks to resources. +Moreover, the engine internals contain a lot of duplication because of it. + +This motivates us to transition resources to components, and while most of the public API will stay the same, some breaking changes are inevitable. + +This PR adds a dummy entity alongside every resource. This entity is inserted and removed alongside resources and doesn't do anything (yet). + +This changes `World::entities().len()` as there are more entities than you might expect there to be. For example, a new world, no longer contains zero entities. This is mostly important for unit tests. + +Two methods have been added `World::entity_count()` and `World::resource_count()`. The former returns the number of entities without the resource entities, while the latter returns the number of resources in the world. + +While the marker component `IsResource` is added to [default query filters](https://docs.rs/bevy/latest/bevy/ecs/entity_disabling/struct.DefaultQueryFilters.html), during world creation, resource entities might still show up in broad queries with [`EntityMutExcept`](https://docs.rs/bevy/latest/bevy/ecs/world/struct.EntityMutExcept.html) and [`EntityRefExcept`](https://docs.rs/bevy/latest/bevy/ecs/world/struct.EntityRefExcept.html). +They also show up in `World::iter_entities()`, `World::iter_entities_mut()` and [`QueryBuilder`](https://docs.rs/bevy/latest/bevy/ecs/prelude/struct.QueryBuilder.html). + +Lastly, because of the entity bump, the input and output of the `bevy_scene` crate is not equivalent to the previous version, meaning that it's unadvisable to read in scenes from the previous version into the current one. From 96aef455696ce0d2d767207c0c81c81e95449973 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 12:31:14 +0000 Subject: [PATCH 12/25] fixed spelling errors to prove I'm not AI --- release-content/migration-guides/resources_as_components.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-content/migration-guides/resources_as_components.md b/release-content/migration-guides/resources_as_components.md index 109a48d809bc8..919dbfbaf7b4a 100644 --- a/release-content/migration-guides/resources_as_components.md +++ b/release-content/migration-guides/resources_as_components.md @@ -3,10 +3,10 @@ title: Resources as Components pull_requests: [19711] --- -Resources are very similair to Components: they are both data that can be stored in the ECS and queried. +Resources are very similar to Components: they are both data that can be stored in the ECS and queried. The only real difference between them is that querying a resource will return either one or zero resources, whereas querying for a component can return any number of entities that match it. -Even so, resources and components have always been seperate concepts within the ECS. +Even so, resources and components have always been separate concepts within the ECS. This leads to some annoying restrictions. While components have [`ComponentHooks`](https://docs.rs/bevy/latest/bevy/ecs/component/struct.ComponentHooks.html), it's not possible to add lifecycle hooks to resources. Moreover, the engine internals contain a lot of duplication because of it. From 57581349b984732360cc43ff52a83a5bb6e0509d Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 13 Jul 2025 18:38:50 +0000 Subject: [PATCH 13/25] addressed comments --- crates/bevy_ecs/src/entity_disabling.rs | 3 +++ crates/bevy_ecs/src/query/state.rs | 5 +---- crates/bevy_ecs/src/world/mod.rs | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 5d62011174dac..5c412ca75e05b 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -70,6 +70,7 @@ use crate::{ component::{ComponentId, Components, StorageType}, query::FilteredAccess, + resource::IsResource, world::{FromWorld, World}, }; use bevy_ecs_macros::{Component, Resource}; @@ -143,6 +144,8 @@ impl FromWorld for DefaultQueryFilters { let mut filters = DefaultQueryFilters::empty(); let disabled_component_id = world.register_component::(); filters.register_disabling_component(disabled_component_id); + let is_resource_component_id = world.register_component::(); + filters.register_disabling_component(is_resource_component_id); filters } } diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index a89c4c77cafb2..afb3f4e022875 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -2174,10 +2174,7 @@ mod tests { world.spawn((B(0), C(0))); world.spawn(C(0)); - let mut df = DefaultQueryFilters::empty(); - df.register_disabling_component(world.register_component::()); - world.insert_resource(df); - world.register_disabling_component::(); + world.register_disabling_component::(); // Without only matches the first entity let mut query = QueryState::<()>::new(&mut world); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 1cd215e15790c..4ccd1a15748f0 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -22,7 +22,7 @@ use crate::{ event::BufferedEvent, lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, prelude::{Add, Despawn, Insert, Remove, Replace}, - resource::{IsResource, ResourceEntity, TypeErasedResource}, + resource::{ResourceEntity, TypeErasedResource}, }; pub use bevy_ecs_macros::FromWorld; use bevy_utils::prelude::DebugName; @@ -170,7 +170,6 @@ impl World { // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); - self.register_disabling_component::(); } /// Creates a new empty [`World`]. /// From 9824c3a5df04a05889a7423e0ecbeb00c90f5584 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Mon, 14 Jul 2025 22:37:34 +0000 Subject: [PATCH 14/25] testing robustness --- crates/bevy_ecs/src/query/state.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index afb3f4e022875..35251fe9d5179 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1777,7 +1777,6 @@ mod tests { component::Component, entity_disabling::DefaultQueryFilters, prelude::*, - resource::IsResource, system::{QueryLens, RunSystemOnce}, world::{FilteredEntityMut, FilteredEntityRef}, }; @@ -2216,10 +2215,9 @@ mod tests { assert!(query.is_dense); assert_eq!(3, query.iter(&world).count()); - let mut df = DefaultQueryFilters::empty(); - df.register_disabling_component(world.register_component::()); + let df = DefaultQueryFilters::from_world(&mut world); world.insert_resource(df); - world.register_disabling_component::(); + world.register_disabling_component::(); let mut query = QueryState::<()>::new(&mut world); // The query doesn't ask for sparse components, but the default filters adds @@ -2227,10 +2225,9 @@ mod tests { assert!(!query.is_dense); assert_eq!(1, query.iter(&world).count()); - let mut df = DefaultQueryFilters::empty(); - df.register_disabling_component(world.register_component::
()); + let df = DefaultQueryFilters::from_world(&mut world); world.insert_resource(df); - world.register_disabling_component::(); + world.register_disabling_component::
(); let mut query = QueryState::<()>::new(&mut world); // If the filter is instead a table components, the query can still be dense From 4d4e91476bf00d10da4c523af8ef1eeeb8568c27 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Wed, 23 Jul 2025 21:03:43 +0000 Subject: [PATCH 15/25] cleanup --- benches/benches/bevy_ecs/world/world_get.rs | 2 -- crates/bevy_ecs/src/query/builder.rs | 4 ---- crates/bevy_ecs/src/query/iter.rs | 1 - crates/bevy_ecs/src/query/state.rs | 2 -- crates/bevy_ecs/src/world/entity_ref.rs | 10 +++++----- crates/bevy_ecs/src/world/mod.rs | 21 +++++++++++---------- 6 files changed, 16 insertions(+), 24 deletions(-) diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index 4500e694ab4c6..dc5c2c5caf420 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -130,7 +130,6 @@ pub fn world_query_get(criterion: &mut Criterion) { &WideTable<4>, &WideTable<5>, )>(); - let offset = world.resource_count(); bencher.iter(|| { for entity in &entities { @@ -165,7 +164,6 @@ pub fn world_query_get(criterion: &mut Criterion) { &WideSparse<4>, &WideSparse<5>, )>(); - let offset = world.resource_count(); bencher.iter(|| { for entity in &entities { diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index 33266f0c1c874..b545caad8f92c 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -275,7 +275,6 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { mod tests { use crate::{ prelude::*, - resource::IsResource, world::{EntityMutExcept, EntityRefExcept, FilteredEntityMut, FilteredEntityRef}, }; use std::dbg; @@ -333,7 +332,6 @@ mod tests { #[test] fn builder_or() { let mut world = World::new(); - world.spawn((A(0), B(0))); world.spawn(B(0)); world.spawn(C(0)); @@ -487,7 +485,6 @@ mod tests { let mut query = QueryBuilder::<(FilteredEntityMut, EntityMutExcept)>::new(&mut world) .data::() - .filter::>() .build(); // Removing `EntityMutExcept` just leaves A @@ -499,7 +496,6 @@ mod tests { let mut query = QueryBuilder::<(FilteredEntityMut, EntityRefExcept)>::new(&mut world) .data::() - .filter::>() .build(); // Removing `EntityRefExcept` just leaves A, plus read access diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 47e228f6966e3..cab2ee9c9391d 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -2669,7 +2669,6 @@ mod tests { #[test] fn query_iter_sorts() { let mut world = World::new(); - for i in 0..100 { world.spawn(A(i as f32)); world.spawn((A(i as f32), Sparse(i))); diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 0b22b23fb260e..14736f8ced91d 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1850,7 +1850,6 @@ mod tests { #[test] fn can_transmute_empty_tuple() { let mut world = World::new(); - world.register_component::(); let entity = world.spawn(A(10)).id(); @@ -2226,7 +2225,6 @@ mod tests { let mut df = DefaultQueryFilters::from_world(&mut world); df.register_disabling_component(world.register_component::
()); world.insert_resource(df); - world.register_disabling_component::
(); let mut query = QueryState::<()>::new(&mut world); // If the filter is instead a table components, the query can still be dense diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 211b21643d082..2a6fb2c0681f8 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -5041,8 +5041,8 @@ mod tests { use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, + entity_disabling::Internal, prelude::*, - resource::IsResource, system::{assert_is_system, RunSystemOnce as _}, world::{error::EntityComponentError, DeferredWorld, FilteredEntityMut, FilteredEntityRef}, }; @@ -5491,7 +5491,7 @@ mod tests { world.spawn(TestComponent(0)).insert(TestComponent2(0)); - let mut query = world.query::>(); + let mut query = world.query::>(); let mut found = false; for entity_ref in query.iter_mut(&mut world) { @@ -5551,7 +5551,7 @@ mod tests { fn system( _: Query<&mut TestComponent>, - query: Query>, + query: Query>, ) { for entity_ref in query.iter() { assert!(matches!( @@ -5569,7 +5569,7 @@ mod tests { let mut world = World::new(); world.spawn(TestComponent(0)).insert(TestComponent2(0)); - let mut query = world.query::>(); + let mut query = world.query::>(); let mut found = false; for mut entity_mut in query.iter_mut(&mut world) { @@ -5636,7 +5636,7 @@ mod tests { fn system( _: Query<&mut TestComponent>, - mut query: Query>, + mut query: Query>, ) { for mut entity_mut in query.iter_mut() { assert!(entity_mut diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index afd376bbe7500..f3f32f1e61e3a 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -3714,9 +3714,9 @@ mod tests { change_detection::{DetectChangesMut, MaybeLocation}, component::{ComponentCloneBehavior, ComponentDescriptor, ComponentInfo, StorageType}, entity::EntityHashSet, - entity_disabling::{DefaultQueryFilters, Disabled}, + entity_disabling::{DefaultQueryFilters, Disabled, Internal}, ptr::OwningPtr, - resource::{IsResource, Resource}, + resource::Resource, world::{error::EntityMutableFetchError, DeferredWorld}, }; use alloc::{ @@ -4148,11 +4148,9 @@ mod tests { let iterate_and_count_entities = |world: &World, entity_counters: &mut HashMap<_, _>| { entity_counters.clear(); #[expect(deprecated, reason = "remove this test in in 0.17.0")] - for entity in world.iter_entities() { - if !entity.contains::() { - let counter = entity_counters.entry(entity.id()).or_insert(0); - *counter += 1; - } + for entity in world.iter_entities().filter(|e| !e.contains::()) { + let counter = entity_counters.entry(entity.id()).or_insert(0); + *counter += 1; } }; @@ -4250,11 +4248,14 @@ mod tests { assert_eq!(world.entity(b2).get(), Some(&B(4))); #[expect(deprecated, reason = "remove this test in in 0.17.0")] - let mut entities = world.iter_entities_mut().collect::>(); + let mut entities = world + .iter_entities_mut() + .filter(|e| !e.contains::()) + .collect::>(); entities.sort_by_key(|e| e.get::().map(|a| a.0).or(e.get::().map(|b| b.0))); - let (a, b) = entities.split_at_mut(3); + let (a, b) = entities.split_at_mut(2); core::mem::swap( - &mut a[2].get_mut::().unwrap().0, + &mut a[1].get_mut::().unwrap().0, &mut b[0].get_mut::().unwrap().0, ); assert_eq!(world.entity(a1).get(), Some(&A(0))); From 78a06729345a9ec573ad44f0f4728135a5ae14bf Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Wed, 23 Jul 2025 21:35:14 +0000 Subject: [PATCH 16/25] fix more stuff --- crates/bevy_scene/src/lib.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 87bb8a7e648dc..6f95e46151676 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -53,7 +53,9 @@ use { bevy_asset::AssetApp, bevy_ecs::schedule::IntoScheduleConfigs, bevy_ecs::{ - entity_disabling::DefaultQueryFilters, resource::IsResource, resource::ResourceEntity, + entity_disabling::{DefaultQueryFilters, Internal}, + resource::IsResource, + resource::ResourceEntity, }, }; @@ -71,6 +73,7 @@ impl Plugin for ScenePlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::>() .add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain()); @@ -130,9 +133,7 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, - entity_disabling::Internal, hierarchy::{ChildOf, Children}, - query::Allows, reflect::{AppTypeRegistry, ReflectComponent}, world::World, }; @@ -313,11 +314,7 @@ mod tests { scene .world .insert_resource(world.resource::().clone()); - let entities: Vec = scene - .world - .query_filtered::>() - .iter(&scene.world) - .collect(); + let entities: Vec = scene.world.query::().iter(&scene.world).collect(); DynamicSceneBuilder::from_world(&scene.world) .extract_entities(entities.into_iter()) .build() From 4a2416d4600d04b973fead62a3df993433e7e72c Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 24 Jul 2025 08:21:55 +0000 Subject: [PATCH 17/25] update migration guides --- .../migration-guides/internal_entities.md | 5 +++-- .../migration-guides/resources_as_components.md | 17 ++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/release-content/migration-guides/internal_entities.md b/release-content/migration-guides/internal_entities.md index e57d4ba682959..20b18fb63dce6 100644 --- a/release-content/migration-guides/internal_entities.md +++ b/release-content/migration-guides/internal_entities.md @@ -1,10 +1,11 @@ --- title: Internal Entities -pull_requests: [20204] +pull_requests: [20204, 19711] --- Bevy 0.17 introduces internal entities. Entities tagged by the `Internal` component that are hidden from most queries using [`DefaultQueryFilters`](https://docs.rs/bevy/latest/bevy/ecs/entity_disabling/index.html). -Currently, both [`Observer`s](https://docs.rs/bevy/latest/bevy/ecs/observer/struct.Observer.html) and systems that are registered through [`World::register_system`](https://docs.rs/bevy/latest/bevy/prelude/struct.World.html#method.register_system) are considered internal entities. +Currently, [`Observer`s](https://docs.rs/bevy/latest/bevy/ecs/observer/struct.Observer.html) and systems that are registered through [`World::register_system`](https://docs.rs/bevy/latest/bevy/prelude/struct.World.html#method.register_system) are considered internal entities. +The resource entities part of the resources-as-components worked are also marked internal. If you queried them before, add the `Allows` filter to the query to bypass the default filter. diff --git a/release-content/migration-guides/resources_as_components.md b/release-content/migration-guides/resources_as_components.md index 919dbfbaf7b4a..8e0b528bedd15 100644 --- a/release-content/migration-guides/resources_as_components.md +++ b/release-content/migration-guides/resources_as_components.md @@ -4,7 +4,7 @@ pull_requests: [19711] --- Resources are very similar to Components: they are both data that can be stored in the ECS and queried. -The only real difference between them is that querying a resource will return either one or zero resources, whereas querying for a component can return any number of entities that match it. +The only real difference between them is that querying a resource will return either one or zero resources, whereas querying for a component can return any number of matching entities. Even so, resources and components have always been separate concepts within the ECS. This leads to some annoying restrictions. @@ -13,13 +13,16 @@ Moreover, the engine internals contain a lot of duplication because of it. This motivates us to transition resources to components, and while most of the public API will stay the same, some breaking changes are inevitable. -This PR adds a dummy entity alongside every resource. This entity is inserted and removed alongside resources and doesn't do anything (yet). +This PR adds a dummy entity alongside every resource. +This entity is inserted and removed alongside resources and doesn't do anything (yet). -This changes `World::entities().len()` as there are more entities than you might expect there to be. For example, a new world, no longer contains zero entities. This is mostly important for unit tests. +This changes `World::entities().len()` as there are more entities than you might expect there to be. +For example, a new world, no longer contains zero entities. +This is mostly important for unit tests. +If there is any place you are currently using `world.entities().len()`, we recommened you instead use a query `world.query().query(&world).count()`. -Two methods have been added `World::entity_count()` and `World::resource_count()`. The former returns the number of entities without the resource entities, while the latter returns the number of resources in the world. - -While the marker component `IsResource` is added to [default query filters](https://docs.rs/bevy/latest/bevy/ecs/entity_disabling/struct.DefaultQueryFilters.html), during world creation, resource entities might still show up in broad queries with [`EntityMutExcept`](https://docs.rs/bevy/latest/bevy/ecs/world/struct.EntityMutExcept.html) and [`EntityRefExcept`](https://docs.rs/bevy/latest/bevy/ecs/world/struct.EntityRefExcept.html). -They also show up in `World::iter_entities()`, `World::iter_entities_mut()` and [`QueryBuilder`](https://docs.rs/bevy/latest/bevy/ecs/prelude/struct.QueryBuilder.html). +Meanwhile, resource entities are also tagged with `IsResource` and `Internal` marker components. +For more information, checkout the migration guide for internal entities. +In summary, internal entities are added to [default query filters](https://docs.rs/bevy/latest/bevy/ecs/entity_disabling/struct.DefaultQueryFilters.html), and will not show up in most queries. Lastly, because of the entity bump, the input and output of the `bevy_scene` crate is not equivalent to the previous version, meaning that it's unadvisable to read in scenes from the previous version into the current one. From 3942c56ebe2051e7a898cdcab8e88f82c64aebc8 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 24 Jul 2025 08:26:59 +0000 Subject: [PATCH 18/25] spelling --- release-content/migration-guides/resources_as_components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/migration-guides/resources_as_components.md b/release-content/migration-guides/resources_as_components.md index 8e0b528bedd15..32895e04881bc 100644 --- a/release-content/migration-guides/resources_as_components.md +++ b/release-content/migration-guides/resources_as_components.md @@ -19,7 +19,7 @@ This entity is inserted and removed alongside resources and doesn't do anything This changes `World::entities().len()` as there are more entities than you might expect there to be. For example, a new world, no longer contains zero entities. This is mostly important for unit tests. -If there is any place you are currently using `world.entities().len()`, we recommened you instead use a query `world.query().query(&world).count()`. +If there is any place you are currently using `world.entities().len()`, we recommend you instead use a query `world.query().query(&world).count()`. Meanwhile, resource entities are also tagged with `IsResource` and `Internal` marker components. For more information, checkout the migration guide for internal entities. From f442ff69e4c69e5e283dfa27f14dcca9b458c579 Mon Sep 17 00:00:00 2001 From: Bird! Date: Fri, 22 Aug 2025 21:06:42 +0200 Subject: [PATCH 19/25] fix imports --- crates/bevy_scene/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 015bb5b68d46f..456a5105df20c 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -133,7 +133,9 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, + entity_disabling::Internal, hierarchy::{ChildOf, Children}, + prelude::Allow, reflect::{AppTypeRegistry, ReflectComponent}, world::World, }; From acb539e00202b1a0e0d65df3ea408c27c11ad188 Mon Sep 17 00:00:00 2001 From: Bird! Date: Fri, 22 Aug 2025 21:21:45 +0200 Subject: [PATCH 20/25] second attempt at fixing a test --- crates/bevy_scene/src/lib.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 456a5105df20c..af0037ff4c708 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -133,9 +133,7 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, - entity_disabling::Internal, hierarchy::{ChildOf, Children}, - prelude::Allow, reflect::{AppTypeRegistry, ReflectComponent}, world::World, }; @@ -322,11 +320,7 @@ mod tests { scene .world .insert_resource(world.resource::().clone()); - let entities: Vec = scene - .world - .query_filtered::>() - .iter(&scene.world) - .collect(); + let entities: Vec = scene.world.query::().iter(&scene.world).collect(); DynamicSceneBuilder::from_world(&scene.world) .extract_entities(entities.into_iter()) .build() From 7f224be1b8b1284076c45f3964389b3033870278 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 14 Sep 2025 18:58:49 +0200 Subject: [PATCH 21/25] address comments --- crates/bevy_ecs/src/lib.rs | 8 ++++---- crates/bevy_ecs/src/resource.rs | 6 ++++-- crates/bevy_scene/src/serde.rs | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 700e0faf2b113..1db0582b40394 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1553,8 +1553,8 @@ mod tests { let mut world_a = World::new(); let world_b = World::new(); let mut query = world_a.query::<&A>(); - let _ = query.get(&world_a, Entity::from_raw_u32(0).unwrap()); - let _ = query.get(&world_b, Entity::from_raw_u32(0).unwrap()); + let _ = query.get(&world_a, Entity::from_raw_u32(10_000).unwrap()); + let _ = query.get(&world_b, Entity::from_raw_u32(10_000).unwrap()); } #[test] @@ -1794,7 +1794,7 @@ mod tests { fn try_insert_batch() { let mut world = World::default(); let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw_u32(2).unwrap(); + let e1 = Entity::from_raw_u32(10_000).unwrap(); let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; @@ -1818,7 +1818,7 @@ mod tests { fn try_insert_batch_if_new() { let mut world = World::default(); let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw_u32(2).unwrap(); + let e1 = Entity::from_raw_u32(10_000).unwrap(); let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index a8e63565bfcdb..c8e9bb3b40757 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -3,6 +3,7 @@ use crate::entity_disabling::Internal; use crate::prelude::Component; use crate::prelude::ReflectComponent; +use crate::prelude::ReflectResource; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use core::marker::PhantomData; @@ -115,8 +116,9 @@ pub struct IsResource; /// Used in conjunction with [`ResourceEntity`], when no type information is available. /// This is used by [`World::insert_resource_by_id`](crate::world::World). -#[derive(Resource)] -pub(crate) struct TypeErasedResource; +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource, Debug))] +#[derive(Resource, Debug)] +pub struct TypeErasedResource; #[cfg(test)] mod tests { diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index cd16c4b824e8a..0b4c0b23f8e03 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -789,7 +789,7 @@ mod tests { let (scene, deserialized_scene) = roundtrip_ron(&world); - assert_eq!(3, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); // 1 entity and 2 resources assert_scene_eq(&scene, &deserialized_scene); let mut world = create_world(); From 624653951d52eb500a50ee44ce68c128bca95b2e Mon Sep 17 00:00:00 2001 From: Trashtalk217 Date: Sun, 14 Sep 2025 18:59:40 +0200 Subject: [PATCH 22/25] Update crates/bevy_ecs/src/resource.rs Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/resource.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index c8e9bb3b40757..4f6b55894e9f2 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -114,7 +114,7 @@ impl Default for ResourceEntity { #[derive(Component, Default, Debug)] pub struct IsResource; -/// Used in conjunction with [`ResourceEntity`], when no type information is available. +/// Used as the `R` generic of [`ResourceEntity`], when no type information is available. /// This is used by [`World::insert_resource_by_id`](crate::world::World). #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource, Debug))] #[derive(Resource, Debug)] From b83a61705d67ba70a6022f227fdf47a8a7e483ff Mon Sep 17 00:00:00 2001 From: Trashtalk217 Date: Sun, 14 Sep 2025 19:00:06 +0200 Subject: [PATCH 23/25] Update crates/bevy_ecs/src/resource.rs Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/resource.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 4f6b55894e9f2..a651227892f2d 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -82,6 +82,7 @@ pub use bevy_ecs_macros::Resource; pub trait Resource: Send + Sync + 'static {} /// A marker component for the entity that stores the resource of type `T`. +/// Note that until [#20934](https://github.com/bevyengine/bevy/pull/20934) is merged, this does not actually store any data. /// /// This component is automatically inserted when a resource of type `T` is inserted into the world, /// and can be used to find the entity that stores a particular resource. From e98bcd8aff57fce6d3ce6bb16a9bd430fd40e04d Mon Sep 17 00:00:00 2001 From: Trashtalk217 Date: Sun, 14 Sep 2025 19:00:28 +0200 Subject: [PATCH 24/25] Update crates/bevy_ecs/src/resource.rs Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/resource.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index a651227892f2d..663f929dd1ad6 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -92,6 +92,8 @@ pub trait Resource: Send + Sync + 'static {} /// /// This component comes with a hook that ensures that at most one entity has this component for any given `R`: /// adding this component to an entity (or spawning an entity with this component) will despawn any other entity with this component. +/// +/// Note that because [`Internal`] is a required component, this entity will not appear in queries by default. #[derive(Component, Debug)] #[require(Internal, IsResource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Default))] From 09e5ff817b8110daf9451316a44e70453a0b4f84 Mon Sep 17 00:00:00 2001 From: Trashtalk217 Date: Sun, 14 Sep 2025 20:51:39 +0200 Subject: [PATCH 25/25] Update crates/bevy_ecs/src/name.rs Co-authored-by: Janis <130913856+janis-bhm@users.noreply.github.com> --- crates/bevy_ecs/src/name.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 2887475cc648f..f22cfefba812b 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -278,7 +278,7 @@ mod tests { let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} - assert_eq!(d1.to_string(), "1v0"); + assert_eq!(d1.to_string(), std::format!("{e1}")); let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName");