Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
181 changes: 181 additions & 0 deletions crates/bevy_ecs/src/debug_tag.rs
Original file line number Diff line number Diff line change
@@ -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<Cow<'static, str>>) -> 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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[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<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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::<DebugTag>())
}

fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(DebugTag::new(v.to_string()))
}

fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E> {
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);
}
}
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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},
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
1 change: 1 addition & 0 deletions docs/cargo_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
24 changes: 12 additions & 12 deletions examples/ecs/observer_propagation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down Expand Up @@ -76,23 +76,23 @@ fn attack_armor(entities: Query<Entity, With<Armor>>, mut commands: Commands) {
}
}

fn attack_hits(attack: On<Attack>, name: Query<&Name>) {
fn attack_hits(attack: On<Attack>, 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<Attack>, armor: Query<(&Armor, &Name)>) {
fn block_attack(mut attack: On<Attack>, 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");
Expand All @@ -103,17 +103,17 @@ fn block_attack(mut attack: On<Attack>, armor: Query<(&Armor, &Name)>) {
/// or the wearer is attacked directly.
fn take_damage(
attack: On<Attack>,
mut hp: Query<(&mut HitPoints, &Name)>,
mut hp: Query<(&mut HitPoints, &DebugTag)>,
mut commands: Commands,
mut app_exit: MessageWriter<AppExit>,
) {
let (mut hp, name) = hp.get_mut(attack.entity).unwrap();
**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);
}
Expand Down