diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 95e480493a..5f78dc76c6 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -4,7 +4,7 @@ use crate::{ }; use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; -use bevy_utils::get_short_name; +use bevy_utils::ShortName; use crossbeam_channel::{Receiver, Sender}; use std::{ any::TypeId, @@ -206,7 +206,7 @@ impl Default for Handle { impl std::fmt::Debug for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let name = get_short_name(std::any::type_name::()); + let name = ShortName::of::(); match self { Handle::Strong(handle) => { write!( diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index d6d5c6d4be..39b9bee6da 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -17,8 +17,9 @@ use std::time::Duration; /// (`ci_testing_config.ron` by default) and executes its specified actions. For a reference of the /// allowed configuration, see [`CiTestingConfig`]. /// -/// This plugin is included within `DefaultPlugins` and `MinimalPlugins` when the `bevy_ci_testing` -/// feature is enabled. It is recommended to only used this plugin during testing (manual or +/// This plugin is included within `DefaultPlugins`, `HeadlessPlugins` and `MinimalPlugins` +/// when the `bevy_ci_testing` feature is enabled. +/// It is recommended to only used this plugin during testing (manual or /// automatic), and disable it during regular development and for production builds. #[derive(Default)] pub struct CiTestingPlugin; diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index c1023e0012..5db9ebc433 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -628,9 +628,6 @@ impl ExecutorState { /// # Safety /// Caller must ensure no systems are currently borrowed. unsafe fn spawn_exclusive_system_task(&mut self, context: &Context, system_index: usize) { - // SAFETY: `can_run` returned true for this system, which means - // that no other systems currently have access to the world. - let world = unsafe { context.environment.world_cell.world_mut() }; // SAFETY: this system is not running, no other reference exists let system = unsafe { &mut *context.environment.systems[system_index].get() }; // Move the full context object into the new future. @@ -641,6 +638,9 @@ impl ExecutorState { let unapplied_systems = self.unapplied_systems.clone(); self.unapplied_systems.clear(); let task = async move { + // SAFETY: `can_run` returned true for this system, which means + // that no other systems currently have access to the world. + let world = unsafe { context.environment.world_cell.world_mut() }; let res = apply_deferred(&unapplied_systems, context.environment.systems, world); context.system_completed(system_index, res, system); }; @@ -648,6 +648,9 @@ impl ExecutorState { context.scope.spawn_on_scope(task); } else { let task = async move { + // SAFETY: `can_run` returned true for this system, which means + // that no other systems currently have access to the world. + let world = unsafe { context.environment.world_cell.world_mut() }; let res = std::panic::catch_unwind(AssertUnwindSafe(|| { __rust_begin_short_backtrace::run(&mut **system, world); })); @@ -808,4 +811,16 @@ mod tests { schedule.run(&mut world); assert!(world.get_resource::().is_some()); } + + /// Regression test for a weird bug flagged by MIRI in + /// `spawn_exclusive_system_task`, related to a `&mut World` being captured + /// inside an `async` block and somehow remaining alive even after its last use. + #[test] + fn check_spawn_exclusive_system_task_miri() { + let mut world = World::new(); + let mut schedule = Schedule::default(); + schedule.set_executor_kind(ExecutorKind::MultiThreaded); + schedule.add_systems(((|_: Commands| {}), |_: Commands| {}).chain()); + schedule.run(&mut world); + } } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index dad5d99c56..b9ffa1b914 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -1608,7 +1608,7 @@ impl ScheduleGraph { } }; if self.settings.use_shortnames { - name = bevy_utils::get_short_name(&name); + name = bevy_utils::ShortName(&name).to_string(); } name } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 6fb97bb8ab..3700477163 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1,12 +1,14 @@ mod parallel_scope; use core::panic::Location; +use std::marker::PhantomData; use super::{Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource}; use crate::{ self as bevy_ecs, bundle::{Bundle, InsertMode}, - component::{ComponentId, ComponentInfo}, + change_detection::Mut, + component::{Component, ComponentId, ComponentInfo}, entity::{Entities, Entity}, event::{Event, SendEvent}, observer::{Observer, TriggerEvent, TriggerTargets}, @@ -926,6 +928,38 @@ impl EntityCommands<'_> { } } + /// Get an [`EntityEntryCommands`] for the [`Component`] `T`, + /// allowing you to modify it or insert it if it isn't already present. + /// + /// See also [`insert_if_new`](Self::insert_if_new), which lets you insert a [`Bundle`] without overwriting it. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource)] + /// # struct PlayerEntity { entity: Entity } + /// #[derive(Component)] + /// struct Level(u32); + /// + /// fn level_up_system(mut commands: Commands, player: Res) { + /// commands + /// .entity(player.entity) + /// .entry::() + /// // Modify the component if it exists + /// .and_modify(|mut lvl| lvl.0 += 1) + /// // Otherwise insert a default value + /// .or_insert(Level(0)); + /// } + /// # bevy_ecs::system::assert_is_system(level_up_system); + /// ``` + pub fn entry(&mut self) -> EntityEntryCommands { + EntityEntryCommands { + entity_commands: self.reborrow(), + marker: PhantomData, + } + } + /// Adds a [`Bundle`] of components to the entity. /// /// This will overwrite any previous value(s) of the same component type. @@ -1030,6 +1064,9 @@ impl EntityCommands<'_> { /// components will leave the old values instead of replacing them with new /// ones. /// + /// See also [`entry`](Self::entry), which lets you modify a [`Component`] if it's present, + /// as well as initialize it with a default value. + /// /// # Panics /// /// The command will panic when applied if the associated entity does not exist. @@ -1437,6 +1474,113 @@ impl EntityCommands<'_> { } } +/// A wrapper around [`EntityCommands`] with convenience methods for working with a specified component type. +pub struct EntityEntryCommands<'a, T> { + entity_commands: EntityCommands<'a>, + marker: PhantomData, +} + +impl<'a, T: Component> EntityEntryCommands<'a, T> { + /// Modify the component `T` if it exists, using the the function `modify`. + pub fn and_modify(mut self, modify: impl FnOnce(Mut) + Send + Sync + 'static) -> Self { + self.entity_commands = self + .entity_commands + .queue(move |mut entity: EntityWorldMut| { + if let Some(value) = entity.get_mut() { + modify(value); + } + }); + self + } + + /// [Insert](EntityCommands::insert) `default` into this entity, if `T` is not already present. + /// + /// See also [`or_insert_with`](Self::or_insert_with). + /// + /// # Panics + /// + /// Panics if the entity does not exist. + /// See [`or_try_insert`](Self::or_try_insert) for a non-panicking version. + #[track_caller] + pub fn or_insert(mut self, default: T) -> Self { + self.entity_commands = self + .entity_commands + .queue(insert(default, InsertMode::Keep)); + self + } + + /// [Insert](EntityCommands::insert) `default` into this entity, if `T` is not already present. + /// + /// Unlike [`or_insert`](Self::or_insert), this will not panic if the entity does not exist. + /// + /// See also [`or_insert_with`](Self::or_insert_with). + #[track_caller] + pub fn or_try_insert(mut self, default: T) -> Self { + self.entity_commands = self + .entity_commands + .queue(try_insert(default, InsertMode::Keep)); + self + } + + /// [Insert](EntityCommands::insert) the value returned from `default` into this entity, if `T` is not already present. + /// + /// See also [`or_insert`](Self::or_insert) and [`or_try_insert`](Self::or_try_insert). + /// + /// # Panics + /// + /// Panics if the entity does not exist. + /// See [`or_try_insert_with`](Self::or_try_insert_with) for a non-panicking version. + #[track_caller] + pub fn or_insert_with(self, default: impl Fn() -> T) -> Self { + self.or_insert(default()) + } + + /// [Insert](EntityCommands::insert) the value returned from `default` into this entity, if `T` is not already present. + /// + /// Unlike [`or_insert_with`](Self::or_insert_with), this will not panic if the entity does not exist. + /// + /// See also [`or_insert`](Self::or_insert) and [`or_try_insert`](Self::or_try_insert). + #[track_caller] + pub fn or_try_insert_with(self, default: impl Fn() -> T) -> Self { + self.or_try_insert(default()) + } + + /// [Insert](EntityCommands::insert) `T::default` into this entity, if `T` is not already present. + /// + /// See also [`or_insert`](Self::or_insert) and [`or_from_world`](Self::or_from_world). + /// + /// # Panics + /// + /// Panics if the entity does not exist. + #[track_caller] + pub fn or_default(self) -> Self + where + T: Default, + { + #[allow(clippy::unwrap_or_default)] + // FIXME: use `expect` once stable + self.or_insert(T::default()) + } + + /// [Insert](EntityCommands::insert) `T::from_world` into this entity, if `T` is not already present. + /// + /// See also [`or_insert`](Self::or_insert) and [`or_default`](Self::or_default). + /// + /// # Panics + /// + /// Panics if the entity does not exist. + #[track_caller] + pub fn or_from_world(mut self) -> Self + where + T: FromWorld, + { + self.entity_commands = self + .entity_commands + .queue(insert_from_world::(InsertMode::Keep)); + self + } +} + impl Command for F where F: FnOnce(&mut World) + Send + 'static, @@ -1545,6 +1689,25 @@ fn insert(bundle: T, mode: InsertMode) -> impl EntityCommand { } } +/// An [`EntityCommand`] that adds the component using its `FromWorld` implementation. +#[track_caller] +fn insert_from_world(mode: InsertMode) -> impl EntityCommand { + let caller = Location::caller(); + move |entity: Entity, world: &mut World| { + let value = T::from_world(world); + if let Some(mut entity) = world.get_entity_mut(entity) { + entity.insert_with_caller( + value, + mode, + #[cfg(feature = "track_change_detection")] + caller, + ); + } else { + panic!("error[B0003]: {caller}: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", std::any::type_name::(), entity); + } + } +} + /// An [`EntityCommand`] that attempts to add the components in a [`Bundle`] to an entity. /// Does nothing if the entity does not exist. #[track_caller] @@ -1680,7 +1843,7 @@ mod tests { self as bevy_ecs, component::Component, system::{Commands, Resource}, - world::{CommandQueue, World}, + world::{CommandQueue, FromWorld, World}, }; use std::{ any::TypeId, @@ -1717,6 +1880,50 @@ mod tests { world.spawn((W(0u32), W(42u64))); } + impl FromWorld for W { + fn from_world(world: &mut World) -> Self { + let v = world.resource::>(); + Self("*".repeat(v.0)) + } + } + + #[test] + fn entity_commands_entry() { + let mut world = World::default(); + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, &world); + let entity = commands.spawn_empty().id(); + commands + .entity(entity) + .entry::>() + .and_modify(|_| unreachable!()); + queue.apply(&mut world); + assert!(!world.entity(entity).contains::>()); + let mut commands = Commands::new(&mut queue, &world); + commands + .entity(entity) + .entry::>() + .or_insert(W(0)) + .and_modify(|mut val| { + val.0 = 21; + }); + queue.apply(&mut world); + assert_eq!(21, world.get::>(entity).unwrap().0); + let mut commands = Commands::new(&mut queue, &world); + commands + .entity(entity) + .entry::>() + .and_modify(|_| unreachable!()) + .or_insert(W(42)); + queue.apply(&mut world); + assert_eq!(42, world.get::>(entity).unwrap().0); + world.insert_resource(W(5_usize)); + let mut commands = Commands::new(&mut queue, &world); + commands.entity(entity).entry::>().or_from_world(); + queue.apply(&mut world); + assert_eq!("*****", &world.get::>(entity).unwrap().0); + } + #[test] fn commands() { let mut world = World::default(); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 39e5472239..a4b4904708 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -162,6 +162,8 @@ impl Drop for World { drop(unsafe { Box::from_raw(self.command_queue.bytes.as_ptr()) }); // SAFETY: Pointers in internal command queue are only invalidated here drop(unsafe { Box::from_raw(self.command_queue.cursor.as_ptr()) }); + // SAFETY: Pointers in internal command queue are only invalidated here + drop(unsafe { Box::from_raw(self.command_queue.panic_recovery.as_ptr()) }); } } diff --git a/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs b/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs index 21cde05f52..b190885723 100644 --- a/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs +++ b/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs @@ -4,7 +4,7 @@ use std::marker::PhantomData; use crate::Parent; use bevy_ecs::prelude::*; #[cfg(feature = "bevy_app")] -use bevy_utils::{get_short_name, HashSet}; +use bevy_utils::{HashSet, ShortName}; /// When enabled, runs [`check_hierarchy_component_has_valid_parent`]. /// @@ -67,7 +67,7 @@ pub fn check_hierarchy_component_has_valid_parent( bevy_utils::tracing::warn!( "warning[B0004]: {name} with the {ty_name} component has a parent without {ty_name}.\n\ This will cause inconsistent behaviors! See: https://bevyengine.org/learn/errors/b0004", - ty_name = get_short_name(std::any::type_name::()), + ty_name = ShortName::of::(), name = name.map_or_else(|| format!("Entity {}", entity), |s| format!("The {s} entity")), ); } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 7aadcb50fb..74008dafaf 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -71,7 +71,56 @@ plugin_group! { /// /// [`DefaultPlugins`] contains all the plugins typically required to build /// a *Bevy* application which includes a *window* and presentation components. - /// For *headless* cases – without a *window* or presentation, see [`MinimalPlugins`]. + /// For *headless* cases – without a *window* or presentation, see [`HeadlessPlugins`]. + /// For the absolute minimum number of plugins needed to run a Bevy application, see [`MinimalPlugins`]. +} + +plugin_group! { + /// This plugin group will add all the default plugins for a headless (no *window* or rendering) *Bevy* application: + pub struct HeadlessPlugins { + bevy_app:::PanicHandlerPlugin, + bevy_log:::LogPlugin, + bevy_core:::TaskPoolPlugin, + bevy_core:::TypeRegistrationPlugin, + bevy_core:::FrameCountPlugin, + bevy_time:::TimePlugin, + bevy_transform:::TransformPlugin, + bevy_hierarchy:::HierarchyPlugin, + bevy_diagnostic:::DiagnosticsPlugin, + bevy_app:::ScheduleRunnerPlugin, + #[custom(cfg(not(target_arch = "wasm32")))] + bevy_app:::TerminalCtrlCHandlerPlugin, + #[cfg(feature = "bevy_asset")] + bevy_asset:::AssetPlugin, + #[cfg(feature = "bevy_scene")] + bevy_scene:::ScenePlugin, + #[cfg(feature = "bevy_animation")] + bevy_animation:::AnimationPlugin, + #[cfg(feature = "bevy_state")] + bevy_state::app:::StatesPlugin, + #[cfg(feature = "bevy_ci_testing")] + bevy_dev_tools::ci_testing:::CiTestingPlugin, + #[doc(hidden)] + :IgnoreAmbiguitiesPlugin, + } + /// This group of plugins is intended for use for *headless* programs, for example: dedicated game servers. + /// See the [*Bevy* *headless* example](https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs) + /// + /// [`HeadlessPlugins`] obeys *Cargo* *feature* flags. Users may exert control over this plugin group + /// by disabling `default-features` in their `Cargo.toml` and enabling only those features + /// that they wish to use. + /// + /// [`HeadlessPlugins`] contains all the plugins typically required to build + /// a *Bevy* application. In contrast with [`DefaultPlugins`], it leaves out *window* and presentation components. + /// This allows applications built using this plugin group to run on devices that do not have a screen or rendering + /// capabilities. + /// It includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) + /// to provide functionality that would otherwise be driven by a windowed application's + /// *event loop* or *message loop*. + /// + /// Windowed applications that wish to use a reduced set of plugins should consider the + /// [`DefaultPlugins`] plugin group which can be controlled with *Cargo* *feature* flags. + /// For the absolute minimum number of plugins needed to run a Bevy application, see [`MinimalPlugins`]. } #[derive(Default)] @@ -110,12 +159,12 @@ plugin_group! { #[cfg(feature = "bevy_ci_testing")] bevy_dev_tools::ci_testing:::CiTestingPlugin, } - /// This group of plugins is intended for use for minimal, *headless* programs – - /// see the [*Bevy* *headless* example](https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs) - /// – and includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) + /// This plugin group represents the absolute minimum, bare-bones, bevy application. + /// Use this if you want to have absolute control over the plugins used. + /// If you are looking to make a *headless* application - without a *window* or rendering, + /// it is usually best to use [`HeadlessPlugins`]. + /// + /// It includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) /// to provide functionality that would otherwise be driven by a windowed application's /// *event loop* or *message loop*. - /// - /// Windowed applications that wish to use a reduced set of plugins should consider the - /// [`DefaultPlugins`] plugin group which can be controlled with *Cargo* *feature* flags. } diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 812a674ed9..bec80694fd 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -2,7 +2,8 @@ pub use crate::{ app::prelude::*, core::prelude::*, ecs::prelude::*, hierarchy::prelude::*, input::prelude::*, log::prelude::*, math::prelude::*, reflect::prelude::*, time::prelude::*, - transform::prelude::*, utils::prelude::*, window::prelude::*, DefaultPlugins, MinimalPlugins, + transform::prelude::*, utils::prelude::*, window::prelude::*, DefaultPlugins, HeadlessPlugins, + MinimalPlugins, }; pub use bevy_derive::{bevy_main, Deref, DerefMut}; diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 5505bad824..8685fb07d8 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -2366,9 +2366,7 @@ bevy_reflect::tests::Test { fn short_type_path() -> &'static str { static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| { - bevy_utils::get_short_name(std::any::type_name::()) - }) + CELL.get_or_insert::(|| bevy_utils::ShortName::of::().to_string()) } fn type_ident() -> Option<&'static str> { diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index dcab26622a..13e37ee56c 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -10,7 +10,6 @@ use crate::TextureSlicer; /// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle). #[derive(Component, Debug, Default, Clone, Reflect)] #[reflect(Component, Default, Debug)] -#[repr(C)] pub struct Sprite { /// The sprite's color tint pub color: Color, diff --git a/crates/bevy_sprite/src/texture_atlas.rs b/crates/bevy_sprite/src/texture_atlas.rs index a4a752865d..70e10e5914 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -21,7 +21,6 @@ use bevy_utils::HashMap; #[derive(Asset, Reflect, Debug, Clone)] #[reflect(Debug)] pub struct TextureAtlasLayout { - // TODO: add support to Uniforms derive to write dimensions and sprites to the same buffer pub size: UVec2, /// The specific areas of the atlas where each texture can be found pub textures: Vec, diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index d729dfa33d..3d231cf0cf 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -22,10 +22,8 @@ pub mod prelude { } pub mod futures; -#[cfg(feature = "alloc")] mod short_names; -#[cfg(feature = "alloc")] -pub use short_names::get_short_name; +pub use short_names::ShortName; pub mod synccell; pub mod syncunsafecell; diff --git a/crates/bevy_utils/src/short_names.rs b/crates/bevy_utils/src/short_names.rs index d78b9c5394..d14548b681 100644 --- a/crates/bevy_utils/src/short_names.rs +++ b/crates/bevy_utils/src/short_names.rs @@ -1,62 +1,115 @@ -use alloc::string::String; - -/// Shortens a type name to remove all module paths. +/// Lazily shortens a type name to remove all module paths. /// /// The short name of a type is its full name as returned by /// [`std::any::type_name`], but with the prefix of all paths removed. For /// example, the short name of `alloc::vec::Vec>` /// would be `Vec>`. -pub fn get_short_name(full_name: &str) -> String { - // Generics result in nested paths within <..> blocks. - // Consider "bevy_render::camera::camera::extract_cameras". - // To tackle this, we parse the string from left to right, collapsing as we go. - let mut index: usize = 0; - let end_of_string = full_name.len(); - let mut parsed_name = String::new(); +/// +/// Shortening is performed lazily without allocation. +#[cfg_attr( + feature = "alloc", + doc = r#" To get a [`String`] from this type, use the [`to_string`](`alloc::string::ToString::to_string`) method."# +)] +/// +/// # Examples +/// +/// ```rust +/// # use bevy_utils::ShortName; +/// # +/// # mod foo { +/// # pub mod bar { +/// # pub struct Baz; +/// # } +/// # } +/// // Baz +/// let short_name = ShortName::of::(); +/// ``` +#[derive(Clone, Copy)] +pub struct ShortName<'a>(pub &'a str); - while index < end_of_string { - let rest_of_string = full_name.get(index..end_of_string).unwrap_or_default(); - - // Collapse everything up to the next special character, - // then skip over it - if let Some(special_character_index) = rest_of_string.find(|c: char| { - (c == ' ') - || (c == '<') - || (c == '>') - || (c == '(') - || (c == ')') - || (c == '[') - || (c == ']') - || (c == ',') - || (c == ';') - }) { - let segment_to_collapse = rest_of_string - .get(0..special_character_index) - .unwrap_or_default(); - parsed_name += collapse_type_name(segment_to_collapse); - // Insert the special character - let special_character = - &rest_of_string[special_character_index..=special_character_index]; - parsed_name.push_str(special_character); - - match special_character { - ">" | ")" | "]" - if rest_of_string[special_character_index + 1..].starts_with("::") => - { - parsed_name.push_str("::"); - // Move the index past the "::" - index += special_character_index + 3; - } - // Move the index just past the special character - _ => index += special_character_index + 1, - } - } else { - // If there are no special characters left, we're done! - parsed_name += collapse_type_name(rest_of_string); - index = end_of_string; - } +impl ShortName<'static> { + /// Gets a shortened version of the name of the type `T`. + pub fn of() -> Self { + Self(core::any::type_name::()) + } +} + +impl<'a> ShortName<'a> { + /// Gets the original name before shortening. + pub const fn original(&self) -> &'a str { + self.0 + } +} + +impl<'a> From<&'a str> for ShortName<'a> { + fn from(value: &'a str) -> Self { + Self(value) + } +} + +impl<'a> core::fmt::Debug for ShortName<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let &ShortName(full_name) = self; + // Generics result in nested paths within <..> blocks. + // Consider "bevy_render::camera::camera::extract_cameras". + // To tackle this, we parse the string from left to right, collapsing as we go. + let mut index: usize = 0; + let end_of_string = full_name.len(); + + while index < end_of_string { + let rest_of_string = full_name.get(index..end_of_string).unwrap_or_default(); + + // Collapse everything up to the next special character, + // then skip over it + if let Some(special_character_index) = rest_of_string.find(|c: char| { + (c == ' ') + || (c == '<') + || (c == '>') + || (c == '(') + || (c == ')') + || (c == '[') + || (c == ']') + || (c == ',') + || (c == ';') + }) { + let segment_to_collapse = rest_of_string + .get(0..special_character_index) + .unwrap_or_default(); + + f.write_str(collapse_type_name(segment_to_collapse))?; + + // Insert the special character + let special_character = + &rest_of_string[special_character_index..=special_character_index]; + + f.write_str(special_character)?; + + match special_character { + ">" | ")" | "]" + if rest_of_string[special_character_index + 1..].starts_with("::") => + { + f.write_str("::")?; + // Move the index past the "::" + index += special_character_index + 3; + } + // Move the index just past the special character + _ => index += special_character_index + 1, + } + } else { + // If there are no special characters left, we're done! + f.write_str(collapse_type_name(rest_of_string))?; + index = end_of_string; + } + } + + Ok(()) + } +} + +impl<'a> core::fmt::Display for ShortName<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + ::fmt(self, f) } - parsed_name } #[inline(always)] @@ -77,49 +130,52 @@ fn collapse_type_name(string: &str) -> &str { } } -#[cfg(test)] +#[cfg(all(test, feature = "alloc"))] mod name_formatting_tests { - use super::get_short_name; + use super::ShortName; #[test] fn trivial() { - assert_eq!(get_short_name("test_system"), "test_system"); + assert_eq!(ShortName("test_system").to_string(), "test_system"); } #[test] fn path_separated() { assert_eq!( - get_short_name("bevy_prelude::make_fun_game"), + ShortName("bevy_prelude::make_fun_game").to_string(), "make_fun_game" ); } #[test] fn tuple_type() { - assert_eq!(get_short_name("(String, String)"), "(String, String)"); + assert_eq!( + ShortName("(String, String)").to_string(), + "(String, String)" + ); } #[test] fn array_type() { - assert_eq!(get_short_name("[i32; 3]"), "[i32; 3]"); + assert_eq!(ShortName("[i32; 3]").to_string(), "[i32; 3]"); } #[test] fn trivial_generics() { - assert_eq!(get_short_name("a"), "a"); + assert_eq!(ShortName("a").to_string(), "a"); } #[test] fn multiple_type_parameters() { - assert_eq!(get_short_name("a"), "a"); + assert_eq!(ShortName("a").to_string(), "a"); } #[test] fn enums() { - assert_eq!(get_short_name("Option::None"), "Option::None"); - assert_eq!(get_short_name("Option::Some(2)"), "Option::Some(2)"); + assert_eq!(ShortName("Option::None").to_string(), "Option::None"); + assert_eq!(ShortName("Option::Some(2)").to_string(), "Option::Some(2)"); assert_eq!( - get_short_name("bevy_render::RenderSet::Prepare"), + ShortName("bevy_render::RenderSet::Prepare").to_string(), "RenderSet::Prepare" ); } @@ -127,7 +183,7 @@ mod name_formatting_tests { #[test] fn generics() { assert_eq!( - get_short_name("bevy_render::camera::camera::extract_cameras"), + ShortName("bevy_render::camera::camera::extract_cameras").to_string(), "extract_cameras" ); } @@ -135,7 +191,7 @@ mod name_formatting_tests { #[test] fn nested_generics() { assert_eq!( - get_short_name("bevy::mad_science::do_mad_science, bavy::TypeSystemAbuse>"), + ShortName("bevy::mad_science::do_mad_science, bavy::TypeSystemAbuse>").to_string(), "do_mad_science, TypeSystemAbuse>" ); } @@ -143,13 +199,16 @@ mod name_formatting_tests { #[test] fn sub_path_after_closing_bracket() { assert_eq!( - get_short_name("bevy_asset::assets::Assets::asset_event_system"), + ShortName("bevy_asset::assets::Assets::asset_event_system").to_string(), "Assets::asset_event_system" ); assert_eq!( - get_short_name("(String, String)::default"), + ShortName("(String, String)::default").to_string(), "(String, String)::default" ); - assert_eq!(get_short_name("[i32; 16]::default"), "[i32; 16]::default"); + assert_eq!( + ShortName("[i32; 16]::default").to_string(), + "[i32; 16]::default" + ); } } diff --git a/examples/app/headless.rs b/examples/app/headless.rs index 329a28bf6e..c6ee157b37 100644 --- a/examples/app/headless.rs +++ b/examples/app/headless.rs @@ -8,21 +8,26 @@ //! # replace "*" with the most recent version of bevy //! ``` -use bevy::{app::ScheduleRunnerPlugin, prelude::*, utils::Duration}; +use bevy::{app::ScheduleRunnerPlugin, log::LogPlugin, prelude::*, utils::Duration}; fn main() { // This app runs once App::new() - .add_plugins(MinimalPlugins.set(ScheduleRunnerPlugin::run_once())) + .add_plugins(HeadlessPlugins.set(ScheduleRunnerPlugin::run_once())) .add_systems(Update, hello_world_system) .run(); // This app loops forever at 60 fps App::new() .add_plugins( - MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64( - 1.0 / 60.0, - ))), + HeadlessPlugins + .set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64( + 1.0 / 60.0, + ))) + // The log and ctrl+c plugin can only be registered once globally, + // which means we need to disable it here, because it was already registered with the + // app that runs once. + .disable::(), ) .add_systems(Update, counter) .run(); diff --git a/examples/picking/sprite_picking.rs b/examples/picking/sprite_picking.rs index c423b20da7..67abff50b5 100644 --- a/examples/picking/sprite_picking.rs +++ b/examples/picking/sprite_picking.rs @@ -1,5 +1,5 @@ //! Demonstrates picking for sprites and sprite atlases. The picking backend only tests against the -//! sprite bounds, so the sprite atlas can be picked by clicking on its trnasparent areas. +//! sprite bounds, so the sprite atlas can be picked by clicking on its transparent areas. use bevy::{prelude::*, sprite::Anchor}; use std::fmt::Debug;