use std::{
any::Any,
sync::{atomic::AtomicBool, Arc},
};
use bevy::{
ecs::{
component::{ComponentId, Tick},
world::{Command, DeferredWorld},
},
prelude::*,
utils::HashSet,
};
use crate::{AnyCallback, UnregisterCallbackCmd};
#[derive(Clone)]
pub(crate) enum HookState {
Entity(Entity),
Mutable(Entity, ComponentId),
Callback(Arc<dyn AnyCallback + Send + Sync>),
Effect(Arc<dyn Any + Send + Sync + 'static>),
Memo(Arc<dyn Any + Send + Sync + 'static>),
}
#[derive(Component)]
pub struct TrackingScope {
hook_states: Vec<HookState>,
next_hook_index: usize,
component_deps: HashSet<(Entity, ComponentId, bool)>,
resource_deps: HashSet<ComponentId>,
changed: AtomicBool,
pub(crate) tick: Tick,
#[allow(clippy::type_complexity)]
pub(crate) cleanups: Vec<Box<dyn FnOnce(&mut DeferredWorld) + 'static + Sync + Send>>,
}
#[derive(Resource)]
pub struct TrackingScopeTracing(pub Vec<Entity>);
impl FromWorld for TrackingScopeTracing {
fn from_world(_world: &mut World) -> Self {
Self(Vec::new())
}
}
impl TrackingScope {
pub fn new(tick: Tick) -> Self {
Self {
hook_states: Vec::new(),
next_hook_index: 0,
component_deps: HashSet::default(),
resource_deps: HashSet::default(),
changed: AtomicBool::new(false),
tick,
cleanups: Vec::new(),
}
}
pub(crate) fn replace_hook(&mut self, hook: HookState) {
assert!(self.next_hook_index <= self.hook_states.len());
assert!(self.next_hook_index > 0);
self.hook_states[self.next_hook_index - 1] = hook;
}
pub(crate) fn push_hook(&mut self, hook: HookState) {
assert!(self.next_hook_index == self.hook_states.len());
self.hook_states.push(hook);
self.next_hook_index += 1;
}
pub(crate) fn next_hook(&mut self) -> Option<HookState> {
if self.next_hook_index < self.hook_states.len() {
let hook = self.hook_states[self.next_hook_index].clone();
self.next_hook_index += 1;
Some(hook)
} else {
None
}
}
pub(crate) fn add_cleanup(
&mut self,
cleanup: impl FnOnce(&mut DeferredWorld) + 'static + Sync + Send,
) {
self.cleanups.push(Box::new(cleanup));
}
pub(crate) fn track_resource<T: Resource>(&mut self, world: &World) {
self.resource_deps.insert(
world
.components()
.resource_id::<T>()
.unwrap_or_else(|| panic!("Unknown resource type: {}", std::any::type_name::<T>())),
);
}
pub(crate) fn track_component_id(
&mut self,
entity: Entity,
component: ComponentId,
exists: bool,
) {
self.component_deps.insert((entity, component, exists));
}
pub(crate) fn set_changed(&self) {
self.changed
.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn dependencies_changed(&self, world: &World, tick: Tick) -> bool {
self.components_changed(world, tick)
|| self.resources_changed(world, tick)
|| self.changed.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn components_changed(&self, world: &World, tick: Tick) -> bool {
self.component_deps.iter().any(|(e, c, exists)| {
world.get_entity(*e).map_or(false, |e| {
e.get_change_ticks_by_id(*c)
.map(|ct| ct.is_changed(self.tick, tick))
.unwrap_or(false)
|| *exists && e.get_by_id(*c).is_none()
})
})
}
fn resources_changed(&self, world: &World, tick: Tick) -> bool {
self.resource_deps.iter().any(|c| {
world
.get_resource_change_ticks_by_id(*c)
.map(|ct| ct.is_changed(self.tick, tick))
.unwrap_or(false)
})
}
pub(crate) fn take_deps(&mut self, other: &mut Self) {
self.component_deps = std::mem::take(&mut other.component_deps);
self.resource_deps = std::mem::take(&mut other.resource_deps);
self.cleanups = std::mem::take(&mut other.cleanups);
self.hook_states = std::mem::take(&mut other.hook_states);
self.changed.store(
other.changed.load(std::sync::atomic::Ordering::Relaxed),
std::sync::atomic::Ordering::Relaxed,
);
}
pub(crate) fn take_hooks(&mut self, other: &mut Self) {
self.hook_states = std::mem::take(&mut other.hook_states)
}
}
struct DespawnEntityCmd(Entity);
impl Command for DespawnEntityCmd {
fn apply(self, world: &mut World) {
world.despawn(self.0);
}
}
pub(crate) fn cleanup_tracking_scopes(world: &mut World) {
world
.register_component_hooks::<TrackingScope>()
.on_remove(|mut world, entity, _component| {
let mut scope = world.get_mut::<TrackingScope>(entity).unwrap();
let mut cleanups = std::mem::take(&mut scope.cleanups);
let mut hooks = std::mem::take(&mut scope.hook_states);
for cleanup_fn in cleanups.drain(..) {
cleanup_fn(&mut world);
}
for hook in hooks.drain(..).rev() {
match hook {
HookState::Entity(ent) => {
world.commands().add(DespawnEntityCmd(ent));
}
HookState::Mutable(mutable_ent, _) => {
world.commands().add(DespawnEntityCmd(mutable_ent));
}
HookState::Callback(callback) => {
world.commands().add(UnregisterCallbackCmd(callback));
}
HookState::Effect(_) | HookState::Memo(_) => {
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Resource, Default)]
struct TestResource(bool);
#[test]
fn test_resource_deps_changed() {
let mut world = World::default();
let tick = world.change_tick();
let mut scope = TrackingScope::new(tick);
assert!(!scope.dependencies_changed(&world, tick));
world.increment_change_tick();
world.insert_resource(TestResource(false));
scope.track_resource::<TestResource>(&world);
assert!(scope.resource_deps.len() == 1);
let tick = world.change_tick();
assert!(scope.dependencies_changed(&world, tick));
scope.tick = tick;
assert!(!scope.dependencies_changed(&world, tick));
world.increment_change_tick();
world.get_resource_mut::<TestResource>().unwrap().0 = true;
let tick = world.change_tick();
assert!(scope.dependencies_changed(&world, tick));
}
}