diff --git a/Cargo.toml b/Cargo.toml index ad106d740ec7e..d04b6ce19ad38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -689,6 +689,9 @@ hotpatching = ["bevy_internal/hotpatching"] # Enable collecting debug information about systems and components to help with diagnostics debug = ["bevy_internal/debug"] +# XXX TODO: Document. +debug_tag = ["bevy_internal/debug_tag"] + [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.18.0-dev", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index eb39ed859e33a..37ec1cb6db88c 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -49,6 +49,9 @@ bevy_debug_stepping = [] ## This will often provide more detailed error messages. track_location = [] +## XXX TODO: Document. +debug_tag = [] + # Executor Backend ## Uses `async-executor` as a task execution backend. diff --git a/crates/bevy_ecs/src/debug_tag.rs b/crates/bevy_ecs/src/debug_tag.rs new file mode 100644 index 0000000000000..fba7b77d6240d --- /dev/null +++ b/crates/bevy_ecs/src/debug_tag.rs @@ -0,0 +1,181 @@ +//! XXX TODO: Document. + +use crate::component::Component; +use alloc::borrow::Cow; + +#[cfg(feature = "serialize")] +use { + alloc::string::{String, ToString}, + serde::{ + de::{Error, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, + }, +}; + +#[cfg(feature = "bevy_reflect")] +use { + crate::reflect::ReflectComponent, + bevy_reflect::{std_traits::ReflectDefault, Reflect}, +}; + +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// XXX TODO: Document +#[derive(Component, Clone)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Component, Default, Debug, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Deserialize, Serialize) +)] +pub struct DebugTag { + #[cfg(feature = "debug_tag")] + tag: Cow<'static, str>, +} + +impl DebugTag { + /// XXX TODO: Document + #[cfg_attr( + not(feature = "debug_tag"), + expect( + unused_variables, + reason = "The value will be ignored if the `debug_tag` feature is not enabled" + ) + )] + pub fn new(tag: impl Into>) -> Self { + #[cfg(feature = "debug_tag")] + let out = Self { tag: tag.into() }; + + #[cfg(not(feature = "debug_tag"))] + let out = Self {}; + + out + } +} + +/// XXX TODO: Document +#[macro_export] +macro_rules! debug_tag { + ($arg:expr) => { + if cfg!(feature = "debug_tag") { + DebugTag::new($arg) + } else { + DebugTag::default() + } + }; +} + +impl Default for DebugTag { + fn default() -> Self { + #[cfg(feature = "debug_tag")] + let out = Self::new(""); + + #[cfg(not(feature = "debug_tag"))] + let out = Self {}; + + out + } +} + +#[cfg(feature = "serialize")] +impl Serialize for DebugTag { + fn serialize(&self, serializer: S) -> Result { + #[cfg(feature = "debug_tag")] + let out = serializer.serialize_str(&self.tag); + + // XXX TODO: Think this through. Any potential for issues if it's serialized + // when disabled but then deserialized when enabled? Depends on use cases. + #[cfg(not(feature = "debug_tag"))] + let out = serializer.serialize_str(DEBUG_TAG_DISABLED); + + out + } +} + +#[cfg(feature = "serialize")] +impl<'de> Deserialize<'de> for DebugTag { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(DebugTagVisitor) + } +} + +#[cfg(feature = "serialize")] +struct DebugTagVisitor; + +#[cfg(feature = "serialize")] +impl<'de> Visitor<'de> for DebugTagVisitor { + type Value = DebugTag; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str(core::any::type_name::()) + } + + fn visit_str(self, v: &str) -> Result { + Ok(DebugTag::new(v.to_string())) + } + + fn visit_string(self, v: String) -> Result { + Ok(DebugTag::new(v)) + } +} + +#[cfg(not(feature = "debug_tag"))] +const DEBUG_TAG_DISABLED: &str = "[REDACTED]"; + +impl core::fmt::Debug for DebugTag { + #[inline(always)] + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + #[cfg(feature = "debug_tag")] + f.write_str(self.tag.as_ref())?; + + #[cfg(not(feature = "debug_tag"))] + f.write_str(DEBUG_TAG_DISABLED)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::format; + + #[test] + fn test_debug_format() { + #[cfg(feature = "debug_tag")] + let expected = "hello"; + + #[cfg(not(feature = "debug_tag"))] + let expected = DEBUG_TAG_DISABLED; + + let tag = DebugTag::new(expected); + + assert_eq!(format!("{tag:?}"), expected); + } +} + +#[cfg(all(test, feature = "serialize"))] +mod serde_tests { + use super::*; + use serde_test::{assert_ser_tokens, Token}; + + #[test] + fn test_serde() { + #[cfg(feature = "debug_tag")] + let expected = "hello"; + + #[cfg(not(feature = "debug_tag"))] + let expected = DEBUG_TAG_DISABLED; + + let tag = DebugTag::new(expected); + let tokens = &[Token::String(expected)]; + + // TODO: Also test deserialization? We can't use `serde_test::assert_de_tokens` + // as it requires the value to be `PartialEq`. + assert_ser_tokens(&tag, tokens); + } +} diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 2e9174bf2404c..fc7d60252fcf6 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -34,6 +34,7 @@ pub mod batching; pub mod bundle; pub mod change_detection; pub mod component; +pub mod debug_tag; pub mod entity; pub mod entity_disabling; pub mod error; @@ -77,6 +78,7 @@ pub mod prelude { change_detection::{DetectChanges, DetectChangesMut, Mut, Ref}, children, component::Component, + debug_tag::DebugTag, entity::{ContainsEntity, Entity, EntityMapper}, error::{BevyError, Result}, event::{EntityEvent, Event, EventReader, EventWriter, Events}, diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 3077662631c51..7475b74084b9d 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -427,6 +427,8 @@ hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] debug = ["bevy_utils/debug"] +debug_tag = ["bevy_ecs/debug_tag"] + [dependencies] # bevy (no_std) bevy_app = { path = "../bevy_app", version = "0.18.0-dev", default-features = false, features = [ diff --git a/docs/cargo_features.md b/docs/cargo_features.md index fb36c8f225000..f51ab205f8232 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -110,6 +110,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |dds|DDS compressed texture support| |debug|Enable collecting debug information about systems and components to help with diagnostics| |debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam| +|debug_tag|XXX TODO: Document.| |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in| |dlss|NVIDIA Deep Learning Super Sampling| diff --git a/examples/ecs/observer_propagation.rs b/examples/ecs/observer_propagation.rs index 321c44ecf2fec..8fba9ee395af8 100644 --- a/examples/ecs/observer_propagation.rs +++ b/examples/ecs/observer_propagation.rs @@ -26,17 +26,17 @@ fn main() { // the attack it will continue up and hit the goblin. fn setup(mut commands: Commands) { commands - .spawn((Name::new("Goblin"), HitPoints(50))) + .spawn((DebugTag::new("Goblin"), HitPoints(50))) .observe(take_damage) .with_children(|parent| { parent - .spawn((Name::new("Helmet"), Armor(5))) + .spawn((DebugTag::new("Helmet"), Armor(5))) .observe(block_attack); parent - .spawn((Name::new("Socks"), Armor(10))) + .spawn((DebugTag::new("Socks"), Armor(10))) .observe(block_attack); parent - .spawn((Name::new("Shirt"), Armor(15))) + .spawn((DebugTag::new("Shirt"), Armor(15))) .observe(block_attack); }); } @@ -76,23 +76,23 @@ fn attack_armor(entities: Query>, mut commands: Commands) { } } -fn attack_hits(attack: On, name: Query<&Name>) { +fn attack_hits(attack: On, name: Query<&DebugTag>) { if let Ok(name) = name.get(attack.entity) { - info!("Attack hit {}", name); + info!("Attack hit {:?}", name); } } /// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage. -fn block_attack(mut attack: On, armor: Query<(&Armor, &Name)>) { +fn block_attack(mut attack: On, armor: Query<(&Armor, &DebugTag)>) { let (armor, name) = armor.get(attack.entity).unwrap(); let damage = attack.damage.saturating_sub(**armor); if damage > 0 { - info!("🩸 {} damage passed through {}", damage, name); + info!("🩸 {} damage passed through {:?}", damage, name); // The attack isn't stopped by the armor. We reduce the damage of the attack, and allow // it to continue on to the goblin. attack.damage = damage; } else { - info!("🛡️ {} damage blocked by {}", attack.damage, name); + info!("🛡️ {} damage blocked by {:?}", attack.damage, name); // Armor stopped the attack, the event stops here. attack.propagate(false); info!("(propagation halted early)\n"); @@ -103,7 +103,7 @@ fn block_attack(mut attack: On, armor: Query<(&Armor, &Name)>) { /// or the wearer is attacked directly. fn take_damage( attack: On, - mut hp: Query<(&mut HitPoints, &Name)>, + mut hp: Query<(&mut HitPoints, &DebugTag)>, mut commands: Commands, mut app_exit: MessageWriter, ) { @@ -111,9 +111,9 @@ fn take_damage( **hp = hp.saturating_sub(attack.damage); if **hp > 0 { - info!("{} has {:.1} HP", name, hp.0); + info!("{:?} has {:.1} HP", name, hp.0); } else { - warn!("💀 {} has died a gruesome death", name); + warn!("💀 {:?} has died a gruesome death", name); commands.entity(attack.entity).despawn(); app_exit.write(AppExit::Success); }