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>);
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>(
163 world: UnsafeWorldCell<'w>,
164 state: &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>(
206 fetch: &mut Self::Fetch<'w>,
207 state: &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>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) {
220 if let Some(inner) = &mut fetch.inner {
221 unsafe {
223 <&A>::set_table(inner, &state.asset_id, table);
224 }
225 }
226 }
227
228 #[inline]
229 fn update_component_access(state: &Self::State, access: &mut FilteredAccess<ComponentId>) {
230 <&A>::update_component_access(&state.asset_id, access);
231 access.add_resource_read(state.resource_id);
232 }
233
234 fn init_state(world: &mut World) -> AssetChangedState<A> {
235 let resource_id = world.init_resource::<AssetChanges<A::Asset>>();
236 let asset_id = world.register_component::<A>();
237 AssetChangedState {
238 asset_id,
239 resource_id,
240 _asset: PhantomData,
241 }
242 }
243
244 fn get_state(components: &Components) -> Option<Self::State> {
245 let resource_id = components.resource_id::<AssetChanges<A::Asset>>()?;
246 let asset_id = components.component_id::<A>()?;
247 Some(AssetChangedState {
248 asset_id,
249 resource_id,
250 _asset: PhantomData,
251 })
252 }
253
254 fn matches_component_set(
255 state: &Self::State,
256 set_contains_id: &impl Fn(ComponentId) -> bool,
257 ) -> bool {
258 set_contains_id(state.asset_id)
259 }
260}
261
262#[expect(unsafe_code, reason = "QueryFilter is an unsafe trait.")]
263unsafe impl<A: AsAssetId> QueryFilter for AssetChanged<A> {
265 const IS_ARCHETYPAL: bool = false;
266
267 #[inline]
268 unsafe fn filter_fetch(
269 fetch: &mut Self::Fetch<'_>,
270 entity: Entity,
271 table_row: TableRow,
272 ) -> bool {
273 fetch.inner.as_mut().is_some_and(|inner| {
274 unsafe {
276 let handle = <&A>::fetch(inner, entity, table_row);
277 fetch.check.has_changed(handle)
278 }
279 })
280 }
281}
282
283#[cfg(test)]
284#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
285mod tests {
286 use crate::{AssetEvents, AssetPlugin, Handle};
287 use alloc::{vec, vec::Vec};
288 use core::num::NonZero;
289 use std::println;
290
291 use crate::{AssetApp, Assets};
292 use bevy_app::{App, AppExit, PostUpdate, Startup, TaskPoolPlugin, Update};
293 use bevy_ecs::schedule::IntoScheduleConfigs;
294 use bevy_ecs::{
295 component::Component,
296 event::EventWriter,
297 resource::Resource,
298 system::{Commands, IntoSystem, Local, Query, Res, ResMut},
299 };
300 use bevy_reflect::TypePath;
301
302 use super::*;
303
304 #[derive(Asset, TypePath, Debug)]
305 struct MyAsset(usize, &'static str);
306
307 #[derive(Component)]
308 struct MyComponent(Handle<MyAsset>);
309
310 impl AsAssetId for MyComponent {
311 type Asset = MyAsset;
312
313 fn as_asset_id(&self) -> AssetId<Self::Asset> {
314 self.0.id()
315 }
316 }
317
318 fn run_app<Marker>(system: impl IntoSystem<(), (), Marker>) {
319 let mut app = App::new();
320 app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
321 .init_asset::<MyAsset>()
322 .add_systems(Update, system);
323 app.update();
324 }
325
326 #[test]
329 fn handle_filter_pos_ok() {
330 fn compatible_filter(
331 _query: Query<&mut MyComponent, AssetChanged<MyComponent>>,
332 mut exit: EventWriter<AppExit>,
333 ) {
334 exit.write(AppExit::Error(NonZero::<u8>::MIN));
335 }
336 run_app(compatible_filter);
337 }
338
339 #[derive(Default, PartialEq, Debug, Resource)]
340 struct Counter(Vec<u32>);
341
342 fn count_update(
343 mut counter: ResMut<Counter>,
344 assets: Res<Assets<MyAsset>>,
345 query: Query<&MyComponent, AssetChanged<MyComponent>>,
346 ) {
347 for handle in query.iter() {
348 let asset = assets.get(&handle.0).unwrap();
349 counter.0[asset.0] += 1;
350 }
351 }
352
353 fn update_some(mut assets: ResMut<Assets<MyAsset>>, mut run_count: Local<u32>) {
354 let mut update_index = |i| {
355 let id = assets
356 .iter()
357 .find_map(|(h, a)| (a.0 == i).then_some(h))
358 .unwrap();
359 let asset = assets.get_mut(id).unwrap();
360 println!("setting new value for {}", asset.0);
361 asset.1 = "new_value";
362 };
363 match *run_count {
364 0 | 1 => update_index(0),
365 2 => {}
366 3 => {
367 update_index(0);
368 update_index(1);
369 }
370 4.. => update_index(1),
371 };
372 *run_count += 1;
373 }
374
375 fn add_some(
376 mut assets: ResMut<Assets<MyAsset>>,
377 mut cmds: Commands,
378 mut run_count: Local<u32>,
379 ) {
380 match *run_count {
381 1 => {
382 cmds.spawn(MyComponent(assets.add(MyAsset(0, "init"))));
383 }
384 0 | 2 => {}
385 3 => {
386 cmds.spawn(MyComponent(assets.add(MyAsset(1, "init"))));
387 cmds.spawn(MyComponent(assets.add(MyAsset(2, "init"))));
388 }
389 4.. => {
390 cmds.spawn(MyComponent(assets.add(MyAsset(3, "init"))));
391 }
392 };
393 *run_count += 1;
394 }
395
396 #[track_caller]
397 fn assert_counter(app: &App, assert: Counter) {
398 assert_eq!(&assert, app.world().resource::<Counter>());
399 }
400
401 #[test]
402 fn added() {
403 let mut app = App::new();
404
405 app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
406 .init_asset::<MyAsset>()
407 .insert_resource(Counter(vec![0, 0, 0, 0]))
408 .add_systems(Update, add_some)
409 .add_systems(PostUpdate, count_update.after(AssetEvents));
410
411 app.update(); assert_counter(&app, Counter(vec![0, 0, 0, 0]));
414 app.update(); assert_counter(&app, Counter(vec![1, 0, 0, 0]));
416 app.update(); assert_counter(&app, Counter(vec![1, 0, 0, 0]));
418 app.update(); assert_counter(&app, Counter(vec![1, 1, 1, 0]));
420 app.update(); assert_counter(&app, Counter(vec![1, 1, 1, 1]));
422 }
423
424 #[test]
425 fn changed() {
426 let mut app = App::new();
427
428 app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
429 .init_asset::<MyAsset>()
430 .insert_resource(Counter(vec![0, 0]))
431 .add_systems(
432 Startup,
433 |mut cmds: Commands, mut assets: ResMut<Assets<MyAsset>>| {
434 let asset0 = assets.add(MyAsset(0, "init"));
435 let asset1 = assets.add(MyAsset(1, "init"));
436 cmds.spawn(MyComponent(asset0.clone()));
437 cmds.spawn(MyComponent(asset0));
438 cmds.spawn(MyComponent(asset1.clone()));
439 cmds.spawn(MyComponent(asset1.clone()));
440 cmds.spawn(MyComponent(asset1));
441 },
442 )
443 .add_systems(Update, update_some)
444 .add_systems(PostUpdate, count_update.after(AssetEvents));
445
446 app.update(); assert_counter(&app, Counter(vec![2, 3]));
451
452 app.update(); assert_counter(&app, Counter(vec![4, 3]));
456
457 app.update(); assert_counter(&app, Counter(vec![4, 3]));
460
461 app.update(); assert_counter(&app, Counter(vec![6, 6]));
464
465 app.update(); assert_counter(&app, Counter(vec![6, 9]));
468 app.update(); assert_counter(&app, Counter(vec![6, 12]));
471 }
472}