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