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