use std::marker::PhantomData;
use bevy::{
ecs::{component::Mutable, entity::MapEntities},
prelude::*,
};
use crate::{LoadWorld, LoadWorldSystems, RollbackEntityMap};
pub struct ComponentMapEntitiesPlugin<C>
where
C: Component<Mutability = Mutable> + MapEntities,
{
_phantom: PhantomData<C>,
}
impl<C> Default for ComponentMapEntitiesPlugin<C>
where
C: Component<Mutability = Mutable> + MapEntities,
{
fn default() -> Self {
Self {
_phantom: default(),
}
}
}
impl<C> ComponentMapEntitiesPlugin<C>
where
C: Component<Mutability = Mutable> + MapEntities,
{
pub fn update(world: &mut World) {
world.resource_scope(|world: &mut World, map: Mut<RollbackEntityMap>| {
apply_rollback_map_to_component_inner::<C>(world, map);
});
}
}
fn apply_rollback_map_to_component_inner<C>(world: &mut World, map: Mut<RollbackEntityMap>)
where
C: Component<Mutability = Mutable> + MapEntities,
{
for (original, _new) in map.iter() {
if let Some(mut component) = world.get_mut::<C>(original) {
component.map_entities(&mut map.as_ref());
}
}
trace!("Mapped {}", disqualified::ShortName::of::<C>());
}
impl<C> Plugin for ComponentMapEntitiesPlugin<C>
where
C: Component<Mutability = Mutable> + MapEntities,
{
fn build(&self, app: &mut App) {
app.add_systems(LoadWorld, Self::update.in_set(LoadWorldSystems::Mapping));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snapshot::{
AdvanceWorld, Rollback, RollbackApp, SnapshotPlugin,
tests::{advance_frame, load_world, save_world},
};
use bevy::log::LogPlugin;
#[derive(Resource, Default)]
enum Input {
#[default]
None,
SpawnFriend,
DespawnFriends,
}
#[derive(Component, MapEntities, Clone, Copy)]
struct Likes(#[entities] Entity);
#[derive(Component, Clone, Copy)]
#[require(Rollback)]
struct Friend;
#[derive(Component)]
#[require(Rollback)]
struct Player;
fn like_single_friend(
mut commands: Commands,
player: Single<Entity, With<Player>>,
friends: Query<Entity, With<Friend>>,
) {
if let Ok(friend) = friends.single() {
commands.entity(player.entity()).insert(Likes(friend));
} else {
commands.entity(player.entity()).remove::<Likes>();
}
}
fn spawn_friend(mut commands: Commands, inputs: Res<Input>) {
if let Input::SpawnFriend = *inputs {
commands.spawn(Friend);
}
}
fn despawn_friends(
inputs: Res<Input>,
friends: Query<Entity, With<Friend>>,
mut commands: Commands,
) {
if let Input::DespawnFriends = *inputs {
for friend in &friends {
commands.entity(friend).despawn();
}
}
}
fn spawn_player(mut commands: Commands) {
commands.spawn(Player);
}
#[test]
fn test_map_entities() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins(SnapshotPlugin);
app.rollback_component_with_copy::<Likes>();
app.update_component_with_map_entities::<Likes>();
app.add_systems(AdvanceWorld, (spawn_friend, like_single_friend).chain());
app.add_systems(Startup, spawn_player);
app.update();
let get_friend_entity = |world: &mut World| {
world
.query_filtered::<Entity, With<Friend>>()
.single(world)
.ok()
};
let get_liked_entity = |world: &mut World| {
world
.query::<&Likes>()
.single(world)
.ok()
.map(|likes| likes.0)
};
save_world(app.world_mut());
assert_eq!(get_friend_entity(app.world_mut()), None);
assert_eq!(get_liked_entity(app.world_mut()), None);
app.world_mut().insert_resource(Input::SpawnFriend);
advance_frame(app.world_mut());
let initial_friend_entity = get_friend_entity(app.world_mut()).unwrap();
let initial_liked_entity = get_liked_entity(app.world_mut()).unwrap();
assert_eq!(initial_friend_entity, initial_liked_entity);
load_world(app.world_mut(), 0);
assert_eq!(get_friend_entity(app.world_mut()), None);
assert_eq!(get_liked_entity(app.world_mut()), None);
app.world_mut().insert_resource(Input::SpawnFriend);
advance_frame(app.world_mut());
{
let friend_entity = get_friend_entity(app.world_mut()).unwrap();
let liked_entity = get_liked_entity(app.world_mut()).unwrap();
assert_eq!(friend_entity, liked_entity);
assert_ne!(friend_entity, initial_friend_entity);
assert_ne!(liked_entity, initial_liked_entity);
}
}
#[test]
fn restores_entity_mappings_after_despawning() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins(LogPlugin::default());
app.add_plugins(SnapshotPlugin);
app.rollback_component_with_copy::<Friend>();
app.rollback_component_with_copy::<Likes>();
app.update_component_with_map_entities::<Likes>();
app.add_systems(
AdvanceWorld,
(spawn_friend, like_single_friend, despawn_friends).chain(),
);
app.add_systems(Startup, spawn_player);
app.update();
let get_friend_entity = |world: &mut World| {
world
.query_filtered::<Entity, With<Friend>>()
.single(world)
.ok()
};
let get_liked_entity = |world: &mut World| {
world
.query::<&Likes>()
.single(world)
.ok()
.map(|likes| likes.0)
};
app.world_mut().insert_resource(Input::SpawnFriend);
advance_frame(app.world_mut());
let initial_friend_entity = get_friend_entity(app.world_mut()).unwrap();
let initial_liked_entity = get_liked_entity(app.world_mut()).unwrap();
assert_eq!(initial_friend_entity, initial_liked_entity);
save_world(app.world_mut());
app.world_mut().insert_resource(Input::DespawnFriends);
advance_frame(app.world_mut());
assert_eq!(get_friend_entity(app.world_mut()), None);
load_world(app.world_mut(), 1);
{
let friend_entity = get_friend_entity(app.world_mut()).unwrap();
let liked_entity = get_liked_entity(app.world_mut()).unwrap();
assert_eq!(friend_entity, liked_entity);
assert_ne!(friend_entity, initial_friend_entity);
assert_ne!(liked_entity, initial_liked_entity);
}
}
}