use bevy_ecs::prelude::*; use bevy_reflect::{Reflect, ReflectComponent}; use bevy_utils::{HashMap, HashSet}; use std::{ borrow::Cow, fmt::Debug, ops::{Deref, DerefMut}, }; /// A collection of labels #[derive(Default, Reflect)] #[reflect(Component)] pub struct Labels { labels: HashSet>, } impl Debug for Labels { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut list = f.debug_list(); for label in self.iter() { list.entry(&label); } list.finish() } } impl<'a, T, L: Into>> From for Labels where T: IntoIterator, { fn from(value: T) -> Self { let mut labels = HashSet::default(); for label in value { labels.insert(label.into()); } Self { labels } } } impl Labels { pub fn contains>>(&self, label: T) -> bool { self.labels.contains(&label.into()) } pub fn insert>>(&mut self, label: T) { self.labels.insert(label.into()); } pub fn remove>>(&mut self, label: T) { self.labels.remove(&label.into()); } pub fn iter(&self) -> impl Iterator { self.labels.iter().map(|label| label.deref()) } } /// Maintains a mapping from [Entity](bevy_ecs::prelude::Entity) ids to entity labels and entity labels to [Entities](bevy_ecs::prelude::Entity). #[derive(Debug, Default)] pub struct EntityLabels { label_entities: HashMap, Vec>, entity_labels: HashMap>>, } impl EntityLabels { pub fn get(&self, label: &str) -> &[Entity] { self.label_entities .get(label) .map(|entities| entities.as_slice()) .unwrap_or(&[]) } } pub(crate) fn entity_labels_system( mut entity_labels: ResMut, query: Query<(Entity, &Labels), Changed>, ) { let entity_labels = entity_labels.deref_mut(); for entity in query.removed::() { if let Some(labels) = entity_labels.entity_labels.get(entity) { for label in labels.iter() { if let Some(entities) = entity_labels.label_entities.get_mut(label) { entities.retain(|e| e != entity); } } } } for (entity, labels) in query.iter() { let current_labels = entity_labels .entity_labels .entry(entity) .or_insert_with(HashSet::default); for removed_label in current_labels.difference(&labels.labels) { if let Some(entities) = entity_labels.label_entities.get_mut(removed_label) { entities.retain(|e| *e != entity); } } for added_label in labels.labels.difference(¤t_labels) { entity_labels .label_entities .entry(added_label.clone()) .or_insert_with(Vec::new) .push(entity); } *current_labels = labels.labels.clone(); } } #[cfg(test)] mod tests { use super::*; use bevy_ecs::Stage; fn setup() -> (World, Resources, bevy_ecs::Schedule) { let world = World::new(); let mut resources = Resources::default(); resources.insert(EntityLabels::default()); let mut schedule = bevy_ecs::Schedule::default(); schedule.add_stage("test", SystemStage::single_threaded()); schedule.add_system_to_stage("test", entity_labels_system.system()); (world, resources, schedule) } fn holy_cow() -> Labels { Labels::from(["holy", "cow"].iter().cloned()) } fn holy_shamoni() -> Labels { Labels::from(["holy", "shamoni"].iter().cloned()) } #[test] fn adds_spawned_entity() { let (mut world, mut resources, mut schedule) = setup(); let e1 = world.spawn((holy_cow(),)); schedule.run(&mut world, &mut resources); let entity_labels = resources.get::().unwrap(); assert_eq!(entity_labels.get("holy"), &[e1], "holy"); assert_eq!(entity_labels.get("cow"), &[e1], "cow"); assert_eq!(entity_labels.get("shalau"), &[], "shalau"); } #[test] fn add_labels() { let (mut world, mut resources, mut schedule) = setup(); let e1 = world.spawn((holy_cow(),)); schedule.run(&mut world, &mut resources); world.get_mut::(e1).unwrap().insert("shalau"); schedule.run(&mut world, &mut resources); let entity_labels = resources.get::().unwrap(); assert_eq!(entity_labels.get("holy"), &[e1], "holy"); assert_eq!(entity_labels.get("cow"), &[e1], "cow"); assert_eq!(entity_labels.get("shalau"), &[e1], "shalau"); } #[test] fn remove_labels() { let (mut world, mut resources, mut schedule) = setup(); let e1 = world.spawn((holy_cow(),)); schedule.run(&mut world, &mut resources); world.get_mut::(e1).unwrap().remove("holy"); schedule.run(&mut world, &mut resources); let entity_labels = resources.get::().unwrap(); assert_eq!(entity_labels.get("holy"), &[], "holy"); assert_eq!(entity_labels.get("cow"), &[e1], "cow"); assert_eq!(entity_labels.get("shalau"), &[], "shalau"); } #[test] fn removes_despawned_entity() { let (mut world, mut resources, mut schedule) = setup(); let e1 = world.spawn((holy_cow(),)); schedule.run(&mut world, &mut resources); world.despawn(e1).unwrap(); schedule.run(&mut world, &mut resources); let entity_labels = resources.get::().unwrap(); assert_eq!(entity_labels.get("holy"), &[], "holy"); assert_eq!(entity_labels.get("cow"), &[], "cow"); assert_eq!(entity_labels.get("shalau"), &[], "shalau"); } #[test] fn removes_labels_when_component_removed() { let (mut world, mut resources, mut schedule) = setup(); let e1 = world.spawn((holy_cow(),)); schedule.run(&mut world, &mut resources); world.remove_one::(e1).unwrap(); schedule.run(&mut world, &mut resources); let entity_labels = resources.get::().unwrap(); assert_eq!(entity_labels.get("holy"), &[], "holy"); assert_eq!(entity_labels.get("cow"), &[], "cow"); assert_eq!(entity_labels.get("shalau"), &[], "shalau"); } #[test] fn adds_another_spawned_entity() { let (mut world, mut resources, mut schedule) = setup(); let e1 = world.spawn((holy_cow(),)); schedule.run(&mut world, &mut resources); let e2 = world.spawn((holy_shamoni(),)); schedule.run(&mut world, &mut resources); let entity_labels = resources.get::().unwrap(); assert_eq!(entity_labels.get("holy"), &[e1, e2], "holy"); assert_eq!(entity_labels.get("cow"), &[e1], "cow"); assert_eq!(entity_labels.get("shamoni"), &[e2], "shamoni"); assert_eq!(entity_labels.get("shalau"), &[], "shalau"); } #[test] fn removes_despawned_entity_but_leaves_other() { let (mut world, mut resources, mut schedule) = setup(); let e1 = world.spawn((holy_cow(),)); schedule.run(&mut world, &mut resources); let e2 = world.spawn((holy_shamoni(),)); schedule.run(&mut world, &mut resources); world.despawn(e1).unwrap(); schedule.run(&mut world, &mut resources); let entity_labels = resources.get::().unwrap(); assert_eq!(entity_labels.get("holy"), &[e2], "holy"); assert_eq!(entity_labels.get("cow"), &[], "cow"); assert_eq!(entity_labels.get("shamoni"), &[e2], "shamoni"); assert_eq!(entity_labels.get("shalau"), &[], "shalau"); } }