#![allow(deprecated)] #![doc = include_str!("../README.md")]
#![warn(missing_docs)]
pub mod prelude {
pub use crate::{tag_filter, tags, Tag, TagFilter, TagPlugin, Tags};
pub use crate::{ComponentTags, WithTags};
}
mod filter;
pub extern crate inventory;
use std::borrow::Cow;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use bevy_app::{App, Plugin};
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::world::DeferredWorld;
use bevy_platform::collections::{HashMap, HashSet};
use bevy_reflect::prelude::*;
use itertools::Itertools;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use moonshine_util::prelude::*;
pub use self::filter::*;
pub struct TagPlugin;
impl Plugin for TagPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<TagNames>()
.register_type::<Tag>()
.register_type::<Tags>()
.register_type::<HashSet<Tag>>()
.register_type_data::<HashSet<Tag>, ReflectSerialize>()
.register_type_data::<HashSet<Tag>, ReflectDeserialize>()
.register_type::<TagFilter>()
.register_type::<Box<TagFilter>>()
.register_type_data::<Box<TagFilter>, ReflectSerialize>()
.register_type_data::<Box<TagFilter>, ReflectDeserialize>();
}
}
#[macro_export]
macro_rules! tags {
($(#[$meta:meta])* $v:vis $name:ident $(,)?) => {
$(#[$meta])*
$v const $name: $crate::Tag = $crate::Tag::new(stringify!($name));
$crate::inventory::submit! {
$crate::TagMeta { tag: $name, name: stringify!($name) }
}
};
($(#[$meta:meta])* $v0:vis $n0:ident, $($v:vis $n:ident),* $(,)?) => {
$(#[$meta])*
$v0 const $n0: $crate::Tag = $crate::Tag::new(stringify!($n0));
$crate::inventory::submit! {
$crate::TagMeta { tag: $n0, name: stringify!($n0) }
}
$crate::tags!($(#[$meta])* $($v $n),*);
};
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Reflect)]
#[cfg_attr(not(feature = "pretty-serde"), derive(Serialize, Deserialize))]
#[reflect(Hash, PartialEq)]
pub struct Tag(u64);
impl Tag {
pub const fn new(source: &str) -> Self {
Self::from_hash(const_fnv1a_hash::fnv1a_hash_str_64(source))
}
pub const fn from_hash(hash: u64) -> Self {
Self(hash)
}
pub const fn hash(&self) -> u64 {
self.0
}
pub fn pretty_hash(&self) -> String {
base31::encode(self.0)
}
pub fn matches(&self, filter: &TagFilter) -> bool {
use TagFilter::*;
match filter {
Equal(a) => a.iter().exactly_one().is_ok_and(|tag| tag == *self),
AllOf(a) => a.is_empty() || a.iter().exactly_one().is_ok_and(|tag| tag == *self),
AnyOf(a) => a.contains(*self),
And(a, b) => self.matches(a) && self.matches(b),
Or(a, b) => self.matches(a) || self.matches(b),
Not(a) => !self.matches(a),
}
}
pub fn resolve_name(&self) -> Option<&'static str> {
TagMeta::iter()
.find(|meta| meta.tag == *self)
.map(|meta| meta.name)
}
pub fn pretty_name(&self) -> Cow<'static, str> {
self.resolve_name()
.map(|name| name.into())
.unwrap_or_else(|| self.pretty_hash().into())
}
pub fn iter_names() -> impl Iterator<Item = &'static str> {
TagMeta::iter().map(|meta| meta.name)
}
}
impl fmt::Debug for Tag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Tag({})", self.0)
}
}
impl Hash for Tag {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u64(self.0)
}
}
impl FromWorld for Tag {
fn from_world(_: &mut World) -> Self {
Self(u64::MAX)
}
}
impl IntoIterator for Tag {
type Item = Tag;
type IntoIter = std::iter::Once<Tag>;
fn into_iter(self) -> Self::IntoIter {
std::iter::once(self)
}
}
#[cfg(feature = "pretty-serde")]
impl Serialize for Tag {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if let Some(name) = TagNames::global().get_cached(*self) {
serializer.serialize_str(&name)
} else {
serializer.serialize_u64(self.0)
}
}
}
#[cfg(feature = "pretty-serde")]
impl<'de> Deserialize<'de> for Tag {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(TagVisitor)
}
}
#[cfg(feature = "pretty-serde")]
struct TagVisitor;
#[cfg(feature = "pretty-serde")]
impl<'de> serde::de::Visitor<'de> for TagVisitor {
type Value = Tag;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "string or numeric hash")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Tag::new(v))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Tag::new(&v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Tag::from_hash(v))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Reflect)]
pub struct TagMeta {
pub tag: Tag,
pub name: &'static str,
}
impl TagMeta {
pub fn iter() -> impl Iterator<Item = &'static TagMeta> {
inventory::iter::<TagMeta>()
}
}
inventory::collect!(TagMeta);
#[derive(Resource, Default)]
pub struct TagNames(HashMap<Tag, Cow<'static, str>>);
impl TagNames {
pub fn generate() -> Self {
let mut inner = HashMap::new();
for &TagMeta { tag, name } in TagMeta::iter() {
inner.insert(tag, name.into());
}
Self(inner)
}
pub fn global() -> &'static Self {
static GLOBAL: Lazy<TagNames> = Lazy::new(TagNames::generate);
&GLOBAL
}
pub fn get(&mut self, tag: Tag) -> Cow<'static, str> {
self.0
.entry(tag)
.or_insert_with(|| tag.pretty_name())
.clone()
}
pub fn get_cached(&self, tag: Tag) -> Option<Cow<'static, str>> {
self.0.get(&tag).cloned()
}
pub fn iter(&self) -> impl Iterator<Item = (&Tag, &Cow<'static, str>)> {
self.0.iter()
}
}
#[derive(Component, Default, Debug, Clone, PartialEq, Eq, Reflect)]
#[cfg_attr(not(feature = "pretty-serde"), derive(Serialize, Deserialize))]
#[reflect(Component)]
pub struct Tags(HashSet<Tag>);
impl Tags {
pub fn static_empty() -> &'static Tags {
static EMPTY: Lazy<Tags> = Lazy::new(Tags::new);
&EMPTY
}
#[doc(hidden)]
#[deprecated(since = "0.3.0", note = "use `static_empty` instead")]
pub fn empty() -> &'static Tags {
Self::static_empty()
}
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn contains(&self, tag: Tag) -> bool {
self.0.contains(&tag)
}
pub fn insert(&mut self, tag: Tag) -> bool {
self.0.insert(tag)
}
pub fn remove(&mut self, tag: Tag) -> bool {
self.0.remove(&tag)
}
pub fn retain(&mut self, f: impl FnMut(&Tag) -> bool) {
self.0.retain(f)
}
pub fn iter(&self) -> impl Iterator<Item = Tag> + '_ {
self.0.iter().copied()
}
#[must_use]
pub fn union(mut self, other: impl Into<Tags>) -> Self {
self.extend(other.into());
self
}
pub fn extend(&mut self, other: impl Into<Tags>) {
self.0.extend(other.into());
}
pub fn is_disjoint(&self, tags: &Tags) -> bool {
self.0.is_disjoint(&tags.0)
}
pub fn is_subset(&self, tags: &Tags) -> bool {
self.0.is_subset(&tags.0)
}
pub fn is_superset(&self, tags: &Tags) -> bool {
self.0.is_superset(&tags.0)
}
pub fn matches(&self, filter: &TagFilter) -> bool {
use TagFilter::*;
match filter {
Equal(a) => a == self,
AllOf(a) => a.is_subset(self),
AnyOf(a) => !a.is_disjoint(self),
And(a, b) => self.matches(a) && self.matches(b),
Or(a, b) => self.matches(a) || self.matches(b),
Not(a) => !self.matches(a),
}
}
pub fn to_pretty_string(&self) -> String {
self.0
.iter()
.map(|tag| {
tag.resolve_name()
.map(|name| name.to_string())
.unwrap_or_else(|| tag.pretty_hash())
})
.join(", ")
}
}
impl From<Tag> for Tags {
fn from(tag: Tag) -> Self {
let mut tags = Tags::new();
tags.insert(tag);
tags
}
}
impl FromIterator<Tag> for Tags {
fn from_iter<T: IntoIterator<Item = Tag>>(iter: T) -> Self {
Self(HashSet::from_iter(iter))
}
}
impl<const N: usize> From<[Tag; N]> for Tags {
fn from(tags: [Tag; N]) -> Self {
Tags::from_iter(tags)
}
}
impl<const N: usize> PartialEq<[Tag; N]> for Tags {
fn eq(&self, other: &[Tag; N]) -> bool {
self.0.len() == N && other.iter().all(|tag| self.0.contains(tag))
}
}
impl<const N: usize> PartialEq<[Tag; N]> for &Tags {
fn eq(&self, other: &[Tag; N]) -> bool {
self.0.len() == N && other.iter().all(|tag| self.0.contains(tag))
}
}
impl<const N: usize> PartialEq<Tags> for [Tag; N] {
fn eq(&self, other: &Tags) -> bool {
other == self
}
}
impl MergeComponent for Tags {
fn merge(&mut self, other: Self) {
self.0.extend(other);
}
}
impl IntoIterator for Tags {
type Item = Tag;
type IntoIter = bevy_platform::collections::hash_set::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[cfg(feature = "pretty-serde")]
impl Serialize for Tags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(Some(self.len()))?;
for tag in self.iter() {
seq.serialize_element(&tag)?;
}
seq.end()
}
}
#[cfg(feature = "pretty-serde")]
impl<'de> Deserialize<'de> for Tags {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(TagsVisitor)
}
}
#[cfg(feature = "pretty-serde")]
struct TagsVisitor;
#[cfg(feature = "pretty-serde")]
impl<'de> serde::de::Visitor<'de> for TagsVisitor {
type Value = Tags;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "string or numeric hash")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Tag::new(v).into())
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Tag::new(&v).into())
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Tag::from_hash(v).into())
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut tags = Tags::new();
while let Ok(Some(tag)) = seq.next_element::<Tag>() {
tags.insert(tag);
}
Ok(tags)
}
}
#[derive(Component)]
#[component(storage = "SparseSet")]
#[component(on_add = Self::on_add)]
#[deprecated(since = "0.3.0", note = "use `AddWith` from `moonshine-util` instead")]
pub struct WithTags<I: IntoIterator<Item = Tag>, F: FnOnce() -> I>(pub F)
where
F: 'static + Send + Sync,
I: 'static + Send + Sync;
impl<I: IntoIterator<Item = Tag>, F: FnOnce() -> I> WithTags<I, F>
where
F: 'static + Send + Sync,
I: 'static + Send + Sync,
{
fn on_add(mut world: DeferredWorld, ctx: HookContext) {
let entity = ctx.entity;
world.commands().queue(move |world: &mut World| {
let mut entity = world.entity_mut(entity);
let this = entity.take::<Self>().unwrap();
let new_tags = Tags::from_iter(this.0());
if let Some(mut tags) = entity.get_mut::<Tags>() {
tags.extend(new_tags);
} else {
entity.insert(new_tags);
}
});
}
}
#[derive(Component)]
#[component(storage = "SparseSet")]
#[component(on_add = Self::on_add)]
#[deprecated(since = "0.3.0", note = "use `AddFrom` from `moonshine-util` instead")]
pub struct ComponentTags<T: Component>(Tags, PhantomData<T>);
impl<T: Component> ComponentTags<T> {
fn on_add(mut world: DeferredWorld, ctx: HookContext) {
let entity = ctx.entity;
world.commands().queue(move |world: &mut World| {
let mut entity = world.entity_mut(entity);
let ComponentTags(new_tags, ..) = entity.take::<Self>().unwrap();
if let Some(mut tags) = entity.get_mut::<Tags>() {
tags.extend(new_tags);
} else {
entity.insert(new_tags);
}
});
}
}
impl<T: Component, I: Into<Tags>> From<I> for ComponentTags<T> {
fn from(tags: I) -> Self {
Self(tags.into(), PhantomData)
}
}
#[cfg(test)]
mod tests {
use super::*;
tags!(A, B, C);
#[test]
fn match_eq() {
assert_eq!(A, A);
assert_ne!(A, B);
}
pub fn matches(tags: impl Into<Tags>, filter: &TagFilter) -> bool {
tags.into().matches(filter)
}
#[test]
fn match_empty() {
assert!(matches([], &tag_filter!([])));
assert!(matches([], &tag_filter!([..])));
assert!(matches([], &tag_filter!([..] | [])));
assert!(matches([], &tag_filter!([..] & [])));
assert!(!matches([], &tag_filter!(![])));
assert!(!matches([], &tag_filter!([A])));
assert!(!matches([], &tag_filter!([A, ..])));
assert!(!matches([], &tag_filter!([A, B])));
assert!(!matches([], &tag_filter!([A | B])));
}
#[test]
fn match_tag() {
assert!(matches(A, &tag_filter!([..])));
assert!(matches(A, &tag_filter!([..] | [])));
assert!(matches(A, &tag_filter!(![])));
assert!(matches(A, &tag_filter!([A])));
assert!(matches(A, &tag_filter!([A | B])));
assert!(matches(A, &tag_filter!([B | A])));
assert!(matches(A, &tag_filter!(![B])));
assert!(matches(A, &tag_filter!([A, ..])));
assert!(matches(A, &tag_filter!([A, ..] | [B])));
assert!(A.matches(&tag_filter!([..])));
assert!(A.matches(&tag_filter!([..] | [])));
assert!(A.matches(&tag_filter!(![])));
assert!(A.matches(&tag_filter!([A])));
assert!(A.matches(&tag_filter!([A | B])));
assert!(A.matches(&tag_filter!([B | A])));
assert!(A.matches(&tag_filter!(![B])));
assert!(A.matches(&tag_filter!([A, ..])));
assert!(A.matches(&tag_filter!([A, ..] | [B])));
assert!(!matches(A, &tag_filter!([])));
assert!(!matches(A, &tag_filter!(![A])));
assert!(!matches(A, &tag_filter!([B])));
assert!(!matches(A, &tag_filter!([A, B, ..])));
assert!(!matches(A, &tag_filter!([B, A, ..])));
assert!(!matches(A, &tag_filter!([B | C])));
assert!(!matches(A, &tag_filter!([B, C, ..])));
assert!(!A.matches(&tag_filter!([])));
assert!(!A.matches(&tag_filter!(![A])));
assert!(!A.matches(&tag_filter!([B])));
assert!(!A.matches(&tag_filter!([A, B, ..])));
assert!(!A.matches(&tag_filter!([B, A, ..])));
assert!(!A.matches(&tag_filter!([B | C])));
assert!(!A.matches(&tag_filter!([B, C, ..])));
}
#[test]
fn match_tags() {
assert!(matches([A, B], &tag_filter!([..])));
assert!(matches([A, B], &tag_filter!([..] | [])));
assert!(matches([A, B], &tag_filter!(![])));
assert!(matches([A, B], &tag_filter!([A, B])));
assert!(matches([A, B], &tag_filter!([B, A])));
assert!(matches([A, B], &tag_filter!([A, ..])));
assert!(matches([A, B], &tag_filter!([A, B, ..])));
assert!(matches([A, B], &tag_filter!([A, B, ..] | [B, C])));
assert!(!matches([A, B], &tag_filter!([])));
assert!(!matches([A, B], &tag_filter!([A])));
assert!(!matches([A, B], &tag_filter!([A, B, C])));
assert!(!matches([A, B], &tag_filter!(![A, B])));
assert!(!matches([A, B], &tag_filter!([A, C])));
assert!(!matches([A, B], &tag_filter!([A, C, ..])));
assert!(!matches([A, B], &tag_filter!([A, B, C, ..])));
assert!(!matches([A, B], &tag_filter!([A, C, ..] | [B, C])));
assert!(!matches([A, B], &tag_filter!([C, ..])));
}
#[test]
fn with_tags() {
let mut world = World::new();
let entity = world
.spawn((WithTags(|| [A]), WithTags(|| [A, B]), WithTags(|| [B, C])))
.id();
world.flush();
assert_eq!(
world.get::<Tags>(entity).unwrap_or(Tags::static_empty()),
[A, B, C]
);
}
#[test]
fn with_tags_existing() {
let mut world = World::new();
let entity = world
.spawn((Tags::from(A), WithTags(|| [A, B]), WithTags(|| [B, C])))
.id();
world.flush();
assert_eq!(
world.get::<Tags>(entity).unwrap_or(Tags::static_empty()),
[A, B, C]
);
}
#[test]
fn tags_macro_usage() {
tags! {
pub N00,
}
tags! {
pub N01,
N02,
}
tags! {
pub N03,
pub N04,
}
tags! {
#[allow(non_upper_case_globals)]
pub n05,
}
tags! {
#[allow(non_upper_case_globals)]
n06,
}
tags! {
#[allow(non_upper_case_globals)] n07,
n08,
}
tags! {
#[allow(non_upper_case_globals)]
pub n09,
n10,
}
}
#[cfg(feature = "pretty-serde")]
#[test]
fn test_pretty_serde() {
let ux = tag_filter![A, B, ..];
let sx = "AllOf([\"A\",\"B\"])";
let s = ron::to_string(&ux).unwrap();
assert_eq!(s, sx);
let u: TagFilter = ron::from_str(&s).unwrap();
assert_eq!(u, ux);
}
}