diff --git a/Cargo.toml b/Cargo.toml index fae29567b7bb4..1395387c01c6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3050,6 +3050,18 @@ description = "Demonstrates FPS overlay" category = "Dev tools" wasm = true +[[example]] +name = "dev_cli" +path = "examples/dev_tools/dev_cli.rs" +doc-scrape-examples = true +required-features = ["bevy_dev_tools"] + +[package.metadata.example.dev_cli] +name = "Developer in-game command line interface" +description = "Demonstrates the Bevy Developer CLI with dev tools abstraction" +category = "Dev tools" +wasm = false + [[example]] name = "visibility_range" path = "examples/3d/visibility_range.rs" diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 4599c15ce924c..b1a9764376ae5 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -9,9 +9,10 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["bevy_ui_debug"] +default = ["bevy_ui_debug", "bevy_cli_dev_tool"] bevy_ci_testing = ["serde", "ron"] bevy_ui_debug = [] +bevy_cli_dev_tool = ["serde", "ron"] [dependencies] # bevy @@ -36,10 +37,17 @@ bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev", features = [ bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } bevy_text = { path = "../bevy_text", version = "0.14.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } +bevy_state = { path = "../bevy_state", version = "0.14.0-dev" } + +# macros +bevy_dev_tools_macros = { path = "macros/", version = "0.14.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } ron = { version = "0.8.0", optional = true } +nom = "7" +rustyline = "14.0.0" [lints] workspace = true diff --git a/crates/bevy_dev_tools/macros/Cargo.toml b/crates/bevy_dev_tools/macros/Cargo.toml new file mode 100644 index 0000000000000..6093c0bd0737f --- /dev/null +++ b/crates/bevy_dev_tools/macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "bevy_dev_tools_macros" +version = "0.14.0-dev" +edition = "2021" +description = "Collection of developer tools for the Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +syn = "1.0" +quote = "1.0" +proc-macro2 = "1.0" + +[lib] +proc-macro = true \ No newline at end of file diff --git a/crates/bevy_dev_tools/macros/src/lib.rs b/crates/bevy_dev_tools/macros/src/lib.rs new file mode 100644 index 0000000000000..5d0011a91b47a --- /dev/null +++ b/crates/bevy_dev_tools/macros/src/lib.rs @@ -0,0 +1,22 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(DevCommand)] +pub fn dev_command_derive(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let input = parse_macro_input!(input as DeriveInput); + + // Get the name of the struct + let name = &input.ident; + + // Generate the implementation of DevCommand for the struct + let expanded = quote! { + impl DevCommand for #name {} + }; + + // Convert the generated code into a TokenStream and return it + TokenStream::from(expanded) +} diff --git a/crates/bevy_dev_tools/src/cli_toolbox/cli_deserialize.rs b/crates/bevy_dev_tools/src/cli_toolbox/cli_deserialize.rs new file mode 100644 index 0000000000000..ae9e637303f26 --- /dev/null +++ b/crates/bevy_dev_tools/src/cli_toolbox/cli_deserialize.rs @@ -0,0 +1,644 @@ +use bevy_reflect::{TypeRegistration, TypeRegistry}; +use nom::{ + branch::alt, bytes::complete::{is_not, tag, take_while, take_while1}, character::complete::{char, space0}, combinator::{map, opt, recognize}, multi::{many0, separated_list0}, sequence::{delimited, preceded}, IResult +}; +use serde::{de::{self, value::StringDeserializer, Deserializer, Error, MapAccess, Visitor}, forward_to_deserialize_any}; +use std::collections::HashMap; + +/// Works only with TypedReflectDeserializer and direct deserialization +struct TypedCliDeserializer<'a> { + input: &'a str, + //contains a hashmap with indication which fields are strings. Its allow to convert value to string withou brackets + is_string: Option> +} + +impl<'a> TypedCliDeserializer<'a> { + #[allow(dead_code)] //used in tests + fn from_str(input: &'a str) -> Result { + Ok(Self { input, is_string: None }) + } + + fn from_registry(input: &'a str, type_registration: &'a TypeRegistration) -> Result { + + let mut is_string = vec![]; + + match type_registration.type_info() { + bevy_reflect::TypeInfo::Struct(s) => { + for field in s.iter() { + is_string.push(field.is::()); + } + }, + bevy_reflect::TypeInfo::TupleStruct(s) => { + for field in s.iter() { + is_string.push(field.is::()); + } + }, + bevy_reflect::TypeInfo::Tuple(s) => { + for field in s.iter() { + is_string.push(field.is::()); + } + }, + bevy_reflect::TypeInfo::List(s) => { + is_string.push(s.is::()); + }, + bevy_reflect::TypeInfo::Array(_) => { + unimplemented!("Array deserialization not implemented"); + }, + bevy_reflect::TypeInfo::Map(_) => { + unimplemented!("Map deserialization not implemented"); + }, + bevy_reflect::TypeInfo::Enum(_) => { + unimplemented!("Enum deserialization not implemented"); + }, + bevy_reflect::TypeInfo::Value(_) => { + unimplemented!("Value deserialization not implemented"); + }, + } + + Ok(Self { input, is_string: Some(is_string) }) + } +} + +/// Deserialize cli string to box +/// Must be used with ReflectDeserializer +/// Example: +/// let mut type_registry = TypeRegistry::default(); +/// type_registry.register::(); +/// let reflect_deserializer = ReflectDeserializer::new(&type_registry); +/// let input = "setgoldreflect 100"; +/// let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); +/// let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); +/// let val = SetGoldReflect::from_reflect(reflect_value.as_ref()).unwrap(); +/// assert_eq!(val, SetGoldReflect { gold: 100 }); +/// +pub struct CliDeserializer<'a> { + input: &'a str, + type_registration: &'a TypeRegistry, +} + +impl<'a> CliDeserializer<'a> { + /// Creates a new CliDeserializer + pub fn from_str(input: &'a str, type_registration: &'a TypeRegistry) -> Result { + Ok(Self { input, type_registration }) + } +} + +fn is_not_space(c: char) -> bool { + c != ' ' && c != '\t' && c != '\n' +} + +fn is_not_template_splitter(c: char) -> bool { + c != ',' +} + +fn parse_quoted_string(input: &str) -> IResult<&str, &str> { + recognize(delimited(char('"'), is_not("\""), char('"')))(input) +} + +fn parse_ron_value(input: &str) -> IResult<&str, &str> { + recognize(delimited(char('('), is_not(")"), char(')')))(input) +} + +//code for parsing short type paths into cli + +fn is_identifier_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +fn parse_identifier(input: &str) -> IResult<&str, &str> { + take_while1(is_identifier_char)(input) +} + +fn is_generic_separator_or_space(c: char) -> bool { + c == ',' || c == ' ' +} + +fn parse_generic_args(input: &str) -> IResult<&str, Vec> { + delimited( + char('<'), + + separated_list0 + ( + take_while1(is_generic_separator_or_space), + map(parse_short_type_path, |mut v| { + if let Some(id) = v.get(0).cloned() { + v.remove(0); + if v.len() > 0 { + return format!("{}<{}>", id, v.join(",")); + } else { + return id; + } + } else { + return "".to_string(); + } + }), + ), + char('>') + )(input) +} + +fn parse_short_type_path(input: &str) -> IResult<&str, Vec> { + let (input, _) = take_while(|c| c == ' ')(input)?; + let (input, id) = parse_identifier(input)?; + let (input, _) = take_while(|c| c == ' ')(input)?; + let (input, generic_args) = opt(parse_generic_args)(input)?; + + let mut result = vec![id.to_string()]; + if let Some(args) = generic_args { + for arg in args { + result.push(arg); + } + } + + Ok((input, result)) +} + +/// Converts short type path to cli command name +pub fn get_cli_command_name(short_type_path: &str) -> String { + parse_short_type_path(short_type_path).unwrap().1.join(" ") +} + +///cli args parsing +fn parse_value(input: &str) -> IResult<&str, &str> { + preceded(space0, alt((parse_quoted_string, parse_ron_value, take_while1(is_not_space))))(input) +} + +fn parse_argument(input: &str) -> IResult<&str, (&str, Option<&str>)> { + let (input, _) = space0(input)?; + if input.starts_with("--") { + let (input, key) = preceded(tag("--"), take_while1(|c| c != ' '))(input)?; + let (input, value) = opt(preceded(space0, parse_value))(input)?; + Ok((input, (key, value))) + } else { + let (input, value) = parse_value(input)?; + Ok((input, (value, None))) + } +} + +fn parse_arguments<'a>(input: &'a str, fields: &'static [&'static str]) -> IResult<&'a str, HashMap> { + let (input, args) = many0(parse_argument)(input)?; + // println!("{:?}", args); + let mut positional_index = 0; + let mut map = HashMap::new(); + for (key, value) in args { + // println!("{}: {:?}", key, value); + if value.is_some() { + map.insert(key.to_string(), value.unwrap()); + } else { + map.insert(fields[positional_index].to_string(), key); + positional_index += 1; + } + } + Ok((input, map)) +} + +struct CliMapVisitor<'a> { + values: HashMap, + index: usize, + keys: Vec, + is_string: Option> +} + +impl<'a> CliMapVisitor<'a> { + fn new(values: HashMap, is_string: Option>) -> Self { + let keys = values.keys().cloned().collect(); + Self { values, keys, index: 0, is_string } + } +} + +impl<'de> MapAccess<'de> for CliMapVisitor<'de> { + type Error = de::value::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + if self.index < self.keys.len() { + let key = self.keys[self.index].clone(); + seed.deserialize(StringDeserializer::new(key)).map(Some) + } else { + Ok(None) + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + if self.index < self.keys.len() { + let key = self.keys[self.index].clone(); + let value = self.values[&key]; + self.index += 1; + + println!("{:?}", self.keys); + + //check string + if let Some(is_string) = &self.is_string { + if is_string[self.index - 1] { + let value = value.trim().trim_matches('"').to_string(); + return seed.deserialize(StringDeserializer::new(value)); + } + } + + seed.deserialize(&mut ron::de::Deserializer::from_str(value).unwrap()) + .map_err(|ron_err| de::Error::custom(ron_err.to_string())) + } else { + Err(de::Error::custom("Value without a key")) + } + } +} + +impl<'de> Deserializer<'de> for TypedCliDeserializer<'de> { + type Error = de::value::Error; + + fn deserialize_any(self, _: V) -> Result + where + V: Visitor<'de>, + { + unimplemented!("deserialize_any not implemented") + } + + + fn deserialize_struct( + self, + _name: &'static str, + fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + + let (_, values) = parse_arguments(self.input, fields).map_err(|_| de::Error::custom("Parse error"))?; + let mut is_string: Option> = None; + if let Some(is_string_tmp) = self.is_string { + let mut tmp = vec![]; + for (k, _) in values.iter() { + let Some(field_idx) = fields.iter().position(|f| f == k) else { + return Err(de::Error::custom(format!("Field {} not found", k))); + }; + tmp.push(is_string_tmp[field_idx]); + } + is_string = Some(tmp); + } + // println!("{:?}", values); + visitor.visit_map(CliMapVisitor::new(values, is_string)) + } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf option + unit unit_struct newtype_struct seq tuple tuple_struct map enum identifier ignored_any + } +} + + +impl<'de> Deserializer<'de> for CliDeserializer<'de> { + type Error = de::value::Error; + + fn deserialize_any(self, _: V) -> Result + where + V: Visitor<'de>, + { + unimplemented!("deserialize_any not implemented") + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de> { + let lowercase_input = self.input.to_lowercase(); + + let mut registration = None; + let mut skip = 0; + for reg in self.type_registration.iter() { + let short_name = reg.type_info().type_path_table().short_path(); + let Ok((_, type_vec)) = parse_short_type_path(short_name) else { + continue; + }; + + let cli_type_path = type_vec.join(" "); + if lowercase_input.starts_with(cli_type_path.to_lowercase().as_str()) { + registration = Some(reg); + skip = cli_type_path.len(); + break; + } + } + + if registration.is_none() { + return Err(de::value::Error::custom("No type registration found")); + } + + struct SingleMapDeserializer<'a> { + args: &'a str, + type_path: String, + registration: &'a TypeRegistration, + } + + impl<'de> MapAccess<'de> for SingleMapDeserializer<'de> { + type Error = de::value::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + if self.type_path == "" { + Ok(None) + } else { + let res = seed.deserialize(StringDeserializer::new(self.type_path.clone())).map(Some); + self.type_path = "".to_string(); + res + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + seed.deserialize(TypedCliDeserializer::from_registry(self.args, self.registration).unwrap()) + } + } + + visitor.visit_map(SingleMapDeserializer { args: &self.input[skip..], type_path: registration.unwrap().type_info().type_path().to_string(), registration: registration.unwrap() }) + } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf option + unit unit_struct newtype_struct seq tuple tuple_struct struct enum identifier ignored_any + } +} + +#[cfg(test)] +mod tests { + use bevy_reflect::{prelude::*, serde::*, TypeRegistry}; + use serde::Deserialize; + use serde::de::DeserializeSeed; + + use super::*; + + #[derive(Debug, Deserialize, Default, PartialEq)] + struct SetGold { + gold: usize, + } + + #[derive(Debug, Deserialize, Default, PartialEq)] + struct TestSimpleArgs { + arg0: usize, + arg1: String, + } + + #[test] + fn single_positional() { + let input = "100"; + let deserializer = TypedCliDeserializer::from_str(input).unwrap(); + let set_gold = SetGold::deserialize(deserializer).unwrap(); + assert_eq!(set_gold, SetGold { gold: 100 }); + } + + #[test] + fn single_key() { + let input = "--gold 100"; + let deserializer = TypedCliDeserializer::from_str(input).unwrap(); + let set_gold = SetGold::deserialize(deserializer).unwrap(); + assert_eq!(set_gold, SetGold { gold: 100 }); + } + + #[test] + fn multiple_positional() { + let input = "100 \"200 \""; + let deserializer = TypedCliDeserializer::from_str(input).unwrap(); + let set_gold = TestSimpleArgs::deserialize(deserializer).unwrap(); + assert_eq!(set_gold, TestSimpleArgs { arg0: 100, arg1: "200 ".to_string() }); + } + + #[test] + fn multiple_key() { + let input = "--arg0 100 --arg1 \"200 \""; + let deserializer = TypedCliDeserializer::from_str(input).unwrap(); + let set_gold = TestSimpleArgs::deserialize(deserializer).unwrap(); + assert_eq!(set_gold, TestSimpleArgs { arg0: 100, arg1: "200 ".to_string() }); + } + + #[test] + fn mixed_key_positional() { + let input = "100 --arg1 \"200 \""; + let deserializer = TypedCliDeserializer::from_str(input).unwrap(); + let set_gold = TestSimpleArgs::deserialize(deserializer).unwrap(); + assert_eq!(set_gold, TestSimpleArgs { arg0: 100, arg1: "200 ".to_string() }); + } + + #[derive(Debug, Deserialize, Default, PartialEq)] + struct ComplexInput { + arg0: Option, + gold: SetGold, + text_input: String, + } + + #[test] + fn complex_input() { + let input = "Some(100) --text_input \"Some text\" --gold (gold : 200) "; + let deserializer = TypedCliDeserializer::from_str(input).unwrap(); + let set_gold = ComplexInput::deserialize(deserializer).unwrap(); + assert_eq!(set_gold, ComplexInput { arg0: Some(100), gold: SetGold { gold: 200 }, text_input: "Some text".to_string() }); + } + + #[derive(Debug, Reflect, PartialEq, Default)] + pub struct SetGoldReflect { + pub gold: usize, + } + + #[derive(Debug, Reflect, PartialEq, Default)] + #[reflect(Default)] + struct ReflectMultiArgs { + arg0: usize, + arg1: String, + arg2: SetGoldReflect, + } + + #[test] + fn test_typed_reflect_deserialize() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::(); + + let registration = type_registry + .get(std::any::TypeId::of::()) + .unwrap(); + + let reflect_deserializer = TypedReflectDeserializer::new(registration, &type_registry); + let input = "100"; + + let deserializer = TypedCliDeserializer::from_str(input).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + + let val = SetGoldReflect::from_reflect(reflect_value.as_ref()).unwrap(); + assert_eq!(val, SetGoldReflect { gold: 100 }); + } + + #[test] + fn test_untyped_reflect_deserialize() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::(); + + + let reflect_deserializer = ReflectDeserializer::new(&type_registry); + let input = "setgoldreflect 100"; + let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + println!("Reflect value: {:?}", reflect_value); + + let val = SetGoldReflect::from_reflect(reflect_value.as_ref()).unwrap(); + assert_eq!(val, SetGoldReflect { gold: 100 }); + } + + #[test] + fn test_untyped_reflect_with_key_val() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::(); + + let reflect_deserializer = ReflectDeserializer::new(&type_registry); + let input = "setgoldreflect --gold 100"; + let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + println!("Reflect value: {:?}", reflect_value); + } + + #[test] + fn test_untyped_reflect_complex() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::(); + type_registry.register::(); + + let reflect_deserializer = ReflectDeserializer::new(&type_registry); + let input = "ReflectMultiArgs 100 --arg2 (gold : 200) --arg1 \"Some text\""; + let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + println!("Reflect value: {:?}", reflect_value); + + let val = ReflectMultiArgs::from_reflect(reflect_value.as_ref()).unwrap(); + assert_eq!(val, ReflectMultiArgs { arg0: 100, arg1: "Some text".to_string(), arg2: SetGoldReflect { gold: 200 } }); + } + + #[test] + fn test_with_complex_default() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::(); + type_registry.register::(); + + let reflect_deserializer = ReflectDeserializer::new(&type_registry); + let input = "ReflectMultiArgs 100 --arg2 (gold : 200)"; + let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + + let val = ReflectMultiArgs::from_reflect(reflect_value.as_ref()).unwrap(); + assert_eq!(val, ReflectMultiArgs { arg0: 100, arg1: "".to_string(), arg2: SetGoldReflect { gold: 200 } }); + } + + #[derive(Debug, Reflect, Default, PartialEq)] + struct Enable { + arg0: T, + } + + #[derive(Debug, Reflect, Default, PartialEq)] + struct Marker { + #[reflect(ignore)] + _marker: std::marker::PhantomData, + } + + #[test] + fn test_generic() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::>(); + type_registry.register::>(); + type_registry.register::>(); + let reflect_deserializer = ReflectDeserializer::new(&type_registry); + + let input = "enable usize 100"; + let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + let val = Enable::::from_reflect(reflect_value.as_ref()).unwrap(); + assert_eq!(val, Enable { arg0: 100 }); + + + let reflect_deserializer = ReflectDeserializer::new(&type_registry); + let input = "enable String \"Some text\""; + let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + let val = Enable::::from_reflect(reflect_value.as_ref()).unwrap(); + assert_eq!(val, Enable { arg0: "Some text".to_string() }); + + + let reflect_deserializer = ReflectDeserializer::new(&type_registry); + let input = "enable SetGoldReflect (gold : 100)"; + let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + let val = Enable::::from_reflect(reflect_value.as_ref()).unwrap(); + assert_eq!(val, Enable { arg0: SetGoldReflect { gold: 100 } }); + } + + #[test] + fn test_generic_marker() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::>(); + type_registry.register::>(); + type_registry.register::>(); + + let reflect_deserializer = ReflectDeserializer::new(&type_registry); + let input = "marker usize"; + let deserializer = CliDeserializer::from_str(input, &type_registry).unwrap(); + let reflect_value = reflect_deserializer.deserialize(deserializer).unwrap(); + let val = Marker::::from_reflect(reflect_value.as_ref()).unwrap(); + assert_eq!(val, Marker { _marker: std::marker::PhantomData }); + } + + #[test] + fn test_single_identifier() { + let result = parse_short_type_path("SetGold").unwrap().1; + assert_eq!(result, vec!["SetGold"]); + } + + #[test] + fn test_generic_single_arg() { + let result = parse_short_type_path("GenericStruct").unwrap().1; + assert_eq!(result, vec!["GenericStruct", "SetGold"]); + } + + #[test] + fn test_generic_multiple_args() { + let result = parse_short_type_path("UltraGeneric").unwrap().1; + assert_eq!(result, vec!["UltraGeneric", "SetA", "SetB"]); + } + + #[test] + fn test_nested_generic_args() { + let result = parse_short_type_path("Outer, SetB>").unwrap().1; + assert_eq!(result, vec!["Outer", "Inner", "SetB"]); + } + + #[test] + fn test_complex_identifier() { + let result = parse_short_type_path("ComplexIdentifier_123").unwrap().1; + assert_eq!(result, vec!["ComplexIdentifier_123", "InnerValue"]); + let result = parse_short_type_path("ComplexIdentifier_123").unwrap().1; + assert_eq!(result, vec!["ComplexIdentifier_123", "InnerValue"]); + let result = parse_short_type_path("ComplexIdentifier_123< InnerValue>").unwrap().1; + assert_eq!(result, vec!["ComplexIdentifier_123", "InnerValue"]); + let result = parse_short_type_path("ComplexIdentifier_123< InnerValue >").unwrap().1; + assert_eq!(result, vec!["ComplexIdentifier_123", "InnerValue"]); + } + + #[test] + fn test_empty_generic() { + let result = parse_short_type_path("EmptyGeneric<>").unwrap().1; + assert_eq!(result, vec!["EmptyGeneric"]); + } + + #[test] + fn test_no_generic() { + let result = parse_short_type_path("NoGeneric").unwrap().1; + assert_eq!(result, vec!["NoGeneric"]); + } + + #[test] + fn test_multiple_commas() { + let result = parse_short_type_path("MultiComma").unwrap().1; + assert_eq!(result, vec!["MultiComma", "A", "B", "C"]); + } +} \ No newline at end of file diff --git a/crates/bevy_dev_tools/src/cli_toolbox/cli_toolbox.rs b/crates/bevy_dev_tools/src/cli_toolbox/cli_toolbox.rs new file mode 100644 index 0000000000000..6f997d0b09a40 --- /dev/null +++ b/crates/bevy_dev_tools/src/cli_toolbox/cli_toolbox.rs @@ -0,0 +1,74 @@ +use bevy_app::{Plugin, PreUpdate}; +use bevy_ecs::{event::EventReader, reflect::AppTypeRegistry, system::{Commands, Res}, world::Command}; +use bevy_reflect::{serde::ReflectDeserializer, std_traits::ReflectDefault, Reflect}; +use serde::de::DeserializeSeed; +use crate::{cli_toolbox::get_cli_command_name, dev_command::ReflectDevCommand, prelude::DevCommand}; + +use super::{CliDeserializer, ConsoleInput, ConsoleReaderPlugin}; + +/// A plugin that adds the cli dev command executor to the app +pub struct CLIToolbox; + +impl Plugin for CLIToolbox { + fn build(&self, app: &mut bevy_app::App) { + if !app.is_plugin_added::() { + app.add_plugins(ConsoleReaderPlugin); + } + app.register_type::(); + app.add_systems(PreUpdate, parse_command); + } +} + + +fn parse_command( + mut commands: Commands, + mut console_input: EventReader, + app_registry: Res +) { + for input in console_input.read() { + match input { + ConsoleInput::Text(text) => { + let registry = app_registry.read(); + let des = CliDeserializer::from_str(text.as_str(), ®istry).unwrap(); + let refl_des = ReflectDeserializer::new(®istry); + + if let Ok(boxed_cmd) = refl_des.deserialize(des) { + // println!("Deserialized command: {:?}", boxed_cmd); + // println!("Type path: {:?}", boxed_cmd.get_represented_type_info().unwrap().type_path()); + let Some(type_info) = registry.get_with_type_path(boxed_cmd.get_represented_type_info().unwrap().type_path()) else { + println!("Failed to get type info"); + continue; + }; + + let Some(dev_command_data) = registry.get_type_data::(type_info.type_id()) else { + println!("Failed to get dev command metadata"); + continue; + }; + + (dev_command_data.metadata.self_to_commands)(boxed_cmd.as_ref(), &mut commands); + } else { + println!("Failed to deserialize command"); + } + } + _ => {} + } + } + console_input.clear(); +} + +#[derive(Reflect, Default, DevCommand)] +#[reflect(Default, DevCommand)] +struct PrintCommands; +impl Command for PrintCommands { + fn apply(self, world: &mut bevy_ecs::world::World) { + let app_registry = world.get_resource::().unwrap(); + let registry = app_registry.read(); + let mut names = vec![]; + for info in registry.iter_with_data::() { + let cli_name = get_cli_command_name(info.0.type_info().type_path_table().short_path()); + names.push(cli_name); + } + + println!("Available commands: {:?}", names); + } +} \ No newline at end of file diff --git a/crates/bevy_dev_tools/src/cli_toolbox/console_reader_plugin.rs b/crates/bevy_dev_tools/src/cli_toolbox/console_reader_plugin.rs new file mode 100644 index 0000000000000..2b1e0784c6f4b --- /dev/null +++ b/crates/bevy_dev_tools/src/cli_toolbox/console_reader_plugin.rs @@ -0,0 +1,92 @@ +/// Contains async console read in world + +use std::sync::{mpsc::{Receiver, Sender}, Arc, Mutex, RwLock}; + +use bevy_app::{Plugin, PreUpdate}; +use bevy_ecs::{event::{Event, EventWriter}, system::{Res, Resource}}; + +/// Async simple console reader +pub struct ConsoleReaderPlugin; + +impl Plugin for ConsoleReaderPlugin { + fn build(&self, app: &mut bevy_app::App) { + let console_reader = create_console_bridge(); + + app.insert_resource(console_reader); + app.add_event::(); + + app.add_systems(PreUpdate, console_reader_system); + } +} + +fn console_reader_system( + console_reader: Res, + mut events: EventWriter, + mut app_exit_events: EventWriter +) { + while let Ok(input) = console_reader.receiver.lock().unwrap().try_recv() { + if input == ConsoleInput::Quit { + app_exit_events.send(bevy_app::AppExit::Success); + } + events.send(input); + } +} + +fn async_console_reader(reader: AsyncConsoleReader) { + let mut editor = rustyline::DefaultEditor::new().unwrap(); + editor.set_cursor_visibility(true); + while true { + let result_input = editor.readline(""); + + match result_input { + Ok(input) => { + reader.sender.send(ConsoleInput::Text(input)).unwrap(); + } + Err(_) => { + reader.sender.send(ConsoleInput::Quit).unwrap(); + break; + } + } + } +} + +/// Console input event with text or quit command +/// Quit command can be spawned by pressing Ctrl + C +#[derive(Debug, Event, Clone, PartialEq, Eq)] +pub enum ConsoleInput { + /// Text input from console + Text(String), + /// Quit from app command + Quit, +} + +#[derive(Resource)] +struct ConsoleReader { + receiver: Arc>>, + sender: Arc>>, + async_thread: RwLock>>, +} + +enum ToConsoleEditor { + Interrupt, +} +struct AsyncConsoleReader { + sender: Sender, + receiver: Receiver, +} + +fn create_console_bridge() -> ConsoleReader{ + let (sender, receiver) = std::sync::mpsc::channel(); + let (sender_to_editor, receiver_to_editor) = std::sync::mpsc::channel(); + + ConsoleReader { + receiver: Arc::new(Mutex::new(receiver)), + sender: Arc::new(Mutex::new(sender_to_editor)), + async_thread: RwLock::new(Some( + std::thread::spawn(|| async_console_reader(AsyncConsoleReader { + sender, + receiver: receiver_to_editor, + })), + )), + } +} \ No newline at end of file diff --git a/crates/bevy_dev_tools/src/cli_toolbox/mod.rs b/crates/bevy_dev_tools/src/cli_toolbox/mod.rs new file mode 100644 index 0000000000000..36e4a770d7f44 --- /dev/null +++ b/crates/bevy_dev_tools/src/cli_toolbox/mod.rs @@ -0,0 +1,7 @@ +mod console_reader_plugin; +mod cli_toolbox; +mod cli_deserialize; + +pub use cli_deserialize::*; +pub use cli_toolbox::*; +pub use console_reader_plugin::*; \ No newline at end of file diff --git a/crates/bevy_dev_tools/src/dev_command.rs b/crates/bevy_dev_tools/src/dev_command.rs new file mode 100644 index 0000000000000..8a21de876fd16 --- /dev/null +++ b/crates/bevy_dev_tools/src/dev_command.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use bevy_ecs::{system::Commands, world::Command}; +use bevy_log::error; +use bevy_reflect::{FromReflect, FromType, Reflect, Typed}; + +/// DevCommands are commands which speed up the development process +/// and are not intended to be used in production. +/// It can be used to enable or disable dev tools, +/// enter god mode, fly camera, pause game, change resources, spawn entities, etc. +pub trait DevCommand : Command + FromReflect + Reflect + Typed { + /// The metadata of the dev command + fn metadata() -> DevCommandMetadata { + DevCommandMetadata { + self_to_commands: Arc::new(|reflected_self, commands| { + let Some(typed_self) = ::from_reflect(reflected_self) else { + error!("Can not construct self from reflect"); + return; + }; + commands.add(typed_self); + }) + } + } +} + + + +/// Metadata of the dev command +/// Contains method to add reflected clone of self to Commands struct +#[derive(Clone)] +pub struct DevCommandMetadata { + /// Method to add reflected clone of self to Commands + pub self_to_commands: Arc +} + +/// Auto register dev command metadata in TypeRegistry +/// Must use #[reflect(DevCommand)] for auto registration +#[derive(Clone)] +pub struct ReflectDevCommand { + /// Metadata + pub metadata: DevCommandMetadata +} + +impl FromType for ReflectDevCommand { + fn from_type() -> Self { + ReflectDevCommand { + metadata: T::metadata() + } + } +} \ No newline at end of file diff --git a/crates/bevy_dev_tools/src/dev_tool.rs b/crates/bevy_dev_tools/src/dev_tool.rs new file mode 100644 index 0000000000000..36b2421a8a4f3 --- /dev/null +++ b/crates/bevy_dev_tools/src/dev_tool.rs @@ -0,0 +1,208 @@ +use bevy_ecs::{reflect::AppTypeRegistry, system::Resource, world::Command}; +use bevy_log::{error, info}; +use bevy_reflect::{serde::ReflectDeserializer, FromReflect, GetPath, GetTypeRegistration, Reflect, TypePath}; +use serde::de::DeserializeSeed; +use crate::{dev_command::{DevCommand, ReflectDevCommand}, toggable::{Disable, Enable, Toggable}}; + + +/// DevTools are tools that speed up development and are not directly related to the game. +/// This tools can be enabled/disabled/changed at runtime by the DevCommands +pub trait DevTool : Reflect + FromReflect + GetTypeRegistration { + +} + + +/// Useful commands for DevTools +#[derive(Default, Reflect)] +#[reflect(DevCommand)] +pub struct SetTool { + val: T +} + +impl DevCommand for SetTool {} +impl Command for SetTool { + fn apply(self, world: &mut bevy_ecs::world::World) { + world.insert_resource(self.val); + } +} + +/// Command to set a value of a single field in a dev tool +#[derive(Default, Reflect)] +#[reflect(DevCommand)] +pub struct SetField { + field_path: String, + val: String, + + #[reflect(ignore)] + _marker: std::marker::PhantomData +} + +impl DevCommand for SetField {} +impl Command for SetField { + fn apply(self, world: &mut bevy_ecs::world::World) { + + let app_registry = world.resource::().clone(); + let registry = app_registry.read(); + let Some(mut target) = world.get_resource_mut::() else { + error!("Resource {} not found", std::any::type_name::()); + return; + }; + + let SetField { field_path, val, _marker } = self; + + let Ok(field) = target.reflect_path_mut(field_path.as_str()) else { + error!("Field {} not found", field_path); + return; + }; + + + let reflect_deserializer = ReflectDeserializer::new(®istry); + let Ok(_) = ron::from_str::(&val) else { + error!("Failed to parse value {}", val); + return; + }; + + info!("Set value {}", val); + + match field.reflect_mut() { + bevy_reflect::ReflectMut::Struct(s) => { + let boxed_reflect = reflect_deserializer.deserialize(&mut ron::Deserializer::from_str(&val).unwrap()).unwrap(); + s.apply(boxed_reflect.as_ref()); + }, + bevy_reflect::ReflectMut::TupleStruct(s) => { + let boxed_reflect = reflect_deserializer.deserialize(&mut ron::Deserializer::from_str(&val).unwrap()).unwrap(); + s.apply(boxed_reflect.as_ref()); + }, + bevy_reflect::ReflectMut::Tuple(s) => { + let boxed_reflect = reflect_deserializer.deserialize(&mut ron::Deserializer::from_str(&val).unwrap()).unwrap(); + s.apply(boxed_reflect.as_ref()); + }, + bevy_reflect::ReflectMut::List(s) => { + let boxed_reflect = reflect_deserializer.deserialize(&mut ron::Deserializer::from_str(&val).unwrap()).unwrap(); + s.apply(boxed_reflect.as_ref()); + }, + bevy_reflect::ReflectMut::Array(s) => { + let boxed_reflect = reflect_deserializer.deserialize(&mut ron::Deserializer::from_str(&val).unwrap()).unwrap(); + s.apply(boxed_reflect.as_ref()); + }, + bevy_reflect::ReflectMut::Map(s) => { + let boxed_reflect = reflect_deserializer.deserialize(&mut ron::Deserializer::from_str(&val).unwrap()).unwrap(); + s.apply(boxed_reflect.as_ref()); + }, + bevy_reflect::ReflectMut::Enum(s) => { + let boxed_reflect = reflect_deserializer.deserialize(&mut ron::Deserializer::from_str(&val).unwrap()).unwrap(); + s.apply(boxed_reflect.as_ref()); + }, + bevy_reflect::ReflectMut::Value(v) => { + if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as usize", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as bool", val); + } + } else if let Some(v) = v.downcast_mut::() { + *v = val; + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as f32", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as f64", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as i8", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as i16", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as i32", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as i64", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as i128", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as u8", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as u16", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as u32", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as u64", val); + } + } else if let Some(v) = v.downcast_mut::() { + if let Ok(input_value) = ron::de::from_str::(val.to_string().as_str()) { + *v = input_value; + } else { + error!("Failed to parse value {} as u128", val); + } + } else { + error!("Failed to set value {} to {}", val, std::any::type_name::()); + } + }, + } + + } +} + +/// Helpers method to fast register dev tool and commands for it +pub trait AppDevTool { + /// Register a dev tool to the app with default commands + fn register_toggable_dev_tool(&mut self) -> &mut Self; +} + + +impl AppDevTool for bevy_app::App { + fn register_toggable_dev_tool(&mut self) -> &mut Self { + self.register_type::(); + self.register_type::>(); + self.register_type::>(); + self.register_type::>(); + self.register_type::>(); + self + } +} \ No newline at end of file diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index d3f57f998411b..a7e92e15ce9cf 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -11,6 +11,9 @@ use bevy_ecs::{ system::{Commands, Query, Res, Resource}, }; use bevy_hierarchy::BuildChildren; +use bevy_reflect::Reflect; +use bevy_render::view::Visibility; +use bevy_state::{condition::in_state, state::{NextState, OnEnter, State, States}}; use bevy_text::{Font, Text, TextSection, TextStyle}; use bevy_ui::{ node_bundles::{NodeBundle, TextBundle}, @@ -18,6 +21,8 @@ use bevy_ui::{ }; use bevy_utils::default; +use crate::{dev_tool::{AppDevTool, DevTool}, toggable::Toggable}; + /// Global [`ZIndex`] used to render the fps overlay. /// /// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to. @@ -33,7 +38,7 @@ pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32; #[derive(Default)] pub struct FpsOverlayPlugin { /// Starting configuration of overlay, this can be later be changed through [`FpsOverlayConfig`] resource. - pub config: FpsOverlayConfig, + pub config: FpsOverlay, } impl Plugin for FpsOverlayPlugin { @@ -42,28 +47,48 @@ impl Plugin for FpsOverlayPlugin { if !app.is_plugin_added::() { app.add_plugins(FrameTimeDiagnosticsPlugin); } + + app.register_toggable_dev_tool::(); + + app.init_state::(); + app.insert_resource(self.config.clone()) .add_systems(Startup, setup) .add_systems( Update, ( - customize_text.run_if(resource_changed::), + customize_text.run_if(resource_changed::), update_text, - ), - ); + ).run_if(in_state(ShowFpsOverlay::Show)), + ) + .add_systems(OnEnter(ShowFpsOverlay::Hide), hide_text) + .add_systems(OnEnter(ShowFpsOverlay::Show), show_text); + + } } /// Configuration options for the FPS overlay. -#[derive(Resource, Clone)] -pub struct FpsOverlayConfig { +#[derive(Resource, Clone, Reflect)] +pub struct FpsOverlay { /// Configuration of text in the overlay. pub text_config: TextStyle, } -impl Default for FpsOverlayConfig { +/// State of the FPS overlay. Allow to show or hide it. +#[derive(States, Clone, Copy, PartialEq, Eq, Debug, Hash, Default)] +pub enum ShowFpsOverlay { + /// The overlay is shown. + #[default] + Show, + /// The overlay is hidden. + Hide, +} + + +impl Default for FpsOverlay { fn default() -> Self { - FpsOverlayConfig { + FpsOverlay { text_config: TextStyle { font: Handle::::default(), font_size: 32.0, @@ -73,10 +98,26 @@ impl Default for FpsOverlayConfig { } } +impl Toggable for FpsOverlay { + fn enable(world: &mut bevy_ecs::world::World) { + world.resource_mut::>().set(ShowFpsOverlay::Show); + } + + fn disable(world: &mut bevy_ecs::world::World) { + world.resource_mut::>().set(ShowFpsOverlay::Hide); + } + + fn is_enabled(world: &bevy_ecs::world::World) -> bool { + *world.resource::>() == ShowFpsOverlay::Show + } +} + +impl DevTool for FpsOverlay {} + #[derive(Component)] struct FpsText; -fn setup(mut commands: Commands, overlay_config: Res) { +fn setup(mut commands: Commands, overlay_config: Res) { commands .spawn(NodeBundle { style: Style { @@ -110,7 +151,7 @@ fn update_text(diagnostic: Res, mut query: Query<&mut Text, Wi } fn customize_text( - overlay_config: Res, + overlay_config: Res, mut query: Query<&mut Text, With>, ) { for mut text in &mut query { @@ -119,3 +160,19 @@ fn customize_text( } } } + +fn hide_text( + mut query: Query<&mut Visibility, With>, +) { + for mut style in query.iter_mut() { + *style = Visibility::Hidden; + } +} + +fn show_text( + mut query: Query<&mut Visibility, With>, +) { + for mut style in query.iter_mut() { + *style = Visibility::Visible; + } +} \ No newline at end of file diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index 2c4c8fb396b8b..94cb381904dd4 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -18,6 +18,27 @@ pub mod fps_overlay; #[cfg(feature = "bevy_ui_debug")] pub mod ui_debug_overlay; +/// Contains the `DevTool` trait and default dev commands for dev tools +pub mod dev_tool; +/// Contains the `DevTool` trait and reflect registration +pub mod dev_command; +/// Contains the `Toggable` trait and commands for enabling/disabling Toggable dev tools +pub mod toggable; +/// Contains plugins for enable cli parsing and executing dev commands +pub mod cli_toolbox; + +/// Macros for the `bevy_dev_tools` plugin +pub use bevy_dev_tools_macros::*; + +/// Macros for the `bevy_dev_tools` plugin +pub mod prelude { + pub use bevy_dev_tools_macros::*; + pub use crate::dev_tool::*; + pub use crate::dev_command::*; + pub use crate::toggable::*; + pub use crate::cli_toolbox::*; +} + /// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools` /// feature. /// diff --git a/crates/bevy_dev_tools/src/toggable.rs b/crates/bevy_dev_tools/src/toggable.rs new file mode 100644 index 0000000000000..7832ebb41b2e7 --- /dev/null +++ b/crates/bevy_dev_tools/src/toggable.rs @@ -0,0 +1,52 @@ +use crate::dev_command::*; +use bevy_ecs::world::{Command, World}; +use bevy_reflect::std_traits::ReflectDefault; +use bevy_reflect::FromReflect; +use bevy_reflect::Reflect; +use bevy_reflect::TypePath; + +/// Trait that represents a toggable dev tool +pub trait Toggable { + /// Enables the dev tool in the given `world`. + fn enable(world: &mut World); + /// Disables the dev tool in the given `world`. + fn disable(world: &mut World); + /// Checks if the dev tool is enabled in the given `world`. + fn is_enabled(world: &World) -> bool; +} + +/// Command to enable a `Toggable` component or system. +#[derive(Reflect, Default)] +#[reflect(DevCommand, Default)] +pub struct Enable { + /// PhantomData to hold the type `T`. + #[reflect(ignore)] + _phantom: std::marker::PhantomData, +} + +impl DevCommand for Enable {} + +impl Command for Enable { + /// Applies the enable command, enabling the `Toggable` dev tool in the `world`. + fn apply(self, world: &mut World) { + T::enable(world); + } +} + +/// Command to disable a `Toggable` dev tool. +#[derive(Reflect, Default)] +#[reflect(DevCommand, Default)] +pub struct Disable { + /// PhantomData to hold the type `T`. + #[reflect(ignore)] + _phantom: std::marker::PhantomData, +} + +impl DevCommand for Disable {} + +impl Command for Disable { + /// Applies the disable command, disabling the `Toggable` dev tool in the `world`. + fn apply(self, world: &mut World) { + T::disable(world); + } +} \ No newline at end of file diff --git a/examples/dev_tools/dev_cli.rs b/examples/dev_tools/dev_cli.rs new file mode 100644 index 0000000000000..9d2cb69373f80 --- /dev/null +++ b/examples/dev_tools/dev_cli.rs @@ -0,0 +1,137 @@ +//! Show how to use DevCommands, DevTools and cli dev console +//! For starting this example use ` cargo run --example dev_cli --features="bevy_dev_tools" ` +//! To try this demo you must print into your console while app is running +//! Try this: +//! 1. `disable fpsoverlay` -- will hide fps overlay +//! 2. `enable fpsoverlay` -- will show fps overlay +//! 3. `setfield fpsoverlay text_config.font_size 16` -- will change font size in fps overlay +//! 4. `printcommands` -- will list all dev commands +//! 5. `setgold 100` -- will set gold amount +//! 6. `printgold` -- will print gold amount +//! 7. `disable showgold` -- will hide gold overlay (right top corner on screen) +//! 8. Fell free to add and register own dev commands! + +use bevy::dev_tools::fps_overlay::FpsOverlayPlugin; +use bevy::dev_tools::prelude::*; +use bevy::ecs::world::Command; +use bevy::prelude::*; + + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CLIToolbox) + .add_plugins(FpsOverlayPlugin::default()) + + //register dev commands as usual types + .register_type::() + .register_type::() + .register_type::() + .register_type::>() + .register_type::>() + + .init_resource::() + .init_state::() + + //dev tool example + .add_systems(Update, show_gold_system.run_if(in_state(ShowGold::Show))) + .add_systems(OnEnter(ShowGold::Show), create_gold_node) + .add_systems(OnExit(ShowGold::Show), destroy_gold_node) + + .add_systems(Startup, setup) + + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); +} + + +/// Code for showing gold and dev commands for gold + + +/// Contains showing gold +#[derive(Resource, Default)] +pub struct Gold(pub usize); + +/// DevCommand to change gold value +/// Example: +/// `setgold 100` -- you need to print this into your console +/// +/// You must implement Default, Reflect, DevCommand and Command to register it as dev command +/// Dont forget to add app.register_type::() to make it visible for CLIToolbox +#[derive(Reflect, Default, DevCommand)] +#[reflect(DevCommand, Default)] +pub struct SetGold { + pub gold: usize, +} +impl Command for SetGold { + fn apply(self, world: &mut World) { + world.insert_resource(Gold(self.gold)); + } +} + +/// DevCommand to print gold amount +#[derive(Reflect, Default, DevCommand)] +#[reflect(DevCommand, Default)] +pub struct PrintGold {} + +impl Command for PrintGold { + fn apply(self, world: &mut World) { + let gold = world.get_resource::().unwrap(); + info!("Gold: {}", gold.0); + } +} + +//We can create toggable dev state +//It will toggle between show and hide by `enable showgold` and `disable showgold` commands +#[derive(States, Debug, Clone, Eq, PartialEq, Hash, Default, Reflect)] +enum ShowGold { + #[default] + Show, + Hide, +} + +impl Toggable for ShowGold { + fn enable(world: &mut World) { + world.resource_mut::>().set(ShowGold::Show); + } + + fn disable(world: &mut World) { + world.resource_mut::>().set(ShowGold::Hide); + } + + fn is_enabled(world: &World) -> bool { + *world.resource::>() == ShowGold::Show + } +} + +/// UI stuff +#[derive(Component)] +struct ShowGoldNode; + +fn create_gold_node(mut commands: Commands) { + commands.spawn(ShowGoldNode); +} + +fn destroy_gold_node(mut commands: Commands, q_node: Query>) { + if let Ok(node) = q_node.get_single() { + commands.entity(node).despawn(); + } +} + +fn show_gold_system( + mut commands: Commands, + q_node: Query>, + gold : Res, +) { + if let Ok(node) = q_node.get_single() { + commands.entity(node).insert(TextBundle::from_section(format!("Gold: {}", gold.0), TextStyle::default())) + .insert(Style { + position_type: PositionType::Absolute, + right: Val::Px(10.), + ..default() + }); + } +} diff --git a/examples/dev_tools/fps_overlay.rs b/examples/dev_tools/fps_overlay.rs index a2718ecc3c5c0..0f32c0c2ec6a9 100644 --- a/examples/dev_tools/fps_overlay.rs +++ b/examples/dev_tools/fps_overlay.rs @@ -1,7 +1,7 @@ //! Showcase how to use and configure FPS overlay. use bevy::{ - dev_tools::fps_overlay::{FpsOverlayConfig, FpsOverlayPlugin}, + dev_tools::fps_overlay::{FpsOverlay, FpsOverlayPlugin}, prelude::*, }; @@ -10,7 +10,7 @@ fn main() { .add_plugins(( DefaultPlugins, FpsOverlayPlugin { - config: FpsOverlayConfig { + config: FpsOverlay { text_config: TextStyle { // Here we define size of our overlay font_size: 50.0, @@ -57,7 +57,7 @@ fn setup(mut commands: Commands) { }); } -fn customize_config(input: Res>, mut overlay: ResMut) { +fn customize_config(input: Res>, mut overlay: ResMut) { if input.just_pressed(KeyCode::Digit1) { // Changing resource will affect overlay overlay.text_config.color = Color::srgb(1.0, 0.0, 0.0);