diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index 0562a857d047d..b1d10425711e7 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -10,11 +10,13 @@ use bevy_ecs::{ change_detection::Tick, component::ComponentId, prelude::{Entity, Resource, World}, - query::{FilteredAccess, QueryData, QueryFilter, ReadFetch, WorldQuery}, + query::{FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, ReadFetch, WorldQuery}, + resource::IS_RESOURCE, storage::{Table, TableRow}, world::unsafe_world_cell::UnsafeWorldCell, }; use bevy_platform::collections::HashMap; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use disqualified::ShortName; use tracing::error; @@ -168,9 +170,8 @@ unsafe impl WorldQuery for AssetChanged { // SAFETY: // - `state.resource_id` was obtained from `world.init_resource::>()`, // so the untyped pointer returned by `get_resource_by_id` can safely be dereferenced into that type. - // - `update_component_access` declares a read on `state.resource_id`, so it is safe to - // read that resource here (see trait-level safety comments on `WorldQuery`, regarding - // readonly resource access in `init_fetch`) + // - `init_nested_access` declares a read on `state.resource_id`, so it is safe to + // read that resource here (see trait-level safety comments on `WorldQuery`) let Some(changes) = (unsafe { world .get_resource_by_id(state.resource_id) @@ -235,7 +236,38 @@ unsafe impl WorldQuery for AssetChanged { #[inline] fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { <&A>::update_component_access(&state.asset_id, access); - access.add_resource_read(state.resource_id); + } + + // ChangedAsset accesses both the asset and the AssetChanges resource. + // In order to access two different entities we implement init_nested_access. + fn init_nested_access( + state: &Self::State, + system_name: Option<&str>, + component_access_set: &mut FilteredAccessSet, + _world: UnsafeWorldCell, + ) { + let combined_access = component_access_set.combined_access(); + assert!( + !combined_access.has_resource_write(state.resource_id), + "error[B0002]: AssetChanged<{}> in system {:?} conflicts with a previous system param. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), + system_name, + ); + + let mut filter = FilteredAccess::default(); + filter.add_component_read(state.resource_id); + filter.add_resource_read(state.resource_id); + filter.and_with(IS_RESOURCE); + + assert!(component_access_set + .get_conflicts_single(&filter) + .is_empty(), + "error[B0002]: AssetChanged<{}> in system {:?} conflicts with a previous system param. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), + system_name, + ); + + component_access_set.add(filter); } fn init_state(world: &mut World) -> AssetChangedState { @@ -304,7 +336,7 @@ mod tests { component::Component, message::MessageWriter, resource::Resource, - system::{Commands, IntoSystem, Local, Query, Res, ResMut}, + system::{assert_is_system, Commands, IntoSystem, Local, Query, Res, ResMut}, }; use bevy_reflect::TypePath; @@ -324,6 +356,20 @@ mod tests { } } + #[test] + #[should_panic] + fn should_conflict() { + #[derive(Component)] + struct Foo; + + fn system( + _: Query<&Foo, AssetChanged>, + _: Query<&mut AssetChanges, bevy_ecs::query::Without>, + ) { + } + assert_is_system(system); + } + fn run_app(system: impl IntoSystem<(), (), Marker>) { let mut app = create_app().0; app.init_asset::().add_systems(Update, system);