use crate::bevy::components::Guid;
#[cfg(test)]
use crate::core::persist::Persist;
use crate::core::session::PersistenceSession;
use bevy::prelude::*;
use std::any::TypeId;
pub fn auto_dirty_tracking_entity_system<T: Component + 'static>(
mut session: ResMut<PersistenceSession>,
changed: Query<Entity, Changed<T>>,
added: Query<Entity, Added<T>>,
) {
let type_id = TypeId::of::<T>();
for entity in changed.iter() {
bevy::log::debug!(
"Marking entity {:?} as dirty due to changed component {}",
entity,
std::any::type_name::<T>()
);
session.track_component_changed(entity, type_id, added.contains(entity));
}
for entity in added.iter() {
bevy::log::debug!(
"Marking entity {:?} as dirty due to added component {}",
entity,
std::any::type_name::<T>()
);
session.track_component_added(entity, type_id);
}
}
pub fn auto_dirty_tracking_resource_system<T: Resource + 'static>(
mut session: ResMut<PersistenceSession>,
resource: Option<Res<T>>,
) {
if let Some(resource) = resource {
if resource.is_changed() {
session.track_resource_changed(TypeId::of::<T>());
}
}
}
#[cfg(not(feature = "bevy_many_relationship_edges"))]
pub fn auto_dirty_tracking_bevy_relationship_system<
R: Component + bevy::ecs::relationship::Relationship,
>(
mut session: ResMut<PersistenceSession>,
changed_query: Query<Entity, Or<(Added<R>, Changed<R>)>>,
mut removed: RemovedComponents<R>,
) {
for entity in changed_query.iter() {
bevy::log::debug!(
"Marking entity {:?} as relationship-dirty due to Relationship<{}>",
entity,
std::any::type_name::<R>()
);
session.track_relationship_entity_changed(entity);
}
for entity in removed.read() {
bevy::log::debug!(
"Marking entity {:?} as relationship-dirty due to removal of Relationship<{}>",
entity,
std::any::type_name::<R>()
);
session.track_relationship_entity_changed(entity);
}
}
#[cfg(feature = "bevy_many_relationship_edges")]
pub fn auto_dirty_tracking_relationship_system<R: Send + Sync + 'static>(
mut session: ResMut<PersistenceSession>,
query: Query<Entity, Changed<bevy_many_relationships::OutgoingRelationships<R>>>,
mut removed: RemovedComponents<bevy_many_relationships::OutgoingRelationships<R>>,
) {
for entity in query.iter() {
bevy::log::debug!(
"Marking entity {:?} as relationship-dirty due to OutgoingRelationships<{}>",
entity,
std::any::type_name::<R>()
);
session.track_relationship_entity_changed(entity);
}
for entity in removed.read() {
bevy::log::debug!(
"Marking entity {:?} as relationship-dirty due to removal of OutgoingRelationships<{}>",
entity,
std::any::type_name::<R>()
);
session.track_relationship_entity_changed(entity);
}
}
pub(crate) fn auto_despawn_tracking_resource_system(ecs: &mut World) {
let presence_snapshot = {
let session = ecs.resource::<PersistenceSession>();
session.resource_presence_snapshot(ecs)
};
let mut session = ecs.resource_mut::<PersistenceSession>();
for (type_id, is_present) in presence_snapshot {
let was_present = session.update_resource_presence(type_id, is_present);
if was_present && !is_present {
session.mark_resource_despawned_type_id(type_id);
}
}
}
pub(crate) fn auto_despawn_tracking_system(
mut session: ResMut<PersistenceSession>,
mut removed: RemovedComponents<Guid>,
) {
for entity in removed.read() {
session.mark_despawned(entity);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Component, Debug, Clone, PartialEq, Serialize, Deserialize)]
struct TestHealth {
value: i32,
}
impl Persist for TestHealth {
fn name() -> &'static str {
"TestHealth"
}
}
#[test]
fn read_only_access_does_not_mark_dirty() {
let mut app = App::new();
let session = PersistenceSession::new();
app.insert_resource(session);
app.add_systems(Update, auto_dirty_tracking_entity_system::<TestHealth>);
let entity = app.world_mut().spawn(TestHealth { value: 100 }).id();
app.update();
{
let mut session = app.world_mut().resource_mut::<PersistenceSession>();
session.clear_dirty_entity_components();
}
{
let health = app.world().get::<TestHealth>(entity).unwrap();
assert_eq!(health.value, 100);
}
app.update();
{
let session = app.world().resource::<PersistenceSession>();
assert!(
!session.is_entity_dirty(entity),
"Entity was incorrectly marked dirty after read-only access"
);
}
{
let mut health = app.world_mut().get_mut::<TestHealth>(entity).unwrap();
health.value = 200;
}
app.update();
{
let session = app.world().resource::<PersistenceSession>();
assert!(
session.is_entity_dirty(entity),
"Entity should be marked dirty after modification"
);
}
}
#[test]
fn hydration_scope_suppresses_added_dirt() {
use super::super::ecs_plumbing::finish_hydration;
use super::super::plugin::PersistenceSystemSet;
let mut app = App::new();
app.add_plugins(bevy::prelude::MinimalPlugins);
app.configure_sets(
PostUpdate,
(
PersistenceSystemSet::TrackChanges,
PersistenceSystemSet::FinishHydration,
)
.chain(),
);
let mut session = PersistenceSession::new();
session.register_component_named::<TestHealth>("TestHealth");
app.insert_resource(session);
app.add_systems(
PostUpdate,
auto_dirty_tracking_entity_system::<TestHealth>.in_set(PersistenceSystemSet::TrackChanges),
);
app.add_systems(
PostUpdate,
finish_hydration.in_set(PersistenceSystemSet::FinishHydration),
);
let entity = app.world_mut().spawn_empty().id();
app.world_mut().resource_scope(|world, mut session: Mut<PersistenceSession>| {
session
.hydrate_entity_component(
world,
entity,
"TestHealth",
json!({ "value": 1 }),
)
.expect("hydrate should succeed");
});
app.update();
let session = app.world().resource::<PersistenceSession>();
assert!(
!session.is_entity_dirty(entity),
"components inserted under hydration scope must not mark dirty"
);
assert!(
!session.is_hydrating(),
"hydration scope should end after finish_hydration"
);
}
#[test]
fn post_load_mutation_marks_dirty() {
let mut app = App::new();
app.add_plugins(bevy::prelude::MinimalPlugins);
let mut session = PersistenceSession::new();
session.register_component_named::<TestHealth>("TestHealth");
app.insert_resource(session);
app.add_systems(
Update,
auto_dirty_tracking_entity_system::<TestHealth>,
);
let entity = app.world_mut().spawn_empty().id();
app.world_mut()
.entity_mut(entity)
.insert(TestHealth { value: 100 });
{
let mut health = app.world_mut().get_mut::<TestHealth>(entity).unwrap();
health.value = 200;
}
app.update();
{
let session = app.world().resource::<PersistenceSession>();
assert!(
session.is_entity_dirty(entity),
"post-load user mutation must mark dirty"
);
}
}
}