From 6e2f701844a38e91ca714dc69e2a64de67a86185 Mon Sep 17 00:00:00 2001 From: Doonv <58695417+doonv@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:45:13 +0200 Subject: [PATCH] Huge rework and stuff --- Cargo.lock | 71 ++++++------- Cargo.toml | 20 +++- README.md | 43 ++++++++ doc/console.png | Bin 0 -> 50076 bytes examples/basic.rs | 17 +++- examples/resource.rs | 11 +- src/builtin_parser.rs | 55 ++++++++++ src/{command => builtin_parser}/lexer.rs | 2 +- src/{command => builtin_parser}/parser.rs | 19 ++-- src/{command => builtin_parser}/runner.rs | 60 +++-------- .../runner/environment.rs | 79 +++++++++------ src/builtin_parser/runner/stdlib.rs | 94 ++++++++++++++++++ .../runner/value.rs | 78 ++++++++++++--- src/command.rs | 66 +++++++----- src/command/runner/stdlib.rs | 47 --------- src/lib.rs | 39 +++++++- src/logging/log_plugin.rs | 93 +++++++---------- src/prelude.rs | 5 +- src/ui.rs | 17 +++- 19 files changed, 530 insertions(+), 286 deletions(-) create mode 100644 README.md create mode 100644 doc/console.png create mode 100644 src/builtin_parser.rs rename src/{command => builtin_parser}/lexer.rs (99%) rename src/{command => builtin_parser}/parser.rs (96%) rename src/{command => builtin_parser}/runner.rs (91%) rename src/{command => builtin_parser}/runner/environment.rs (74%) create mode 100644 src/builtin_parser/runner/stdlib.rs rename src/{command => builtin_parser}/runner/value.rs (62%) delete mode 100644 src/command/runner/stdlib.rs diff --git a/Cargo.lock b/Cargo.lock index 3ba2d89..91d3d57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,7 +458,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -524,20 +524,23 @@ checksum = "f484318350462c58ba3942a45a656c1fd6b6e484a6b6b7abc3a787ad1a51e500" dependencies = [ "bevy_macro_utils", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] name = "bevy_dev_console" version = "0.1.0" dependencies = [ - "ahash", + "android_log-sys", "bevy", "bevy_egui", "chrono", + "console_error_panic_hook", + "instant", "logos", "tracing-log 0.2.0", "tracing-subscriber", + "tracing-wasm", ] [[package]] @@ -585,14 +588,14 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] name = "bevy_egui" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85450af551b7e1cb766f710763b60a12a82ffd6323945a8f776c6334c59ccdc1" +checksum = "c90c01202dbcebc03315a01ea71553b35e1f20b0da6b1cc8c2605344032a3d96" dependencies = [ "arboard", "bevy", @@ -770,7 +773,7 @@ dependencies = [ "proc-macro2", "quote", "rustc-hash", - "syn 2.0.39", + "syn 2.0.40", "toml_edit 0.20.7", ] @@ -853,7 +856,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "uuid", ] @@ -912,7 +915,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1082,7 +1085,7 @@ checksum = "7aafecc952b6b8eb1a93c12590bd867d25df2f4ae1033a01dfdfc3c35ebccfff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1143,7 +1146,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1253,7 +1256,7 @@ checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1569,18 +1572,18 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "ecolor" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf4e52dbbb615cfd30cf5a5265335c217b5fd8d669593cea74a517d9c605af" +checksum = "4b7637fc2e74d17e52931bac90ff4fc061ac776ada9c7fa272f24cdca5991972" dependencies = [ "bytemuck", ] [[package]] name = "egui" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd69fed5fcf4fbb8225b24e80ea6193b61e17a625db105ef0c4d71dde6eb8b7" +checksum = "c55bcb864b764eb889515a38b8924757657a250738ad15126637ee2df291ee6b" dependencies = [ "ahash", "epaint", @@ -1589,9 +1592,9 @@ dependencies = [ [[package]] name = "emath" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef2b29de53074e575c18b694167ccbe6e5191f7b25fe65175a0d905a32eeec0" +checksum = "a045c6c0b44b35e98513fc1e9d183ab42881ac27caccb9fa345465601f56cce4" dependencies = [ "bytemuck", ] @@ -1625,14 +1628,14 @@ checksum = "3fe2568f851fd6144a45fa91cfed8fe5ca8fc0b56ba6797bfc1ed2771b90e37c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] name = "epaint" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58067b840d009143934d91d8dcb8ded054d8301d7c11a517ace0a99bb1e1595e" +checksum = "7d1b9e000d21bab9b535ce78f9f7745be28b3f777f6c7223936561c5c7fefab8" dependencies = [ "ab_glyph", "ahash", @@ -1777,7 +1780,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1953,7 +1956,7 @@ dependencies = [ "inflections", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2426,7 +2429,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2748,7 +2751,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -3204,7 +3207,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -3316,9 +3319,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e" dependencies = [ "proc-macro2", "quote", @@ -3386,7 +3389,7 @@ checksum = "e4c60d69f36615a077cc7663b9cb8e42275722d23e58a7fa3d2c7f2915d09d04" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -3397,7 +3400,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -3483,7 +3486,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -3678,7 +3681,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "wasm-bindgen-shared", ] @@ -3712,7 +3715,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4215,5 +4218,5 @@ checksum = "e1012d89e3acb79fad7a799ce96866cfb8098b74638465ea1b1533d35900ca90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] diff --git a/Cargo.toml b/Cargo.toml index c1cb489..13fb80d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,32 @@ + [package] name = "bevy_dev_console" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["builtin-parser"] + +builtin-parser = ["dep:logos"] [dependencies] -ahash = "0.8.6" bevy = "0.12.1" -bevy_egui = "0.23.0" +bevy_egui = "0.24.0" chrono = "0.4.31" -logos = "0.13.0" +instant = "0.1.12" tracing-log = "0.2.0" tracing-subscriber = "0.3.18" +logos = { version = "0.13.0", optional = true } + +[target.'cfg(target_os = "android")'.dependencies] +android_log-sys = "0.3.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1.7" +tracing-wasm = "0.2.1" + # Enable a small amount of optimization in debug mode [profile.dev] @@ -25,3 +38,4 @@ opt-level = 3 [lints] clippy.useless_format = "allow" +rust.missing_docs = "warn" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dee8666 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# bevy_dev_console + +`bevy_dev_console` is a [Source](https://en.wikipedia.org/wiki/Source_(game_engine))-like developer console plugin for the [Bevy Game Engine](https://github.com/bevyengine/bevy). + +![Image of the developer console](doc/console.png) + +> **Warning:** `bevy_dev_console` is currently in its early development stages. Expect breaking changes in the near future (especially when using the built-in command parser). For this reason its only available as a git package at the moment. + +## Usage + +1. Add the `bevy_dev_console` git package. +```bash +cargo add --git https://github.com/doonv/bevy_dev_console.git +``` +2. Import the `prelude`. +```rs +use bevy_dev_console::prelude::*; +``` +3. Add the plugins. +```rs +App::new() + .add_plugins(( + // Start capturing logs before the default plugins initiate. + ConsoleLogPlugin::default(), + // Add the default plugins without the LogPlugin. + // Not removing the LogPlugin will cause a panic! + DefaultPlugins.build().disable::(), + // Add the dev console plugin itself. + DevConsolePlugin, + )) +``` +4. That should be it! You can now press the `` ` `` or `~` key on your keyboard and it should open the console! + +## Features + +`builtin-parser (default)` allows you to optionally remove the built-in parser and replace it with your own. + +## Bevy Compatibility + + +| bevy | bevy_dev_console | +| ------ | ---------------- | +| 0.12.* | 0.1.0 | \ No newline at end of file diff --git a/doc/console.png b/doc/console.png new file mode 100644 index 0000000000000000000000000000000000000000..01a6d76b011a7e530c99fb7614e5dab543bf71d5 GIT binary patch literal 50076 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 literal 0 HcmV?d00001 diff --git a/examples/basic.rs b/examples/basic.rs index b842700..b016a86 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,18 +1,27 @@ -use bevy::{log::LogPlugin, prelude::*}; +//! A simple exmaple + +use bevy::{ + log::{Level, LogPlugin}, + prelude::*, +}; use bevy_dev_console::prelude::*; fn main() { App::new() .add_plugins(( - bevy_dev_console::prelude::LogPlugin::default(), + // Start capturing logs before the default plugins initiate. + // Also append a filter that shows `TRACE` logs from this module. + ConsoleLogPlugin::default().append_filter(module_path!(), Level::TRACE), + // Add the default plugins without the LogPlugin. + // Not removing the LogPlugin will cause a panic! DefaultPlugins.build().disable::(), - DevConsolePlugin, + // Add the dev console plugin itself. )) .add_systems(Startup, test) .run(); } -pub fn test() { +fn test() { trace!("tracing"); debug!("solving issues..."); info!("hello :)"); diff --git a/examples/resource.rs b/examples/resource.rs index caa4aa8..a633903 100644 --- a/examples/resource.rs +++ b/examples/resource.rs @@ -1,7 +1,8 @@ -use bevy::{ - log::{Level, LogPlugin}, - prelude::*, -}; +//! Example of modifying resources via the console via reflection. +//! +//! **Warning:** This is very experimental, might not work. + +use bevy::{log::LogPlugin, prelude::*}; use bevy_dev_console::prelude::*; #[derive(Resource, Reflect, Default, Debug)] @@ -36,7 +37,7 @@ fn main() { string: "hi there :)".to_string(), }) .add_plugins(( - bevy_dev_console::prelude::LogPlugin::default(), + ConsoleLogPlugin::default(), DefaultPlugins.build().disable::(), DevConsolePlugin, )) diff --git a/src/builtin_parser.rs b/src/builtin_parser.rs new file mode 100644 index 0000000..a24d9b5 --- /dev/null +++ b/src/builtin_parser.rs @@ -0,0 +1,55 @@ +//! [`bevy_dev_console`](crate)'s built-in command parser. +//! +//! Currently the built-in command parser is in very early development. +//! It's purpose is to provide a simple, yet powerful method of modifying +//! the game world via commands. + +use bevy::prelude::*; +use logos::Span; + +use crate::command::{CommandParser, DefaultCommandParser}; + +use self::{lexer::TokenStream, parser::parse}; + +pub(crate) mod lexer; +pub(crate) mod parser; +pub(crate) mod runner; + +pub use runner::environment::Environment; + +/// Wrapper around `T` that stores a [Span] (A location in the source code) +#[derive(Debug, Clone)] +pub struct Spanned { + /// The location of `T` in the source. + pub span: Span, + /// The value of `T`. + pub value: T, +} + +impl Default for DefaultCommandParser { + fn default() -> Self { + Self(Box::new(BuiltinCommandParser)) + } +} + +/// [`bevy_dev_console`](crate)'s built-in command parser. +/// +/// See the [module level documentation for more](self). +#[derive(Default)] +pub struct BuiltinCommandParser; +impl CommandParser for BuiltinCommandParser { + fn parse(&self, command: &str, world: &mut World) { + let mut tokens = TokenStream::new(command); + + let environment = world.remove_non_send_resource::().unwrap(); + let ast = parse(&mut tokens, &environment); + world.insert_non_send_resource(environment); + + match ast { + Ok(ast) => { + runner::run(ast, world); + } + Err(err) => error!("{err:#?}"), + } + } +} diff --git a/src/command/lexer.rs b/src/builtin_parser/lexer.rs similarity index 99% rename from src/command/lexer.rs rename to src/builtin_parser/lexer.rs index 2be2490..089fe7a 100644 --- a/src/command/lexer.rs +++ b/src/builtin_parser/lexer.rs @@ -131,7 +131,7 @@ impl<'a> TokenStream<'a> { } /// Get a [`str`] slice of the next [`Token`]. - pub fn peek_slice(&self) -> &str { + pub fn _peek_slice(&self) -> &str { self.lexer.slice() } diff --git a/src/command/parser.rs b/src/builtin_parser/parser.rs similarity index 96% rename from src/command/parser.rs rename to src/builtin_parser/parser.rs index e8860b0..9c44dbe 100644 --- a/src/command/parser.rs +++ b/src/builtin_parser/parser.rs @@ -1,7 +1,5 @@ -// use std::collections::HashMap as AHashMap; - -use ahash::AHashMap; use logos::Span; +use std::collections::HashMap; use super::{ lexer::{FailedToLexCharacter, Token, TokenStream}, @@ -56,7 +54,7 @@ pub enum Expression { Dereference(Box>), StructObject { name: String, - map: AHashMap>, + map: HashMap>, }, String(String), Borrow(Box>), @@ -65,7 +63,7 @@ pub enum Expression { name: String, arguments: Vec>, }, - Object(AHashMap>), + Object(HashMap>), } #[derive(Debug, Clone)] @@ -341,8 +339,8 @@ fn parse_var_assign( fn parse_object( tokens: &mut TokenStream, environment: &Environment, -) -> Result>, ParseError> { - let mut map = AHashMap::new(); +) -> Result>, ParseError> { + let mut map = HashMap::new(); while let Some(Ok(Token::Identifer)) = tokens.peek() { tokens.next(); let ident = tokens.slice().to_string(); @@ -363,9 +361,10 @@ fn parse_object( #[cfg(test)] mod tests { - use crate::command::Environment; - - use super::{super::lexer::TokenStream, parse}; + use super::{ + super::{lexer::TokenStream, Environment}, + parse, + }; #[test] fn var_assign() { diff --git a/src/command/runner.rs b/src/builtin_parser/runner.rs similarity index 91% rename from src/command/runner.rs rename to src/builtin_parser/runner.rs index 5984a31..c37e20d 100644 --- a/src/command/runner.rs +++ b/src/builtin_parser/runner.rs @@ -1,13 +1,11 @@ -use std::rc::Weak; -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; -use self::environment::{Environment, ResultContainer}; +use self::environment::Environment; use super::{ parser::{Ast, Expression, Operator}, Spanned, }; -use ahash::AHashMap; use bevy::reflect::ReflectRef; use bevy::{ prelude::*, @@ -55,7 +53,7 @@ pub fn run(ast: Ast, world: &mut World) { // Same thing here (this time we are doing it because we are passing a `&mut World` to `eval_expression`) let Some(registry) = world.remove_resource::() else { - error!("The AppTypeRegistry doesn't exist, not executing command. (What have you done to cause this?)"); + error!("The AppTypeRegistry doesn't exist, not executing command. (What have you done to cause this? o_O)"); return; }; @@ -145,7 +143,7 @@ fn eval_expression( let rc = Rc::new(RefCell::new(value)); let weak = Rc::downgrade(&rc); - environment.set(var, rc)?; + environment.set(var, rc); Ok(Value::Reference(weak)) } @@ -163,7 +161,7 @@ fn eval_expression( info!(name: "console_result", "> {}", fancy_debug_print(registration, world)); Ok(Value::None) } else { - // let value = &*; + #[allow(clippy::single_match)] match &*environment.get(&variable, expr.span.clone())?.borrow() { Value::Number(number) => return Ok(Value::Number(*number)), _ => {} @@ -282,31 +280,7 @@ fn eval_expression( )?; Ok(Value::Object(hashmap)) } - Expression::Dereference(inner) => { - // if let Expression::Variable(variable) = inner.value { - // let cell = environment.get(&variable, inner.span.clone())?; - // // This line of code is stupid. However I believe that - // // Ref::leak (unstable) will give a less-shitty approach to this. - // if Rc::strong_count(&*cell.borrow()) == 1 { - // Ok(Rc::try_unwrap( - // environment - // .remove(&variable, inner.span.clone())? - // .into_inner(), - // ) - // .unwrap()) - // } else { - // Err(RunError::CouldntDereferenceValue(expr.span)) - // } - // // let value = environment.get(&variable, inner.span)?.get_mut().; - // // if let Ok(value) = Rc::try_unwrap(value) { - // // Ok(value) - // // } else { - // // Err(RunError::CouldntDereferenceValue(expr.span)) - // // } - // } else { - // // Err() - // todo!() - // } + Expression::Dereference(_inner) => { todo!() } Expression::Borrow(inner) => { @@ -379,20 +353,16 @@ fn eval_member_expression<'a>( let reflect = var.reflect_ref(); match reflect { - ReflectRef::Struct(_) => { - Ok(Reflectable::StructField(ReflectStructField { - string: right, - mut_reflect: var, - })) - } + ReflectRef::Struct(_) => Ok(Reflectable::StructField(ReflectStructField { + string: right, + mut_reflect: var, + })), _ => todo!(), } } else { let reference = environment.get(&variable, left_span)?.borrow(); match &*reference { - Value::Number(number) => { - return Ok(Reflectable::Value(Value::Number(*number))) - } + Value::Number(number) => return Ok(Reflectable::Value(Value::Number(*number))), Value::Dynamic(value) => { dbg!(value); Ok(Reflectable::Value(Value::None)) @@ -422,9 +392,7 @@ fn eval_member_expression<'a>( match left { Value::StructObject { map, .. } => { if let Some(value) = map.get(&right) { - Ok(Reflectable::Value(Value::Reference( - Rc::downgrade(value), - ))) + Ok(Reflectable::Value(Value::Reference(Rc::downgrade(value)))) } else { Err(RunError::FieldNotFoundInStruct(left_span)) } @@ -535,13 +503,13 @@ fn set_resource( } } fn eval_object( - map: AHashMap>, + map: HashMap>, EvalParams { world, environment, registrations, }: EvalParams, -) -> Result>>, RunError> { +) -> Result>>, RunError> { let map = map .into_iter() .map( diff --git a/src/command/runner/environment.rs b/src/builtin_parser/runner/environment.rs similarity index 74% rename from src/command/runner/environment.rs rename to src/builtin_parser/runner/environment.rs index 95e53a3..f236817 100644 --- a/src/command/runner/environment.rs +++ b/src/builtin_parser/runner/environment.rs @@ -1,10 +1,12 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; -use ahash::AHashMap; +use bevy::{ecs::world::World, reflect::TypeRegistration}; use logos::Span; -use super::{eval_expression, stdlib, EvalParams, RunError, Value}; -use crate::command::{parser::Expression, Spanned}; +use super::{ + super::{parser::Expression, Spanned}, + eval_expression, stdlib, EvalParams, RunError, Value, +}; /// Get around implementation of Result causing stupid errors pub(super) struct ResultContainer(pub Result); @@ -40,26 +42,35 @@ macro_rules! count_idents { ($($tts:ident)*) => {0usize $(+ replace_expr!($tts 1usize))*}; } +pub struct FunctionParameterData<'world, 'env, 'reg, 'world2, 'env2, 'reg2> { + pub value: Spanned, + pub world: &'world2 mut Option<&'world mut World>, + pub environment: &'env2 mut Option<&'env mut Environment>, + pub registrations: &'reg2 [&'reg TypeRegistration], +} macro_rules! impl_into_function { ( $($( $params:ident ),+)? ) => { - impl>),+ )?, R> IntoFunction<( $($($params,)+)? )> for F + impl + TryFrom> + ),+ )?, R> IntoFunction<( $($($params,)+)? )> for F where F: Fn($($($params),+)?) -> R + 'static, R: Into>, { fn into_function(self) -> Function { + #[allow(unused_variables, unused_mut)] let body = Box::new(move |args: Vec>, params: EvalParams| { let EvalParams { world, environment, registrations, } = params; - #[allow(unused_variables, unused_mut)] - let mut args = args.into_iter().map(move |expr| { + let mut args = args.into_iter().map(|expr| { Spanned { span: expr.span.clone(), value: eval_expression( @@ -71,18 +82,24 @@ macro_rules! impl_into_function { } ) } - }); + }).collect::>().into_iter(); + let world = &mut Some(world); + let environment = &mut Some(environment); self( $($({ let _: $params; // Tell rust im talking abouts the $params - let arg = args.next() - .unwrap(); - Spanned { - span: arg.span, - value: arg.value? + let arg = args.next().unwrap(); + FunctionParameterData { + value: Spanned { + span: arg.span, + value: arg.value? + }, + world, + environment, + registrations } .try_into() - .unwrap_or_else(|_| unreachable!()) + .unwrap_or_else(|_| todo!()) }),+)? ) .into().into() @@ -94,16 +111,15 @@ macro_rules! impl_into_function { } } } - impl_into_function!(); impl_into_function!(T1); impl_into_function!(T1, T2); impl_into_function!(T1, T2, T3); impl_into_function!(T1, T2, T3, T4); -impl_into_function!(T1, T2, T3, T4, T5); -impl_into_function!(T1, T2, T3, T4, T5, T6); -impl_into_function!(T1, T2, T3, T4, T5, T6, T7); -impl_into_function!(T1, T2, T3, T4, T5, T6, T7, T8); +// impl_into_function!(T1, T2, T3, T4, T5); +// impl_into_function!(T1, T2, T3, T4, T5, T6); +// impl_into_function!(T1, T2, T3, T4, T5, T6, T7); +// impl_into_function!(T1, T2, T3, T4, T5, T6, T7, T8); pub enum Variable { Unmoved(Rc>), @@ -114,14 +130,14 @@ pub enum Variable { /// The environment stores all variables and functions. pub struct Environment { parent: Option>, - variables: AHashMap, + variables: HashMap, } impl Default for Environment { fn default() -> Self { let mut env = Self { parent: None, - variables: AHashMap::new(), + variables: HashMap::new(), }; stdlib::register(&mut env); @@ -131,22 +147,22 @@ impl Default for Environment { } impl Environment { - pub fn set(&mut self, name: String, value: Rc>) -> Result<(), RunError> { + /// Set a variable. + pub fn set(&mut self, name: String, value: Rc>) { self.variables.insert(name, Variable::Unmoved(value)); - - Ok(()) } + + /// Returns a reference to a function if it exists. pub fn get_function(&self, name: &str) -> Option<&Function> { let (env, _) = self.resolve(name, 0..0).ok()?; match env.variables.get(name) { - Some(Variable::Unmoved(_)) => None, - Some(Variable::Moved) => None, Some(Variable::Function(function)) => Some(function), - None => None, + _ => None, } } - pub fn function_scope( + + pub(crate) fn function_scope( &mut self, name: &str, function: impl FnOnce(&mut Self, &Function) -> T, @@ -174,6 +190,7 @@ impl Environment { return_result } + /// Returns a reference to a variable. pub fn get(&self, name: &str, span: Span) -> Result<&Rc>, RunError> { let (env, span) = self.resolve(name, span)?; @@ -184,6 +201,8 @@ impl Environment { None => Err(RunError::VariableNotFound(span)), } } + + /// "moves" a variable, giving you ownership over it. However it will no longer be able to be used. pub fn move_var(&mut self, name: &str, span: Span) -> Result>, RunError> { let (env, span) = self.resolve_mut(name, span)?; @@ -225,7 +244,7 @@ impl Environment { /// Registers a function for use inside the language. /// /// All parameters must implement [`TryFrom`]. - /// There is a hard limit of 8 parameters. + /// There is a limit of 8 parameters. /// /// The return value of the function must implement [`Into`] /// @@ -233,7 +252,7 @@ impl Environment { pub fn register_fn( &mut self, name: impl Into, - function: impl IntoFunction + 'static, + function: impl IntoFunction, ) -> &mut Self { self.variables .insert(name.into(), Variable::Function(function.into_function())); diff --git a/src/builtin_parser/runner/stdlib.rs b/src/builtin_parser/runner/stdlib.rs new file mode 100644 index 0000000..e2913e7 --- /dev/null +++ b/src/builtin_parser/runner/stdlib.rs @@ -0,0 +1,94 @@ +use std::{cell::Ref, ops::Range}; + +use bevy::log::info; + +use super::{RunError, Value, Environment, Spanned}; + +fn print(value: Spanned) -> Result<(), RunError> { + match value.value { + Value::String(string) => info!("{string}"), + _ => { + let string = value.value.try_format(value.span)?; + info!("{string}"); + } + } + Ok(()) +} + +fn dbg(any: Value) { + info!("Value::{any:?}"); +} + +fn ref_depth(Spanned { span, value }: Spanned) -> Result { + fn ref_depth_reference(value: Ref, span: Range) -> Result { + Ok(match &*value { + Value::Reference(reference) => { + ref_depth_reference( + reference + .upgrade() + .ok_or(RunError::ReferenceToMovedData(span.clone()))? + .borrow(), + span, + )? + 1.0 + } + _ => 0.0, + }) + } + + Ok(match value { + Value::Reference(reference) => { + ref_depth_reference( + reference + .upgrade() + .ok_or(RunError::ReferenceToMovedData(span.clone()))? + .borrow(), + span, + )? + 1.0 + } + _ => 0.0, + }) +} + +/// Macro for mass registering functions. +/// +/// ``` +/// fn a() {} +/// fn b() {} +/// fn c() {} +/// +/// # let mut environment = bevy_dev_console::builtin_parser::Environment::default(); +/// # use bevy_dev_console::register; +/// register!(environment => { +/// fn a; +/// fn b; +/// fn c; +/// }); +/// ``` +#[macro_export] +macro_rules! register { + { + $environment:expr => fn $fn_name:ident; + } => { + $environment + .register_fn(stringify!($fn_name), $fn_name) + }; + { + $environment:expr => { + $( + fn $fn_name:ident; + )* + } + } => { + $environment + $( + .register_fn(stringify!($fn_name), $fn_name) + )* + }; +} +pub fn register(environment: &mut Environment) { + register!(environment => { + fn print; + fn dbg; + fn ref_depth; + }); +} diff --git a/src/command/runner/value.rs b/src/builtin_parser/runner/value.rs similarity index 62% rename from src/command/runner/value.rs rename to src/builtin_parser/runner/value.rs index 9620c7e..e1a5ca1 100644 --- a/src/command/runner/value.rs +++ b/src/builtin_parser/runner/value.rs @@ -1,11 +1,11 @@ +use std::collections::HashMap; use std::rc::Weak; use std::{cell::RefCell, rc::Rc}; -use super::environment::ResultContainer; -use super::RunError; +use super::environment::{FunctionParameterData, ResultContainer}; +use super::{RunError, super::Spanned}; -use super::super::Spanned; -use ahash::AHashMap; +use bevy::ecs::world::World; use bevy::reflect::Reflect; use logos::Span; @@ -15,7 +15,7 @@ use logos::Span; pub enum Value { /// Nothing at all None, - /// A number, for simplicity only f64s are used. + /// A number, for simplicity only f64s are used. (However this will probably change in the future) Number(f64), /// A string... there isn't much to say about this one. String(String), @@ -32,11 +32,11 @@ pub enum Value { Reference(Weak>), StructObject { name: String, - map: AHashMap>>, + map: HashMap>>, }, /// A reference to a dynamic value. (aka a reference.) Dynamic(Box), - Object(AHashMap>>), + Object(HashMap>>), } impl Value { @@ -58,7 +58,7 @@ impl Value { for (key, value) in map { string += &format!("\n\t{key}: {},", value.borrow().try_format(span.clone())?); } - if map.len() > 0 { + if !map.is_empty() { string.push('\n'); } string.push('}'); @@ -70,7 +70,7 @@ impl Value { for (key, value) in map { string += &format!("\n\t{key}: {},", value.borrow().try_format(span.clone())?); } - if map.len() > 0 { + if !map.is_empty() { string.push('\n'); } string.push('}'); @@ -112,17 +112,36 @@ impl From for Value { } } -impl TryFrom> for Value { +impl<'world, 'env, 'reg, 'world2, 'env2, 'reg2> + TryFrom> for Spanned +{ type Error = RunError; - fn try_from(value: Spanned) -> Result { + fn try_from( + FunctionParameterData { value, .. }: FunctionParameterData, + ) -> Result { + Ok(value) + } +} +impl<'world, 'env, 'reg, 'world2, 'env2, 'reg2> + TryFrom> for Value +{ + type Error = RunError; + + fn try_from( + FunctionParameterData { value, .. }: FunctionParameterData, + ) -> Result { Ok(value.value) } } -impl TryFrom> for f64 { +impl<'world, 'env, 'reg, 'world2, 'env2, 'reg2> + TryFrom> for f64 +{ type Error = RunError; - fn try_from(value: Spanned) -> Result { + fn try_from( + FunctionParameterData { value, .. }: FunctionParameterData, + ) -> Result { if let Value::Number(number) = value.value { Ok(number) } else { @@ -130,10 +149,15 @@ impl TryFrom> for f64 { } } } -impl TryFrom> for String { + +impl<'world, 'env, 'reg, 'world2, 'env2, 'reg2> + TryFrom> for String +{ type Error = RunError; - fn try_from(value: Spanned) -> Result { + fn try_from( + FunctionParameterData { value, .. }: FunctionParameterData, + ) -> Result { if let Value::String(string) = value.value { Ok(string) } else { @@ -141,3 +165,27 @@ impl TryFrom> for String { } } } + +impl<'world, 'env, 'reg, 'world2, 'env2, 'reg2> + TryFrom> + for &'world mut World +{ + type Error = RunError; + + fn try_from( + FunctionParameterData { world, .. }: FunctionParameterData< + 'world, + 'env, + 'reg, + 'world2, + 'env2, + 'reg2, + >, + ) -> Result { + if let Some(world) = world.take() { + Ok(world) + } else { + todo!() + } + } +} diff --git a/src/command.rs b/src/command.rs index f7c8f44..89206db 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,36 +1,48 @@ -//! Command parser +//! Command execution functionality. -use bevy::{ecs::system::Command, prelude::*}; -use logos::Span; +use bevy::{prelude::*, ecs::system::Command}; -use self::{lexer::TokenStream, parser::parse}; +/// The command parser currrently being used by the dev console. +#[derive(Resource)] +pub struct DefaultCommandParser(pub Box); -mod lexer; -mod parser; -mod runner; - -pub use runner::environment::Environment; +impl DefaultCommandParser { + /// Shortcut method for calling `parser.0.parse(command, world)`. + pub fn parse(&self, command: &str, world: &mut World) { + self.0.parse(command, world) + } +} -#[derive(Debug, Clone)] -pub struct Spanned { - pub span: Span, - pub value: T, +/// The trait that all [`CommandParser`]s implement. +/// You can take a look at the [builtin parser](crate::builtin_parser) for an example. +/// +/// ``` +/// # use bevy::ecs::world::World; +/// # use bevy_dev_console::command::CommandParser; +/// # use bevy::log::info; +/// +/// pub struct MyCustomParser; +/// impl CommandParser for MyCustomParser { +/// fn parse(&self, command: &str, world: &mut World) { +/// // The `name: "console_result"` tells the console this is a result from +/// // the parser and then formats it accordingly. +/// info!(name: "console_result", "You just entered the command {command}") +/// } +/// } +/// ``` +pub trait CommandParser: Send + Sync { + /// The method called by the developer console when a command is ran. + fn parse(&self, command: &str, world: &mut World); } -pub struct ExecuteConsoleCommand(pub String); -impl Command for ExecuteConsoleCommand { +pub(crate) struct ExecuteCommand(pub String); +impl Command for ExecuteCommand { fn apply(self, world: &mut World) { - let mut tokens = TokenStream::new(&self.0); - - let environment = world.remove_non_send_resource::().unwrap(); - let ast = parse(&mut tokens, &environment); - world.insert_non_send_resource(environment); - - match ast { - Ok(ast) => { - runner::run(ast, world); - } - Err(err) => error!("{err:#?}"), + if let Some(parser) = world.remove_resource::() { + parser.parse(&self.0, world); + world.insert_resource(parser); + } else { + error!("Default command parser doesn't exist, cannot execute command."); } } -} +} \ No newline at end of file diff --git a/src/command/runner/stdlib.rs b/src/command/runner/stdlib.rs deleted file mode 100644 index fcd6d5d..0000000 --- a/src/command/runner/stdlib.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::cell::Ref; - -use bevy::log::info; - -use crate::command::{Environment, Spanned}; - -use super::{RunError, Value}; - -fn print(value: Spanned) -> Result<(), RunError> { - match value.value { - Value::String(string) => info!("{string}"), - _ => { - let string = value.value.try_format(value.span)?; - info!("{string}"); - } - } - Ok(()) -} - -fn dbg(any: Value) { - info!("Value::{any:?}"); -} - -fn ref_depth(value: Value) -> f64 { - fn ref_depth_reference(value: Ref) -> f64 { - match &*value { - Value::Reference(reference) => { - ref_depth_reference(reference.upgrade().unwrap().borrow()) + 1.0 - } - _ => 0.0, - } - } - - match value { - Value::Reference(reference) => { - ref_depth_reference(reference.upgrade().unwrap().borrow()) + 1.0 - } - _ => 0.0, - } -} - -pub fn register(environment: &mut Environment) { - environment - .register_fn("print", print) - .register_fn("dbg", dbg) - .register_fn("ref_depth", ref_depth); -} diff --git a/src/lib.rs b/src/lib.rs index 8719000..f882c03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,41 @@ +//! `bevy_dev_console` is a [Source](https://en.wikipedia.org/wiki/Source_(game_engine))-like +//! developer console plugin for the [Bevy Game Engine](https://github.com/bevyengine/bevy). +//! +//! `bevy_dev_console` is currently in its early development stages. +//! Expect breaking changes in the near future (espically when using the built-in command parser). +//! For this reason its only available as a git package at the moment. +//! +//! ## Example +//! +//! ```no_run +//! use bevy::prelude::*; +//! use bevy_dev_console::prelude::*; +//! +//! fn main() { +//! App::new() +//! .add_plugins(( +//! ConsoleLogPlugin::default(), +//! DefaultPlugins.build().disable::(), +//! DevConsolePlugin, +//! )) +//! .run(); +//! } +//! ``` + use bevy::prelude::*; use bevy_egui::EguiPlugin; -use command::Environment; use ui::ConsoleUiState; +#[cfg(feature = "builtin-parser")] +pub mod builtin_parser; +pub mod command; mod logging; -mod command; pub mod prelude; mod ui; +/// Adds a Developer Console to your Bevy application. +/// +/// Requires [ConsoleLogPlugin](logging::log_plugin::ConsoleLogPlugin). pub struct DevConsolePlugin; impl Plugin for DevConsolePlugin { fn build(&self, app: &mut App) { @@ -15,8 +43,13 @@ impl Plugin for DevConsolePlugin { app.add_plugins(EguiPlugin); } + #[cfg(feature = "builtin-parser")] + { + app.init_non_send_resource::(); + app.init_resource::(); + } + app.init_resource::() - .init_non_send_resource::() .add_systems(Update, ui::ui); } } diff --git a/src/logging/log_plugin.rs b/src/logging/log_plugin.rs index 82e91d6..f507c48 100644 --- a/src/logging/log_plugin.rs +++ b/src/logging/log_plugin.rs @@ -5,19 +5,16 @@ //! The macros provided for logging are reexported from [`tracing`](https://docs.rs/tracing), //! and behave identically to it. //! -//! By default, the [`LogPlugin`] from this crate is included in Bevy's `DefaultPlugins` +//! By default, the [`ConsoleLogPlugin`] from this crate is included in Bevy's `DefaultPlugins` //! and the logging macros can be used out of the box, if used. //! -//! For more fine-tuned control over logging behavior, set up the [`LogPlugin`] or +//! For more fine-tuned control over logging behavior, set up the [`ConsoleLogPlugin`] or //! `DefaultPlugins` during app initialization. +use instant::SystemTime; #[cfg(feature = "trace")] use std::panic; -use std::{ - sync::{Arc, Mutex}, - time::SystemTime, -}; - +use std::sync::{Arc, Mutex}; #[cfg(target_os = "android")] mod android_tracing; @@ -42,55 +39,18 @@ use tracing_log::LogTracer; use tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}; use tracing_subscriber::{field::Visit, layer::Layer, prelude::*, registry::Registry, EnvFilter}; -/// Adds logging to Apps. This plugin is part of the `DefaultPlugins`. Adding -/// this plugin will setup a collector appropriate to your target platform: -/// * Using [`tracing-subscriber`](https://crates.io/crates/tracing-subscriber) by default, -/// logging to `stdout`. -/// * Using [`android_log-sys`](https://crates.io/crates/android_log-sys) on Android, -/// logging to Android logs. -/// * Using [`tracing-wasm`](https://crates.io/crates/tracing-wasm) in WASM, logging -/// to the browser console. -/// -/// You can configure this plugin. -/// ```no_run -/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup}; -/// # use bevy_log::LogPlugin; -/// # use bevy::utils::tracing::Level; -/// fn main() { -/// App::new() -/// .add_plugins(DefaultPlugins.set(LogPlugin { -/// level: Level::DEBUG, -/// filter: "wgpu=error,bevy_render=info,bevy::ecs=trace".to_string(), -/// })) -/// .run(); -/// } -/// ``` -/// -/// Log level can also be changed using the `RUST_LOG` environment variable. -/// For example, using `RUST_LOG=wgpu=error,bevy_render=info,bevy::ecs=trace cargo run ..` -/// -/// It has the same syntax as the field [`LogPlugin::filter`], see [`EnvFilter`]. -/// If you define the `RUST_LOG` environment variable, the [`LogPlugin`] settings -/// will be ignored. -/// -/// If you want to setup your own tracing collector, you should disable this -/// plugin from `DefaultPlugins`: -/// ```no_run -/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup}; -/// # use bevy_log::LogPlugin; -/// fn main() { -/// App::new() -/// .add_plugins(DefaultPlugins.build().disable::()) -/// .run(); -/// } -/// ``` +/// [`bevy_dev_console`](crate)'s custom [LogPlugin](bevy::log::LogPlugin). +/// This plugin allows [`bevy_dev_console`](crate) to access Bevy logs +/// and display them in the developer console. /// /// # Panics /// /// This plugin should not be added multiple times in the same process. This plugin /// sets up global logging configuration for **all** Apps in a given process, and /// rerunning the same initialization multiple times will lead to a panic. -pub struct LogPlugin { +/// +/// **This means you have to remove Bevy's built-in [`LogPlugin`](bevy::log::LogPlugin) for this to work!** +pub struct ConsoleLogPlugin { /// Filters logs using the [`EnvFilter`] format pub filter: String, @@ -99,7 +59,30 @@ pub struct LogPlugin { pub level: Level, } -impl Default for LogPlugin { +impl ConsoleLogPlugin { + /// Appends a filter to the [`ConsoleLogPlugin`], allowing you to change the + /// log level of a specific module/crate. + /// + /// ## Examples + /// + /// Changing the log level of the current module to `TRACE`. + /// ``` + /// # use bevy_dev_console::prelude::ConsoleLogPlugin; + /// # use bevy::log::Level; + /// ConsoleLogPlugin::default() + /// .append_filter(module_path!(), Level::TRACE) + /// # ; + /// ``` + pub fn append_filter(mut self, target: &str, level: Level) -> Self { + self.filter.push(','); + self.filter += target; + self.filter.push('='); + self.filter += level.as_str(); + self + } +} + +impl Default for ConsoleLogPlugin { fn default() -> Self { Self { filter: "wgpu=error,naga=warn,bevy_dev_console=trace".to_string(), @@ -108,7 +91,7 @@ impl Default for LogPlugin { } } -impl Plugin for LogPlugin { +impl Plugin for ConsoleLogPlugin { #[cfg_attr(not(feature = "tracing-chrome"), allow(unused_variables))] fn build(&self, app: &mut App) { #[cfg(feature = "trace")] @@ -212,10 +195,10 @@ impl Plugin for LogPlugin { match (logger_already_set, subscriber_already_set) { (true, true) => warn!( - "Could not set global logger and tracing subscriber as they are already set. Consider disabling LogPlugin." + "Could not set global logger and tracing subscriber as they are already set. Consider disabling ConsoleLogPlugin." ), - (true, _) => warn!("Could not set global logger as it is already set. Consider disabling LogPlugin."), - (_, true) => warn!("Could not set global tracing subscriber as it is already set. Consider disabling LogPlugin."), + (true, _) => warn!("Could not set global logger as it is already set. Consider disabling ConsoleLogPlugin."), + (_, true) => warn!("Could not set global tracing subscriber as it is already set. Consider disabling ConsoleLogPlugin."), _ => (), } } diff --git a/src/prelude.rs b/src/prelude.rs index f00ba48..711660e 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,3 +1,4 @@ -pub use crate::logging::log_plugin::LogPlugin; +//! `use bevy_dev_console::prelude::*` to quickly import the required plugins for `bevy_dev_console`. + +pub use crate::logging::log_plugin::ConsoleLogPlugin; pub use crate::DevConsolePlugin; -pub use crate::command::Environment; diff --git a/src/ui.rs b/src/ui.rs index c4ed82c..dc8a607 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,8 +4,10 @@ use bevy::prelude::*; use bevy::utils::tracing::Level; use bevy_egui::*; +use chrono::TimeZone; +use instant::SystemTime; -use crate::{logging::log_plugin::LogMessage, command::ExecuteConsoleCommand}; +use crate::{command::ExecuteCommand, logging::log_plugin::LogMessage}; #[derive(Default, Resource)] pub struct ConsoleUiState { @@ -14,6 +16,13 @@ pub struct ConsoleUiState { command: String, } +fn system_time_to_chorno_utc(t: SystemTime) -> chrono::DateTime { + let dur = t.duration_since(instant::SystemTime::UNIX_EPOCH).unwrap(); + let (sec, nsec) = (dur.as_secs() as i64, dur.subsec_nanos()); + + chrono::Utc.timestamp_opt(sec, nsec).unwrap() +} + pub fn ui( mut commands: Commands, mut contexts: EguiContexts, @@ -33,7 +42,7 @@ pub fn ui( info!(name: "console_command", "$ {}", state.command.trim()); // Get the owned command by replacing it with an empty string let command = std::mem::take(&mut state.command); - commands.add(ExecuteConsoleCommand(command)); + commands.add(ExecuteCommand(command)); } if state.open { @@ -98,8 +107,8 @@ fn add_log( const FONT_ID: egui::FontId = egui::FontId::monospace(CONSOLE_FONT_SIZE); ui.push_id(id, |ui| { - let time_utc = chrono::DateTime::::from(*time); - let time = chrono::DateTime::::from(*time); + let time_utc = system_time_to_chorno_utc(*time); + let time: chrono::DateTime:: = time_utc.into(); let res = ui .horizontal_wrapped(|ui| { ui.label(egui::RichText::new(time.format("%H:%M").to_string()).font(FONT_ID));