From 987dccaf7695464226f98da3fdc0fc183b765a4b Mon Sep 17 00:00:00 2001 From: yukkop Date: Thu, 11 Jan 2024 01:58:21 +0100 Subject: [PATCH] feat!: no error without features --- Cargo.lock | 83 ++++++- Cargo.toml | 25 +- crate/bevy_controls/Cargo.toml | 26 ++ crate/bevy_controls/src/contract.rs | 109 +++++++++ {src => crate/bevy_controls/src}/lib.rs | 2 + crate/bevy_controls/src/plugin.rs | 245 +++++++++++++++++++ {src => crate/bevy_controls/src}/resource.rs | 150 ++++-------- crate/bevy_controls/src/util.rs | 179 ++++++++++++++ src/util.rs | 74 ------ 9 files changed, 685 insertions(+), 208 deletions(-) create mode 100644 crate/bevy_controls/Cargo.toml create mode 100644 crate/bevy_controls/src/contract.rs rename {src => crate/bevy_controls/src}/lib.rs (57%) create mode 100644 crate/bevy_controls/src/plugin.rs rename {src => crate/bevy_controls/src}/resource.rs (73%) create mode 100644 crate/bevy_controls/src/util.rs delete mode 100644 src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 039678d..6c3e75b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -447,11 +447,13 @@ name = "bevy_controls" version = "0.1.0" dependencies = [ "bevy-inspector-egui", + "bevy_app", "bevy_derive", "bevy_ecs", "bevy_input", "bevy_reflect", "bevy_utils", + "env_logger", "log", "serde", "strum", @@ -1333,6 +1335,19 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "epaint" version = "0.24.1" @@ -1363,6 +1378,16 @@ dependencies = [ "serde", ] +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "error-code" version = "2.3.1" @@ -1672,6 +1697,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "hexasphere" version = "9.1.0" @@ -1697,6 +1728,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "idna" version = "0.5.0" @@ -1754,6 +1791,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "jni" version = "0.21.1" @@ -1854,6 +1902,12 @@ dependencies = [ "redox_syscall 0.4.1", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + [[package]] name = "lock_api" version = "0.4.11" @@ -2331,9 +2385,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -2469,6 +2523,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -2492,18 +2559,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", @@ -3432,9 +3499,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.5.32" +version = "0.5.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8434aeec7b290e8da5c3f0d628cb0eac6cabcb31d14bb74f779a08109a5914d6" +checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 118e46d..087bdf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,3 @@ -[package] -name = "bevy_controls" -version = "0.1.0" -edition = "2021" - -[features] -default = [] -serialize = ["bevy_input/serialize", "serde"] -reflect = ["dep:bevy_reflect"] -inspector-egui = ["reflect", "dep:bevy-inspector-egui"] - -[dependencies] -bevy-inspector-egui = { version = "0.22.1", optional = true } -bevy_derive = "0.12.1" -bevy_ecs = "0.12.1" -bevy_input = "0.12.1" -bevy_reflect = { version = "0.12.1", optional = true } -bevy_utils = "0.12.1" -log = "0.4.20" -serde = { version = "1.0.194", optional = true } -strum = "0.25.0" -strum_macros = "0.25.3" +[workspace] +resolver = "2" +members = ["crate/*"] \ No newline at end of file diff --git a/crate/bevy_controls/Cargo.toml b/crate/bevy_controls/Cargo.toml new file mode 100644 index 0000000..2206005 --- /dev/null +++ b/crate/bevy_controls/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "bevy_controls" +version = "0.1.0" +edition = "2021" + +[features] +default = [] +serialize = ["bevy_input/serialize", "serde"] +reflect = ["dep:bevy_reflect"] +inspector-egui = ["reflect", "dep:bevy-inspector-egui"] + +[dependencies] +bevy-inspector-egui = { version = "0.22.1", optional = true } +bevy_app = "0.12.1" +bevy_derive = "0.12.1" +bevy_ecs = "0.12.1" +bevy_input = "0.12.1" +bevy_reflect = { version = "0.12.1", optional = true } +bevy_utils = "0.12.1" +log = "0.4.20" +serde = { version = "1.0.194", optional = true } +strum = "0.25.0" +strum_macros = "0.25.3" + +[dev-dependencies] +env_logger = "0.10.1" \ No newline at end of file diff --git a/crate/bevy_controls/src/contract.rs b/crate/bevy_controls/src/contract.rs new file mode 100644 index 0000000..692de83 --- /dev/null +++ b/crate/bevy_controls/src/contract.rs @@ -0,0 +1,109 @@ +use std::hash::Hash; + +use bevy_ecs::{system::Resource, schedule::States}; +use serde::{Serialize, Deserialize}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +use crate::resource::PlayerInputs; + +pub trait InputsContainer: 'static + std::marker::Sync + std::marker::Send + Resource { + /// The method returning an iterator over references to the PlayerInputs. + /// Note: the use of 'a for both the method's lifetime and the returned Iterator's lifetime. + fn iter_inputs<'a>(&'a self) -> Box> + 'a>; + /// The method returning an player inputs of this client. + fn me<'a>(&self) -> Option<&'a PlayerInputs>; + /// The method returning an mutable player inputs of this client. + fn me_mut<'a>(&self) -> Option<&'a mut PlayerInputs>; +} + +#[cfg(all(not(feature = "serialize"), not(feature = "reflect"), not(feature = "inspector-egui")))] +pub trait Action: 'static + std::marker::Sync + std::marker::Send + PartialEq + Eq + Hash + IntoEnumIterator + Clone + Copy { } + +#[cfg(all(feature = "serialize", not(feature = "reflect"), not(feature = "inspector-egui")))] +pub trait Action<'a>: 'static + std::marker::Sync + std::marker::Send + PartialEq + Eq + Hash + IntoEnumIterator + Clone + Copy + Serialize + Deserialize<'a> { } + +#[cfg(all(not(feature = "serialize"), feature = "reflect", not(feature = "inspector-egui")))] +pub trait Action: 'static + std::marker::Sync + std::marker::Send + PartialEq + Eq + Hash + IntoEnumIterator + Clone + Copy + Serialize + Deserialize + Reflect { } + + +pub trait GameState: 'static + std::marker::Sync + std::marker::Send + States { + +} + +#[cfg(test)] +mod test { + use bevy_ecs::{entity::Entity, schedule::States}; + use bevy_utils::{HashMap, prelude::default}; + use strum_macros::EnumIter; + + #[cfg(feature = "serialize")] + use serde::{Serialize, Deserialize}; + + use crate::{hashmap, resource::PlayerInputs, common_traits_conditions}; + + common_traits_conditions! { + #[derive(Debug, Clone, PartialEq)] + #[cfg_attr(feature = "reflect", reflect(Resource))] + pub struct Lobby { + pub me: PlayerId, + pub players: HashMap, + } + } + + impl Default for Lobby { + fn default() -> Self { + Self { + me: PlayerId::Host, + players: hashmap! { + PlayerId::Host => Player::default() + }, + } + } + } + + common_traits_conditions! { + #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] + pub enum PlayerId { + /// Host or alone + Host, + /// Client + Client(()), + } + + #[derive(Default, Debug, PartialEq, Clone)] + pub struct Player { + /// Client do not need to know about other clients + #[cfg_attr(feature = "serialize", serde(skip))] + pub inputs: PlayerInputs, + #[cfg_attr(feature = "serialize", serde(skip))] + pub entity: Option, + } + } + + #[derive(Debug, PartialEq, Clone, States, Default, Hash, Eq)] + pub enum GameState { + #[default] + LevelEditor, + InGame, + } + + impl super::GameState for GameState {} + + // TODO: it is just plug, it must be a trait + /// Actions that can be performed by player + #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, EnumIter)] + pub enum Action { + /// Move forward + LeverEditorForward, + /// Move backward + LevelEditorBackward, + /// Move left + LevelEditorLeft, + /// Move right + LevelEditorRight, + LevelEditorFly, + } + + impl super::Action for Action {} +} diff --git a/src/lib.rs b/crate/bevy_controls/src/lib.rs similarity index 57% rename from src/lib.rs rename to crate/bevy_controls/src/lib.rs index 30f9728..40bb6de 100644 --- a/src/lib.rs +++ b/crate/bevy_controls/src/lib.rs @@ -1,4 +1,6 @@ pub mod resource; +pub mod plugin; +pub mod contract; mod util; pub use util::*; diff --git a/crate/bevy_controls/src/plugin.rs b/crate/bevy_controls/src/plugin.rs new file mode 100644 index 0000000..ee55d41 --- /dev/null +++ b/crate/bevy_controls/src/plugin.rs @@ -0,0 +1,245 @@ +use bevy_app::{Plugin, App, Update}; +use bevy_ecs::{system::{Res, ResMut, Resource}, schedule::{State, States}}; +use bevy_input::{Input, keyboard::KeyCode, mouse::MouseButton}; + +use crate::{resource::*, contract::{Action, GameState, InputsContainer}}; + +pub struct ControlsPlugin { + _action: std::marker::PhantomData, + _inputs_container: std::marker::PhantomData, + _game_state: std::marker::PhantomData, +} + +impl< + A: Action, + Ic: InputsContainer, + Gs: GameState +> Plugin for ControlsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::>() + .add_systems(Update, Self::save_input); + + #[cfg(feature = "reflect")] + app.register_type::>() + } + +} + +impl< + A: Action, + Ic: InputsContainer, + Gs: GameState +> ControlsPlugin { + /// Process all hard inputs and bindings to update [`PlayerInputs`] + fn save_input( + keyboard_input: Res>, + mouse_input: Res>, + lobby: ResMut, + controls: Res>, + game_state: Res>, + ) { + if let Some(inputs) = lobby.me_mut() { + for (action, config) in controls.iter() { + 'bindings_loop: for binding in config.bindings.iter() { + for condition in &binding.conditions { + match condition { + BindingCondition::InGameState(state) => { + if *state != *game_state.get() { + continue 'bindings_loop; + } + } + BindingCondition::DuringPauseMenu(_value) => { + todo!(); + } + BindingCondition::ListeningForText(_value) => { + todo!(); + } + } + break; + } + + match &binding.input { + ButtonCombination::Single(button) => match button { + InputType::Keyboard(key) => { + inputs.forced_set(*action, keyboard_input.pressed(*key)); + } + InputType::Mouse(input) => { + match input { + MouseInput::Axis(_axis) => { + todo!(); + } + MouseInput::Button(button) => { + if !mouse_input + .get_pressed() + .any(|b| b == button) + { + continue 'bindings_loop; + } + } + MouseInput::Wheel(_axis) => { + todo!(); + } + } + } + }, + ButtonCombination::Chord(buttons) => { + // If any button in chord is not pressed we skip this `binding` + for button in buttons { + match button { + InputType::Keyboard(key) => { + if !keyboard_input.pressed(*key) { + continue 'bindings_loop; + } + } + InputType::Mouse(input) => { + match input { + MouseInput::Axis(_axis) => { + todo!(); + } + MouseInput::Button(button) => { + if !mouse_input + .get_pressed() + .any(|b| b == button) + { + continue 'bindings_loop; + } + } + MouseInput::Wheel(_axis) => { + todo!(); + } + } + } + } + } + + // TODO: should be [`Chord`](ButtonCombination::Chord) only [`Boolean`](InputValue::Boolean) type + inputs.forced_set(*action, true); + } + } + } + } + } else { + log::error!("You like [`Player`] is not in lobby") + } + } +} + + +// #[cfg(test)] +// mod performance_test { +// use crate::{ util::test::{enable_loggings, measure_time, Times}, resource::PlayerInputs}; + +// use super::Controls; +// use std::time::Duration; + +// /// Test for execution time for [`Controls`] get +// /// +// /// Example: +// /// ``` +// /// cargo test --package pih-pah-app --lib --features "dev, ui_egui" -- controls::test::controls_get --exact --nocapture +// /// ``` +// #[test] +// fn controls_get() { +// enable_loggings(); + +// let controls = Controls::default(); + +// let duration = measure_time( +// || { +// controls.get(Action::LeverEditorForward); +// controls.get(Action::LevelEditorBackward); +// controls.get(Action::LevelEditorLeft); +// controls.get(Action::LevelEditorRight); +// }, +// Times::default(), +// ); + +// log::info!("time: {:?}", duration); +// } + +// /// Test for execution speed for [`PlayerInputs`] get +// fn player_inputs_get() -> Duration { +// enable_loggings(); + +// let inputs = PlayerInputs::default(); + +// let duration = measure_time( +// || { +// inputs.get(Action::LeverEditorForward); +// inputs.get(Action::LevelEditorBackward); +// inputs.get(Action::LevelEditorLeft); +// inputs.get(Action::LevelEditorRight); +// }, +// Times::default(), +// ); + +// duration +// } + +// /// Test for execution speed for [`PlayerInputs`] get_many +// fn player_inputs_get_many() -> Duration { +// enable_loggings(); + +// let inputs = PlayerInputs::default(); + +// let duration = measure_time( +// || { +// inputs.get_many(vec![ +// Action::LeverEditorForward, +// Action::LevelEditorBackward, +// Action::LevelEditorLeft, +// Action::LevelEditorRight, +// ]); +// }, +// Times::default(), +// ); + +// duration +// } + +// /// Test for execution speed for [`PlayerInputs`] get and get_many +// /// +// /// Example: +// /// ``` +// /// cargo test --package pih-pah-app --lib --features "dev, ui_egui" -- controls::test::compare_player_inputs_get_and_get_many --exact --nocapture +// /// ``` +// #[test] +// fn compare_player_inputs_get_and_get_many() { +// let get = player_inputs_get(); +// let get_many = player_inputs_get_many(); + +// log::info!("get: {:?}", get); +// log::info!("get_many: {:?}", get_many); +// } + +// /// Test for execution speed for [`InputValue`] casting +// #[test] +// fn action_casting() { +// enable_loggings(); + +// let inputs = PlayerInputs::default(); + +// let duration = measure_time( +// || { +// let _ = (inputs.get(Action::LeverEditorForward).as_boolean() as i8 +// - inputs.get(Action::LevelEditorBackward).as_boolean() as i8) +// as f32; +// }, +// 10000000.into(), +// ); + +// log::info!("with casting: {:?}", duration); + +// let inputs = PlayerInputs::default(); + +// let duration = measure_time( +// || { +// let _ = inputs.get(Action::LeverEditorForward); +// let _ = inputs.get(Action::LevelEditorBackward); +// }, +// 10000000.into(), +// ); + +// log::info!("without casting: {:?}", duration); +// } +// } \ No newline at end of file diff --git a/src/resource.rs b/crate/bevy_controls/src/resource.rs similarity index 73% rename from src/resource.rs rename to crate/bevy_controls/src/resource.rs index a2afb32..bb9fec9 100644 --- a/src/resource.rs +++ b/crate/bevy_controls/src/resource.rs @@ -17,7 +17,7 @@ use strum_macros::EnumIter; #[cfg(feature = "reflect")] use {bevy_ecs::prelude::ReflectResource, bevy_reflect::Reflect}; -use crate::{common_traits_conditions, hashmap}; +use crate::{common_traits_conditions, hashmap, contract::{Action, GameState}}; /// Validate that all enum variants are in the hash map pub fn validate_hash_map(hash_map: &HashMap) -> bool @@ -42,26 +42,26 @@ where common_traits_conditions! { /// Struct that contains user's inputs corresponding to actions #[derive(Debug, PartialEq, Clone, Deref, DerefMut)] - pub struct Inputs(HashMap); + pub struct Inputs(HashMap); /// Resource that contains current user inputs #[derive(Debug, PartialEq, Clone)] - pub struct PlayerInputs { - current: Inputs, - previous: Inputs, + pub struct PlayerInputs { + current: Inputs, + previous: Inputs, } } -impl Default for PlayerInputs { +impl Default for PlayerInputs { fn default() -> Self { PlayerInputs { current: Inputs( - Action::iter() + A::iter() .map(|action| (action, InputValue::Empty)) .collect(), ), previous: Inputs( - Action::iter() + A::iter() .map(|action| (action, InputValue::Empty)) .collect(), ), @@ -69,18 +69,18 @@ impl Default for PlayerInputs { } } -impl PlayerInputs { +impl PlayerInputs { /// Returns current input value for action /// /// in any case faster that `get_many` method - pub fn get(&self, action: Action) -> InputValue { + pub fn get(&self, action: A) -> InputValue { // SAFETY: action is always valid // because we iterate over all actions in `default` method *self.current.get(&action).unwrap() } /// Returns current input values for actions - pub fn get_many(&self, actions: Vec) -> Vec { + pub fn get_many(&self, actions: Vec) -> Vec { // SAFETY: action is always valid // because we iterate over all actions in `default` method actions @@ -91,7 +91,7 @@ impl PlayerInputs { /// Returns `true` if current `InputValue` has become `InputValue::Boolean(true)` in this frame /// If input has not same type as `InputValue` on previous frame call `panic` - pub fn just_pressed(&self, action: Action) -> bool { + pub fn just_pressed(&self, action: A) -> bool { // SAFETY: action is always valid // because we iterate over all actions in `default` method if let InputValue::Boolean(current) = *self.current.get(&action).unwrap() { @@ -105,7 +105,7 @@ impl PlayerInputs { /// Returns `true` if current `InputValue` has become `InputValue::Boolean(true)` in this frame /// If input has not same type as `InputValue` on previous frame return `Error()` - pub fn get_just_pressed(&self, action: Action) -> Result> { + pub fn get_just_pressed(&self, action: A) -> Result> { // SAFETY: action is always valid // because we iterate over all actions in `default` method if let InputValue::Boolean(current) = *self.current.get(&action).unwrap() { @@ -118,7 +118,7 @@ impl PlayerInputs { } /// Set input value to new - pub fn forced_set(&mut self, action: Action, value: impl Into) { + pub fn forced_set(&mut self, action: A, value: impl Into) { // SAFETY: action is always valid // because we iterate over all actions in `default` method let current_input = self.current.get_mut(&action).unwrap(); @@ -130,7 +130,7 @@ impl PlayerInputs { /// if it is not the same InputValue type return `Error()` pub fn set( &mut self, - action: Action, + action: A, value: impl Into, ) -> Result<(), Box> { // SAFETY: action is always valid @@ -149,7 +149,7 @@ impl PlayerInputs { } /// Update current inputs - pub fn forced_update(&mut self, inputs: Inputs) { + pub fn forced_update(&mut self, inputs: Inputs) { *self.previous = self.current.clone().0; *self.current = inputs.0; } @@ -157,7 +157,7 @@ impl PlayerInputs { /// Update current inputs /// if any input is not the same InputValue type return `Error()` /// ! remember that it work safely but slow - pub fn update(&mut self, inputs: Inputs) -> Result<(), Box> { + pub fn update(&mut self, inputs: Inputs) -> Result<(), Box> { // SAFETY: action is always valid // because we iterate over all actions in `default` method for (action, value) in inputs.0.iter() { @@ -176,13 +176,6 @@ impl PlayerInputs { } common_traits_conditions! { - // TODO: it must be a trait - #[derive(Debug, PartialEq, Clone)] - pub enum GameState { - LevelEditor, - InGame, - } - #[derive(Debug, PartialEq, Clone)] pub enum InputType { Keyboard(KeyCode), @@ -274,39 +267,24 @@ common_traits_conditions! { } #[derive(Debug, PartialEq, Clone)] - pub enum BindingCondition { + pub enum BindingCondition { /// During specific game state - InGameState(GameState), + InGameState(Gs), /// Binding is active only if player is in pause menu DuringPauseMenu(bool), /// Binding is active only if player types text ListeningForText(bool), } - // TODO: it is just plug, it must be a trait - /// Actions that can be performed by player - #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, EnumIter)] - pub enum Action { - /// Move forward - LeverEditorForward, - /// Move backward - LevelEditorBackward, - /// Move left - LevelEditorLeft, - /// Move right - LevelEditorRight, - LevelEditorFly, - } - /// Binding is a combination of buttons that triggers action #[derive(Debug, PartialEq, Clone)] - pub struct Binding { + pub struct Binding { pub input: ButtonCombination, - pub conditions: Vec, + pub conditions: Vec>, } } -impl From for Binding { +impl From for Binding { fn from(input: ButtonCombination) -> Self { Binding { input, @@ -315,7 +293,7 @@ impl From for Binding { } } -impl Binding { +impl Binding { pub fn new(input: ButtonCombination) -> Self { Self { input, @@ -323,7 +301,7 @@ impl Binding { } } - pub fn with_condition(mut self, condition: BindingCondition) -> Self { + pub fn with_condition(mut self, condition: BindingCondition) -> Self { self.conditions.push(condition); self } @@ -393,15 +371,15 @@ impl InputValue { } common_traits_conditions! { - pub struct Bindings { + pub struct Bindings { /// List of bindings for action - list: Vec, + list: Vec>, /// Possibility to change this binding options: OptionsMode, } } -impl Default for Bindings { +impl Default for Bindings { fn default() -> Self { Self { list: Vec::new(), @@ -410,15 +388,15 @@ impl Default for Bindings { } } -impl Bindings { - pub fn new(bindings: Vec, options: OptionsMode) -> Self { +impl Bindings { + pub fn new(bindings: Vec>, options: OptionsMode) -> Self { Self { list: bindings, options, } } - pub fn push(&mut self, binding: Binding) { + pub fn push(&mut self, binding: Binding) { if self.options == OptionsMode::Immutable { warn!("You try to push binding to immutable bindings"); return; @@ -436,7 +414,7 @@ impl Bindings { pub fn retain(&mut self, f: F) where - F: FnMut(&Binding) -> bool, + F: FnMut(&Binding) -> bool, { if self.options == OptionsMode::Immutable { warn!("You try to retain immutable bindings"); @@ -445,13 +423,13 @@ impl Bindings { self.list.retain(f); } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator> { self.list.iter() } pub fn iter_mut( &mut self, - ) -> Result, Box> { + ) -> Result>, Box> { if self.options == OptionsMode::Immutable { warn!("You try to iterate over immutable bindings"); return Err("You try to iterate over immutable bindings".into()); @@ -472,15 +450,15 @@ common_traits_conditions! { /// Contains all bindings for action /// and different activation options #[derive(Default)] - pub struct BindingConfig { - pub bindings: Bindings, + pub struct BindingConfig { + pub bindings: Bindings, #[cfg_attr(feature = "serialize", serde(skip))] pub activation: ActivationOptions, } } -impl BindingConfig { - pub fn new(bindings: Bindings, activation: ActivationOptions) -> Self { +impl BindingConfig { + pub fn new(bindings: Bindings, activation: ActivationOptions) -> Self { Self { bindings, activation, @@ -493,49 +471,13 @@ common_traits_conditions! { #[derive(Resource)] #[cfg_attr(feature = "reflect", reflect(Resource))] // TODO: Add delay for input - pub struct Controls(HashMap); // TODO: change HashMap to Vec for faster iteration + pub struct Controls(HashMap>); // TODO: change HashMap to Vec for faster iteration } -impl Default for Controls { +impl Default for Controls { fn default() -> Self { // TODO: default must create empty controls or fill empty / default BindingConfig on any actions - let controls = Self(hashmap! { - Action::LeverEditorForward => BindingConfig::new( - Bindings::new( - vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::W))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))], - OptionsMode::Customizable, - ), - ActivationOptions::new(ActivationMode::Hold, OptionsMode::Immutable), - ), - Action::LevelEditorBackward => BindingConfig::new( - Bindings::new( - vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::S))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))], - OptionsMode::Customizable, - ), - ActivationOptions::new(ActivationMode::Hold, OptionsMode::Immutable), - ), - Action::LevelEditorLeft => BindingConfig::new( - Bindings::new( - vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::A))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))], - OptionsMode::Customizable, - ), - ActivationOptions::new(ActivationMode::Hold, OptionsMode::Immutable), - ), - Action::LevelEditorRight => BindingConfig::new( - Bindings::new( - vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::D))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))], - OptionsMode::Customizable, - ), - ActivationOptions::new(ActivationMode::Hold, OptionsMode::Immutable), - ), - Action::LevelEditorFly => BindingConfig::new( - Bindings::new( - vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::Space))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))], - OptionsMode::Customizable, - ), - ActivationOptions::new(ActivationMode::Hold, OptionsMode::Customizable), - ) - }); + let controls = Self(HashMap::new()); // If you see this error, you may add new action in menu_actions // or make sure that you have only one MenuAction with the same name in the MenuActions @@ -545,14 +487,14 @@ impl Default for Controls { } } -impl Controls { +impl Controls { /// Returns bindings for action - pub fn get(&self, action: Action) -> Option<&BindingConfig> { + pub fn get(&self, action: A) -> Option<&BindingConfig> { self.0.get(&action) } /// Push new binding for action - pub fn push(&mut self, action: Action, binding: Binding) { + pub fn push(&mut self, action: A, binding: Binding) { // TODO: warning if config is not exist yet self.0 .entry(action) @@ -562,7 +504,7 @@ impl Controls { } /// Remove all bindings for action - pub fn remove(&mut self, action: Action) { + pub fn remove(&mut self, action: A) { // TODO: warning if config is not exist yet self.0 .entry(action) @@ -571,13 +513,13 @@ impl Controls { .clear(); } - pub fn remove_binding(&mut self, action: Action, binding: Binding) { + pub fn remove_binding(&mut self, action: A, binding: Binding) { if let Some(config) = self.0.get_mut(&action) { config.bindings.retain(|b| *b == binding); } } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator)> { self.0.iter() } } diff --git a/crate/bevy_controls/src/util.rs b/crate/bevy_controls/src/util.rs new file mode 100644 index 0000000..01a06df --- /dev/null +++ b/crate/bevy_controls/src/util.rs @@ -0,0 +1,179 @@ +/// Creates a [`HashMap`](bevy_utils::HashMap) from a list of key-value pairs. +/// +/// Example: +/// ```ignore +/// let map = hashmap! { +/// "key1" => "value1", +/// "key2" => "value2" +/// }; +#[macro_export] +macro_rules! hashmap { + ($( $key: expr => $val: expr ),*) => {{ + let mut map = HashMap::new(); + $( + map.insert($key, $val); + )* + map + }}; +} + +/// Applies common traits conditionally to items. +/// +/// This macro is designed to reduce repetition and maintenance overhead by +/// automatically deriving specified traits for each item passed to it based +/// on the feature flags. It's intended for use with almost all types in this module. +/// +/// # Features: +/// - `serialize`: Derives `Serialize` and `Deserialize` traits and applies +/// reflection for serialization if the `serialize` feature is enabled. +/// - `reflect`: Derives the `Reflect` trait if the `reflect` feature is enabled. +/// - `inspector-egui`: Derives `InspectorOptions` trait and applies reflection +/// for the inspector if the `inspector-egui` feature is enabled. +/// +/// # Usage +/// To use this macro, wrap any item(s) you want to apply the common traits to. +/// Each item can be a struct or enum that you want to derive traits for based +/// on the feature flags. +/// +/// # Example: +/// ```ignore +/// common_traits_conditions! { +/// struct MyStruct { +/// // fields +/// } +/// +/// enum MyEnum { +/// // variants +/// } +/// } +/// ``` +#[macro_export] +macro_rules! common_traits_conditions { + ($($item:item)*) => { + $( + #[cfg_attr( + feature = "serialize", + derive(Serialize, Deserialize), + )] + #[cfg_attr( + feature = "reflect", + derive(Reflect), + )] + #[cfg_attr( + all(feature = "reflect", feature = "serialize"), + reflect(Serialize, Deserialize), + )] + #[cfg_attr( + feature = "inspector-egui", + derive(InspectorOptions), + reflect(InspectorOptions), + )] + $item + )* + }; +} + +#[cfg(test)] +pub mod test { + use std::time::{Duration, Instant}; + + use bevy_derive::{Deref, DerefMut}; + use log::Level; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deref, DerefMut)] + pub struct Times(u64); + + impl Into for Times { + fn into(self) -> u64 { + self.0 + } + } + + impl Into for Times { + fn into(self) -> usize { + self.0 as usize + } + } + + impl Into for Times { + fn into(self) -> u32 { + self.0 as u32 + } + } + + impl Into for Times { + fn into(self) -> i32 { + self.0 as i32 + } + } + + impl From for Times { + fn from(times: u64) -> Self { + Self(times) + } + } + + impl From for Times { + fn from(times: usize) -> Self { + Self(times as u64) + } + } + + impl From for Times { + fn from(times: u32) -> Self { + Self(times as u64) + } + } + + impl From for Times { + fn from(times: i32) -> Self { + Self(times as u64) + } + } + + impl Default for Times { + /// Value that may be enough for most cases + fn default() -> Self { + Self(100000) + } + } + + /// Enable logging for debug + pub fn enable_loggings() { + use std::env; + use std::io::Write; + + let _ = env::set_var("RUST_LOG", "debug"); + // FIXME: colorize logs + // TODO: colorize thorwed args + let _ = env_logger::builder() + .is_test(true) + .format(|buf, record| { + let mut style = buf.style(); + let level = record.level(); + match level { + Level::Trace => style.set_color(env_logger::fmt::Color::Magenta), + Level::Debug => style.set_color(env_logger::fmt::Color::Blue), + Level::Info => style.set_color(env_logger::fmt::Color::Green), + Level::Warn => style.set_color(env_logger::fmt::Color::Yellow), + Level::Error => style.set_color(env_logger::fmt::Color::Red), + }; + + writeln!(buf, "{}: {}", style.value(level), record.args()) + }) + .try_init(); + } + + /// Measure time of predicate + pub fn measure_time(predicate: F, times: Times) -> Duration + where + F: FnOnce() -> (), + { + let start = Instant::now(); + for _ in 0..times.clone().into() { + predicate(); + } + let global_duration = start.elapsed(); + global_duration / times.into() + } +} \ No newline at end of file diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index b1d2f4a..0000000 --- a/src/util.rs +++ /dev/null @@ -1,74 +0,0 @@ -/// Creates a [`HashMap`](bevy_utils::HashMap) from a list of key-value pairs. -/// -/// Example: -/// ```ignore -/// let map = hashmap! { -/// "key1" => "value1", -/// "key2" => "value2" -/// }; -#[macro_export] -macro_rules! hashmap { - ($( $key: expr => $val: expr ),*) => {{ - let mut map = HashMap::new(); - $( - map.insert($key, $val); - )* - map - }}; -} - -/// Applies common traits conditionally to items. -/// -/// This macro is designed to reduce repetition and maintenance overhead by -/// automatically deriving specified traits for each item passed to it based -/// on the feature flags. It's intended for use with almost all types in this module. -/// -/// # Features: -/// - `serialize`: Derives `Serialize` and `Deserialize` traits and applies -/// reflection for serialization if the `serialize` feature is enabled. -/// - `reflect`: Derives the `Reflect` trait if the `reflect` feature is enabled. -/// - `inspector-egui`: Derives `InspectorOptions` trait and applies reflection -/// for the inspector if the `inspector-egui` feature is enabled. -/// -/// # Usage -/// To use this macro, wrap any item(s) you want to apply the common traits to. -/// Each item can be a struct or enum that you want to derive traits for based -/// on the feature flags. -/// -/// # Example: -/// ```ignore -/// common_traits_conditions! { -/// struct MyStruct { -/// // fields -/// } -/// -/// enum MyEnum { -/// // variants -/// } -/// } -/// ``` -#[macro_export] -macro_rules! common_traits_conditions { - ($($item:item)*) => { - $( - #[cfg_attr( - feature = "serialize", - derive(Serialize, Deserialize), - )] - #[cfg_attr( - feature = "reflect", - derive(Reflect), - )] - #[cfg_attr( - all(feature = "reflect", feature = "serialize"), - reflect(Serialize, Deserialize), - )] - #[cfg_attr( - feature = "inspector-egui", - derive(InspectorOptions), - reflect(InspectorOptions), - )] - $item - )* - }; -}