1use crate::{AsAssetId, Asset, AssetId};
7use bevy_ecs::component::Components;
8use bevy_ecs::{
9 archetype::Archetype,
10 change_detection::Tick,
11 component::ComponentId,
12 prelude::{Entity, Resource, World},
13 query::{FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, ReadFetch, WorldQuery},
14 resource::IS_RESOURCE,
15 storage::{Table, TableRow},
16 world::unsafe_world_cell::UnsafeWorldCell,
17};
18use bevy_platform::collections::HashMap;
19use bevy_utils::prelude::DebugName;
20use core::marker::PhantomData;
21use disqualified::ShortName;
22use tracing::error;
23
24#[derive(Resource)]
33pub(crate) struct AssetChanges<A: Asset> {
34 change_ticks: HashMap<AssetId<A>, Tick>,
35 last_change_tick: Tick,
36}
37
38impl<A: Asset> AssetChanges<A> {
39 pub(crate) fn insert(&mut self, asset_id: AssetId<A>, tick: Tick) {
40 self.last_change_tick = tick;
41 self.change_ticks.insert(asset_id, tick);
42 }
43 pub(crate) fn remove(&mut self, asset_id: &AssetId<A>) {
44 self.change_ticks.remove(asset_id);
45 }
46}
47
48impl<A: Asset> Default for AssetChanges<A> {
49 fn default() -> Self {
50 Self {
51 change_ticks: Default::default(),
52 last_change_tick: Tick::new(0),
53 }
54 }
55}
56
57struct AssetChangeCheck<'w, A: AsAssetId> {
58 change_ticks: Option<&'w HashMap<AssetId<A::Asset>, Tick>>,
61 last_run: Tick,
62 this_run: Tick,
63}
64
65impl<A: AsAssetId> Clone for AssetChangeCheck<'_, A> {
66 fn clone(&self) -> Self {
67 *self
68 }
69}
70
71impl<A: AsAssetId> Copy for AssetChangeCheck<'_, A> {}
72
73impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> {
74 fn new(changes: &'w AssetChanges<A::Asset>, last_run: Tick, this_run: Tick) -> Self {
75 Self {
76 change_ticks: Some(&changes.change_ticks),
77 last_run,
78 this_run,
79 }
80 }
81 fn has_changed(&self, handle: &A) -> bool {
84 let is_newer = |tick: &Tick| tick.is_newer_than(self.last_run, self.this_run);
85 let id = handle.as_asset_id();
86
87 self.change_ticks
88 .is_some_and(|change_ticks| change_ticks.get(&id).is_some_and(is_newer))
89 }
90}
91
92pub struct AssetChanged<A: AsAssetId>(PhantomData<A>);
128
129#[doc(hidden)]
131pub struct AssetChangedFetch<'w, A: AsAssetId> {
132 inner: Option<ReadFetch<'w, A>>,
133 check: AssetChangeCheck<'w, A>,
134}
135
136impl<'w, A: AsAssetId> Clone for AssetChangedFetch<'w, A> {
137 fn clone(&self) -> Self {
138 Self {
139 inner: self.inner,
140 check: self.check,
141 }
142 }
143}
144
145#[doc(hidden)]
147pub struct AssetChangedState<A: AsAssetId> {
148 asset_id: ComponentId,
149 resource_id: ComponentId,
150 _asset: PhantomData<fn(A)>,
151}
152
153#[expect(unsafe_code, reason = "WorldQuery is an unsafe trait.")]
154unsafe impl<A: AsAssetId> WorldQuery for AssetChanged<A> {
156 type Fetch<'w> = AssetChangedFetch<'w, A>;
157
158 type State = AssetChangedState<A>;
159
160 fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {
161 fetch
162 }
163
164 unsafe fn init_fetch<'w, 's>(
165 world: UnsafeWorldCell<'w>,
166 state: &'s Self::State,
167 last_run: Tick,
168 this_run: Tick,
169 ) -> Self::Fetch<'w> {
170 let Some(changes) = (unsafe {
176 world
177 .get_resource_by_id(state.resource_id)
178 .map(|ptr| ptr.deref::<AssetChanges<A::Asset>>())
179 }) else {
180 error!(
181 "AssetChanges<{ty}> resource was removed, please do not remove \
182 AssetChanges<{ty}> when using the AssetChanged<{ty}> world query",
183 ty = ShortName::of::<A>()
184 );
185
186 return AssetChangedFetch {
187 inner: None,
188 check: AssetChangeCheck {
189 change_ticks: None,
190 last_run,
191 this_run,
192 },
193 };
194 };
195 let has_updates = changes.last_change_tick.is_newer_than(last_run, this_run);
196
197 AssetChangedFetch {
198 inner: has_updates.then(||
199 unsafe {
201 <&A>::init_fetch(world, &state.asset_id, last_run, this_run)
202 }),
203 check: AssetChangeCheck::new(changes, last_run, this_run),
204 }
205 }
206
207 const IS_DENSE: bool = <&A>::IS_DENSE;
208
209 unsafe fn set_archetype<'w, 's>(
210 fetch: &mut Self::Fetch<'w>,
211 state: &'s Self::State,
212 archetype: &'w Archetype,
213 table: &'w Table,
214 ) {
215 if let Some(inner) = &mut fetch.inner {
216 unsafe {
218 <&A>::set_archetype(inner, &state.asset_id, archetype, table);
219 }
220 }
221 }
222
223 unsafe fn set_table<'w, 's>(
224 fetch: &mut Self::Fetch<'w>,
225 state: &Self::State,
226 table: &'w Table,
227 ) {
228 if let Some(inner) = &mut fetch.inner {
229 unsafe {
231 <&A>::set_table(inner, &state.asset_id, table);
232 }
233 }
234 }
235
236 #[inline]
237 fn update_component_access(state: &Self::State, access: &mut FilteredAccess) {
238 <&A>::update_component_access(&state.asset_id, access);
239 }
240
241 fn init_nested_access(
244 state: &Self::State,
245 system_name: Option<&str>,
246 component_access_set: &mut FilteredAccessSet,
247 _world: UnsafeWorldCell,
248 ) {
249 let mut filter = FilteredAccess::default();
250 filter.add_read(state.resource_id);
251 filter.and_with(IS_RESOURCE);
252
253 let conflicts = component_access_set.get_conflicts_single(&filter);
254 if conflicts.is_empty() {
255 component_access_set.add(filter);
256 return;
257 }
258 panic!("error[B0002]: AssetChanged<{}> in system {:?} conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", DebugName::type_name::<A>(), system_name);
259 }
260
261 fn init_state(world: &mut World) -> AssetChangedState<A> {
262 let resource_id = world.init_resource::<AssetChanges<A::Asset>>();
263 let asset_id = world.register_component::<A>();
264 AssetChangedState {
265 asset_id,
266 resource_id,
267 _asset: PhantomData,
268 }
269 }
270
271 fn get_state(components: &Components) -> Option<Self::State> {
272 let resource_id = components.component_id::<AssetChanges<A::Asset>>()?;
273 let asset_id = components.component_id::<A>()?;
274 Some(AssetChangedState {
275 asset_id,
276 resource_id,
277 _asset: PhantomData,
278 })
279 }
280
281 fn matches_component_set(
282 state: &Self::State,
283 set_contains_id: &impl Fn(ComponentId) -> bool,
284 ) -> bool {
285 set_contains_id(state.asset_id)
286 }
287}
288
289#[expect(unsafe_code, reason = "QueryFilter is an unsafe trait.")]
290unsafe impl<A: AsAssetId> QueryFilter for AssetChanged<A> {
292 const IS_ARCHETYPAL: bool = false;
293
294 #[inline]
295 unsafe fn filter_fetch(
296 state: &Self::State,
297 fetch: &mut Self::Fetch<'_>,
298 entity: Entity,
299 table_row: TableRow,
300 ) -> bool {
301 fetch.inner.as_mut().is_some_and(|inner| {
302 unsafe {
304 let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row);
305 handle.is_some_and(|handle| fetch.check.has_changed(handle))
306 }
307 })
308 }
309}
310
311#[cfg(test)]
312#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
313mod tests {
314 use crate::tests::create_app;
315 use crate::{AssetEventSystems, Handle};
316 use alloc::{vec, vec::Vec};
317 use bevy_ecs::system::assert_is_system;
318 use core::num::NonZero;
319 use std::println;
320
321 use crate::{AssetApp, Assets};
322 use bevy_app::{App, AppExit, PostUpdate, Startup, Update};
323 use bevy_ecs::schedule::IntoScheduleConfigs;
324 use bevy_ecs::{
325 component::Component,
326 message::MessageWriter,
327 resource::Resource,
328 system::{Commands, IntoSystem, Local, Query, Res, ResMut},
329 };
330 use bevy_reflect::TypePath;
331
332 use super::*;
333
334 #[derive(Asset, TypePath, Debug)]
335 struct MyAsset(usize, &'static str);
336
337 #[derive(Component)]
338 struct MyComponent(Handle<MyAsset>);
339
340 impl AsAssetId for MyComponent {
341 type Asset = MyAsset;
342
343 fn as_asset_id(&self) -> AssetId<Self::Asset> {
344 self.0.id()
345 }
346 }
347
348 #[test]
349 #[should_panic]
350 fn should_conflict() {
351 #[derive(Component)]
352 struct Foo;
353
354 fn system(
355 _: Query<&Foo, AssetChanged<MyComponent>>,
356 _: Query<&mut AssetChanges<MyAsset>, bevy_ecs::query::Without<Foo>>,
357 ) {
358 }
359 assert_is_system(system);
360 }
361
362 fn run_app<Marker>(system: impl IntoSystem<(), (), Marker>) {
363 let mut app = create_app().0;
364 app.init_asset::<MyAsset>().add_systems(Update, system);
365 app.update();
366 }
367
368 #[test]
371 fn handle_filter_pos_ok() {
372 fn compatible_filter(
373 _query: Query<&mut MyComponent, AssetChanged<MyComponent>>,
374 mut exit: MessageWriter<AppExit>,
375 ) {
376 exit.write(AppExit::Error(NonZero::<u8>::MIN));
377 }
378 run_app(compatible_filter);
379 }
380
381 #[derive(Default, PartialEq, Debug, Resource)]
382 struct Counter(Vec<u32>);
383
384 fn count_update(
385 mut counter: ResMut<Counter>,
386 assets: Res<Assets<MyAsset>>,
387 query: Query<&MyComponent, AssetChanged<MyComponent>>,
388 ) {
389 for handle in query.iter() {
390 let asset = assets.get(&handle.0).unwrap();
391 counter.0[asset.0] += 1;
392 }
393 }
394
395 fn update_some(mut assets: ResMut<Assets<MyAsset>>, mut run_count: Local<u32>) {
396 let mut update_index = |i| {
397 let id = assets
398 .iter()
399 .find_map(|(h, a)| (a.0 == i).then_some(h))
400 .unwrap();
401 let mut asset = assets.get_mut(id).unwrap();
402 println!("setting new value for {}", asset.0);
403 asset.1 = "new_value";
404 };
405 match *run_count {
406 0 | 1 => update_index(0),
407 2 => {}
408 3 => {
409 update_index(0);
410 update_index(1);
411 }
412 4.. => update_index(1),
413 };
414 *run_count += 1;
415 }
416
417 fn add_some(
418 mut assets: ResMut<Assets<MyAsset>>,
419 mut cmds: Commands,
420 mut run_count: Local<u32>,
421 ) {
422 match *run_count {
423 1 => {
424 cmds.spawn(MyComponent(assets.add(MyAsset(0, "init"))));
425 }
426 0 | 2 => {}
427 3 => {
428 cmds.spawn(MyComponent(assets.add(MyAsset(1, "init"))));
429 cmds.spawn(MyComponent(assets.add(MyAsset(2, "init"))));
430 }
431 4.. => {
432 cmds.spawn(MyComponent(assets.add(MyAsset(3, "init"))));
433 }
434 };
435 *run_count += 1;
436 }
437
438 #[track_caller]
439 fn assert_counter(app: &App, assert: Counter) {
440 assert_eq!(&assert, app.world().resource::<Counter>());
441 }
442
443 #[test]
444 fn added() {
445 let mut app = create_app().0;
446
447 app.init_asset::<MyAsset>()
448 .insert_resource(Counter(vec![0, 0, 0, 0]))
449 .add_systems(Update, add_some)
450 .add_systems(PostUpdate, count_update.after(AssetEventSystems));
451
452 app.update(); assert_counter(&app, Counter(vec![0, 0, 0, 0]));
455 app.update(); assert_counter(&app, Counter(vec![1, 0, 0, 0]));
457 app.update(); assert_counter(&app, Counter(vec![1, 0, 0, 0]));
459 app.update(); assert_counter(&app, Counter(vec![1, 1, 1, 0]));
461 app.update(); assert_counter(&app, Counter(vec![1, 1, 1, 1]));
463 }
464
465 #[test]
466 fn changed() {
467 let mut app = create_app().0;
468
469 app.init_asset::<MyAsset>()
470 .insert_resource(Counter(vec![0, 0]))
471 .add_systems(
472 Startup,
473 |mut cmds: Commands, mut assets: ResMut<Assets<MyAsset>>| {
474 let asset0 = assets.add(MyAsset(0, "init"));
475 let asset1 = assets.add(MyAsset(1, "init"));
476 cmds.spawn(MyComponent(asset0.clone()));
477 cmds.spawn(MyComponent(asset0));
478 cmds.spawn(MyComponent(asset1.clone()));
479 cmds.spawn(MyComponent(asset1.clone()));
480 cmds.spawn(MyComponent(asset1));
481 },
482 )
483 .add_systems(Update, update_some)
484 .add_systems(PostUpdate, count_update.after(AssetEventSystems));
485
486 app.update(); assert_counter(&app, Counter(vec![2, 3]));
491
492 app.update(); assert_counter(&app, Counter(vec![4, 3]));
496
497 app.update(); assert_counter(&app, Counter(vec![4, 3]));
500
501 app.update(); assert_counter(&app, Counter(vec![6, 6]));
504
505 app.update(); assert_counter(&app, Counter(vec![6, 9]));
508 app.update(); assert_counter(&app, Counter(vec![6, 12]));
511 }
512}