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 fn update_archetypes(_state: &mut Self::State, _world: UnsafeWorldCell) {}
289}
290
291#[expect(unsafe_code, reason = "QueryFilter is an unsafe trait.")]
292unsafe impl<A: AsAssetId> QueryFilter for AssetChanged<A> {
294 const IS_ARCHETYPAL: bool = false;
295
296 #[inline]
297 unsafe fn filter_fetch(
298 state: &Self::State,
299 fetch: &mut Self::Fetch<'_>,
300 entity: Entity,
301 table_row: TableRow,
302 ) -> bool {
303 fetch.inner.as_mut().is_some_and(|inner| {
304 unsafe {
306 let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row);
307 handle.is_some_and(|handle| fetch.check.has_changed(handle))
308 }
309 })
310 }
311}
312
313#[cfg(test)]
314#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
315mod tests {
316 use crate::tests::create_app;
317 use crate::{AssetEventSystems, Handle};
318 use alloc::{vec, vec::Vec};
319 use bevy_ecs::system::assert_is_system;
320 use core::num::NonZero;
321 use std::println;
322
323 use crate::{AssetApp, Assets};
324 use bevy_app::{App, AppExit, PostUpdate, Startup, Update};
325 use bevy_ecs::schedule::IntoScheduleConfigs;
326 use bevy_ecs::{
327 component::Component,
328 message::MessageWriter,
329 resource::Resource,
330 system::{Commands, IntoSystem, Local, Query, Res, ResMut},
331 };
332 use bevy_reflect::TypePath;
333
334 use super::*;
335
336 #[derive(Asset, TypePath, Debug)]
337 struct MyAsset(usize, &'static str);
338
339 #[derive(Component)]
340 struct MyComponent(Handle<MyAsset>);
341
342 impl AsAssetId for MyComponent {
343 type Asset = MyAsset;
344
345 fn as_asset_id(&self) -> AssetId<Self::Asset> {
346 self.0.id()
347 }
348 }
349
350 #[test]
351 #[should_panic]
352 fn should_conflict() {
353 #[derive(Component)]
354 struct Foo;
355
356 fn system(
357 _: Query<&Foo, AssetChanged<MyComponent>>,
358 _: Query<&mut AssetChanges<MyAsset>, bevy_ecs::query::Without<Foo>>,
359 ) {
360 }
361 assert_is_system(system);
362 }
363
364 fn run_app<Marker>(system: impl IntoSystem<(), (), Marker>) {
365 let mut app = create_app().0;
366 app.init_asset::<MyAsset>().add_systems(Update, system);
367 app.update();
368 }
369
370 #[test]
373 fn handle_filter_pos_ok() {
374 fn compatible_filter(
375 _query: Query<&mut MyComponent, AssetChanged<MyComponent>>,
376 mut exit: MessageWriter<AppExit>,
377 ) {
378 exit.write(AppExit::Error(NonZero::<u8>::MIN));
379 }
380 run_app(compatible_filter);
381 }
382
383 #[derive(Default, PartialEq, Debug, Resource)]
384 struct Counter(Vec<u32>);
385
386 fn count_update(
387 mut counter: ResMut<Counter>,
388 assets: Res<Assets<MyAsset>>,
389 query: Query<&MyComponent, AssetChanged<MyComponent>>,
390 ) {
391 for handle in query.iter() {
392 let asset = assets.get(&handle.0).unwrap();
393 counter.0[asset.0] += 1;
394 }
395 }
396
397 fn update_some(mut assets: ResMut<Assets<MyAsset>>, mut run_count: Local<u32>) {
398 let mut update_index = |i| {
399 let id = assets
400 .iter()
401 .find_map(|(h, a)| (a.0 == i).then_some(h))
402 .unwrap();
403 let mut asset = assets.get_mut(id).unwrap();
404 println!("setting new value for {}", asset.0);
405 asset.1 = "new_value";
406 };
407 match *run_count {
408 0 | 1 => update_index(0),
409 2 => {}
410 3 => {
411 update_index(0);
412 update_index(1);
413 }
414 4.. => update_index(1),
415 };
416 *run_count += 1;
417 }
418
419 fn add_some(
420 mut assets: ResMut<Assets<MyAsset>>,
421 mut cmds: Commands,
422 mut run_count: Local<u32>,
423 ) {
424 match *run_count {
425 1 => {
426 cmds.spawn(MyComponent(assets.add(MyAsset(0, "init"))));
427 }
428 0 | 2 => {}
429 3 => {
430 cmds.spawn(MyComponent(assets.add(MyAsset(1, "init"))));
431 cmds.spawn(MyComponent(assets.add(MyAsset(2, "init"))));
432 }
433 4.. => {
434 cmds.spawn(MyComponent(assets.add(MyAsset(3, "init"))));
435 }
436 };
437 *run_count += 1;
438 }
439
440 #[track_caller]
441 fn assert_counter(app: &App, assert: Counter) {
442 assert_eq!(&assert, app.world().resource::<Counter>());
443 }
444
445 #[test]
446 fn added() {
447 let mut app = create_app().0;
448
449 app.init_asset::<MyAsset>()
450 .insert_resource(Counter(vec![0, 0, 0, 0]))
451 .add_systems(Update, add_some)
452 .add_systems(PostUpdate, count_update.after(AssetEventSystems));
453
454 app.update(); assert_counter(&app, Counter(vec![0, 0, 0, 0]));
457 app.update(); assert_counter(&app, Counter(vec![1, 0, 0, 0]));
459 app.update(); assert_counter(&app, Counter(vec![1, 0, 0, 0]));
461 app.update(); assert_counter(&app, Counter(vec![1, 1, 1, 0]));
463 app.update(); assert_counter(&app, Counter(vec![1, 1, 1, 1]));
465 }
466
467 #[test]
468 fn changed() {
469 let mut app = create_app().0;
470
471 app.init_asset::<MyAsset>()
472 .insert_resource(Counter(vec![0, 0]))
473 .add_systems(
474 Startup,
475 |mut cmds: Commands, mut assets: ResMut<Assets<MyAsset>>| {
476 let asset0 = assets.add(MyAsset(0, "init"));
477 let asset1 = assets.add(MyAsset(1, "init"));
478 cmds.spawn(MyComponent(asset0.clone()));
479 cmds.spawn(MyComponent(asset0));
480 cmds.spawn(MyComponent(asset1.clone()));
481 cmds.spawn(MyComponent(asset1.clone()));
482 cmds.spawn(MyComponent(asset1));
483 },
484 )
485 .add_systems(Update, update_some)
486 .add_systems(PostUpdate, count_update.after(AssetEventSystems));
487
488 app.update(); assert_counter(&app, Counter(vec![2, 3]));
493
494 app.update(); assert_counter(&app, Counter(vec![4, 3]));
498
499 app.update(); assert_counter(&app, Counter(vec![4, 3]));
502
503 app.update(); assert_counter(&app, Counter(vec![6, 6]));
506
507 app.update(); assert_counter(&app, Counter(vec![6, 9]));
510 app.update(); assert_counter(&app, Counter(vec![6, 12]));
513 }
514}