From 65f87df1e06baf002db7608158e56a5b4e593c55 Mon Sep 17 00:00:00 2001 From: asimon-1 <40246417+asimon-1@users.noreply.github.com> Date: Sat, 2 Dec 2023 12:02:43 -0500 Subject: [PATCH] Weighted Probability Menu Selections + TUI Backend Redesign (#655) * Add replacement training_mod_tui crate * Begin work on converting over to byteflags * Allow StatefulTable.iter_mut() * Additional merge work * Fixing more compile errors from the merge * Replace training_mod_tui with training_mod_tui_2, update byteflags dependency for random selection * Fix button_config, hero buffs, caps issues. Move button logic into App * Restore some functions that I cut too agressively * Move to_vec into byteflags crate * Fix src/training/ui * Set `_` match arms on byteflags to `unreachable!()` * Fix final few compiler errors * Run formatter * Reconsider whether unreachable parts are actually unreachable * Adjust logging and remove dead code * Fix some menu bugs * Adjust icon sizes * Separate checkmark from is_single_option * Allow fast increment / decrement of slider handles with up/down * Allow resetting current menu * Change button behavior: `R/Z` now resets either the whole menu or the selected submenu depending on app.page, and `Y` now clears the current toggle so you don't have to press A six times. Only show keyhelp for `Y` if its relevant. * Remove unneeded command line interface * Take care of some minor nomenclature TODO's * Increase stack size for menu::load_from_file() to avoid crashes * Implement confirmation page before restoring from defaults * Run rustfmt * Fix warnings --- Cargo.toml | 3 +- src/common/button_config.rs | 14 +- src/common/input.rs | 17 +- src/common/menu.rs | 65 +- src/common/release.rs | 16 +- src/hazard_manager/mod.rs | 2 +- src/hitbox_visualizer/mod.rs | 8 +- src/lib.rs | 15 +- src/static/layout.arc | Bin 3823684 -> 4204916 bytes src/training/buff.rs | 19 +- src/training/character_specific/items.rs | 6 +- src/training/character_specific/pikmin.rs | 3 +- src/training/combo.rs | 2 +- src/training/crouch.rs | 2 +- src/training/input_log.rs | 728 +++--- src/training/input_record.rs | 26 +- src/training/ledge.rs | 2 +- src/training/mash.rs | 31 +- src/training/mod.rs | 2 +- src/training/save_states.rs | 41 +- src/training/shield.rs | 20 +- src/training/tech.rs | 14 +- src/training/ui/input_log.rs | 4 +- src/training/ui/menu.rs | 521 +++-- src/training/ui/mod.rs | 2 +- training_mod_consts/Cargo.toml | 7 +- training_mod_consts/src/lib.rs | 1537 ++++++------ training_mod_consts/src/options.rs | 2064 ++++++----------- training_mod_tui/Cargo.toml | 16 +- training_mod_tui/src/containers/app.rs | 390 ++++ training_mod_tui/src/containers/mod.rs | 24 + training_mod_tui/src/containers/submenu.rs | 168 ++ training_mod_tui/src/containers/tab.rs | 54 + training_mod_tui/src/containers/toggle.rs | 36 + training_mod_tui/src/gauge.rs | 27 - training_mod_tui/src/lib.rs | 916 +------- training_mod_tui/src/list.rs | 209 -- training_mod_tui/src/main.rs | 360 --- training_mod_tui/src/structures/mod.rs | 6 + .../src/structures/stateful_list.rs | 124 + .../src/structures/stateful_slider.rs | 146 ++ .../src/structures/stateful_table.rs | 274 +++ training_mod_tui/tests/test_stateful_list.rs | 182 ++ .../tests/test_stateful_slider.rs | 288 +++ training_mod_tui/tests/test_stateful_table.rs | 342 +++ training_mod_tui/tests/test_submenu.rs | 502 ++++ training_mod_tui/tests/test_toggle.rs | 44 + training_mod_tui/training_mod_tui.iml | 12 - 48 files changed, 4922 insertions(+), 4369 deletions(-) create mode 100644 training_mod_tui/src/containers/app.rs create mode 100644 training_mod_tui/src/containers/mod.rs create mode 100644 training_mod_tui/src/containers/submenu.rs create mode 100644 training_mod_tui/src/containers/tab.rs create mode 100644 training_mod_tui/src/containers/toggle.rs delete mode 100644 training_mod_tui/src/gauge.rs delete mode 100644 training_mod_tui/src/list.rs delete mode 100644 training_mod_tui/src/main.rs create mode 100644 training_mod_tui/src/structures/mod.rs create mode 100644 training_mod_tui/src/structures/stateful_list.rs create mode 100644 training_mod_tui/src/structures/stateful_slider.rs create mode 100644 training_mod_tui/src/structures/stateful_table.rs create mode 100644 training_mod_tui/tests/test_stateful_list.rs create mode 100644 training_mod_tui/tests/test_stateful_slider.rs create mode 100644 training_mod_tui/tests/test_stateful_table.rs create mode 100644 training_mod_tui/tests/test_submenu.rs create mode 100644 training_mod_tui/tests/test_toggle.rs delete mode 100644 training_mod_tui/training_mod_tui.iml diff --git a/Cargo.toml b/Cargo.toml index 76573fb..bbf6795 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,10 +41,11 @@ byte-unit = "4.0.18" zip = { version = "0.6", default-features = false, features = ["deflate"] } anyhow = "1.0.72" - [patch.crates-io] native-tls = { git = "https://github.com/skyline-rs/rust-native-tls", branch = "switch-timeout-panic" } nnsdk = { git = "https://github.com/ultimate-research/nnsdk-rs" } +rand = { git = "https://github.com/skyline-rs/rand" } +getrandom = { git = "https://github.com/Raytwo/getrandom" } [profile.dev] panic = "abort" diff --git a/src/common/button_config.rs b/src/common/button_config.rs index f188abc..c92a960 100644 --- a/src/common/button_config.rs +++ b/src/common/button_config.rs @@ -45,7 +45,7 @@ pub fn button_mapping( ButtonConfig::MINUS => b.minus(), ButtonConfig::LSTICK => b.stick_l(), ButtonConfig::RSTICK => b.stick_r(), - _ => false, + _ => panic!("Invalid value in button_mapping: {}", button_config), } } @@ -166,7 +166,11 @@ pub enum ButtonCombo { InputPlayback, } -pub const DEFAULT_OPEN_MENU_CONFIG: ButtonConfig = ButtonConfig::B.union(ButtonConfig::DPAD_UP); +pub const DEFAULT_OPEN_MENU_CONFIG: ButtonConfig = ButtonConfig { + B: 1, + DPAD_UP: 1, + ..ButtonConfig::empty() +}; unsafe fn get_combo_keys(combo: ButtonCombo) -> ButtonConfig { match combo { @@ -196,14 +200,14 @@ fn _combo_passes(p1_controller: Controller, combo: ButtonCombo) -> bool { let combo_keys = get_combo_keys(combo).to_vec(); let mut this_combo_passes = false; - for hold_button in &combo_keys[..] { + for hold_button in combo_keys.iter() { if button_mapping( *hold_button, p1_controller.style, p1_controller.current_buttons, ) && combo_keys .iter() - .filter(|press_button| **press_button != *hold_button) + .filter(|press_button| press_button != &hold_button) .all(|press_button| { button_mapping(*press_button, p1_controller.style, p1_controller.just_down) }) @@ -238,7 +242,7 @@ pub fn handle_final_input_mapping(player_idx: i32, controller_struct: &mut SomeC let mut start_menu_request = false; let menu_close_wait_frame = frame_counter::get_frame_count(*menu::MENU_CLOSE_FRAME_COUNTER); - if unsafe { MENU.menu_open_start_press == OnOff::On } { + if unsafe { MENU.menu_open_start_press == OnOff::ON } { let start_hold_frames = &mut *START_HOLD_FRAMES.lock(); if p1_controller.current_buttons.plus() { *start_hold_frames += 1; diff --git a/src/common/input.rs b/src/common/input.rs index 92a916b..8335a6b 100644 --- a/src/common/input.rs +++ b/src/common/input.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] // TODO: Yeah don't do this -use crate::extra_bitflag_impls; use bitflags::bitflags; use modular_bitfield::{bitfield, specifiers::*}; @@ -215,14 +213,25 @@ bitflags! { } // This requires some imports to work -use training_mod_consts::{random_option, ToggleTrait}; impl std::fmt::Display for Buttons { fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { todo!() } } -extra_bitflag_impls!(Buttons); +impl Buttons { + pub fn to_vec(&self) -> Vec { + // Reimplemented for bitflags + let mut vec = Vec::::new(); + let mut field = Buttons::from_bits_truncate(self.bits); + while !field.is_empty() { + let flag = Buttons::from_bits(1u32 << field.bits.trailing_zeros()).unwrap(); + field -= flag; + vec.push(flag); + } + vec + } +} // Controller class used internally by the game #[derive(Debug, Default, Copy, Clone)] diff --git a/src/common/menu.rs b/src/common/menu.rs index 0ed2a0c..340f7f7 100644 --- a/src/common/menu.rs +++ b/src/common/menu.rs @@ -1,11 +1,12 @@ use once_cell::sync::Lazy; use std::collections::HashMap; use std::fs; +use std::io::BufReader; use lazy_static::lazy_static; use parking_lot::Mutex; use skyline::nn::hid::GetNpadStyleSet; -use training_mod_consts::MenuJsonStruct; +use training_mod_consts::{create_app, MenuJsonStruct}; use training_mod_tui::AppPage; use crate::common::button_config::button_mapping; @@ -24,11 +25,15 @@ pub unsafe fn menu_condition() -> bool { } pub fn load_from_file() { + // Note that this function requires a larger stack size + // With the switch default, it'll crash w/o a helpful error message info!("Checking for previous menu in {MENU_OPTIONS_PATH}..."); + let err_msg = format!("Could not read {}", MENU_OPTIONS_PATH); if fs::metadata(MENU_OPTIONS_PATH).is_ok() { - let menu_conf = fs::read_to_string(MENU_OPTIONS_PATH) - .unwrap_or_else(|_| panic!("Could not remove {}", MENU_OPTIONS_PATH)); - if let Ok(menu_conf_json) = serde_json::from_str::(&menu_conf) { + let menu_conf = fs::File::open(MENU_OPTIONS_PATH).expect(&err_msg); + let reader = BufReader::new(menu_conf); + if let Ok(menu_conf_json) = serde_json::from_reader::, MenuJsonStruct>(reader) + { unsafe { MENU = menu_conf_json.menu; DEFAULTS_MENU = menu_conf_json.defaults_menu; @@ -36,16 +41,22 @@ pub fn load_from_file() { } } else { warn!("Previous menu found but is invalid. Deleting..."); - fs::remove_file(MENU_OPTIONS_PATH).unwrap_or_else(|_| { - panic!( - "{} has invalid schema but could not be deleted!", - MENU_OPTIONS_PATH - ) - }); + let err_msg = format!( + "{} has invalid schema but could not be deleted!", + MENU_OPTIONS_PATH + ); + fs::remove_file(MENU_OPTIONS_PATH).expect(&err_msg); } } else { info!("No previous menu file found."); } + info!("Setting initial menu selections..."); + unsafe { + let mut app = QUICK_MENU_APP.lock(); + app.serialized_default_settings = + serde_json::to_string(&DEFAULTS_MENU).expect("Could not serialize DEFAULTS_MENU"); + app.update_all_from_json(&serde_json::to_string(&MENU).expect("Could not serialize MENU")); + } } pub unsafe fn set_menu_from_json(message: &str) { @@ -72,6 +83,8 @@ pub unsafe fn set_menu_from_json(message: &str) { pub fn spawn_menu() { unsafe { QUICK_MENU_ACTIVE = true; + let mut app = QUICK_MENU_APP.lock(); + app.page = AppPage::SUBMENU; *MENU_RECEIVED_INPUT.data_ptr() = true; } } @@ -89,14 +102,10 @@ enum DirectionButton { } lazy_static! { - pub static ref QUICK_MENU_APP: Mutex = Mutex::new( - training_mod_tui::App::new(unsafe { ui_menu(MENU) }, unsafe { - ( - ui_menu(DEFAULTS_MENU), - serde_json::to_string(&DEFAULTS_MENU).unwrap(), - ) - }) - ); + pub static ref QUICK_MENU_APP: Mutex> = Mutex::new({ + info!("Initialized lazy_static: QUICK_MENU_APP"); + unsafe { create_app() } + }); pub static ref P1_CONTROLLER_STYLE: Mutex = Mutex::new(ControllerStyle::default()); static ref DIRECTION_HOLD_FRAMES: Mutex> = { @@ -185,43 +194,42 @@ pub fn handle_final_input_mapping( } }); - let app = &mut *QUICK_MENU_APP.data_ptr(); + let app = &mut *QUICK_MENU_APP.data_ptr(); // TODO: Why aren't we taking a lock here? button_mapping(ButtonConfig::A, style, button_presses).then(|| { app.on_a(); received_input = true; }); button_mapping(ButtonConfig::B, style, button_presses).then(|| { received_input = true; - if app.page != AppPage::SUBMENU { - app.on_b() - } else { + app.on_b(); + if app.page == AppPage::CLOSE { // Leave menu. frame_counter::start_counting(*MENU_CLOSE_FRAME_COUNTER); QUICK_MENU_ACTIVE = false; - let menu_json = app.get_menu_selections(); + let menu_json = app.get_serialized_settings_with_defaults(); set_menu_from_json(&menu_json); EVENT_QUEUE.push(Event::menu_open(menu_json)); } }); button_mapping(ButtonConfig::X, style, button_presses).then(|| { - app.save_defaults(); + app.on_x(); received_input = true; }); button_mapping(ButtonConfig::Y, style, button_presses).then(|| { - app.reset_all_submenus(); + app.on_y(); received_input = true; }); button_mapping(ButtonConfig::ZL, style, button_presses).then(|| { - app.previous_tab(); + app.on_zl(); received_input = true; }); button_mapping(ButtonConfig::ZR, style, button_presses).then(|| { - app.next_tab(); + app.on_zr(); received_input = true; }); button_mapping(ButtonConfig::R, style, button_presses).then(|| { - app.reset_current_submenu(); + app.on_r(); received_input = true; }); @@ -263,7 +271,6 @@ pub fn handle_final_input_mapping( if received_input { direction_hold_frames.iter_mut().for_each(|(_, f)| *f = 0); - set_menu_from_json(&app.get_menu_selections()); *MENU_RECEIVED_INPUT.lock() = true; } } diff --git a/src/common/release.rs b/src/common/release.rs index efe71e5..7a7b1d8 100644 --- a/src/common/release.rs +++ b/src/common/release.rs @@ -10,9 +10,12 @@ use serde_json::Value; use zip::ZipArchive; lazy_static! { - pub static ref CURRENT_VERSION: Mutex = Mutex::new(match get_current_version() { - Ok(v) => v, - Err(e) => panic!("Could not find current modpack version!: {}", e), + pub static ref CURRENT_VERSION: Mutex = Mutex::new({ + info!("Initialized lazy_static: CURRENT_VERSION"); + match get_current_version() { + Ok(v) => v, + Err(e) => panic!("Could not find current modpack version!: {}", e), + } }); } @@ -178,12 +181,13 @@ pub fn perform_version_check() { let update_policy = get_update_policy(); info!("Update Policy is {}", update_policy); let mut release_to_apply = match update_policy { - UpdatePolicy::Stable => get_release(false), - UpdatePolicy::Beta => get_release(true), - UpdatePolicy::Disabled => { + UpdatePolicy::STABLE => get_release(false), + UpdatePolicy::BETA => get_release(true), + UpdatePolicy::DISABLED => { // User does not want to update at all Err(anyhow!("Updates are disabled per UpdatePolicy")) } + _ => panic!("Invalid value in perform_version_check: {}", update_policy), }; if release_to_apply.is_ok() { let published_at = release_to_apply.as_ref().unwrap().published_at.clone(); diff --git a/src/hazard_manager/mod.rs b/src/hazard_manager/mod.rs index a5643ee..05fbc0a 100644 --- a/src/hazard_manager/mod.rs +++ b/src/hazard_manager/mod.rs @@ -96,7 +96,7 @@ fn hazard_intercept(ctx: &skyline::hooks::InlineCtx) { fn mod_handle_hazards() { unsafe { - *HAZARD_FLAG_ADDRESS = (MENU.stage_hazards == OnOff::On) as u8; + *HAZARD_FLAG_ADDRESS = (MENU.stage_hazards == OnOff::ON) as u8; } } diff --git a/src/hitbox_visualizer/mod.rs b/src/hitbox_visualizer/mod.rs index eaa445a..c734941 100644 --- a/src/hitbox_visualizer/mod.rs +++ b/src/hitbox_visualizer/mod.rs @@ -135,7 +135,7 @@ pub unsafe fn get_command_flag_cat(module_accessor: &mut app::BattleObjectModule // Resume Effect AnimCMD incase we don't display hitboxes MotionAnimcmdModule::set_sleep_effect(module_accessor, false); - if MENU.hitbox_vis == OnOff::Off { + if MENU.hitbox_vis == OnOff::OFF { return; } @@ -205,7 +205,7 @@ unsafe fn mod_handle_attack(lua_state: u64) { // necessary if param object fails // hacky way of forcing no shield damage on all hitboxes - if MENU.shield_state == Shield::Infinite { + if MENU.shield_state == Shield::INFINITE { let mut hitbox_params: Vec = (0..36).map(|i| l2c_agent.pop_lua_stack(i + 1)).collect(); l2c_agent.clear_lua_stack(); @@ -219,7 +219,7 @@ unsafe fn mod_handle_attack(lua_state: u64) { } // Hitbox Visualization - if MENU.hitbox_vis == OnOff::On { + if MENU.hitbox_vis == OnOff::ON { // get all necessary grabbox params let id = l2c_agent.pop_lua_stack(1); // int let joint = l2c_agent.pop_lua_stack(3); // hash40 @@ -274,7 +274,7 @@ unsafe fn handle_catch(lua_state: u64) { } unsafe fn mod_handle_catch(lua_state: u64) { - if MENU.hitbox_vis == OnOff::Off { + if MENU.hitbox_vis == OnOff::OFF { return; } diff --git a/src/lib.rs b/src/lib.rs index dcbb0c0..8890177 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ use std::fs; use std::path::PathBuf; use skyline::nro::{self, NroInfo}; -use training_mod_consts::{extra_bitflag_impls, OnOff, LEGACY_TRAINING_MODPACK_ROOT}; +use training_mod_consts::{OnOff, LEGACY_TRAINING_MODPACK_ROOT}; use crate::common::button_config::DEFAULT_OPEN_MENU_CONFIG; use crate::common::events::events_loop; @@ -66,7 +66,7 @@ pub fn main() { skyline::error::show_error( 69, "SSBU Training Modpack has panicked! Please open the details and send a screenshot to the developer, then close the game.\n", - err_msg.as_str(), + &err_msg, ); })); init_logger().unwrap(); @@ -101,7 +101,14 @@ pub fn main() { }); } - menu::load_from_file(); + info!("Performing saved data check..."); + let data_loader = std::thread::Builder::new() + .stack_size(0x20000) + .spawn(move || { + menu::load_from_file(); + }) + .unwrap(); + let _result = data_loader.join(); if !is_emulator() { info!("Performing version check..."); @@ -120,7 +127,7 @@ pub fn main() { notification("Training Modpack".to_string(), "Welcome!".to_string(), 60); notification( "Open Menu".to_string(), - if MENU.menu_open_start_press == OnOff::On { + if MENU.menu_open_start_press == OnOff::ON { "Hold Start".to_string() } else { DEFAULT_OPEN_MENU_CONFIG.to_string() diff --git a/src/static/layout.arc b/src/static/layout.arc index 75611c47d9263e3f485fe873c85ed1b9533e2bff..da059da6af7cb79273b42fd569b6164d31ca4579 100644 GIT binary patch delta 40254 zcmdq~d0ds%_6Lsda}LVk3~(3@9OeT8GJ6ONk)e^( zBCi9PnK?yf9j;lCnwQL!%F4V}+^!Y3t3_qyHInap507&Cbo;zMzu)(-@6&7d-s{S?QUnC6%Cgd~3O$9ZNhqVgYC#UpiIKqOPqd ztpUyB<>y58~ z=J8#q>DyyJ+}I47$9LVMXDN5TdCNPTMm&F>o>{_zCVm2%$9FxbXMLZsO!_BJN4#Oa zp51-y=Skmz=J9iAY4OOC$-jU;x1dVT+863s_%mPMrZX@dkGCz-v!eCBQ>>tQeAIG1 zTl2DeN*HJ!ANshSJr%QmN-xknUQ~--mHjX!12m62R_K}U3$Ceopm{ufrJntL_y<#m zfadZ1C-iL9LH(VhLFZS2FxKhWSF^$@ZpI59w_&Un-m_?WIcOetuGX^&$M;R237W^x zuGO#;51PlFq-Q02AGv1VnaeUFEUxMcGD16(HIiEfH9ZyHxwnNXp$SkhyG_WciCoDVl z?5`E`YkT@I9glY+^r_nXL;z?W_t_2F{6L)pG><2~tY@Pm_tiy%PDXqmCd)N`2UaJ6 z=JD+we3mtb)?|U^@dHhI*8h!^wMC$Ly!~B0`{BEX)?VvVg>Md`tQ`zrf4z1bGVu76 ze_^70w{_hl&^+FDPS2h^=>OC-&^+FLUeA8|Ea<7(pm}`zw|e${)7gy=g68r3pYYw^ z4cxpKw21$#XZLNtb8{`|bi~gh+}`rY=K3nU;KY=Rm_&>3c=~D3Jl=|M$LtrjYzNKb zjsMZJ&$C;$zYLnk8!qWt+UB<1uY>0ChF|sUwRQXdd4% z!Ofx{YWuJmG>_L$bh9_lKk)HU&^&ILj0QKq_VG#3Jf2YQX7^ML*nawRyx_#r+uZD@ z2L}HA-=KNC^L96zGiL1HFM{Uri+8x0`=#gquJ4HkAYON;n^iUF&R9Y7c=8N4TlvmA zpGSb^@$EC+?2{R@+Y&(Y_<`AOc5`EATQ+DOXZM2t=J!W_QG^$q*gDtEvRA$M&uc;R z_=5Z0?4N_4JbN=}9zQV8&FF#XugiG>h&$%H*-a<2zMch|$CDp&v(TNpzIgyNk2frE zvuB;=e=h~ib+f$ICFi$+=JAkaZua{5 zxNrA@=5gaAZuZ<*{r3kz^Z1%t3}H^>_eVkV_{A5{i2Hk7I0>4^jeFhfZy|dx{DbF5 zyy7J{tF3wO!gru~ykcLKoBjOrCl~(1U+i8o!=v=! zkM0iEd3Fcu-{{$+?k>jWdG3kO2kWZpdop$_V{C?wv4;`XA?!qGM)(Nf9Kt08Up-@X zgm{DugaU+O1ZWLxJOWe%b|1n*gcS(biWsCnYeaYz0XsN5hHwhuEJ6nYL{$~@GcXp8 zkbsbdP=GKJVLZY;2#+IdLfDIN7~vCyeErgE{&LRAY;D?cm zMd*XzMi`B7JHiZv2N51ZScp)C@G!y>gk=biBHS^1=H$w|?wEWBy>pGh;yE9szvyGj zV%aQ*<+8pkkM(2uZ1tyu`&8{pVXPeArxxFUf0Ja48`K1bZGn}e`z;8S=*mTeO0;SN zf&(>FBeWx6vau!vr;$M{W7rVN{7@D`vI%7&Ttp~0qbvlMg|T`BW@W6xitp2kNWMQ~ z4G6{nRDy5{p~%MAb_81>CKEy%LP-!lID#V>4M1o|C=Eda5S(^20HFh+EEEku$PZ&| zAA-xl*e-)79gBO7!?C%gs51?mLi-(80Tc{ z0760>MhxKs!i0EC9E9{8$=lJMOSP9hZaMx6+jWX9(B;eTfk zhNgJt_tJlnKyi2IqE^=pvH9Gffw#&6(a`~al52u2 zjQ9V~3Y#di_4{A4O_JI8aQ#_nlRa5;^+UF=wzw?15_&k=Fl2RkVYw$g#&9yuW8D*z zZya2F-MBI=06ac8D_M$8V;Nz-;{OHA6;Z&hD|0%Z#hZF8NB(qtE+aLV`TQw+>9&-c zbyXdhul%XUyyQ<2<{*FCG4J?OhdIWd{8Wi;=@M)DNOWaOJe4c4x}U`40TNpaBw{ZX zj}w!CKP^KARI$oyM1(m;N^Bk_v0{uw+gOQB<0O{fEYUb#V&epfWs@bc+a+$FBC+&N zi5Ib*@TXyh#FE(@t1vM4$QSi|Chwla}qCpEBcQS_+Gv!xgfFQXNmRykyz9zvHh~dI-O2L@_i(> z^^{oSClRYgq@VJaSRE)aIYeS>sKj~URWjm?lGqX>u`*7gBSB(wlEm#P5=+x1Ud)u( zmL;(!S7K#9iDd&M77db^TqLozSmL~@VKU+@k=Qa)V&!Owj#7!uV@|to-;h{$kmC@j1#ih0*y_kaSRybWoCXP?K~}lqL@yF@y_bzW?!(a00mU z^kI5-7&;Gsc$$k9vZ2s(_`~J3o&~V+iVg%F$Nbo0U!Ka-7;5ZS#lBJnLfv8hER}76 z!o;6a1S9LomMi)OLHn|M70oL!p_-+t0U@C6%*mO=!~2;GO{-p27!KOREX<+OIc;WD zR@SI8@B)2V6x**VfX>7W=)oM-1KvO%7R+K)I$WoY1rSk^*vuO5(N-qap6@?c*s)sy-nWeEdH6n4KWASD8sRr^PO~N;vqi9}X zJd0uDRDRB%fUg{-=v2_XkmIuY#L0qAV0kJ%olEjW4AKv(Kt3c%_=Yzrnitp`L)fl* zz(*t*9|4Cr@$gR&h8`YO>3qmj@zD}ghj{)pG_XbG_W@nRo@Nay-G@u^0c;EVNWI`t z#$bL>G@sQy*xl?EMe|uZ7b|9)q79%kK%ebKH?SG(=WcX=_9VNkXx^^^tg;{rB6wV= z9~0wd7NuVBj*Md?S+b&e1?g-Q8=&ZX(E02dHeAuXM|`c6DVkR>hYe#>6wS+9$tu|W ziY@`2gC?}&^epBIW-gltU%H@qvFLde1vqBfj4wG)LG97NXU)P6kP!NMs_W0RW#>c&8pc!L1X;0@nR4g&GxAbF3@Ax zO>CQ@a|E3)Pi~0w$y~|?vbt^s=COO(Vny@vu0@kBss{58-o@rIBMy1|;ZtEMBy1?B z`S<6N?ncalB$a{p=sGkYw;MegbEH_&yq@*=Xk!Bq!Nb4lt!yF-QuIjB18E6wSZ+gP0Zk?93ltUI|M0LD76tFJ{j$J!izj1y>b5yu&8?&mck=rqoQ9sWR{( zorQJ0K^5QvJsV%GQPF{*2cil5pw1uOU_t}OcB9u~PTa2O1f&;X-OTTno{5E8Tcuv` zb-sz+$u=pPuj@zIX0}DqeA3K^AllZAUIx*$ryG42t6}>U?L__uz|o>;-oYm@2~R4T z&#^j;Syh{Qk&71#*#h>1qPZwu#ITxqSMhLBJc})1CPniekHRu?bfX8ehnZ8+yhHb6 z(32JI0{sXE-KA*$$?j)MtJnbb!ig8l*h8#H(OICEfTcvyyuzgzld+0U23>;&mnoV< zA*+=i7xBReE7)Jy4=SC@k;mC`7SBIE9xkXJXH~WAtjbW17mu+G%%(E%fmn$#C|BvJ zpr1ekrzx7R^E!-KrK0&K%Vp291&Zc9T!l|st!UnXeh@uN70oC0nko$0@@@>*v!__S zqB+#Fb+G6of`@++f@Ek=H2);^Y%Ob4G@tcPA%Baa`R=zKEB%zBu^`d^jVx1j@Gj6* zE70R&_2M?X*oYy!NzuGVb1^L|6wM{yX10dS=teiN>se(t`e`h@Io;?jkoEHv&F923 zn3f9^jq*b=SACO2-DNkvkYl; zW|E;Gb*S2Jogy7uX1t1Mv`Wa2lbj>&G4!GZw;6_}X!@$kz}gbLIoni8Z8Ug`F6^bs z6rG;_^$_Y!pMrWd?N;Ni>Fv#31A7>MYLaBV{U0_Y(7V$NLsB(0*eTMn zj>nZEICYok(%pt>l)Tkoi#U}o)3pDeQoN_qWZugnZ>pwAJ5|2wJ{p6>>I{XMU~}lH z`e9wqbu$d*DVoadRQZ#$RYart;>MFKEN2*#Uv+xDp02&eFf~O}&z&L%W361!bbDG= zS*^lECV74IWS(G{nyRVtPL+RNziy0(&g(1bd3mCrdXwVQRDP$*?o?JCMKWi;j5+&D zK8xbhjs_ZE3)daGJuRx7mVuJbU-4-t2&c+h>F&maXyqVDR~AY>o1T8X+mJ#B7GcV0 z#|@|AaTKeFMidvg4O`Ixo^H3GhXjNGA5XpSMXmJWzmZE0uAfqu?T;t7L zF-+1G!v(YMG%bI`FfCO(?`Ukck=|@JzIo!YjgWP2G0@|W;A?8P@fpW@^BGGeZMYR24 zbc#gW7EDkPjaKDszg6=6toXFEqQ+M`*_*F)qNGbFNj|*~Uy3QTVUwXCMLSq(%oq8F z(e1gY3cKjhZ8kXcdVe2^d>S%9JAgV>-i9gNm=JBaUD6G!pRCD%2>7>7ALJvK$hl`Q zQMCiBQ?ZrYsUjLZRYppt$wr5CV`@4*1m(}&RP+!xVo?Fg=M*3I$eth-mzb>a>a&F-Psr}}Uaok49c z8S+!LGrLwm{sZ0xq;b>X2Q|fO30h7WUZu2)=9P&!A32gadGjKcis-g+fgs_>Pvc3fA_VDHhc3GZIpD;CRLxWs4qn`D>+s6_NTr1+EqD_51(-X z+#>l>HS?3kSGU!huWpN^>z+~d8D;-8GnK}d|C~2p{Eu^b`DVKNBg3>5 zi+ruIwf@zctyLA%+N7$p(BtJk(^9P7Z1WCyv(4Kt>3OfJ+AMVISE%iGY|b~l*_^Mb zVqaIaS!L%m3!zhGZ+X+3uSJ#9(yXdee3~UuIsWF& z=Xgufjw7l*$w%8SV?{T=?VY(LSG=Pt3824}{?DPe zo$zL}y(?+kaaEm7R;R3>%&D?Bz30u>q{?Y(Rn;jz%~-1Om7nzHD}P_o{7}-yk5qku^tc%lzVQ?9e2t2)@suh*NS4n%TlnJ?|A#I8jEZRVX&EW| zyDDGtX~tWpmi==rqVQ9NpQ-wSMSavB>|=}A{txfW+ZFTnHdTqtoT^!Xohp0jS#Q45 zFC<<1PgO~Xs3b*Nim!I+;#b~$7gae>dlP)P)RlaxnnBsAvN!z8o3G((NjJ2s`s{KL zHN&&US8~pqujCs^m;77xPw@%YA|z>i9p8HMb*OSW&a3)D=^ZCNZvFS(`Rc!ubbW^^ zKU5C(sa_cDq6^-ci++%F(T}RoFga+N_1mekxBu+T*RIOJR+S?LP4cB`R&tH6?mymq zb-zfu?xL#CAqP!Z(~+d{<#&4XnEQ{}Y%rs@lq{nL!| z8eff0r?CjE>5^m(-FCqz913@rPTvwvueyAur7API#_Cf302;qbFN<;+bf_~_uP=!Z zb*5ZrjbhX;7&OLK<;b8hwyJ_!ty+yy^zR#e=0?s_o(@{Zc>%J9c{Z(vDA_da zhC-{ssazv8MrV*@bOviRL`z2PDnnyzQN9!!V~Z-N#je$;7_~bOjj=LZ)}S#~hDo~8 zq16~8Yt$}BG)9MV*3cLo5t7jnsnw_$wObU8u~~U{XpGIOpyp_;##m7!)!%`yToLEp zzKU2$S2(q5WbQQWGDfS$mf+23iRRaZ zRG(>S+U1VMSFXHLG`{lQvcmFY$v3zpPA0V|Y**N!@CSt#6#lI6qQXlw{chB&T_S1q zmMAY6t=^JUorspCsg8EW(L>++Orr;9`2<9EC|?;ZcZcHc$WXZ##M9rW`bXmbi##rA+R#WeijK!VtGHF3Ci5nDdRk%&zc7;0>?ozm?7u|ig&r~|~AYa_dg-5Hc zI$PFOouiuB*h@B3yApG%oXP!UMvXDKucVXnR5M)(GO1BvtHM(XPb)m5@H2&);T(RQ zVtll_H?3mZ0Pl)z`I5HvR~4UC1x-nm4OptMR^dv8bqZH0T%&MZqNq5)t2JFML$YW23cz3XioBv~H*?Q&D& zs~qOdS2;w|l|yBN_0@_|y9L!49m*|?smC!~GCHnN6(-9HwcApSuX&_*h0Us*<`G(j zl9Afia7j1XJ9ovklCBt~YDtl`Xm_q!EjHzxrnSy?o#eBP(P~kQwZEGO>^ltMmtrfK)Q8kbRd%4v->-XytRF)tj;O4oR0zQH%|e zF-5!8))+6|>CJdi6@)eaEZ07r>9RuY_FLm?nC{Kjpd1n#?$Rn$j4Y6eM`J9R>CISj zx1>vEXkE;ZHEQ?i8e_+7Z^jN)P{%B-#tcd$i2bI+SOfLx=+$Yb5vG8kNV0f+MT`DruGNCS=&`n(B1c8^ytOEg7aL3PXS&01U1#o z_s(87PttV{sxDM#iY}yezxwB+m%q@PG5;Y+=P%%lm`(aOd{W8sxzDJ`HaISJf7%pl z+alGQ=igLqO4A$&oGO3KVsF-(YDw2TteRKUM>bFMG|(7b%e)y~OC;@Ds_OjD5v(rF zA3@_f^{6-3DOJj;M-$4H{SS3U99D z$0eOytLpmxIO@_oAT+MlC%m~@RVl4271vYmflG6f(75KU^5&XXC+T@lDz3pN!KFD< zXk5;<-dxVrl6J09T>ttAT$;;;#??aJTrH}UmUW8j$xpzgIc8{FmFvB^D(fX(`IO@7 ze+G*q|2s@s$42k0j=xCSu|Z}1$7iTO^X$+nXx{A2)vQWs-o&}^k(U1hA4&87(6}nL zcym=WNV?)_Rae$o)TMceXk4~uy}4}9NZPhlaeeZYCZsig5{zl<_x3pv6sF1*h`Aur&TBzQ)%=CpMhzb z>x{;|-Q&%@U6r=|6;*4l6djt2jmB5H-%Z4tg_|G)ubVP03hXp2x(~rVuVo z;xQ^TDYPi`S7=ih)K6Bd`4BlJJ9E9|U9szsq+N$q#UZMIH467BJfQG(h0O{NDm<+4 zh^km~R?;eVw0KwSI3j7s-&Dmds(=#ZHBzClQsKP{=P0~i;XH-&^JR}UuPCiz_KtVO z>}^FKRTVE#1#~Km>MzGCL1B`@WQC~;(-mg+mlbR7S6apG?|N60;@0DmZatwYKC23tGC=lpslr-?D;3r$T%~Z0 z!gT{=k2QZXt>UH+yen=}KaOa6Usim5y(+*lP*$9;ut;IC!l4R>D=bksQeyGgaU(i# zx7T5&Z>IX(l(sZT7N@nN;Un*MG^ihEG<>KAV3aEFn8I@kFDU$3;YEd)6m}}SJkX=t z6EoDfx-#W$>ge+E(>n(|`fHaDLJx$V2)+nL1V025f*HYrU`6mp2tcqQ1R?|>1S5nX z*bzbz!Vnw?;Rq23kqA);(FidJu?S9tID~kFUI+;Yi3mvuy%CZTQV>!R(h$-SG7vHm z`XIOvvJkQnau9M6`Xb~Z^z;1MmEWVRY|)&77@CNBiG0N70d5qdzTO;+jP3ms+7+j?6JrKV8xr6f-{DNQ<7Z1<>X~pKKZv zYwAH$s!hSZ;!dWylP=dqY$ekaqnWHLP4@0hq0kl+#ST zLQ(@MdP`^^RX%O;p@=NMDB5t?YNQE!EURczL{J#rI40CapXS)2Y5aSEL7t%r8U8xn zHm9Iz&V5D~O?WTR_D8Y*ujF1kC14Yo3ei)HZ-^Kx&v?6KyUvII63KID&5;7!Uo(*L_E)=kiOo+#YmvdAN=1*1PK zDU1#;>VGW?C|%nX6God#@bxY?C-(FVE%&>>Ctd1cTS5yr#;4GGVl)U_0|CMcPb!jlUA zNT~f`>%{_DAC;a)``bE$nOAj-h*k%rvYYPLh z#O#(+2*1q>pw@#)@svF-eSjx@ndx-{r7rdFOOpp?^rWdPvNAYR8SU6HLj*<&`2dn=};vaun&zTZil8y${VhPrcwv0q6 zp<>h#8Au^ZjhVd6BKqQ;Kqp1y_&K?FG|{nd6I1E_3%#=__PS7iivCB0m2ST;#7XOC zA)lOJok4}Pye#bo`t)-A7)sx799OPq2Pe!DfW(21O z(Pg(YlhXUdImj}_7(thJ8++1}7cFUYBHrXnAFT>4Lf&v1Gb7i&dTL4}eIA|_N~1?4 zT4mElT}(6+ePEjDIs8TNVgvOKNwxlN{{#R3ZK%30ZvKrMx{Yzvrbx)F^2i)=V@-Na z?y_U^#2{ggL6mxiwq#%D;QYxDVm)lta$c<;U`wO778z~y;Q?DRt#~adnvQPhJ%DO2 zS&Tki2K?;8Rcj3)`)L!OGPzV_>0zYaTf!sxI?kXc7COQ_k8H>pXQ25T;}d97jGw<) zQMCN9)kL)!5jphTi#fgchc(eIf7`7n#Vq9vMpdW=rM#7ibfN)T#<`y|oOFJY#ZJGK z^$1=U&?Ck>Zv)S36YtFGnwg8()$rPvO~Ve8MmvsLBGzuejEguKZ6**qrgARU|7fgNMj2Pq@*^=F$r~S1i3w;}D>PaUfY&M?&1J?g*rsycT z5Mm3W&lU&v#1u15lImVp*SbRENZP+QG?y~pG8NK-cxDSh_ATnzW2ASVrYIx>j%ePB(Z@z({I%?8W^{xml?dpbh;hv;ZR zW`xZWpkDJ7YI`CgftJSEuDkk$___&_GHv7NaM90>gWVs&Bh7-zN@o~Z_UeeAzcA#lkebLIa1sKT~xWlFVq78dh!o9 z?5o@P)lcrFWp&fXj6G=nefDgs4GapU?7PwuscUe6FU``Wr;GK(XM&rGLR0OY-+Yrk zHPGp!n9FA(vF9zAZo-y+1`=n?LqSG*U|v#y-0h-1PBhU?0YRBGbZ7bqT2bdn<$o$k zJk3sPvQ9A$mUDgN!0?M)x|Z@e zhZ#eq_5Kkw(vlj?GY#U!Hp5v=^U~?+YNo6{$zfD@#vG!l0);g0H&-eZ?zf9RE~Z`O zDJD9;*4&%=j=>|e(S6}iAq4YN*S4yQS!WwF(56_!p z=~#Qe81!v|IBy{mMzPPD_#YYyxWo;zQ) zM;f?_W~JFBP-7BW^Ct1`lu^VHb1Kzt!JinaOxY%f!SqpH1aU1 zhFBaB0)ik?#zVs0v?aqu(}wv4(6GmPKS3J`;`}N5`;561d#lY%_eO=>$Y62*Cw)|y$%u{K`_ZAx+*kR2g>n*8vT3jAIkQNqN z5^20S0~_NFx%Xp88suCYrL)}&s*+ZImSLp4Tw5Sj?eO#EYS?5t|3P3pjai&}pJ&U) z-fiM+)6j#8BV&r`@ML==Z7zuGCC@MTS|+jtV#}MnIlhtt4&;pSe6}eRYL!o%{gv*z z!|&te2HHO}FyDGr*m21ca9iw8G$F>%M2pP5Y!vfSQm7aVvQc0U`f@{L3T1Ax`BIOM z%s0~b_XEvj-zSMGU8J{?l7%B9%Hhmo|r( z5@~5&Lg91x;&^u~(87;gAvp13^{hObHUo8Z|t>&%Jn$9 zhb5d|c_DlP&sRi?pASqRN0uL->Hadur&)2&)9ZDRjMt5TWXuhXj@P@p$d(kE%0HKL z?RpdDv~RSP3f?e2LW|$A=hA0~`&sx3fSmg!3*WNQl*!94qxDb4M)AUY4Q{lr;h~TO zRFo#$GxY;&^y)K@nJB#+(+LI*s1IFsp+bh!ikEDDH0?XA!@h@80(`n~GL+KvU}HwM z8JmgNU$}bjP7nJCg$7W;m>e6IJVvTLZ8rKC++AHl;?r-f=ESx0{cRL<-ZF>+Udpr4 z8!t!p;)}*bN1M}wD0f4|^-?xY*a}_f>lLO*IX>SM^LdhL%c9-cHYaWD3J9QGtAiX= zn`L=~PmO#Z(vR{Fq0sBX-G*jt?)<>Ic1CKD=c5BTnL4g*gc>N~H*-84Seood9~S4> zOxP#nF49S#Z#1vwCL0@X&2~C^KE|%&YRT<%`Zcsx^AC{CtQ|KqfK^Dl61wlrAXER;C!x%~%))-^x(%=AdFN4$`Iy#!MDe=EoBA*UC zJchwEtotS}fog}C%(`amFgW4Qe<#mE*Zpb>r#@Mho(8Z|^-^<$P#EdJ-i$zgT#Bab zsF+~dc-ZR8XJ9kA-o!+^Kfw~kC4-Ar?Kk$NJ4<6CX=EivVAP1j;e1@nX=F!lGn(-u zwFM@|lWDrW7cCqd;o??-XwfaqIOlsAF_gJ2q$gcCif{B)qo0i{G;M`y3V_?vWTKifD@WQ!r(>B7&l^a;;t}f(& zfI}Try*M;6k_CwAbqE$guMEwd((Vx1O`Q>&Q&-ms!SUmd9bDo0QfI&m9{cqVRT~dJ z_Mh7#YOu#YmzP7M8^Y{7w^2t&e~q?NY)#J2t0JjFs6Cj(eUgG{;c;^xnz1a*MQ7^# zlj!!&2kY7Gt}xZ2hTZAD%E>_ zjds$vbNvIU&kAU0>ue?{&D?ac-hy5B+37iPbmT=_KNMC)2NvSg({p9)^IoUKB7RiN zAWJRO?dgWZD0<}e*hO-YkNU-BqL0?)WQu9U$7G(+i!2n|nUZ*=m9pW`mwxwNwSNAy zwhKNq$(+&C0FgrF>-yO#6!K8+Oruu7c6YE(QUq;S;-5-KW=9mfRG9*${`ESXa5tF3 zY^!Icl+uC6vr1tcNDr0M{P5$}B0p%vVnzgzwI=5$UK>84X@1(ptIHWLr zj@+6Y!M`0w<}u=j4x0JBC4e>*+A`Jtb+w@B>DlqLsnEwHbP3P=otco01el zXJVt0`0D$Dk6*GUeM6sj^z?CTRNDXYk``|3(+4eil6IN!dF`DkeSX(mNd8n$`l`%t zbW)#z!8!;wazB%2QZN5GJ>-1lwbXIN)k2@!t>JX%%48e$`#$6GtJC>^YWFp;z|HlC zwe15l?2RV-aNd0%n(xShe!~y-^Jhh8kUJpMDvfUoZp=%j31+AU>79@%V^%UkD10)Vm6(qbX-Rmrznbu`Yf-gQ+#0 zYM)9ROPh9DU9@t6Ie=y^^Y=9rxZUJ?2D{-`Px{AD)OjdO`^&h)&ifU_;_u zlQzfSL$jYux z(k?8j<2zsj{`F+pbPeSxUQhnv5YBon}5l+Mdr1e(u9jhG(S1 zBTUJVPNr}}v#=(N$B(jL##VN0KK-MFRD_Dr0L^=sUk)_R^L{-n27_z}q(m1rj*S>c z6F#uR3R7b2fwL+<2^}=2qCc*J0_yW}j-6WslK3|;g=pJ0M#asC22Y1HJE=U+@ct^@)#VoNf5eU^$_wG?{OQ6up5viI z%I+@Ap}}39^7RRIw*Rr?N4fcVd_oiV`2N|tt`JjVh*a~TqgbvU)#=QKRzEsln6Q=? zP_E~CrIRoPn|Z8A=%Df)*GB(aYy&M?-)AX}KWP51S)fy?0!9*q*U$g0sP6e@goF*D z>TP|}v4-xv!Y`D!R?dHQU72+qP^8|+Ukm30Z&=4~sG{D^Z1Ul{E7dh5$MOS$nRl#+ z;`hT2JAN-LNrxN#xPf&RFFTE99&xPYa;}JqZ%nV|<57g^#799Jk2O;2kKfVfl)9{V z?m}NHY_ZrtDpTh1!)pRvT9zF`>uMaa^krA{D(bRfLmB(H!{ym>PeiGYH29%Z(-m`~ zg%-A@PbAa7(ii;R7~6DL;w&lUPyZu5T3Qm_6nIE9%Y$aw&_aI4^6P84C>kGzV}!6tA3I=j5G>v}Q1BLxdNI5gefMhk9Vmri%cjyC zQ>v;KT2K&e2#GY4`)Pl(XZrb&d;=Ygu|Zfp34`gX;PXC(X8Q}UD_piswLn>(C3E6tGTMaKTLqRk$Q zI~E5;ZNa^t=ySog{BLP`d2}(ID>1?1qn1mikdR`tdI_gD9fjC%6S6jK1Ax zwov>{(Y?4mCDlu{k_LEBe0Wlw;p$SY&`D#|sxu+Elyt_NK&hUbL>l7`%ISV`9_d%j z`-@xZs=xuBUmnOw(+OL^)q?)#UfwEC&UEji=62fOh|h}N;VZ`m;86CL5L}f_#sPB1 zlS$KLf@bmUr8Zw1&`kP5Bd$1k&U4?x@Ff0cxvO^!EgzOxB(J{Cjey*qy*#U@cC-$r z+1KE_x~|YCkfyE-veOrVi4OW=Vs@J}V5}$qS83|sQ*oJ&HE8S-Dm`YXx^?xXb)zhK zbZ!T$Q4tTFO3R=!D3f4#JARDVXSZXU%F9uHJ$HGd@~M47p;6#2^NRI zqgT|W5LtA3mc@^LONn&QPf0QH+(ab=NF4plz0$k#|cw&lDM>SP{gL>V44^M6PAA(=HAD9!mgzg zeS3w`@FT_vbUMh%Z+v{DRx)Tegw>cu;h~-$b++9;6m-y>L(iViixw&{`ZL(`WqD|{ zj!y`eSn~Yn%$K~>O?%Y_gwtUY)Gje*;uaZJ^!`u>uA9(5jNak~PT$|6#5ug4cKBfn z?6WF-2%UZ!m#Pb&3zy?3j=N5V^eRnv$hmX)A#EDke=(vX9gN?I;Uo(h( zz}+RR>eP2Tidi!s%hl((5Zp`rJE4H5L!wb>R#0Esy#q_@+nIrW^z}|$uRo9zWVL8V zS!ghw1FZ`y&w|C zn+>;Qq|jS?({JR*WmwObg~iC&hGk(%{FG&(mDRBw{HTD-%;PvmPZ*o)qS8>k59a-+ zbbblc%x_=7P27y1`-YO~Ib17G=@St{1MW(Trtb=IaJb;lO+y6%el&HrEtU@NO)rFK z9;D;eQSC}>6SSH1+Q?8kx-%o5wpQayuifm|hcEy4`4Ksj4se%>HD6d~t^FnyC(nYo zEPD6b#1sl0h!ayGzr2N=*+if9v8U5=ePlY_|5aW-?fE(9ez}XCtIe5C*X{K4$Kli| z^M3n!1kB+-r1uq{!gHyIb-SLvDKuL22Da|;ka#HoxH|^bKA-HM3oj=2N2ll0ck>hA zGVm%c)YiK4XW~@`S1D|iIXn~>7%xQ!&=e1D&alcNXu=ov6#DW>|7gDG+hMS1PLH6a zIiaihJ&7+}ehYVgJKPBv=1Z2I^19-SuIP=@Y3jm8+#L1}%M_~__XfJumw7RzxT+EN zk+J?c)OTPgHx}H)wV!HQ+zS@N{dQ9%UJa(CkMm;0v6O7D#rLE<3OUAGC2jI+&ttdY zqTBQO^NB;1E%lAa(4LgoCpixMQM}0ic8{`z+bhH9b)Ai0ro7%G_!0iGG=t$Qfx>YE zf38c2AtBE8&CFXRHG}z+j8R;rNvHRBhbPeGo>{_BnyOtUl*&7U->%Dz5aL|Zpwkpaa|3T`84bvZ4k7=MIYtUK#y}?JguW?jH1&?1Cv`?};z-4t z4%@TEvucQ`RM-9b0|ymx;`E%W>l2FoyV9IYrjdRnp3}v#$wG3s;MWw5xFvjLFC^oj zonbf1kr&rhtNf$ypt&oeV`*GxNPimrB`lSvKg7%xD+$Z64~^~&>WjgJh4($oOLfbpj7U7U~DRup_Lf(X|I~n3n3wB`E<1{{rEN^7M z6@xN4jp_mzUZpvVZ?59+WZi$IAI-3i=IpjQDZ7rJwBYU?TAwWR_%*WZtxB_ zFZZ-pY-#A-oUQAby?BwKTXyq(@AkZ$6KB zpJwcZvQ&G$e<;7%NT=h!IYKE>HIUcg;y>ied$~OBsfq!o#9;F z_87k>ilLRqZ15K7G{?}$1tB;}H^4rB_;vUp>^hR?xH2-KbmnCk!RKT|X8GYb$K|}+ zK*x_89m--gE)>cYx3*f%Sh`!YLkCJdQTiF~wDYypn>wvnSz1ZbNrG~3i_W30V4Qx( z?z9F|kIUxY=u#!DGLS`PnyiO6$*yFVxMjedjlUgi+f2f=R6(oW3mTxw(GY(8@T0IT zu=!72hY2d?x41Yvzb9fX1)s*?iOWUpUPPQRV7v&RtCu57Kk8RaojtSs*G`A##Er|` z=1yPzJK)$XmN$GYE-0)&*XeM?Vn_!X?Me384uz zeSJVAZCne5b5;__RW*)Y+#w{0uF32l%?%9vnqoY~7r+DMC`iD(HQ60E|;;*IR#kr)lB;e{Lg4Xb*5ni^7mVXf9;KuYw zn)rU8iSF!-=ygT^N~1diq9Rv+`dBPVeOR6Rvuq}A476(hOmLOHaDF+u4PD#bU*->MsBCQoV!5c^)aa_ zjtU5cQ(!HWQ0yZ(w*M8!gfG|iD@TrGvaW*8Shzbfj{8IPq-9Ts34sUyu6tZ?d3({4 zL1%xn$M6CAg5MRsuD|m2t~!7J-@3xfIz_4P2i{J*evQVBW3cTmD(st{Aa|N~H^!?K zIb(4Ye6?PVOr*mv+PJm{UjymxxRvXx6Y*v>zZtrQeqJ2PjZ(feabrXuT>M*K;a|Z- z3)Ok&c`_IYPPR{x5%{o^joA)+?C6PVzb05h(0XGSs{y`%`>wF!4^&*a3rF$89s_$*v}dgofArMhyiXP)xDz@XR2mSBT2 z)93{u0rE^QcBug&wt@6=a_`=>;XSAfP~x3**aH9M39zoyuzk?jcdri1rmo#Mz9eMl zCetx~?|0Lf02duu)PDr(#+4!WH$3(iIB7mm1$+1Af93V2q&MwRd@LHp;a-i-EYEO# z%waK_jp72ch|h_7X~uwCQSMa>iIPoX$}EqD=ctqnE3#r~!Mc9I6fh>Xd&R%h!3Ao; zP@^B$z&G?BPMW9E)goa)n&a_aA0SyvQGh}k(es~M{r1p;Bhy3d7+-w@_mzYam z@uSz@zA)~Tli9M`Ki>m)^&>dAe9|w1KD5IrZkjoK4h`s=Zo?G7jX^Ut9JM4+x(*w7 z?R}ws^wU4{0%TKgr)4x<*)0szwi8Zx{NOG>@vgZb@?V|&(Qiv=PG*!mlY1_7IS1-!_TEUFr_XuUOOURGa0vUY zBxfd_Zi@(_18=9}?982A4_uoe443|x!7$do9}G{_`efyle9U5V(9Jm}r)JvRNDE`} zbD5F)Xxtx!!nzyLo-sr?dB}aEc4<%=eLTK*UrJw=QcNrDnK(MUV9cV7jafz1yw_%z zB4GWfP@D8jDqI?JfR5_c@(_Bj@25k zqxA`J2A_6 z)wrD+VYN`^6XBu!PA{G=H|5ldbq1%w$whhGi>!bj)dT5?$F2S7$QQ-}KF@D3s(a~9 z`gEiDC%!!Ka}@4=`2Oq0QciwBQUAcN8djbO=l?pPoRh+B_#i=|6k%m`Qoxo_EK@7G zSa5Ium-rDPl-6Dkhq9rYY|Wmbe+eu!z`Pn0MVGVVLTEp^~>Z(7rmj2;Oya$7Fld) zX`b^-VxtW-z$Y*WRv4q~P5K8B`~)EU8sP}Y&8@qu!u{xxYcV_2&Ft1IbwzSp^Mc_SY}R^pRDjEoVlxSh~$@31@z(D{brK>iO_4w zdSg@>Z7hhh(!u?(CBP#O-hdq`*>uxJOf|uef}zq@fhj>B^^32k(K(fsRa;Y-0;^lkrqo9Fez#{N2V#H{p4ElW^S z%^S$RDBAI5J6VY~?{4D_1 zBW%`*xP3l*7}C<|Z;Rm)EL0waFQ@m%FOOcpk2cO+j<-?nQ0SldJ%)ppx^aLPPWr+a z$)0;{miNu_kF?4u#!~kwA=tCJR(*5ZE zJ`pJWa6fSsC4V15xrWdP`gSAiN^@Vff67;DF#U88OK8DST#1Y0^YNFhy=m@*-Yevp z)&72LDUBY5#cBz*IB4jN>HWED+cLHMo(oB$gl1D5&3@X_gT#+|;I`d+L&I2)Hyi7M3WL?qPz?PE@4)s5+pRs-sK*yC-5ZG}@Gl9kQ7gsXm($a%Y(xR@rlX?Q zBC?gWTyH9-!V@7&DPkD3uE}}%J!#lLla2pqV9Zb1y=Z!dznvzZf$?f~PvwVL$J%+qnV9d3!-D^efemddfJy_se=__--F3yv#) zZflSy@NTw-Od1=OF_hr_2y5kt#9@3El~csWePI|kP`QV@@4sgXp*^EgdirohiyHiL z;_>5c=z_R;;hq;O1_$N)!4XyLl%4eLHrNr*?lr-v(t`OlZcbtzv^}BQAqVbm;9|&s zg|YFr2%{eNJpqGYZr8jO`6&YrFI;#lW@6yrt;mOuKRm6YgGCge4!Ex>CN9*RF|t+wdnR_y}ASh`$+tzW~9@cL>z68vMBep57o($F>i^ zzpTVFG^-b`K{7*$KpiU;sAm<5K85+g3+NE2V;2SLnXQPw*RkqinO-kY#~K9cS(BoZ zhspFJfjU+)tct(TvvT#K^%|L>U7(J22-Gtp{&5gq!Mu?&y-uKx)eF?KMnyYE%k+GK zI#wi5&&m|tGDfDi3DmLnDiP6(e@%^7P&rm+s1c}RbprKlyP_R8%5;}N{7aVt^{iCU z%{R;RQv!9YO`x7#RCL97nO-eW$Es>XM9&)33)@7QAz7e~xdiH2iK3e(%k)-(I(AB+ zo^>d?{C1f>Par;tKs~Ehv~j9TcM8P(PZkk9E0Ql*4m_wkBIR)xjzM{9!mg&s`G5=dcM9doFkwA72*ZtfIv*T1u}ziAxFpofsg}= zuCJ2mjRGMD1VRodx@fUXFB1qkAP{mu(d|oRI{w`(c+CVgF_%D zkw8o$MQ`6M)0+ii4hh5@QgrDSnO-3fb4VcOkfJYcmFYI@KjIrZ1Y!~?y5TvQp-CVn zkw8o$MVIW5>E!}3hXi5{DY|2qOgHY9<=X^e4k^0+1({yeC?c3d0x^jcU9?wbC=-ZJ zA`p{E(e3+WI(u1GU=)bGMA3B~nZ8{h=8!M6j6%#AfmqPr#)6 zn=GJSAT|?$*i00id{m|v3B+b15PONDTaU@~c7a&d0KXe`mRI?)Os^4$Wi1eUiJ~2+#riie7b3g? z`2w+-D7yKKEZ~$tY$gJ+nJBvAbD3T(5T8UK_7X+gzL4q302I|V`x2!tF^bmKXh-XaikKp^CRqRYOkk{K$$ zp3@1VRody7U*BULg>2Kp^CRqAyncM`o~H z;)u;eAmo6e8#-lrlR(G;fsg}=F6ol#dH+ z=NTFWLJkOo98h$TuS_o!2st1SazN4TelnezIASjm2sxnWI{fi;UjBB0kOKlC2P9p^ z@&jasQh|^I0wD(!-G)E1&I`CG5ON@h(~tuKG3oFp(0O`;K*#}skOPW#g~{|1fsg|N zAqNCv|2u_0jm`_`5C}OS5ON@bzsID*A1>$V^#UOW1VRodIyqLR7YT$M5C}P-=+-!y z-YyVwKp^BmJl^yDe_n#jP$v*_Kp^CRqMf~EdcHu&0fCSMiZ-Uo^#7-&Gm4PC48!<; z*P)FQbGdiTMHeg4NIAO^!*&=YmZZIjNn%MX6{BRflUOH5qa%%SvXz)htjHM=Q;DIA zhKlW~NnOlcdlP%v^Lw7x!}tJk;*&>`pPD)arcOb{$KB*lOx1v?8mPFln|#ky4VbEdU4Q@jf>*QQ+*A#i zssW9FMn}Em$EIq)R1H+T^?34KQ#D|!1}a`Wk^IWkCt&Im7;1_$8ti4m$W#rOs)35z zPbR-@ss>EeK*jT?l3$v-3(WPV(Ew(2xS#y7sk^{jZ-$dv*wpl7s=Q&{QiGj3m30uL+drDyTH_4Q1Qv@$xlt40#m1;;@(m6eN%UV zsk@-!^J~d(ya{y|h^l{G2DLDLD;rKt9ZOSpLB-wUlh zcxANY`U72U1m$ENy#sXkEg)=BbRQ+>cxAEH`(ezes**st=gz17EVPqWdcOV^i;c ziu{8pdSje z&`iJ0hJ~p^XQujqsXkEgNaI84v=hkvf9eID0#h}hF`sN0 znW_O(HBfQ?x8w(=J^@poK*jpsJpFz3_`5?~q=}dJgt$8sckZp<|1n}_0cT~1Q8L8z z7!Iz47!ng>FbpvahIs$N*`pzjyu_`?LM(HMvA~5`+Cq8M^8M?aKb-|^WQnnbh1k5p zi)TX|Sc!2`h4`aF+)g3Rr4R#Bh@B|J6%^v~39;>jo#8MKXK#d9Ws<=A5@Kcv@uh_A zcS1ZP*P1&TOdVOrr4i!52ys+|I3&W=#~}`e#5fYdV2)7Jv{-KZs=?#DWjv zsRuF1gSg(o`S&5Nbi}yKK@8*|hH$X;ONe6|@em6(9~~A#9}(E>Q^oCxo37!kr0WzJ%~n zLRcdqoQ@F2MF<}vgu@WRFbLuQ|`iSZHh=ix-*4^g-fn-iGrw93|NX6n?ms)1 GUj84@E5K3! delta 22497 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 bool { IS_BUFFING_CPU || IS_BUFFING_PLAYER } -pub unsafe fn set_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor, new_value: i32) { +pub unsafe fn set_buff_rem( + module_accessor: &mut app::BattleObjectModuleAccessor, + new_value: usize, +) { if is_operation_cpu(module_accessor) { BUFF_REMAINING_CPU = new_value; return; @@ -54,7 +57,7 @@ pub unsafe fn set_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor BUFF_REMAINING_PLAYER = new_value; } -pub unsafe fn get_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 { +pub unsafe fn get_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor) -> usize { if is_operation_cpu(module_accessor) { return BUFF_REMAINING_CPU; } @@ -76,7 +79,7 @@ pub unsafe fn handle_buffs( CameraModule::stop_quake(module_accessor, *CAMERA_QUAKE_KIND_M); // stops Psyche-Up quake CameraModule::stop_quake(module_accessor, *CAMERA_QUAKE_KIND_S); // stops Monado Art quake - let menu_vec = MENU.buff_state.to_vec(); + let menu_vec = MENU.buff_state; if fighter_kind == *FIGHTER_KIND_BRAVE { return buff_hero(module_accessor, status); @@ -101,11 +104,11 @@ pub unsafe fn handle_buffs( } unsafe fn buff_hero(module_accessor: &mut app::BattleObjectModuleAccessor, status: i32) -> bool { - let buff_vec = MENU.buff_state.hero_buffs().to_vec(); + let buff_vec: Vec = MENU.buff_state.to_vec(); if !is_buffing(module_accessor) { // Initial set up for spells start_buff(module_accessor); - set_buff_rem(module_accessor, buff_vec.len() as i32); + set_buff_rem(module_accessor, buff_vec.len()); // Since it's the first step of buffing, we need to set up how many buffs there are } if get_buff_rem(module_accessor) <= 0 { @@ -133,7 +136,7 @@ unsafe fn buff_hero_single( } let spell_index = get_buff_rem(module_accessor) - 1; // Used to get spell from our vector - let spell_option = buff_vec.get(spell_index as usize); + let spell_option = buff_vec.get(spell_index); if spell_option.is_none() { // There are no spells selected, or something went wrong with making the vector return; diff --git a/src/training/character_specific/items.rs b/src/training/character_specific/items.rs index 77308c7..99ba1a1 100644 --- a/src/training/character_specific/items.rs +++ b/src/training/character_specific/items.rs @@ -447,15 +447,15 @@ pub unsafe fn apply_item(character_item: CharacterItem) { let cpu_fighter_kind = app::utility::get_kind(&mut *cpu_module_accessor); let character_item_num = character_item.as_idx(); let (item_fighter_kind, variation_idx) = - if character_item_num <= CharacterItem::PlayerVariation8.as_idx() { + if character_item_num <= CharacterItem::PLAYER_VARIATION_8.as_idx() { ( player_fighter_kind, - (character_item_num - CharacterItem::PlayerVariation1.as_idx()) as usize, + (character_item_num - CharacterItem::PLAYER_VARIATION_1.as_idx()), ) } else { ( cpu_fighter_kind, - (character_item_num - CharacterItem::CpuVariation1.as_idx()) as usize, + (character_item_num - CharacterItem::CPU_VARIATION_1.as_idx()), ) }; ALL_CHAR_ITEMS diff --git a/src/training/character_specific/pikmin.rs b/src/training/character_specific/pikmin.rs index edced29..9c69567 100644 --- a/src/training/character_specific/pikmin.rs +++ b/src/training/character_specific/pikmin.rs @@ -1,3 +1,4 @@ +use crate::info; use smash::app::{self, lua_bind::*, smashball::is_training_mode}; use smash::lib::lua_const::*; @@ -128,7 +129,7 @@ pub unsafe fn get_current_pikmin( } }; let held_boid = WorkModule::get_int(module_accessor, held_work_var) as u32; - println!(", boid: {}", held_boid); + info!(", boid: {}", held_boid); pikmin_boid_vec.push(held_boid); } // Next, we get the order of the following pikmin diff --git a/src/training/combo.rs b/src/training/combo.rs index f440a90..d381d5a 100644 --- a/src/training/combo.rs +++ b/src/training/combo.rs @@ -47,7 +47,7 @@ unsafe fn is_actionable(module_accessor: *mut app::BattleObjectModuleAccessor) - fn update_frame_advantage(new_frame_adv: i32) { unsafe { FRAME_ADVANTAGE = new_frame_adv; - if MENU.frame_advantage == OnOff::On { + if MENU.frame_advantage == OnOff::ON { // Prioritize Frame Advantage over Input Recording Playback ui::notifications::clear_notifications("Input Recording"); ui::notifications::clear_notifications("Frame Advantage"); diff --git a/src/training/crouch.rs b/src/training/crouch.rs index 394d7da..39b318b 100644 --- a/src/training/crouch.rs +++ b/src/training/crouch.rs @@ -10,7 +10,7 @@ pub unsafe fn mod_get_stick_y(module_accessor: &mut BattleObjectModuleAccessor) } let fighter_status_kind = StatusModule::status_kind(module_accessor); - if MENU.crouch == OnOff::On + if MENU.crouch == OnOff::ON && [ *FIGHTER_STATUS_KIND_WAIT, *FIGHTER_STATUS_KIND_SQUAT, diff --git a/src/training/input_log.rs b/src/training/input_log.rs index 1d089d1..eb6cbec 100644 --- a/src/training/input_log.rs +++ b/src/training/input_log.rs @@ -1,362 +1,366 @@ -use itertools::Itertools; -use once_cell::sync::Lazy; -use std::collections::VecDeque; - -use crate::common::{input::*, menu::QUICK_MENU_ACTIVE, try_get_module_accessor}; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use skyline::nn::ui2d::ResColor; -use smash::app::{lua_bind::*, utility}; -use training_mod_consts::{FighterId, InputDisplay, MENU}; - -use super::{frame_counter, input_record::STICK_CLAMP_MULTIPLIER}; - -const GREEN: ResColor = ResColor { - r: 22, - g: 156, - b: 0, - a: 0, -}; - -const RED: ResColor = ResColor { - r: 153, - g: 10, - b: 10, - a: 0, -}; - -const CYAN: ResColor = ResColor { - r: 0, - g: 255, - b: 255, - a: 0, -}; - -const BLUE: ResColor = ResColor { - r: 0, - g: 40, - b: 108, - a: 0, -}; - -const PURPLE: ResColor = ResColor { - r: 100, - g: 66, - b: 202, - a: 0, -}; - -pub const YELLOW: ResColor = ResColor { - r: 230, - g: 180, - b: 14, - a: 0, -}; - -pub const WHITE: ResColor = ResColor { - r: 255, - g: 255, - b: 255, - a: 0, -}; - -pub static PER_LOG_FRAME_COUNTER: Lazy = - Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset)); -pub static OVERALL_FRAME_COUNTER: Lazy = - Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset)); - -pub const NUM_LOGS: usize = 15; -pub static mut DRAW_LOG_BASE_IDX: Lazy> = Lazy::new(|| Mutex::new(0)); - -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub enum DirectionStrength { - None, - Weak, - // Strong, -} - -#[derive(Copy, Clone, Default)] -pub struct InputLog { - pub ttl: u32, - pub frames: u32, - pub overall_frame: u32, - pub raw_inputs: Controller, - pub smash_inputs: MappedInputs, - pub status: i32, - pub fighter_kind: i32, -} - -impl PartialEq for InputLog { - fn eq(&self, other: &Self) -> bool { - self.frames == other.frames && !self.is_different(other) - } -} -impl Eq for InputLog {} - -const WALK_THRESHOLD_X: i8 = 20; -const _DASH_THRESHOLD_X: i8 = 102; -const DEADZONE_THRESHOLD_Y: i8 = 30; -const _TAP_JUMP_THRESHOLD_Y: i8 = 90; - -fn bin_stick_values(x: i8, y: i8) -> (DirectionStrength, f32) { - ( - // TODO - DirectionStrength::Weak, - match (x, y) { - // X only - (x, y) if y.abs() < DEADZONE_THRESHOLD_Y => match x { - x if x > WALK_THRESHOLD_X => 0.0, - x if x < -WALK_THRESHOLD_X => 180.0, - _ => return (DirectionStrength::None, 0.0), - }, - // Y only - (x, y) if x.abs() < WALK_THRESHOLD_X => match y { - y if y > DEADZONE_THRESHOLD_Y => 90.0, - y if y < -DEADZONE_THRESHOLD_Y => 270.0, - _ => return (DirectionStrength::None, 0.0), - }, - // Positive Y - (x, y) if y > DEADZONE_THRESHOLD_Y => match x { - x if x > WALK_THRESHOLD_X => 45.0, - x if x < -WALK_THRESHOLD_X => 135.0, - _ => return (DirectionStrength::Weak, 90.0), - }, - // Negative Y - (x, y) if y < DEADZONE_THRESHOLD_Y => match x { - x if x > WALK_THRESHOLD_X => 315.0, - x if x < -WALK_THRESHOLD_X => 225.0, - _ => return (DirectionStrength::Weak, 270.0), - }, - _ => return (DirectionStrength::None, 0.0), - }, - ) -} - -impl InputLog { - pub fn is_different(&self, other: &InputLog) -> bool { - unsafe { - match MENU.input_display { - InputDisplay::Smash => self.is_smash_different(other), - InputDisplay::Raw => self.is_raw_different(other), - InputDisplay::None => false, - } - } - } - - pub fn binned_lstick(&self) -> (DirectionStrength, f32) { - unsafe { - match MENU.input_display { - InputDisplay::Smash => self.smash_binned_lstick(), - InputDisplay::Raw => self.raw_binned_lstick(), - InputDisplay::None => panic!("Invalid input display to log"), - } - } - } - - pub fn binned_rstick(&self) -> (DirectionStrength, f32) { - unsafe { - match MENU.input_display { - InputDisplay::Smash => self.smash_binned_rstick(), - InputDisplay::Raw => self.raw_binned_rstick(), - InputDisplay::None => panic!("Invalid input display to log"), - } - } - } - - pub fn button_icons(&self) -> VecDeque<(&str, ResColor)> { - unsafe { - match MENU.input_display { - InputDisplay::Smash => self.smash_button_icons(), - InputDisplay::Raw => self.raw_button_icons(), - InputDisplay::None => panic!("Invalid input display to log"), - } - } - } - - fn smash_button_icons(&self) -> VecDeque<(&str, ResColor)> { - self.smash_inputs - .buttons - .to_vec() - .iter() - .filter_map(|button| { - Some(match *button { - Buttons::ATTACK | Buttons::ATTACK_RAW => ("a", GREEN), - Buttons::SPECIAL | Buttons::SPECIAL_RAW2 => ("b", RED), - Buttons::JUMP => ("x", CYAN), - Buttons::GUARD | Buttons::GUARD_HOLD => ("lb", BLUE), - Buttons::CATCH => ("zr", PURPLE), - Buttons::STOCK_SHARE => ("plus", WHITE), - Buttons::APPEAL_HI => ("dpad_up", WHITE), - Buttons::APPEAL_LW => ("dpad_down", WHITE), - Buttons::APPEAL_SL => ("dpad_right", WHITE), - Buttons::APPEAL_SR => ("dpad_left", WHITE), - _ => return None, - }) - }) - .unique_by(|(s, _)| *s) - .collect::>() - } - - fn raw_button_icons(&self) -> VecDeque<(&str, ResColor)> { - let buttons = self.raw_inputs.current_buttons; - let mut icons = VecDeque::new(); - if buttons.a() { - icons.push_front(("a", GREEN)); - } - if buttons.b() { - icons.push_front(("b", RED)); - } - if buttons.x() { - icons.push_front(("x", CYAN)); - } - if buttons.y() { - icons.push_front(("y", CYAN)); - } - if buttons.l() || buttons.real_digital_l() { - icons.push_front(("lb", BLUE)); - } - if buttons.r() || buttons.real_digital_r() { - icons.push_front(("rb", BLUE)); - } - if buttons.zl() { - icons.push_front(("zl", PURPLE)); - } - if buttons.zr() { - icons.push_front(("zr", PURPLE)); - } - if buttons.plus() { - icons.push_front(("plus", WHITE)); - } - if buttons.minus() { - icons.push_front(("minus", WHITE)); - } - if buttons.dpad_up() { - icons.push_front(("dpad_up", WHITE)); - } - if buttons.dpad_down() { - icons.push_front(("dpad_down", WHITE)); - } - if buttons.dpad_left() { - icons.push_front(("dpad_left", WHITE)); - } - if buttons.dpad_right() { - icons.push_front(("dpad_right", WHITE)); - } - - icons - } - - fn is_smash_different(&self, other: &InputLog) -> bool { - self.smash_inputs.buttons != other.smash_inputs.buttons - || self.smash_binned_lstick() != other.smash_binned_lstick() - || self.smash_binned_rstick() != other.smash_binned_rstick() - || (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status) - } - - fn smash_binned_lstick(&self) -> (DirectionStrength, f32) { - bin_stick_values(self.smash_inputs.lstick_x, self.smash_inputs.lstick_y) - } - - fn smash_binned_rstick(&self) -> (DirectionStrength, f32) { - bin_stick_values(self.smash_inputs.rstick_x, self.smash_inputs.rstick_y) - } - - fn is_raw_different(&self, other: &InputLog) -> bool { - self.raw_inputs.current_buttons != other.raw_inputs.current_buttons - || self.raw_binned_lstick() != other.raw_binned_lstick() - || self.raw_binned_rstick() != other.raw_binned_rstick() - || (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status) - } - - fn raw_binned_lstick(&self) -> (DirectionStrength, f32) { - let x = (self.raw_inputs.left_stick_x / STICK_CLAMP_MULTIPLIER) as i8; - let y = (self.raw_inputs.left_stick_y / STICK_CLAMP_MULTIPLIER) as i8; - bin_stick_values(x, y) - } - - fn raw_binned_rstick(&self) -> (DirectionStrength, f32) { - let x = (self.raw_inputs.right_stick_x / STICK_CLAMP_MULTIPLIER) as i8; - let y = (self.raw_inputs.right_stick_y / STICK_CLAMP_MULTIPLIER) as i8; - bin_stick_values(x, y) - } -} - -fn insert_in_place(array: &mut [T], value: T, index: usize) { - array[index..].rotate_right(1); - array[index] = value; -} - -fn insert_in_front(array: &mut [T], value: T) { - insert_in_place(array, value, 0); -} - -lazy_static! { - pub static ref P1_INPUT_LOGS: Mutex<[InputLog; NUM_LOGS]> = - Mutex::new([InputLog::default(); NUM_LOGS]); -} - -pub fn handle_final_input_mapping( - player_idx: i32, - controller_struct: &SomeControllerStruct, - out: *mut MappedInputs, -) { - unsafe { - if MENU.input_display == InputDisplay::None { - return; - } - - if QUICK_MENU_ACTIVE { - return; - } - - if player_idx == 0 { - let module_accessor = try_get_module_accessor(FighterId::Player); - if module_accessor.is_none() { - return; - } - let module_accessor = module_accessor.unwrap(); - - let current_frame = frame_counter::get_frame_count(*PER_LOG_FRAME_COUNTER); - let current_overall_frame = frame_counter::get_frame_count(*OVERALL_FRAME_COUNTER); - // We should always be counting - frame_counter::start_counting(*PER_LOG_FRAME_COUNTER); - frame_counter::start_counting(*OVERALL_FRAME_COUNTER); - - let potential_input_log = InputLog { - ttl: 600, - frames: 1, - overall_frame: current_overall_frame, - raw_inputs: *controller_struct.controller, - smash_inputs: *out, - status: StatusModule::status_kind(module_accessor), - fighter_kind: utility::get_kind(&mut *module_accessor), - }; - - let input_logs = &mut *P1_INPUT_LOGS.lock(); - let latest_input_log = input_logs.first_mut().unwrap(); - let prev_overall_frames = latest_input_log.overall_frame; - let prev_ttl = latest_input_log.ttl; - // Only update if we are on a new frame according to the latest log - let is_new_frame = prev_overall_frames != current_overall_frame; - if is_new_frame && latest_input_log.is_different(&potential_input_log) { - frame_counter::reset_frame_count(*PER_LOG_FRAME_COUNTER); - // We should count this frame already - frame_counter::tick_idx(*PER_LOG_FRAME_COUNTER); - insert_in_front(input_logs, potential_input_log); - let draw_log_base_idx = &mut *DRAW_LOG_BASE_IDX.data_ptr(); - *draw_log_base_idx = (*draw_log_base_idx + 1) % NUM_LOGS; - } else if is_new_frame { - *latest_input_log = potential_input_log; - latest_input_log.frames = std::cmp::min(current_frame, 99); - latest_input_log.ttl = prev_ttl; - } - - // Decrease TTL - for input_log in input_logs.iter_mut() { - if input_log.ttl > 0 && is_new_frame { - input_log.ttl -= 1; - } - } - } - } -} +use itertools::Itertools; +use once_cell::sync::Lazy; +use std::collections::VecDeque; + +use crate::common::{input::*, menu::QUICK_MENU_ACTIVE, try_get_module_accessor}; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use skyline::nn::ui2d::ResColor; +use smash::app::{lua_bind::*, utility}; +use training_mod_consts::{FighterId, InputDisplay, MENU}; + +use super::{frame_counter, input_record::STICK_CLAMP_MULTIPLIER}; + +const GREEN: ResColor = ResColor { + r: 22, + g: 156, + b: 0, + a: 0, +}; + +const RED: ResColor = ResColor { + r: 153, + g: 10, + b: 10, + a: 0, +}; + +const CYAN: ResColor = ResColor { + r: 0, + g: 255, + b: 255, + a: 0, +}; + +const BLUE: ResColor = ResColor { + r: 0, + g: 40, + b: 108, + a: 0, +}; + +const PURPLE: ResColor = ResColor { + r: 100, + g: 66, + b: 202, + a: 0, +}; + +pub const YELLOW: ResColor = ResColor { + r: 230, + g: 180, + b: 14, + a: 0, +}; + +pub const WHITE: ResColor = ResColor { + r: 255, + g: 255, + b: 255, + a: 0, +}; + +pub static PER_LOG_FRAME_COUNTER: Lazy = + Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset)); +pub static OVERALL_FRAME_COUNTER: Lazy = + Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset)); + +pub const NUM_LOGS: usize = 15; +pub static mut DRAW_LOG_BASE_IDX: Lazy> = Lazy::new(|| Mutex::new(0)); + +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum DirectionStrength { + None, + Weak, + // Strong, +} + +#[derive(Copy, Clone, Default)] +pub struct InputLog { + pub ttl: u32, + pub frames: u32, + pub overall_frame: u32, + pub raw_inputs: Controller, + pub smash_inputs: MappedInputs, + pub status: i32, + pub fighter_kind: i32, +} + +impl PartialEq for InputLog { + fn eq(&self, other: &Self) -> bool { + self.frames == other.frames && !self.is_different(other) + } +} +impl Eq for InputLog {} + +const WALK_THRESHOLD_X: i8 = 20; +const _DASH_THRESHOLD_X: i8 = 102; +const DEADZONE_THRESHOLD_Y: i8 = 30; +const _TAP_JUMP_THRESHOLD_Y: i8 = 90; + +fn bin_stick_values(x: i8, y: i8) -> (DirectionStrength, f32) { + ( + // TODO + DirectionStrength::Weak, + match (x, y) { + // X only + (x, y) if y.abs() < DEADZONE_THRESHOLD_Y => match x { + x if x > WALK_THRESHOLD_X => 0.0, + x if x < -WALK_THRESHOLD_X => 180.0, + _ => return (DirectionStrength::None, 0.0), + }, + // Y only + (x, y) if x.abs() < WALK_THRESHOLD_X => match y { + y if y > DEADZONE_THRESHOLD_Y => 90.0, + y if y < -DEADZONE_THRESHOLD_Y => 270.0, + _ => return (DirectionStrength::None, 0.0), + }, + // Positive Y + (x, y) if y > DEADZONE_THRESHOLD_Y => match x { + x if x > WALK_THRESHOLD_X => 45.0, + x if x < -WALK_THRESHOLD_X => 135.0, + _ => return (DirectionStrength::Weak, 90.0), + }, + // Negative Y + (x, y) if y < DEADZONE_THRESHOLD_Y => match x { + x if x > WALK_THRESHOLD_X => 315.0, + x if x < -WALK_THRESHOLD_X => 225.0, + _ => return (DirectionStrength::Weak, 270.0), + }, + _ => return (DirectionStrength::None, 0.0), + }, + ) +} + +impl InputLog { + pub fn is_different(&self, other: &InputLog) -> bool { + unsafe { + match MENU.input_display { + InputDisplay::SMASH => self.is_smash_different(other), + InputDisplay::RAW => self.is_raw_different(other), + InputDisplay::NONE => false, + _ => panic!("Invalid value in is_different: {}", MENU.input_display), + } + } + } + + pub fn binned_lstick(&self) -> (DirectionStrength, f32) { + unsafe { + match MENU.input_display { + InputDisplay::SMASH => self.smash_binned_lstick(), + InputDisplay::RAW => self.raw_binned_lstick(), + InputDisplay::NONE => panic!("Invalid input display to log"), + _ => panic!("Invalid value in binned_lstick: {}", MENU.input_display), + } + } + } + + pub fn binned_rstick(&self) -> (DirectionStrength, f32) { + unsafe { + match MENU.input_display { + InputDisplay::SMASH => self.smash_binned_rstick(), + InputDisplay::RAW => self.raw_binned_rstick(), + InputDisplay::NONE => panic!("Invalid input display to log"), + _ => panic!("Invalid value in binned_rstick: {}", MENU.input_display), + } + } + } + + pub fn button_icons(&self) -> VecDeque<(&str, ResColor)> { + unsafe { + match MENU.input_display { + InputDisplay::SMASH => self.smash_button_icons(), + InputDisplay::RAW => self.raw_button_icons(), + InputDisplay::NONE => panic!("Invalid input display to log"), + _ => unreachable!(), + } + } + } + + fn smash_button_icons(&self) -> VecDeque<(&str, ResColor)> { + self.smash_inputs + .buttons + .to_vec() + .iter() + .filter_map(|button| { + Some(match *button { + Buttons::ATTACK | Buttons::ATTACK_RAW => ("a", GREEN), + Buttons::SPECIAL | Buttons::SPECIAL_RAW2 => ("b", RED), + Buttons::JUMP => ("x", CYAN), + Buttons::GUARD | Buttons::GUARD_HOLD => ("lb", BLUE), + Buttons::CATCH => ("zr", PURPLE), + Buttons::STOCK_SHARE => ("plus", WHITE), + Buttons::APPEAL_HI => ("dpad_up", WHITE), + Buttons::APPEAL_LW => ("dpad_down", WHITE), + Buttons::APPEAL_SL => ("dpad_right", WHITE), + Buttons::APPEAL_SR => ("dpad_left", WHITE), + _ => return None, + }) + }) + .unique_by(|(s, _)| *s) + .collect::>() + } + + fn raw_button_icons(&self) -> VecDeque<(&str, ResColor)> { + let buttons = self.raw_inputs.current_buttons; + let mut icons = VecDeque::new(); + if buttons.a() { + icons.push_front(("a", GREEN)); + } + if buttons.b() { + icons.push_front(("b", RED)); + } + if buttons.x() { + icons.push_front(("x", CYAN)); + } + if buttons.y() { + icons.push_front(("y", CYAN)); + } + if buttons.l() || buttons.real_digital_l() { + icons.push_front(("lb", BLUE)); + } + if buttons.r() || buttons.real_digital_r() { + icons.push_front(("rb", BLUE)); + } + if buttons.zl() { + icons.push_front(("zl", PURPLE)); + } + if buttons.zr() { + icons.push_front(("zr", PURPLE)); + } + if buttons.plus() { + icons.push_front(("plus", WHITE)); + } + if buttons.minus() { + icons.push_front(("minus", WHITE)); + } + if buttons.dpad_up() { + icons.push_front(("dpad_up", WHITE)); + } + if buttons.dpad_down() { + icons.push_front(("dpad_down", WHITE)); + } + if buttons.dpad_left() { + icons.push_front(("dpad_left", WHITE)); + } + if buttons.dpad_right() { + icons.push_front(("dpad_right", WHITE)); + } + + icons + } + + fn is_smash_different(&self, other: &InputLog) -> bool { + self.smash_inputs.buttons != other.smash_inputs.buttons + || self.smash_binned_lstick() != other.smash_binned_lstick() + || self.smash_binned_rstick() != other.smash_binned_rstick() + || (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status) + } + + fn smash_binned_lstick(&self) -> (DirectionStrength, f32) { + bin_stick_values(self.smash_inputs.lstick_x, self.smash_inputs.lstick_y) + } + + fn smash_binned_rstick(&self) -> (DirectionStrength, f32) { + bin_stick_values(self.smash_inputs.rstick_x, self.smash_inputs.rstick_y) + } + + fn is_raw_different(&self, other: &InputLog) -> bool { + self.raw_inputs.current_buttons != other.raw_inputs.current_buttons + || self.raw_binned_lstick() != other.raw_binned_lstick() + || self.raw_binned_rstick() != other.raw_binned_rstick() + || (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status) + } + + fn raw_binned_lstick(&self) -> (DirectionStrength, f32) { + let x = (self.raw_inputs.left_stick_x / STICK_CLAMP_MULTIPLIER) as i8; + let y = (self.raw_inputs.left_stick_y / STICK_CLAMP_MULTIPLIER) as i8; + bin_stick_values(x, y) + } + + fn raw_binned_rstick(&self) -> (DirectionStrength, f32) { + let x = (self.raw_inputs.right_stick_x / STICK_CLAMP_MULTIPLIER) as i8; + let y = (self.raw_inputs.right_stick_y / STICK_CLAMP_MULTIPLIER) as i8; + bin_stick_values(x, y) + } +} + +fn insert_in_place(array: &mut [T], value: T, index: usize) { + array[index..].rotate_right(1); + array[index] = value; +} + +fn insert_in_front(array: &mut [T], value: T) { + insert_in_place(array, value, 0); +} + +lazy_static! { + pub static ref P1_INPUT_LOGS: Mutex<[InputLog; NUM_LOGS]> = + Mutex::new([InputLog::default(); NUM_LOGS]); +} + +pub fn handle_final_input_mapping( + player_idx: i32, + controller_struct: &SomeControllerStruct, + out: *mut MappedInputs, +) { + unsafe { + if MENU.input_display == InputDisplay::NONE { + return; + } + + if QUICK_MENU_ACTIVE { + return; + } + + if player_idx == 0 { + let module_accessor = try_get_module_accessor(FighterId::Player); + if module_accessor.is_none() { + return; + } + let module_accessor = module_accessor.unwrap(); + + let current_frame = frame_counter::get_frame_count(*PER_LOG_FRAME_COUNTER); + let current_overall_frame = frame_counter::get_frame_count(*OVERALL_FRAME_COUNTER); + // We should always be counting + frame_counter::start_counting(*PER_LOG_FRAME_COUNTER); + frame_counter::start_counting(*OVERALL_FRAME_COUNTER); + + let potential_input_log = InputLog { + ttl: 600, + frames: 1, + overall_frame: current_overall_frame, + raw_inputs: *controller_struct.controller, + smash_inputs: *out, + status: StatusModule::status_kind(module_accessor), + fighter_kind: utility::get_kind(&mut *module_accessor), + }; + + let input_logs = &mut *P1_INPUT_LOGS.lock(); + let latest_input_log = input_logs.first_mut().unwrap(); + let prev_overall_frames = latest_input_log.overall_frame; + let prev_ttl = latest_input_log.ttl; + // Only update if we are on a new frame according to the latest log + let is_new_frame = prev_overall_frames != current_overall_frame; + if is_new_frame && latest_input_log.is_different(&potential_input_log) { + frame_counter::reset_frame_count(*PER_LOG_FRAME_COUNTER); + // We should count this frame already + frame_counter::tick_idx(*PER_LOG_FRAME_COUNTER); + insert_in_front(input_logs, potential_input_log); + let draw_log_base_idx = &mut *DRAW_LOG_BASE_IDX.data_ptr(); + *draw_log_base_idx = (*draw_log_base_idx + 1) % NUM_LOGS; + } else if is_new_frame { + *latest_input_log = potential_input_log; + latest_input_log.frames = std::cmp::min(current_frame, 99); + latest_input_log.ttl = prev_ttl; + } + + // Decrease TTL + for input_log in input_logs.iter_mut() { + if input_log.ttl > 0 && is_new_frame { + input_log.ttl -= 1; + } + } + } + } +} diff --git a/src/training/input_record.rs b/src/training/input_record.rs index ba6ad6a..0e5fdc8 100644 --- a/src/training/input_record.rs +++ b/src/training/input_record.rs @@ -17,6 +17,7 @@ use crate::common::{ }; use crate::training::mash; use crate::training::ui::notifications::{clear_notifications, color_notification}; +use crate::{error, warn}; #[derive(PartialEq, Debug)] pub enum InputRecordState { @@ -102,17 +103,17 @@ unsafe fn should_mash_playback() { if is_in_hitstun(&mut *cpu_module_accessor) { // if we're in hitstun and want to enter the frame we start hitstop for SDI, start if we're in any damage status instantly - if MENU.hitstun_playback == HitstunPlayback::Instant { + if MENU.hitstun_playback == HitstunPlayback::INSTANT { should_playback = true; } // if we want to wait until we exit hitstop and begin flying away for shield art etc, start if we're not in hitstop - if MENU.hitstun_playback == HitstunPlayback::Hitstop + if MENU.hitstun_playback == HitstunPlayback::HITSTOP && !StopModule::is_stop(cpu_module_accessor) { should_playback = true; } // if we're in hitstun and want to wait till FAF to act, then we want to match our starting status to the correct transition term to see if we can hitstun cancel - if MENU.hitstun_playback == HitstunPlayback::Hitstun && can_transition(cpu_module_accessor) + if MENU.hitstun_playback == HitstunPlayback::HITSTUN && can_transition(cpu_module_accessor) { should_playback = true; } @@ -191,12 +192,12 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA let fighter_kind = utility::get_kind(module_accessor); let fighter_is_nana = fighter_kind == *FIGHTER_KIND_NANA; - CURRENT_RECORD_SLOT = MENU.recording_slot.into_idx(); + CURRENT_RECORD_SLOT = MENU.recording_slot.into_idx().unwrap_or(0); if entry_id_int == 0 && !fighter_is_nana { if button_config::combo_passes(button_config::ButtonCombo::InputPlayback) { playback(MENU.playback_button_slots.get_random().into_idx()); - } else if MENU.record_trigger.contains(RecordTrigger::COMMAND) + } else if MENU.record_trigger.contains(&RecordTrigger::COMMAND) && button_config::combo_passes(button_config::ButtonCombo::InputRecord) { lockout_record(); @@ -215,7 +216,7 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA // If we need to crop the recording for neutral input // INPUT_RECORD_FRAME must be > 0 to prevent bounding errors - if INPUT_RECORD == Record && MENU.recording_crop == OnOff::On && INPUT_RECORD_FRAME > 0 + if INPUT_RECORD == Record && MENU.recording_crop == OnOff::ON && INPUT_RECORD_FRAME > 0 { while INPUT_RECORD_FRAME > 0 && is_input_neutral(INPUT_RECORD_FRAME - 1) { // Discard frames at the end of the recording until the last frame with input @@ -227,7 +228,7 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA INPUT_RECORD_FRAME = 0; - if MENU.playback_loop == OnOff::On && INPUT_RECORD == Playback { + if MENU.playback_loop == OnOff::ON && INPUT_RECORD == Playback { playback(Some(CURRENT_PLAYBACK_SLOT)); } else { INPUT_RECORD = None; @@ -338,11 +339,11 @@ pub unsafe fn lockout_record() { // Returns whether we did playback pub unsafe fn playback(slot: Option) -> bool { if INPUT_RECORD == Pause { - println!("Tried to playback during lockout!"); + warn!("Tried to playback during lockout!"); return false; } if slot.is_none() { - println!("Tried to playback without a slot selected!"); + warn!("Tried to playback without a slot selected!"); return false; } let slot = slot.unwrap(); @@ -425,8 +426,6 @@ pub unsafe fn handle_final_input_mapping(player_idx: i32, out: *mut MappedInputs P1_FINAL_MAPPING.lock()[CURRENT_RECORD_SLOT][INPUT_RECORD_FRAME] = *out; *out = MappedInputs::empty(); // don't control player while recording - - //println!("Stored Player Input! Frame: {}", INPUT_RECORD_FRAME); } // Don't allow for player input during Lockout if POSSESSION == Lockout { @@ -468,7 +467,7 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) { INPUT_RECORD = Record; POSSESSION = Standby; } - Ordering::Less => println!("LOCKOUT_FRAME OUT OF BOUNDS"), + Ordering::Less => error!("LOCKOUT_FRAME OUT OF BOUNDS"), } } @@ -504,8 +503,6 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) { ); } - //println!("Overriding Cpu Player: {}, Frame: {}, BUFFER_FRAME: {}, STARTING_STATUS: {}, INPUT_RECORD: {:#?}, POSSESSION: {:#?}", controller_no, INPUT_RECORD_FRAME, BUFFER_FRAME, STARTING_STATUS, INPUT_RECORD, POSSESSION); - let mut saved_mapped_inputs = P1_FINAL_MAPPING.lock()[if INPUT_RECORD == Record { CURRENT_RECORD_SLOT } else { @@ -538,7 +535,6 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) { }; (*controller_data).clamped_lstick_x = clamped_lstick_x; (*controller_data).clamped_lstick_y = clamped_lstick_y; - //println!("CPU Buttons: {:#018b}", (*controller_data).buttons); // Keep counting frames, unless we're in standby waiting for an input, or are buffering an option // When buffering an option, we keep inputting the first frame of input during the buffer window diff --git a/src/training/ledge.rs b/src/training/ledge.rs index af61b7c..14fd7a4 100644 --- a/src/training/ledge.rs +++ b/src/training/ledge.rs @@ -58,7 +58,7 @@ fn roll_ledge_case() { fn get_ledge_option() -> Option { unsafe { let mut override_action: Option = None; - let regular_action = if MENU.mash_triggers.contains(MashTrigger::LEDGE) { + let regular_action = if MENU.mash_triggers.contains(&MashTrigger::LEDGE) { Some(MENU.mash_state.get_random()) } else { None diff --git a/src/training/mash.rs b/src/training/mash.rs index d54e8b0..d2d7448 100644 --- a/src/training/mash.rs +++ b/src/training/mash.rs @@ -80,14 +80,13 @@ pub fn buffer_action(action: Action) { unsafe { // exit playback if we want to perform mash actions out of it // TODO: Figure out some way to deal with trying to playback into another playback - if MENU.playback_mash == OnOff::On + if MENU.playback_mash == OnOff::ON && input_record::is_playback() && !input_record::is_recording() && !input_record::is_standby() && !is_playback_queued() && !action.is_playback() { - //println!("Stopping mash playback for menu option!"); // if we don't want to leave playback on mash actions, then don't perform the mash if input_record::is_playback() { input_record::stop_playback(); @@ -221,7 +220,7 @@ unsafe fn get_buffered_action( let action = MENU.tech_action_override.get_random(); if action != Action::empty() { Some(action) - } else if MENU.mash_triggers.contains(MashTrigger::TECH) { + } else if MENU.mash_triggers.contains(&MashTrigger::TECH) { Some(MENU.mash_state.get_random()) } else { None @@ -230,7 +229,7 @@ unsafe fn get_buffered_action( let action = MENU.clatter_override.get_random(); if action != Action::empty() { Some(action) - } else if MENU.mash_triggers.contains(MashTrigger::CLATTER) { + } else if MENU.mash_triggers.contains(&MashTrigger::CLATTER) { Some(MENU.mash_state.get_random()) } else { None @@ -241,7 +240,7 @@ unsafe fn get_buffered_action( let action = MENU.tumble_override.get_random(); if action != Action::empty() { Some(action) - } else if MENU.mash_triggers.contains(MashTrigger::TUMBLE) { + } else if MENU.mash_triggers.contains(&MashTrigger::TUMBLE) { Some(MENU.mash_state.get_random()) } else { None @@ -250,7 +249,7 @@ unsafe fn get_buffered_action( let action = MENU.hitstun_override.get_random(); if action != Action::empty() { Some(action) - } else if MENU.mash_triggers.contains(MashTrigger::HIT) { + } else if MENU.mash_triggers.contains(&MashTrigger::HIT) { Some(MENU.mash_state.get_random()) } else { None @@ -259,7 +258,7 @@ unsafe fn get_buffered_action( let action = MENU.parry_override.get_random(); if action != Action::empty() { Some(action) - } else if MENU.mash_triggers.contains(MashTrigger::PARRY) { + } else if MENU.mash_triggers.contains(&MashTrigger::PARRY) { Some(MENU.mash_state.get_random()) } else { None @@ -268,7 +267,7 @@ unsafe fn get_buffered_action( let action = MENU.footstool_override.get_random(); if action != Action::empty() { Some(action) - } else if MENU.mash_triggers.contains(MashTrigger::FOOTSTOOL) { + } else if MENU.mash_triggers.contains(&MashTrigger::FOOTSTOOL) { Some(MENU.mash_state.get_random()) } else { None @@ -277,7 +276,7 @@ unsafe fn get_buffered_action( let action = MENU.trump_override.get_random(); if action != Action::empty() { Some(action) - } else if MENU.mash_triggers.contains(MashTrigger::TRUMP) { + } else if MENU.mash_triggers.contains(&MashTrigger::TRUMP) { Some(MENU.mash_state.get_random()) } else { None @@ -286,20 +285,20 @@ unsafe fn get_buffered_action( let action = MENU.landing_override.get_random(); if action != Action::empty() { Some(action) - } else if MENU.mash_triggers.contains(MashTrigger::LANDING) { + } else if MENU.mash_triggers.contains(&MashTrigger::LANDING) { Some(MENU.mash_state.get_random()) } else { None } - } else if (MENU.mash_triggers.contains(MashTrigger::GROUNDED) && is_grounded(module_accessor)) - || (MENU.mash_triggers.contains(MashTrigger::AIRBORNE) && is_airborne(module_accessor)) - || (MENU.mash_triggers.contains(MashTrigger::DISTANCE_CLOSE) + } else if (MENU.mash_triggers.contains(&MashTrigger::GROUNDED) && is_grounded(module_accessor)) + || (MENU.mash_triggers.contains(&MashTrigger::AIRBORNE) && is_airborne(module_accessor)) + || (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_CLOSE) && fighter_distance < DISTANCE_CLOSE_THRESHOLD) - || (MENU.mash_triggers.contains(MashTrigger::DISTANCE_MID) + || (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_MID) && fighter_distance < DISTANCE_MID_THRESHOLD) - || (MENU.mash_triggers.contains(MashTrigger::DISTANCE_FAR) + || (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_FAR) && fighter_distance < DISTANCE_FAR_THRESHOLD) - || MENU.mash_triggers.contains(MashTrigger::ALWAYS) + || MENU.mash_triggers.contains(&MashTrigger::ALWAYS) { Some(MENU.mash_state.get_random()) } else { diff --git a/src/training/mod.rs b/src/training/mod.rs index 2cd3fc6..98b29f5 100644 --- a/src/training/mod.rs +++ b/src/training/mod.rs @@ -770,7 +770,7 @@ pub unsafe fn handle_reused_ui( // If Little Mac is in the game and we're buffing him, set the meter to 100 if (player_fighter_kind == *FIGHTER_KIND_LITTLEMAC || cpu_fighter_kind == *FIGHTER_KIND_LITTLEMAC) - && MENU.buff_state.to_vec().contains(&BuffOption::KO) + && MENU.buff_state.contains(&BuffOption::KO) { param_2 = 100; } diff --git a/src/training/save_states.rs b/src/training/save_states.rs index 263fbb2..75bf28c 100644 --- a/src/training/save_states.rs +++ b/src/training/save_states.rs @@ -143,7 +143,10 @@ pub struct SaveStateSlots { const NUM_SAVE_STATE_SLOTS: usize = 5; // I actually had to do it this way, a simple load-from-file in main() caused crashes. lazy_static::lazy_static! { - static ref SAVE_STATE_SLOTS : Mutex = Mutex::new(load_from_file()); + static ref SAVE_STATE_SLOTS : Mutex = Mutex::new({ + info!("Initialized lazy_static: SAVE_STATE_SLOTS"); + load_from_file() + }); } pub fn load_from_file() -> SaveStateSlots { @@ -233,7 +236,7 @@ unsafe fn get_slot() -> usize { if random_slot != SaveStateSlot::empty() { RANDOM_SLOT } else { - MENU.save_state_slot.as_idx() as usize + MENU.save_state_slot.into_idx().unwrap_or(0) } } @@ -253,9 +256,13 @@ pub unsafe fn is_loading() -> bool { pub unsafe fn should_mirror() -> f32 { match MENU.save_state_mirroring { - SaveStateMirroring::None => 1.0, - SaveStateMirroring::Alternate => -1.0 * MIRROR_STATE, - SaveStateMirroring::Random => ([-1.0, 1.0])[get_random_int(2) as usize], + SaveStateMirroring::NONE => 1.0, + SaveStateMirroring::ALTERNATE => -1.0 * MIRROR_STATE, + SaveStateMirroring::RANDOM => ([-1.0, 1.0])[get_random_int(2) as usize], + _ => panic!( + "Invalid value in should_mirror: {}", + MENU.save_state_mirroring + ), } } @@ -409,7 +416,7 @@ pub unsafe fn on_death(fighter_kind: i32, module_accessor: &mut app::BattleObjec } pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) { - if MENU.save_state_enable == OnOff::Off { + if MENU.save_state_enable == OnOff::OFF { return; } @@ -441,7 +448,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) .contains(&fighter_kind); // Reset state - let autoload_reset = MENU.save_state_autoload == OnOff::On + let autoload_reset = MENU.save_state_autoload == OnOff::ON && save_state.state == NoAction && is_dead(module_accessor); let mut triggered_reset: bool = false; @@ -452,7 +459,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) if save_state.state == NoAction { let random_slot = MENU.randomize_slots.get_random(); let slot = if random_slot != SaveStateSlot::empty() { - RANDOM_SLOT = random_slot.as_idx(); + RANDOM_SLOT = random_slot.into_idx().unwrap_or(0); RANDOM_SLOT } else { selected_slot @@ -578,7 +585,10 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) set_damage(module_accessor, pct); } SaveDamage::DEFAULT => {} - _ => {} + _ => panic!( + "Invalid value in save_states()::save_damage_player: {}", + MENU.save_damage_player + ), } } else { match MENU.save_damage_cpu { @@ -594,12 +604,15 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) set_damage(module_accessor, pct); } SaveDamage::DEFAULT => {} - _ => {} + _ => panic!( + "Invalid value in save_states()::save_damage_cpu: {}", + MENU.save_damage_cpu + ), } } // Set to held item - if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::None { + if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::NONE { apply_item(MENU.character_item); } @@ -648,7 +661,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) } // if we're recording on state load, record - if MENU.record_trigger.contains(RecordTrigger::SAVESTATE) { + if MENU.record_trigger.contains(&RecordTrigger::SAVESTATE) { input_record::lockout_record(); return; } @@ -693,8 +706,8 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) if button_config::combo_passes(button_config::ButtonCombo::SaveState) { // Don't begin saving state if Nana's delayed input is captured MIRROR_STATE = 1.0; - save_state_player(MENU.save_state_slot.as_idx() as usize).state = Save; - save_state_cpu(MENU.save_state_slot.as_idx() as usize).state = Save; + save_state_player(MENU.save_state_slot.into_idx().unwrap_or(0)).state = Save; + save_state_cpu(MENU.save_state_slot.into_idx().unwrap_or(0)).state = Save; notifications::clear_notifications("Save State"); notifications::notification( "Save State".to_string(), diff --git a/src/training/shield.rs b/src/training/shield.rs index 43e9955..7d3d04e 100644 --- a/src/training/shield.rs +++ b/src/training/shield.rs @@ -107,7 +107,7 @@ pub unsafe fn get_param_float( return None; } - if MENU.shield_state != Shield::None { + if MENU.shield_state != Shield::NONE { handle_oos_offset(module_accessor); } @@ -121,8 +121,8 @@ fn handle_shield_decay(param_type: u64, param_hash: u64) -> Option { menu_state = MENU.shield_state; } - if menu_state != Shield::Infinite - && menu_state != Shield::Constant + if menu_state != Shield::INFINITE + && menu_state != Shield::CONSTANT && !should_pause_shield_decay() { return None; @@ -161,7 +161,7 @@ pub unsafe fn param_installer() { CACHED_SHIELD_DAMAGE_MUL = Some(common_params.shield_damage_mul); } - if is_training_mode() && (MENU.shield_state == Shield::Infinite) { + if is_training_mode() && (MENU.shield_state == Shield::INFINITE) { // if you are in training mode and have infinite shield enabled, // set the game's shield_damage_mul to 0.0 common_params.shield_damage_mul = 0.0; @@ -192,7 +192,7 @@ pub fn should_hold_shield(module_accessor: &mut app::BattleObjectModuleAccessor) // We should hold shield if the state requires it if unsafe { save_states::is_loading() } - || ![Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) + || ![Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) { return false; } @@ -223,7 +223,7 @@ unsafe fn mod_handle_sub_guard_cont(fighter: &mut L2CFighterCommon) { } // Enable shield decay - if MENU.shield_state == Shield::Hold { + if MENU.shield_state == Shield::HOLD { set_shield_decay(true); } @@ -245,7 +245,7 @@ unsafe fn mod_handle_sub_guard_cont(fighter: &mut L2CFighterCommon) { return; } - if MENU.mash_triggers.contains(MashTrigger::SHIELDSTUN) { + if MENU.mash_triggers.contains(&MashTrigger::SHIELDSTUN) { if MENU.shieldstun_override == Action::empty() { mash::external_buffer_menu_mash(MENU.mash_state.get_random()) } else { @@ -360,7 +360,7 @@ fn needs_oos_handling_drop_shield() -> bool { shield_state = &MENU.shield_state; } // If we're supposed to be holding shield, let airdodge make us drop shield - if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) { + if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) { suspend_shield(Action::AIR_DODGE); } return true; @@ -373,7 +373,7 @@ fn needs_oos_handling_drop_shield() -> bool { shield_state = &MENU.shield_state; } // If we're supposed to be holding shield, let airdodge make us drop shield - if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) { + if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) { suspend_shield(Action::AIR_DODGE); } return true; @@ -385,7 +385,7 @@ fn needs_oos_handling_drop_shield() -> bool { shield_state = &MENU.shield_state; } // Don't drop shield on shield hit if we're supposed to be holding shield - if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) { + if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) { return false; } return true; diff --git a/src/training/tech.rs b/src/training/tech.rs index f5fc20a..2af4dba 100644 --- a/src/training/tech.rs +++ b/src/training/tech.rs @@ -123,7 +123,7 @@ unsafe fn handle_grnd_tech( } _ => false, }; - if do_tech && MENU.mash_triggers.contains(MashTrigger::TECH) { + if do_tech && MENU.mash_triggers.contains(&MashTrigger::TECH) { if MENU.tech_action_override == Action::empty() { mash::external_buffer_menu_mash(MENU.mash_state.get_random()) } else { @@ -170,7 +170,7 @@ unsafe fn handle_wall_tech( } _ => false, }; - if do_tech && MENU.mash_triggers.contains(MashTrigger::TECH) { + if do_tech && MENU.mash_triggers.contains(&MashTrigger::TECH) { if MENU.tech_action_override == Action::empty() { mash::external_buffer_menu_mash(MENU.mash_state.get_random()) } else { @@ -205,7 +205,7 @@ unsafe fn handle_ceil_tech( *status_kind = FIGHTER_STATUS_KIND_PASSIVE_CEIL.as_lua_int(); *unk = LUA_TRUE; - if MENU.mash_triggers.contains(MashTrigger::TECH) { + if MENU.mash_triggers.contains(&MashTrigger::TECH) { if MENU.tech_action_override == Action::empty() { mash::external_buffer_menu_mash(MENU.mash_state.get_random()) } else { @@ -277,7 +277,7 @@ pub unsafe fn get_command_flag_cat(module_accessor: &mut BattleObjectModuleAcces if requested_status != 0 { StatusModule::change_status_force(module_accessor, requested_status, true); - if MENU.mash_triggers.contains(MashTrigger::MISTECH) { + if MENU.mash_triggers.contains(&MashTrigger::MISTECH) { if MENU.tech_action_override == Action::empty() { mash::external_buffer_menu_mash(MENU.mash_state.get_random()) } else { @@ -347,7 +347,7 @@ unsafe fn get_snake_laydown_lockout_time(module_accessor: &mut BattleObjectModul } pub unsafe fn hide_tech() { - if !is_training_mode() || MENU.tech_hide == OnOff::Off { + if !is_training_mode() || MENU.tech_hide == OnOff::OFF { return; } let module_accessor = get_module_accessor(FighterId::CPU); @@ -409,7 +409,7 @@ pub unsafe fn handle_fighter_req_quake_pos( return original!()(camera_module, quake_kind); } let status = StatusModule::status_kind(module_accessor); - if status == FIGHTER_STATUS_KIND_DOWN && MENU.tech_hide == OnOff::On { + if status == FIGHTER_STATUS_KIND_DOWN && MENU.tech_hide == OnOff::ON { // We're hiding techs, prevent mistech quake from giving away missed tech return original!()(camera_module, *CAMERA_QUAKE_KIND_NONE); } @@ -452,7 +452,7 @@ pub struct CameraManager { unsafe fn set_fixed_camera_values() { let camera_manager = get_camera_manager(); - if MENU.tech_hide == OnOff::Off { + if MENU.tech_hide == OnOff::OFF { // Use Stage's Default Values for fixed Camera camera_manager.fixed_camera_center = DEFAULT_FIXED_CAM_CENTER; } else { diff --git a/src/training/ui/input_log.rs b/src/training/ui/input_log.rs index 0f209d8..49e8cac 100644 --- a/src/training/ui/input_log.rs +++ b/src/training/ui/input_log.rs @@ -193,9 +193,9 @@ pub unsafe fn draw(root_pane: &Pane) { .find_pane_by_name_recursive("TrModInputLog") .unwrap(); logs_pane.set_visible( - !QUICK_MENU_ACTIVE && !VANILLA_MENU_ACTIVE && MENU.input_display != InputDisplay::None, + !QUICK_MENU_ACTIVE && !VANILLA_MENU_ACTIVE && MENU.input_display != InputDisplay::NONE, ); - if MENU.input_display == InputDisplay::None { + if MENU.input_display == InputDisplay::NONE { return; } diff --git a/src/training/ui/menu.rs b/src/training/ui/menu.rs index 7a1e2cc..81c69fd 100644 --- a/src/training/ui/menu.rs +++ b/src/training/ui/menu.rs @@ -3,19 +3,18 @@ use std::collections::HashMap; use lazy_static::lazy_static; use skyline::nn::ui2d::*; use smash::ui2d::{SmashPane, SmashTextBox}; -use training_mod_tui::gauge::GaugeState; -use training_mod_tui::{App, AppPage, NUM_LISTS}; +use training_mod_tui::{ + App, AppPage, ConfirmationState, SliderState, NX_SUBMENU_COLUMNS, NX_SUBMENU_ROWS, +}; use crate::common::menu::{MENU_CLOSE_FRAME_COUNTER, MENU_CLOSE_WAIT_FRAMES, MENU_RECEIVED_INPUT}; use crate::training::frame_counter; use crate::{common, common::menu::QUICK_MENU_ACTIVE, input::*}; +use training_mod_consts::TOGGLE_MAX; use super::fade_out; use super::set_icon_text; -pub static NUM_MENU_TEXT_OPTIONS: usize = 32; -pub static _NUM_MENU_TABS: usize = 3; - const BG_LEFT_ON_WHITE_COLOR: ResColor = ResColor { r: 0, g: 28, @@ -78,171 +77,188 @@ lazy_static! { ]); } -unsafe fn render_submenu_page(app: &App, root_pane: &Pane) { - let tab_selected = app.tab_selected(); - let tab = app.menu_items.get(tab_selected).unwrap(); - let submenu_ids = app.submenu_ids(); - - (0..NUM_MENU_TEXT_OPTIONS) - // Valid options in this submenu - .filter_map(|idx| tab.idx_to_list_idx_opt(idx)) - .for_each(|(list_section, list_idx)| { - let menu_button_row = root_pane - .find_pane_by_name_recursive(format!("TrModMenuButtonRow{list_idx}").as_str()) - .unwrap(); - menu_button_row.set_visible(true); - - let menu_button = menu_button_row - .find_pane_by_name_recursive(format!("Button{list_section}").as_str()) - .unwrap(); - - let title_text = menu_button - .find_pane_by_name_recursive("TitleTxt") - .unwrap() - .as_textbox(); - - let title_bg = menu_button - .find_pane_by_name_recursive("TitleBg") - .unwrap() - .as_picture(); - - let title_bg_material = &mut *title_bg.material; - - let list = &tab.lists[list_section]; - let submenu = &list.items[list_idx]; - let is_selected = list.state.selected().filter(|s| *s == list_idx).is_some(); - - title_text.set_text_string(submenu.submenu_title.as_str()); - - // In the actual 'layout.arc' file, every icon image is stacked - // into a single container pane, with each image directly on top of another. - // Hide all icon images, and strategically mark the icon that - // corresponds with a particular button to be visible. - submenu_ids.iter().for_each(|id| { - let pane = menu_button.find_pane_by_name_recursive(id); - if let Some(p) = pane { - p.set_visible(id == &submenu.submenu_id); - } - }); - - menu_button - .find_pane_by_name_recursive("check") - .unwrap() - .set_visible(false); - - if is_selected { - root_pane - .find_pane_by_name_recursive("FooterTxt") - .unwrap() - .as_textbox() - .set_text_string(submenu.help_text.as_str()); - - title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); - title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); - - title_text.text_shadow_enable(true); - title_text.text_outline_enable(true); - - title_text.set_color(255, 255, 255, 255); - } else { - title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); - title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); - - title_text.text_shadow_enable(false); - title_text.text_outline_enable(false); - - title_text.set_color(178, 199, 211, 255); - } - - menu_button.set_visible(true); - menu_button - .find_pane_by_name_recursive("Icon") - .unwrap() - .set_visible(true); - }); -} - -unsafe fn render_toggle_page(app: &App, root_pane: &Pane) { - let (_title, _help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states(); - (0..sub_menu_str_lists.len()).for_each(|list_section| { - let sub_menu_str = sub_menu_str_lists[list_section].0.clone(); - let sub_menu_state = &mut sub_menu_str_lists[list_section].1; - sub_menu_str - .iter() - .enumerate() - .for_each(|(list_idx, (checked, name))| { - let menu_button_row = root_pane - .find_pane_by_name_recursive(format!("TrModMenuButtonRow{list_idx}").as_str()) - .unwrap(); - menu_button_row.set_visible(true); - +unsafe fn render_submenu_page(app: &mut App, root_pane: &Pane) { + let tabs_clone = app.tabs.clone(); // Need this to avoid double-borrow later on + let tab = app.selected_tab(); + for row in 0..NX_SUBMENU_ROWS { + let menu_button_row = root_pane + .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str()) + .unwrap(); + menu_button_row.set_visible(true); + for col in 0..NX_SUBMENU_COLUMNS { + if let Some(submenu) = tab.submenus.get(row, col) { + // Find all the panes we need to modify let menu_button = menu_button_row - .find_pane_by_name_recursive(format!("Button{list_section}").as_str()) + .find_pane_by_name_recursive(format!("Button{col}").as_str()) .unwrap(); - menu_button.set_visible(true); - let title_text = menu_button .find_pane_by_name_recursive("TitleTxt") .unwrap() .as_textbox(); - let title_bg = menu_button .find_pane_by_name_recursive("TitleBg") .unwrap() .as_picture(); + let title_bg_material = &mut *title_bg.material; + let is_selected = row == tab.submenus.state.selected_row().unwrap() + && col == tab.submenus.state.selected_col().unwrap(); - let is_selected = sub_menu_state - .selected() - .filter(|s| *s == list_idx) - .is_some(); + // Set Pane Visibility + title_text.set_text_string(submenu.title); - let submenu_ids = app.submenu_ids(); + // In the actual 'layout.arc' file, every icon image is stacked + // into a single container pane, with each image directly on top of another. + // Hide all icon images, and strategically mark the icon that + // corresponds with a particular button to be visible. - submenu_ids.iter().for_each(|id| { - let pane = menu_button.find_pane_by_name_recursive(id); - if let Some(p) = pane { - p.set_visible(false); + for t in tabs_clone.iter() { + for s in t.submenus.iter() { + let pane = menu_button.find_pane_by_name_recursive(s.id); + if let Some(p) = pane { + p.set_visible(s.id == submenu.id); + } } - }); + } - title_text.set_text_string(name); menu_button .find_pane_by_name_recursive("check") .unwrap() - .set_visible(true); + .set_visible(false); + for value in 1..=TOGGLE_MAX { + if let Some(pane) = + menu_button.find_pane_by_name_recursive(format!("{}", value).as_str()) + { + pane.set_visible(false); + } else { + break; + } + } + + if is_selected { + // Help text + root_pane + .find_pane_by_name_recursive("FooterTxt") + .unwrap() + .as_textbox() + .set_text_string(submenu.help_text); + + title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); + title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); + title_text.text_shadow_enable(true); + title_text.text_outline_enable(true); + title_text.set_color(255, 255, 255, 255); + } else { + title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); + title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); + title_text.text_shadow_enable(false); + title_text.text_outline_enable(false); + title_text.set_color(178, 199, 211, 255); + } + menu_button.set_visible(true); menu_button .find_pane_by_name_recursive("Icon") .unwrap() - .set_visible(*checked); + .set_visible(true); + } + } + } +} +unsafe fn render_toggle_page(app: &mut App, root_pane: &Pane) { + let tabs_clone = app.tabs.clone(); // Need this to avoid double-borrow later on + let submenu = app.selected_submenu(); + // If the options can only be toggled on or off, then use the check icon + // instead of the number icons + let use_check_icon = submenu.toggles.get(0, 0).unwrap().max == 1; + for row in 0..NX_SUBMENU_ROWS { + let menu_button_row = root_pane + .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str()) + .unwrap(); + menu_button_row.set_visible(true); + for col in 0..NX_SUBMENU_COLUMNS { + if let Some(toggle) = submenu.toggles.get(row, col) { + let menu_button = menu_button_row + .find_pane_by_name_recursive(format!("Button{col}").as_str()) + .unwrap(); + menu_button.set_visible(true); + let title_text = menu_button + .find_pane_by_name_recursive("TitleTxt") + .unwrap() + .as_textbox(); + let title_bg = menu_button + .find_pane_by_name_recursive("TitleBg") + .unwrap() + .as_picture(); let title_bg_material = &mut *title_bg.material; + let is_selected = row == submenu.toggles.state.selected_row().unwrap() + && col == submenu.toggles.state.selected_col().unwrap(); if is_selected { title_text.text_shadow_enable(true); title_text.text_outline_enable(true); - title_text.set_color(255, 255, 255, 255); - title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); } else { title_text.text_shadow_enable(false); title_text.text_outline_enable(false); - title_text.set_color(178, 199, 211, 255); - title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); } - }); - }); + + // Hide all submenu icons, since we're not on the submenu page + for t in tabs_clone.iter() { + for s in t.submenus.iter() { + let pane = menu_button.find_pane_by_name_recursive(s.id); + if let Some(p) = pane { + p.set_visible(false); + } + } + } + + title_text.set_text_string(toggle.title); + + if use_check_icon { + menu_button + .find_pane_by_name_recursive("check") + .unwrap() + .set_visible(true); + + menu_button + .find_pane_by_name_recursive("Icon") + .unwrap() + .set_visible(toggle.value > 0); + } else { + menu_button + .find_pane_by_name_recursive("check") + .unwrap() + .set_visible(false); + menu_button + .find_pane_by_name_recursive("Icon") + .unwrap() + .set_visible(toggle.value > 0); + + // Note there's no pane for 0 + for value in 1..=toggle.max { + let err_msg = format!("Could not find pane with name {}", value); + menu_button + .find_pane_by_name_recursive(format!("{}", value).as_str()) + .expect(&err_msg) + .set_visible(value == toggle.value); + } + } + } + } + } } -unsafe fn render_slider_page(app: &App, root_pane: &Pane) { - let (title, _help_text, gauge_vals) = app.sub_menu_strs_for_slider(); - let selected_min = gauge_vals.selected_min; - let selected_max = gauge_vals.selected_max; +unsafe fn render_slider_page(app: &mut App, root_pane: &Pane) { + let submenu = app.selected_submenu(); + let slider = submenu.slider.unwrap(); + let selected_min = slider.lower; + let selected_max = slider.upper; let slider_pane = root_pane .find_pane_by_name_recursive("TrModSlider") @@ -257,7 +273,7 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) { .find_pane_by_name_recursive("Header") .unwrap() .as_textbox(); - header.set_text_string(title.as_str()); + header.set_text_string(submenu.title); let min_button = slider_pane .find_pane_by_name_recursive("MinButton") .unwrap() @@ -292,8 +308,8 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) { .as_textbox(); min_title_text.set_text_string("Min"); - match gauge_vals.state { - GaugeState::MinHover | GaugeState::MinSelected => { + match slider.state { + SliderState::LowerHover | SliderState::LowerSelected => { min_title_text.text_shadow_enable(true); min_title_text.text_outline_enable(true); min_title_text.set_color(255, 255, 255, 255); @@ -306,8 +322,8 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) { } max_title_text.set_text_string("Max"); - match gauge_vals.state { - GaugeState::MaxHover | GaugeState::MaxSelected => { + match slider.state { + SliderState::UpperHover | SliderState::UpperSelected => { max_title_text.text_shadow_enable(true); max_title_text.text_outline_enable(true); max_title_text.set_color(255, 255, 255, 255); @@ -323,9 +339,9 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) { max_value_text.set_text_string(&format!("{selected_max}")); let min_title_bg_material = &mut *min_title_bg.as_picture().material; - let min_colors = match gauge_vals.state { - GaugeState::MinHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR), - GaugeState::MinSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR), + let min_colors = match slider.state { + SliderState::LowerHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR), + SliderState::LowerSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR), _ => (BG_LEFT_OFF_WHITE_COLOR, BG_LEFT_OFF_BLACK_COLOR), }; @@ -333,9 +349,9 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) { min_title_bg_material.set_black_res_color(min_colors.1); let max_title_bg_material = &mut *max_title_bg.as_picture().material; - let max_colors = match gauge_vals.state { - GaugeState::MaxHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR), - GaugeState::MaxSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR), + let max_colors = match slider.state { + SliderState::UpperHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR), + SliderState::UpperSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR), _ => (BG_LEFT_OFF_WHITE_COLOR, BG_LEFT_OFF_BLACK_COLOR), }; @@ -352,6 +368,91 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) { }); } +unsafe fn render_confirmation_page(app: &mut App, root_pane: &Pane) { + let show_row = 3; // Row in the middle of the page + let show_cols = [1, 2]; // Columns in the middle of the page + let no_col = show_cols[0]; // Left + let yes_col = show_cols[1]; // Right + let help_text = match app.confirmation_return_page { + AppPage::TOGGLE | AppPage::SLIDER => { + "Are you sure you want to reset the current setting to the defaults?" + } + AppPage::SUBMENU => "Are you sure you want to reset ALL settings to the defaults?", + _ => "", // Shouldn't ever get this case, but don't panic if we do + }; + + // Set help text + root_pane + .find_pane_by_name_recursive("FooterTxt") + .unwrap() + .as_textbox() + .set_text_string(help_text); + + // Show only the buttons that we care about + for row in 0..NX_SUBMENU_ROWS { + let should_show_row = row == show_row; + let menu_button_row = root_pane + .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str()) + .unwrap(); + menu_button_row.set_visible(should_show_row); + if should_show_row { + for col in 0..NX_SUBMENU_COLUMNS { + let should_show_col = show_cols.contains(&col); + let menu_button = menu_button_row + .find_pane_by_name_recursive(format!("Button{col}").as_str()) + .unwrap(); + menu_button.set_visible(should_show_col); + if should_show_col { + let title_text = menu_button + .find_pane_by_name_recursive("TitleTxt") + .unwrap() + .as_textbox(); + let title_bg = menu_button + .find_pane_by_name_recursive("TitleBg") + .unwrap() + .as_picture(); + let title_bg_material = &mut *title_bg.material; + + if col == no_col { + title_text.set_text_string("No"); + } else if col == yes_col { + title_text.set_text_string("Yes"); + } + let is_selected = (col == no_col + && app.confirmation_state == ConfirmationState::HoverNo) + || (col == yes_col + && app.confirmation_state == ConfirmationState::HoverYes); + + if is_selected { + title_text.text_shadow_enable(true); + title_text.text_outline_enable(true); + title_text.set_color(255, 255, 255, 255); + title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); + title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); + } else { + title_text.text_shadow_enable(false); + title_text.text_outline_enable(false); + title_text.set_color(178, 199, 211, 255); + title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); + title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); + } + + // Hide all submenu icons, since we're not on the submenu page + // TODO: Do we want to show the check on "Yes" and a red "X" on "No?" + for t in app.tabs.iter() { + for s in t.submenus.iter() { + let pane = menu_button.find_pane_by_name_recursive(s.id); + if let Some(p) = pane { + p.set_visible(false); + } + } + } + } + } + } + } +} + pub unsafe fn draw(root_pane: &Pane) { // Determine if we're in the menu by seeing if the "help" footer has // begun moving upward. It starts at -80 and moves to 0 over 10 frames @@ -388,25 +489,24 @@ pub unsafe fn draw(root_pane: &Pane) { } // Make all invisible first - (0..NUM_MENU_TEXT_OPTIONS).for_each(|idx| { - let col_idx = idx % NUM_LISTS; - let row_idx = idx / NUM_LISTS; + for row_idx in 0..NX_SUBMENU_ROWS { + for col_idx in 0..NX_SUBMENU_COLUMNS { + let menu_button_row = root_pane + .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row_idx}").as_str()) + .unwrap(); + menu_button_row.set_visible(false); - let menu_button_row = root_pane - .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row_idx}").as_str()) - .unwrap(); - menu_button_row.set_visible(false); + let menu_button = menu_button_row + .find_pane_by_name_recursive(format!("Button{col_idx}").as_str()) + .unwrap(); + menu_button.set_visible(false); - let menu_button = menu_button_row - .find_pane_by_name_recursive(format!("Button{col_idx}").as_str()) - .unwrap(); - menu_button.set_visible(false); - - menu_button - .find_pane_by_name_recursive("ValueTxt") - .unwrap() - .set_visible(false); - }); + menu_button + .find_pane_by_name_recursive("ValueTxt") + .unwrap() + .set_visible(false); + } + } // Make normal training panes invisible if we're active // InfluencedAlpha means "Should my children panes' alpha be influenced by mine, as the parent?" @@ -423,21 +523,20 @@ pub unsafe fn draw(root_pane: &Pane) { // Update menu display // Grabbing lock as read-only, essentially - let app = &*crate::common::menu::QUICK_MENU_APP.data_ptr(); + // We don't really need to change anything, but get_before_selected requires &mut self + let app = &mut *crate::common::menu::QUICK_MENU_APP.data_ptr(); - let app_tabs = &app.tabs.items; - let tab_selected = app.tabs.state.selected().unwrap(); - let prev_tab = if tab_selected == 0 { - app_tabs.len() - 1 - } else { - tab_selected - 1 - }; - let next_tab = if tab_selected == app_tabs.len() - 1 { - 0 - } else { - tab_selected + 1 - }; - let tab_titles = [prev_tab, tab_selected, next_tab].map(|idx| app_tabs[idx].clone()); + let tab_titles = [ + app.tabs + .get_before_selected() + .expect("No tab selected!") + .title, + app.tabs.get_selected().expect("No tab selected!").title, + app.tabs + .get_after_selected() + .expect("No tab selected!") + .title, + ]; let is_gcc = (*common::menu::P1_CONTROLLER_STYLE.data_ptr()) == ControllerStyle::GCController; let button_mapping = if is_gcc { @@ -456,12 +555,11 @@ pub unsafe fn draw(root_pane: &Pane) { button_mapping.get("Z"), ); - let (left_tab_key, right_tab_key, save_defaults_key, reset_current_key, reset_all_key) = - if is_gcc { - (l_key, r_key, x_key, z_key, y_key) - } else { - (zl_key, zr_key, x_key, r_key, y_key) - }; + let (left_tab_key, right_tab_key, save_defaults_key, reset_key, clear_toggle_key) = if is_gcc { + (l_key, r_key, x_key, z_key, y_key) + } else { + (zl_key, zr_key, x_key, r_key, y_key) + }; [ (left_tab_key, "LeftTab"), @@ -494,34 +592,77 @@ pub unsafe fn draw(root_pane: &Pane) { help_pane.set_default_material_colors(); help_pane.set_color(255, 255, 0, 255); } - help_pane.set_text_string(tab_titles[idx].as_str()); + help_pane.set_text_string(tab_titles[idx]); }); - [ - (save_defaults_key, "SaveDefaults", "Save Defaults"), - (reset_current_key, "ResetCurrentDefaults", "Reset Current"), - (reset_all_key, "ResetAllDefaults", "Reset All"), - ] - .iter() - .for_each(|(key, name, title)| { - let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap(); + // Save Defaults Keyhelp + let name = "SaveDefaults"; + let key = save_defaults_key; + let title = "Save Defaults"; + let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap(); + let icon_pane = key_help_pane + .find_pane_by_name_recursive("set_txt_icon") + .unwrap() + .as_textbox(); + set_icon_text(icon_pane, &vec![*key.unwrap()]); + key_help_pane + .find_pane_by_name_recursive("set_txt_help") + .unwrap() + .as_textbox() + .set_text_string(title); + + // Reset Keyhelp + let name = "ResetDefaults"; + let key = reset_key; + let title = match app.page { + AppPage::SUBMENU => "Reset All", + AppPage::SLIDER => "Reset Current", + AppPage::TOGGLE => "Reset Current", + AppPage::CONFIRMATION => "", + AppPage::CLOSE => "", + }; + if !title.is_empty() { + let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap(); let icon_pane = key_help_pane .find_pane_by_name_recursive("set_txt_icon") .unwrap() .as_textbox(); set_icon_text(icon_pane, &vec![*key.unwrap()]); - key_help_pane .find_pane_by_name_recursive("set_txt_help") .unwrap() .as_textbox() .set_text_string(title); - }); + } + + // Clear Toggle Keyhelp + let name = "ClearToggle"; + let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap(); + let icon_pane = key_help_pane + .find_pane_by_name_recursive("set_txt_icon") + .unwrap(); + if app.should_show_clear_keyhelp() { + // This is only displayed when you're in a multiple selection toggle menu w/ toggle.max > 1 + let key = clear_toggle_key; + let title = "Clear Toggle"; + set_icon_text(icon_pane.as_textbox(), &vec![*key.unwrap()]); + key_help_pane + .find_pane_by_name_recursive("set_txt_help") + .unwrap() + .as_textbox() + .set_text_string(title); + icon_pane.set_visible(true); + key_help_pane.set_visible(true); + } else { + icon_pane.set_visible(false); + key_help_pane.set_visible(false); + } match app.page { AppPage::SUBMENU => render_submenu_page(app, root_pane), AppPage::SLIDER => render_slider_page(app, root_pane), AppPage::TOGGLE => render_toggle_page(app, root_pane), - AppPage::CONFIRMATION => todo!(), + AppPage::CONFIRMATION => render_confirmation_page(app, root_pane), + AppPage::CLOSE => {} } } diff --git a/src/training/ui/mod.rs b/src/training/ui/mod.rs index 1a3a129..2a6290f 100644 --- a/src/training/ui/mod.rs +++ b/src/training/ui/mod.rs @@ -71,7 +71,7 @@ pub unsafe fn handle_draw(layout: *mut Layout, draw_info: u64, cmd_buffer: u64) { // InfluencedAlpha means "Should my children panes' alpha be influenced by mine, as the parent?" root_pane.flags |= 1 << PaneFlag::InfluencedAlpha as u8; - root_pane.set_visible(MENU.hud == OnOff::On && !QUICK_MENU_ACTIVE); + root_pane.set_visible(MENU.hud == OnOff::ON && !QUICK_MENU_ACTIVE); } damage::draw(root_pane, &layout_name); diff --git a/training_mod_consts/Cargo.toml b/training_mod_consts/Cargo.toml index 123ff14..e6d0480 100644 --- a/training_mod_consts/Cargo.toml +++ b/training_mod_consts/Cargo.toml @@ -4,9 +4,7 @@ version = "0.1.0" edition = "2018" [dependencies] -bitflags = "1.2.1" -strum = "0.21.0" -strum_macros = "0.21.0" +byteflags = { git = "https://github.com/asimon-1/byteflags.git", branch = "rand-0.7.4" } num = "0.4.0" num-derive = "0.3" num-traits = "0.2" @@ -14,11 +12,12 @@ paste = "1.0" serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1.8" serde_json = "1" -bitflags_serde_shim = "0.2" skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" } skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git", optional = true } toml = "0.5.9" anyhow = "1.0.72" +rand = { git = "https://github.com/skyline-rs/rand" } +training_mod_tui = { path = "../training_mod_tui"} [features] default = ["smash"] diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index e86ffb7..e05a6eb 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -1,11 +1,5 @@ -#![feature(iter_intersperse)] -#[macro_use] -extern crate bitflags; - -#[macro_use] -extern crate bitflags_serde_shim; - -#[macro_use] +#![allow(non_snake_case)] +extern crate byteflags; extern crate num_derive; use serde::{Deserialize, Serialize}; @@ -17,6 +11,11 @@ pub use files::*; pub mod config; pub use config::*; +use paste::paste; +pub use training_mod_tui::*; + +pub const TOGGLE_MAX: u8 = 5; + #[repr(C)] #[derive(Clone, Copy, Serialize, Deserialize, Debug)] pub struct TrainingModpackMenu { @@ -101,10 +100,8 @@ pub struct TrainingModpackMenu { pub struct MenuJsonStruct { pub menu: TrainingModpackMenu, pub defaults_menu: TrainingModpackMenu, - // pub last_focused_submenu: &str } -// Fighter Ids #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FighterId { @@ -112,41 +109,25 @@ pub enum FighterId { CPU = 1, } -#[derive(Clone)] -pub enum SubMenuType { - TOGGLE, - SLIDER, -} - -impl SubMenuType { - pub fn from_string(s: &String) -> SubMenuType { - match s.as_str() { - "toggle" => SubMenuType::TOGGLE, - "slider" => SubMenuType::SLIDER, - _ => panic!("Unexpected SubMenuType!"), - } - } -} - pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu { aerial_delay: Delay::empty(), air_dodge_dir: Direction::empty(), attack_angle: AttackAngle::empty(), buff_state: BuffOption::empty(), - character_item: CharacterItem::None, - clatter_strength: ClatterFrequency::None, - crouch: OnOff::Off, + character_item: CharacterItem::NONE, + clatter_strength: ClatterFrequency::NONE, + crouch: OnOff::OFF, di_state: Direction::empty(), falling_aerials: BoolFlag::FALSE, fast_fall_delay: Delay::empty(), fast_fall: BoolFlag::FALSE, follow_up: Action::empty(), - frame_advantage: OnOff::Off, + frame_advantage: OnOff::OFF, full_hop: BoolFlag::TRUE, - hitbox_vis: OnOff::Off, - input_display: InputDisplay::Smash, - input_display_status: OnOff::Off, - hud: OnOff::On, + hitbox_vis: OnOff::OFF, + input_display: InputDisplay::SMASH, + input_display_status: OnOff::OFF, + hud: OnOff::ON, input_delay: Delay::D0, ledge_delay: LongDelay::empty(), ledge_state: LedgeOption::default(), @@ -160,17 +141,17 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu { save_damage_limits_cpu: DamagePercent::default(), save_damage_player: SaveDamage::DEFAULT, save_damage_limits_player: DamagePercent::default(), - save_state_autoload: OnOff::Off, - save_state_enable: OnOff::On, + save_state_autoload: OnOff::OFF, + save_state_enable: OnOff::ON, save_state_slot: SaveStateSlot::S1, randomize_slots: SaveStateSlot::empty(), - save_state_mirroring: SaveStateMirroring::None, + save_state_mirroring: SaveStateMirroring::NONE, save_state_playback: PlaybackSlot::empty(), sdi_state: Direction::empty(), - sdi_strength: SdiFrequency::None, - shield_state: Shield::None, + sdi_strength: SdiFrequency::NONE, + shield_state: Shield::NONE, shield_tilt: Direction::empty(), - stage_hazards: OnOff::Off, + stage_hazards: OnOff::OFF, tech_state: TechFlags::all(), throw_delay: MedDelay::empty(), throw_state: ThrowOption::NONE, @@ -191,745 +172,793 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu { recording_duration: RecordingDuration::F150, record_trigger: RecordTrigger::COMMAND, playback_button_slots: PlaybackSlot::S1, - hitstun_playback: HitstunPlayback::Hitstun, - playback_mash: OnOff::On, - playback_loop: OnOff::Off, - menu_open_start_press: OnOff::On, - save_state_save: ButtonConfig::ZL.union(ButtonConfig::DPAD_DOWN), - save_state_load: ButtonConfig::ZL.union(ButtonConfig::DPAD_UP), - input_record: ButtonConfig::ZR.union(ButtonConfig::DPAD_DOWN), - input_playback: ButtonConfig::ZR.union(ButtonConfig::DPAD_UP), - recording_crop: OnOff::On, - stale_dodges: OnOff::On, - tech_hide: OnOff::Off, + hitstun_playback: HitstunPlayback::HITSTUN, + playback_mash: OnOff::ON, + playback_loop: OnOff::OFF, + menu_open_start_press: OnOff::ON, + save_state_save: ButtonConfig { + ZL: 1, + DPAD_DOWN: 1, + ..ButtonConfig::empty() + }, + save_state_load: ButtonConfig { + ZL: 1, + DPAD_UP: 1, + ..ButtonConfig::empty() + }, + input_record: ButtonConfig { + ZR: 1, + DPAD_DOWN: 1, + ..ButtonConfig::empty() + }, + input_playback: ButtonConfig { + ZR: 1, + DPAD_UP: 1, + ..ButtonConfig::empty() + }, + recording_crop: OnOff::ON, + stale_dodges: OnOff::ON, + tech_hide: OnOff::OFF, update_policy: UpdatePolicy::default(), }; pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU; -#[derive(Clone, Serialize)] -pub struct Slider { - pub selected_min: u32, - pub selected_max: u32, - pub abs_min: u32, - pub abs_max: u32, +impl_toggletrait! { + OnOff, + "Menu Open Start Press", + "menu_open_start_press", + "Menu Open Start Press: Hold start or press minus to open the mod menu. To open the original menu, press start.\nThe default menu open option is always available as Hold DPad Up + Press B.", + true, + 1, +} +impl_toggletrait! { + ButtonConfig, + "Save State Save", + "save_state_save", + "Save State Save: Hold any one button and press the others to trigger", + false, + 1, +} +impl_toggletrait! { + ButtonConfig, + "Save State Load", + "save_state_load", + "Save State Load: Hold any one button and press the others to trigger", + false, + 1, +} +impl_toggletrait! { + ButtonConfig, + "Input Record", + "input_record", + "Input Record: Hold any one button and press the others to trigger", + false, + 1, +} +impl_toggletrait! { + ButtonConfig, + "Input Playback", + "input_playback", + "Input Playback: Hold any one button and press the others to trigger", + false, + 1, +} +impl_toggletrait! { + Action, + "Mash Toggles", + "mash_state", + "Mash Toggles: Actions to be performed as soon as possible", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Followup Toggles", + "follow_up", + "Followup Toggles: Actions to be performed after a Mash option", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + MashTrigger, + "Mash Triggers", + "mash_triggers", + "Mash triggers: Configure what causes the CPU to perform a Mash option", + false, + 1, +} +impl_toggletrait! { + AttackAngle, + "Attack Angle", + "attack_angle", + "Attack Angle: For attacks that can be angled, such as some forward tilts", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + ThrowOption, + "Throw Options", + "throw_state", + "Throw Options: Throw to be performed when a grab is landed", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + MedDelay, + "Throw Delay", + "throw_delay", + "Throw Delay: How many frames to delay the throw option", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + MedDelay, + "Pummel Delay", + "pummel_delay", + "Pummel Delay: How many frames after a grab to wait before starting to pummel", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + BoolFlag, + "Falling Aerials", + "falling_aerials", + "Falling Aerials: Should aerials be performed when rising or when falling", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + BoolFlag, + "Full Hop", + "full_hop", + "Full Hop: Should the CPU perform a full hop or a short hop", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Delay, + "Aerial Delay", + "aerial_delay", + "Aerial Delay: How long to delay a Mash aerial attack", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + BoolFlag, + "Fast Fall", + "fast_fall", + "Fast Fall: Should the CPU fastfall during a jump", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Delay, + "Fast Fall Delay", + "fast_fall_delay", + "Fast Fall Delay: How many frames the CPU should delay their fastfall", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Delay, + "OoS Offset", + "oos_offset", + "OoS Offset: How many times the CPU shield can be hit before performing a Mash option", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Delay, + "Reaction Time", + "reaction_time", + "Reaction Time: How many frames to delay before performing a mash option", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Ledge Neutral Getup", + "ledge_neutral_override", + "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Ledge Roll", + "ledge_roll_override", + "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Ledge Jump", + "ledge_jump_override", + "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Ledge Attack", + "ledge_attack_override", + "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Tech Action", + "tech_action_override", + "Tech Action Override: Mash Actions to be performed after any tech action", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Clatter", + "clatter_override", + "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab, bury, etc)", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Tumble", + "tumble_override", + "Tumble Override: Mash Actions to be performed after exiting a tumble state", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Hitstun", + "hitstun_override", + "Hitstun Override: Mash Actions to be performed after exiting a hitstun state", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Parry", + "parry_override", + "Parry Override: Mash Actions to be performed after a parry", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Shieldstun", + "shieldstun_override", + "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Footstool", + "footstool_override", + "Footstool Override: Mash Actions to be performed after exiting a footstool state", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Landing", + "landing_override", + "Landing Override: Mash Actions to be performed after landing on the ground", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Action, + "Ledge Trump", + "trump_override", + "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Direction, + "Airdodge Direction", + "air_dodge_dir", + "Airdodge Direction: Direction to angle airdodges", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Direction, + "DI Direction", + "di_state", + "DI Direction: Direction to angle the directional influence during hitlag", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Direction, + "SDI Direction", + "sdi_state", + "SDI Direction: Direction to angle the smash directional influence during hitlag", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + SdiFrequency, + "SDI Strength", + "sdi_strength", + "SDI Strength: Relative strength of the smash directional influence inputs", + true, + TOGGLE_MAX, +} +impl_toggletrait! { + ClatterFrequency, + "Clatter Strength", + "clatter_strength", + "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc.", + true, + TOGGLE_MAX, +} +impl_toggletrait! { + LedgeOption, + "Ledge Options", + "ledge_state", + "Ledge Options: Actions to be taken when on the ledge", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + LongDelay, + "Ledge Delay", + "ledge_delay", + "Ledge Delay: How many frames to delay the ledge option", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + TechFlags, + "Tech Options", + "tech_state", + "Tech Options: Actions to take when slammed into a hard surface", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + MissTechFlags, + "Mistech Options", + "miss_tech_state", + "Mistech Options: Actions to take after missing a tech", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + Shield, + "Shield Toggles", + "shield_state", + "Shield Toggles: CPU Shield Behavior", + true, + 1, +} +impl_toggletrait! { + Direction, + "Shield Tilt", + "shield_tilt", + "Shield Tilt: Direction to tilt the shield", + true, + 1, } -#[derive(Clone, Serialize)] -pub struct Toggle { - pub toggle_value: u32, - pub toggle_title: String, - pub checked: bool, +impl_toggletrait! { + OnOff, + "Crouch", + "crouch", + "Crouch: Have the CPU crouch when on the ground", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Dodge Staling", + "stale_dodges", + "Dodge Staling: Controls whether the CPU's dodges will worsen with repetitive use\n(Note: This can setting can cause combo behavior not possible in the original game)", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Hide Tech Animations", + "tech_hide", + "Hide Tech Animations: Hides tech animations and effects after 7 frames to help with reacting to tech animation startup", + true, + 1, +} +impl_toggletrait! { + SaveStateMirroring, + "Mirroring", + "save_state_mirroring", + "Mirroring: Flips save states in the left-right direction across the stage center", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Auto Save States", + "save_state_autoload", + "Auto Save States: Load save state when any fighter dies", + true, + 1, +} +impl_toggletrait! { + SaveDamage, + "Save Dmg (CPU)", + "save_damage_cpu", + "Save Damage: Should save states retain CPU damage", + true, + 1, +} +impl_slidertrait! { + DamagePercent, + "Dmg Range (CPU)", + "save_damage_limits_cpu", + "Limits on random damage to apply to the CPU when loading a save state", +} +impl_toggletrait! { + SaveDamage, + "Save Dmg (Player)", + "save_damage_player", + "Save Damage: Should save states retain player damage", + true, + 1, +} +impl_slidertrait! { + DamagePercent, + "Dmg Range (Player)", + "save_damage_limits_player", + "Limits on random damage to apply to the player when loading a save state", +} +impl_toggletrait! { + OnOff, + "Enable Save States", + "save_state_enable", + "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.", + true, + 1, +} +impl_toggletrait! { + SaveStateSlot, + "Save State Slot", + "save_state_slot", + "Save State Slot: Save and load states from different slots.", + true, + 1, +} +impl_toggletrait! { + SaveStateSlot, + "Randomize Slots", + "randomize_slots", + "Randomize Slots: Slots to randomize when loading save state.", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + CharacterItem, + "Character Item", + "character_item", + "Character Item: The item to give to the player's fighter when loading a save state", + true, + 1, +} +impl_toggletrait! { + BuffOption, + "Buff Options", + "buff_state", + "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state", + false, + 1, +} +impl_toggletrait! { + PlaybackSlot, + "Save State Playback", + "save_state_playback", + "Save State Playback: Choose which slots to playback input recording upon loading a save state", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + OnOff, + "Frame Advantage", + "frame_advantage", + "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Hitbox Visualization", + "hitbox_vis", + "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)", + true, + 1, +} +impl_toggletrait! { + InputDisplay, + "Input Display", + "input_display", + "Input Display: Log inputs in a queue on the left of the screen", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Input Display Status", + "input_display_status", + "Input Display Status: Group input logs by status in which they occurred", + true, + 1, +} +impl_toggletrait! { + Delay, + "Input Delay", + "input_delay", + "Input Delay: Frames to delay player inputs by", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Stage Hazards", + "stage_hazards", + "Stage Hazards: Turn stage hazards on/off", + true, + 1, +} +impl_toggletrait! { + OnOff, + "HUD", + "hud", + "HUD: Show/hide elements of the UI", + true, + 1, +} +impl_toggletrait! { + UpdatePolicy, + "Auto-Update", + "update_policy", + "Auto-Update: What type of Training Modpack updates to automatically apply. (Console Only!)", + true, + 1, +} +impl_toggletrait! { + RecordSlot, + "Recording Slot", + "recording_slot", + "Recording Slot: Choose which slot to record into", + true, + 1, +} +impl_toggletrait! { + RecordTrigger, + "Recording Trigger", + "record_trigger", + "Recording Trigger: Whether to begin recording via button combination or upon loading a Save State", + false, + 1, +} +impl_toggletrait! { + RecordingDuration, + "Recording Duration", + "recording_duration", + "Recording Duration: How long an input recording should last in frames", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Recording Crop", + "recording_crop", + "Recording Crop: Remove neutral input frames at the end of your recording", + true, + 1, +} +impl_toggletrait! { + PlaybackSlot, + "Playback Button Slots", + "playback_button_slots", + "Playback Button Slots: Choose which slots to playback input recording upon pressing button combination", + false, + TOGGLE_MAX, +} +impl_toggletrait! { + HitstunPlayback, + "Playback Hitstun Timing", + "hitstun_playback", + "Playback Hitstun Timing: When to begin playing back inputs when a hitstun mash trigger occurs", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Playback Mash Interrupt", + "playback_mash", + "Playback Mash Interrupt: End input playback when a mash trigger occurs", + true, + 1, +} +impl_toggletrait! { + OnOff, + "Playback Loop", + "playback_loop", + "Playback Loop: Repeat triggered input playbacks indefinitely", + true, + 1, } -#[derive(Clone, Serialize)] -pub struct SubMenu { - pub submenu_title: String, - pub submenu_id: String, - pub help_text: String, - pub is_single_option: bool, - pub toggles: Vec, - pub slider: Option, - pub _type: String, -} +pub unsafe fn create_app<'a>() -> App<'a> { + // Note that the to_submenu_xxx() functions are defined in the `impl_toggletrait` and `impl_slidertrait` macros + let mut overall_menu = App::new(); -impl SubMenu { - pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: String, checked: bool) { - self.toggles.push(Toggle { - toggle_value, - toggle_title, - checked, - }); - } - pub fn new_with_toggles( - submenu_title: String, - submenu_id: String, - help_text: String, - is_single_option: bool, - initial_value: &u32, - ) -> SubMenu { - let mut instance = SubMenu { - submenu_title: submenu_title, - submenu_id: submenu_id, - help_text: help_text, - is_single_option: is_single_option, - toggles: Vec::new(), - slider: None, - _type: "toggle".to_string(), - }; - - let values = T::to_toggle_vals(); - let titles = T::to_toggle_strings(); - for i in 0..values.len() { - let checked: bool = - (values[i] & initial_value) > 0 || (!values[i] == 0 && initial_value == &0); - instance.add_toggle(values[i], titles[i].clone(), checked); - } - // Select the first option if there's nothing selected atm but it's a single option submenu - if is_single_option && instance.toggles.iter().all(|t| !t.checked) { - instance.toggles[0].checked = true; - } - instance - } - pub fn new_with_slider( - submenu_title: String, - submenu_id: String, - help_text: String, - initial_lower_value: &u32, - initial_upper_value: &u32, - ) -> SubMenu { - let min_max = S::get_limits(); - SubMenu { - submenu_title: submenu_title, - submenu_id: submenu_id, - help_text: help_text, - is_single_option: false, - toggles: Vec::new(), - slider: Some(Slider { - selected_min: *initial_lower_value, - selected_max: *initial_upper_value, - abs_min: min_max.0, - abs_max: min_max.1, - }), - _type: "slider".to_string(), - } - } -} - -#[derive(Serialize, Clone)] -pub struct Tab { - pub tab_id: String, - pub tab_title: String, - pub tab_submenus: Vec, -} - -impl Tab { - pub fn add_submenu_with_toggles( - &mut self, - submenu_title: String, - submenu_id: String, - help_text: String, - is_single_option: bool, - initial_value: &u32, - ) { - self.tab_submenus.push(SubMenu::new_with_toggles::( - submenu_title.to_string(), - submenu_id.to_string(), - help_text.to_string(), - is_single_option, - initial_value, - )); - } - - pub fn add_submenu_with_slider( - &mut self, - submenu_title: String, - submenu_id: String, - help_text: String, - initial_lower_value: &u32, - initial_upper_value: &u32, - ) { - self.tab_submenus.push(SubMenu::new_with_slider::( - submenu_title.to_string(), - submenu_id.to_string(), - help_text.to_string(), - initial_lower_value, - initial_upper_value, - )) - } -} - -#[derive(Serialize, Clone)] -pub struct UiMenu { - pub tabs: Vec, -} - -pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu { - let mut overall_menu = UiMenu { tabs: Vec::new() }; - - let mut button_tab = Tab { - tab_id: "button".to_string(), - tab_title: "Button Config".to_string(), - tab_submenus: Vec::new(), + // Mash Tab + let mut mash_tab_submenus: Vec = Vec::new(); + mash_tab_submenus.push(to_submenu_mash_state()); + mash_tab_submenus.push(to_submenu_follow_up()); + mash_tab_submenus.push(to_submenu_mash_triggers()); + mash_tab_submenus.push(to_submenu_attack_angle()); + mash_tab_submenus.push(to_submenu_throw_state()); + mash_tab_submenus.push(to_submenu_throw_delay()); + mash_tab_submenus.push(to_submenu_pummel_delay()); + mash_tab_submenus.push(to_submenu_falling_aerials()); + mash_tab_submenus.push(to_submenu_full_hop()); + mash_tab_submenus.push(to_submenu_aerial_delay()); + mash_tab_submenus.push(to_submenu_fast_fall()); + mash_tab_submenus.push(to_submenu_fast_fall_delay()); + mash_tab_submenus.push(to_submenu_oos_offset()); + mash_tab_submenus.push(to_submenu_reaction_time()); + let mash_tab = Tab { + id: "mash", + title: "Mash Settings", + submenus: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, mash_tab_submenus), }; - button_tab.add_submenu_with_toggles::( - "Menu Open Start Press".to_string(), - "menu_open_start_press".to_string(), - "Menu Open Start Press: Hold start or press minus to open the mod menu. To open the original menu, press start.\nThe default menu open option is always available as Hold DPad Up + Press B.".to_string(), - true, - &(menu.menu_open_start_press as u32), - ); - button_tab.add_submenu_with_toggles::( - "Save State Save".to_string(), - "save_state_save".to_string(), - "Save State Save: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.save_state_save.bits() as u32), - ); - button_tab.add_submenu_with_toggles::( - "Save State Load".to_string(), - "save_state_load".to_string(), - "Save State Load: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.save_state_load.bits() as u32), - ); - button_tab.add_submenu_with_toggles::( - "Input Record".to_string(), - "input_record".to_string(), - "Input Record: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.input_record.bits() as u32), - ); - button_tab.add_submenu_with_toggles::( - "Input Playback".to_string(), - "input_playback".to_string(), - "Input Playback: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.input_playback.bits() as u32), - ); - overall_menu.tabs.push(button_tab); - - let mut mash_tab = Tab { - tab_id: "mash".to_string(), - tab_title: "Mash Settings".to_string(), - tab_submenus: Vec::new(), - }; - mash_tab.add_submenu_with_toggles::( - "Mash Toggles".to_string(), - "mash_state".to_string(), - "Mash Toggles: Actions to be performed as soon as possible".to_string(), - false, - &(menu.mash_state.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Followup Toggles".to_string(), - "follow_up".to_string(), - "Followup Toggles: Actions to be performed after a Mash option".to_string(), - false, - &(menu.follow_up.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Mash Triggers".to_string(), - "mash_triggers".to_string(), - "Mash triggers: Configure what causes the CPU to perform a Mash option".to_string(), - false, - &(menu.mash_triggers.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Attack Angle".to_string(), - "attack_angle".to_string(), - "Attack Angle: For attacks that can be angled, such as some forward tilts".to_string(), - false, - &(menu.attack_angle.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Throw Options".to_string(), - "throw_state".to_string(), - "Throw Options: Throw to be performed when a grab is landed".to_string(), - false, - &(menu.throw_state.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Throw Delay".to_string(), - "throw_delay".to_string(), - "Throw Delay: How many frames to delay the throw option".to_string(), - false, - &(menu.throw_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Pummel Delay".to_string(), - "pummel_delay".to_string(), - "Pummel Delay: How many frames after a grab to wait before starting to pummel".to_string(), - false, - &(menu.pummel_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Falling Aerials".to_string(), - "falling_aerials".to_string(), - "Falling Aerials: Should aerials be performed when rising or when falling".to_string(), - false, - &(menu.falling_aerials.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Full Hop".to_string(), - "full_hop".to_string(), - "Full Hop: Should the CPU perform a full hop or a short hop".to_string(), - false, - &(menu.full_hop.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Aerial Delay".to_string(), - "aerial_delay".to_string(), - "Aerial Delay: How long to delay a Mash aerial attack".to_string(), - false, - &(menu.aerial_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Fast Fall".to_string(), - "fast_fall".to_string(), - "Fast Fall: Should the CPU fastfall during a jump".to_string(), - false, - &(menu.fast_fall.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Fast Fall Delay".to_string(), - "fast_fall_delay".to_string(), - "Fast Fall Delay: How many frames the CPU should delay their fastfall".to_string(), - false, - &(menu.fast_fall_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "OoS Offset".to_string(), - "oos_offset".to_string(), - "OoS Offset: How many times the CPU shield can be hit before performing a Mash option" - .to_string(), - false, - &(menu.oos_offset.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Reaction Time".to_string(), - "reaction_time".to_string(), - "Reaction Time: How many frames to delay before performing a mash option".to_string(), - false, - &(menu.reaction_time.bits()), - ); overall_menu.tabs.push(mash_tab); - let mut override_tab = Tab { - tab_id: "override".to_string(), - tab_title: "Override Settings".to_string(), - tab_submenus: Vec::new(), + // Mash Override Tab + let mut override_tab_submenus: Vec = Vec::new(); + override_tab_submenus.push(to_submenu_ledge_neutral_override()); + override_tab_submenus.push(to_submenu_ledge_roll_override()); + override_tab_submenus.push(to_submenu_ledge_jump_override()); + override_tab_submenus.push(to_submenu_ledge_attack_override()); + override_tab_submenus.push(to_submenu_tech_action_override()); + override_tab_submenus.push(to_submenu_clatter_override()); + override_tab_submenus.push(to_submenu_tumble_override()); + override_tab_submenus.push(to_submenu_hitstun_override()); + override_tab_submenus.push(to_submenu_parry_override()); + override_tab_submenus.push(to_submenu_shieldstun_override()); + override_tab_submenus.push(to_submenu_footstool_override()); + override_tab_submenus.push(to_submenu_landing_override()); + override_tab_submenus.push(to_submenu_trump_override()); + let override_tab = Tab { + id: "override", + title: "Override Settings", + submenus: StatefulTable::with_items( + NX_SUBMENU_ROWS, + NX_SUBMENU_COLUMNS, + override_tab_submenus, + ), }; - override_tab.add_submenu_with_toggles::( - "Ledge Neutral Getup".to_string(), - "ledge_neutral_override".to_string(), - "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge" - .to_string(), - false, - &(menu.ledge_neutral_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Roll".to_string(), - "ledge_roll_override".to_string(), - "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge" - .to_string(), - false, - &(menu.ledge_roll_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Jump".to_string(), - "ledge_jump_override".to_string(), - "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge" - .to_string(), - false, - &(menu.ledge_jump_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Attack".to_string(), - "ledge_attack_override".to_string(), - "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge" - .to_string(), - false, - &(menu.ledge_attack_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Tech Action".to_string(), - "tech_action_override".to_string(), - "Tech Action Override: Mash Actions to be performed after any tech action".to_string(), - false, - &(menu.tech_action_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Clatter".to_string(), - "clatter_override".to_string(), - "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab.to_string(), bury, etc)".to_string(), - false, - &(menu.clatter_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Tumble".to_string(), - "tumble_override".to_string(), - "Tumble Override: Mash Actions to be performed after exiting a tumble state".to_string(), - false, - &(menu.tumble_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Hitstun".to_string(), - "hitstun_override".to_string(), - "Hitstun Override: Mash Actions to be performed after exiting a hitstun state".to_string(), - false, - &(menu.hitstun_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Parry".to_string(), - "parry_override".to_string(), - "Parry Override: Mash Actions to be performed after a parry".to_string(), - false, - &(menu.parry_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Shieldstun".to_string(), - "shieldstun_override".to_string(), - "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state" - .to_string(), - false, - &(menu.shieldstun_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Footstool".to_string(), - "footstool_override".to_string(), - "Footstool Override: Mash Actions to be performed after exiting a footstool state" - .to_string(), - false, - &(menu.footstool_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Landing".to_string(), - "landing_override".to_string(), - "Landing Override: Mash Actions to be performed after landing on the ground".to_string(), - false, - &(menu.landing_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Trump".to_string(), - "trump_override".to_string(), - "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state" - .to_string(), - false, - &(menu.trump_override.bits()), - ); overall_menu.tabs.push(override_tab); - let mut defensive_tab = Tab { - tab_id: "defensive".to_string(), - tab_title: "Defensive Settings".to_string(), - tab_submenus: Vec::new(), + // Defensive Tab + let mut defensive_tab_submenus: Vec = Vec::new(); + defensive_tab_submenus.push(to_submenu_air_dodge_dir()); + defensive_tab_submenus.push(to_submenu_di_state()); + defensive_tab_submenus.push(to_submenu_sdi_state()); + defensive_tab_submenus.push(to_submenu_sdi_strength()); + defensive_tab_submenus.push(to_submenu_clatter_strength()); + defensive_tab_submenus.push(to_submenu_ledge_state()); + defensive_tab_submenus.push(to_submenu_ledge_delay()); + defensive_tab_submenus.push(to_submenu_tech_state()); + defensive_tab_submenus.push(to_submenu_miss_tech_state()); + defensive_tab_submenus.push(to_submenu_shield_state()); + defensive_tab_submenus.push(to_submenu_shield_tilt()); + defensive_tab_submenus.push(to_submenu_crouch()); + defensive_tab_submenus.push(to_submenu_stale_dodges()); + defensive_tab_submenus.push(to_submenu_tech_hide()); + let defensive_tab = Tab { + id: "defensive", + title: "Defensive Settings", + submenus: StatefulTable::with_items( + NX_SUBMENU_ROWS, + NX_SUBMENU_COLUMNS, + defensive_tab_submenus, + ), }; - defensive_tab.add_submenu_with_toggles::( - "Airdodge Direction".to_string(), - "air_dodge_dir".to_string(), - "Airdodge Direction: Direction to angle airdodges".to_string(), - false, - &(menu.air_dodge_dir.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "DI Direction".to_string(), - "di_state".to_string(), - "DI Direction: Direction to angle the directional influence during hitlag".to_string(), - false, - &(menu.di_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "SDI Direction".to_string(), - "sdi_state".to_string(), - "SDI Direction: Direction to angle the smash directional influence during hitlag" - .to_string(), - false, - &(menu.sdi_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "SDI Strength".to_string(), - "sdi_strength".to_string(), - "SDI Strength: Relative strength of the smash directional influence inputs".to_string(), - true, - &(menu.sdi_strength as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Clatter Strength".to_string(), - "clatter_strength".to_string(), - "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc." - .to_string(), - true, - &(menu.clatter_strength as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Ledge Options".to_string(), - "ledge_state".to_string(), - "Ledge Options: Actions to be taken when on the ledge".to_string(), - false, - &(menu.ledge_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Ledge Delay".to_string(), - "ledge_delay".to_string(), - "Ledge Delay: How many frames to delay the ledge option".to_string(), - false, - &(menu.ledge_delay.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Tech Options".to_string(), - "tech_state".to_string(), - "Tech Options: Actions to take when slammed into a hard surface".to_string(), - false, - &(menu.tech_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Mistech Options".to_string(), - "miss_tech_state".to_string(), - "Mistech Options: Actions to take after missing a tech".to_string(), - false, - &(menu.miss_tech_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Shield Toggles".to_string(), - "shield_state".to_string(), - "Shield Toggles: CPU Shield Behavior".to_string(), - true, - &(menu.shield_state as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Shield Tilt".to_string(), - "shield_tilt".to_string(), - "Shield Tilt: Direction to tilt the shield".to_string(), - false, // TODO: Should this be true? - &(menu.shield_tilt.bits()), - ); - - defensive_tab.add_submenu_with_toggles::( - "Crouch".to_string(), - "crouch".to_string(), - "Crouch: Have the CPU crouch when on the ground".to_string(), - true, - &(menu.crouch as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Dodge Staling".to_string(), - "stale_dodges".to_string(), - "Dodge Staling: Controls whether the CPU's dodges will worsen with repetitive use\n(Note: This can setting can cause combo behavior not possible in the original game)" - .to_string(), - true, - &(menu.stale_dodges as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Hide Tech Animations".to_string(), - "tech_hide".to_string(), - "Hide Tech Animations: Hides tech animations and effects after 7 frames to help with reacting to tech animation startup" - .to_string(), - true, - &(menu.tech_hide as u32), - ); overall_menu.tabs.push(defensive_tab); - let mut save_state_tab = Tab { - tab_id: "save_state".to_string(), - tab_title: "Save States".to_string(), - tab_submenus: Vec::new(), + // Input Recording Tab + let mut input_recording_tab_submenus: Vec = Vec::new(); + input_recording_tab_submenus.push(to_submenu_recording_slot()); + input_recording_tab_submenus.push(to_submenu_record_trigger()); + input_recording_tab_submenus.push(to_submenu_recording_duration()); + input_recording_tab_submenus.push(to_submenu_recording_crop()); + input_recording_tab_submenus.push(to_submenu_playback_button_slots()); + input_recording_tab_submenus.push(to_submenu_hitstun_playback()); + input_recording_tab_submenus.push(to_submenu_save_state_playback()); + input_recording_tab_submenus.push(to_submenu_playback_mash()); + input_recording_tab_submenus.push(to_submenu_playback_loop()); + let input_tab = Tab { + id: "input", + title: "Input Recording", + submenus: StatefulTable::with_items( + NX_SUBMENU_ROWS, + NX_SUBMENU_COLUMNS, + input_recording_tab_submenus, + ), + }; + overall_menu.tabs.push(input_tab); + + // Button Tab + let mut button_tab_submenus: Vec = Vec::new(); + button_tab_submenus.push(to_submenu_menu_open_start_press()); + button_tab_submenus.push(to_submenu_save_state_save()); + button_tab_submenus.push(to_submenu_save_state_load()); + button_tab_submenus.push(to_submenu_input_record()); + button_tab_submenus.push(to_submenu_input_playback()); + let button_tab = Tab { + id: "button", + title: "Button Config", + submenus: StatefulTable::with_items( + NX_SUBMENU_ROWS, + NX_SUBMENU_COLUMNS, + button_tab_submenus, + ), + }; + overall_menu.tabs.push(button_tab); + + // Save State Tab + let mut save_state_tab_submenus: Vec = Vec::new(); + save_state_tab_submenus.push(to_submenu_save_state_mirroring()); + save_state_tab_submenus.push(to_submenu_save_state_autoload()); + save_state_tab_submenus.push(to_submenu_save_damage_cpu()); + save_state_tab_submenus.push(to_submenu_save_damage_limits_cpu()); + save_state_tab_submenus.push(to_submenu_save_damage_player()); + save_state_tab_submenus.push(to_submenu_save_damage_limits_player()); + save_state_tab_submenus.push(to_submenu_save_state_enable()); + save_state_tab_submenus.push(to_submenu_save_state_slot()); + save_state_tab_submenus.push(to_submenu_randomize_slots()); + save_state_tab_submenus.push(to_submenu_character_item()); + save_state_tab_submenus.push(to_submenu_buff_state()); + let save_state_tab = Tab { + id: "save_state", + title: "Save States", + submenus: StatefulTable::with_items( + NX_SUBMENU_ROWS, + NX_SUBMENU_COLUMNS, + save_state_tab_submenus, + ), }; - save_state_tab.add_submenu_with_toggles::( - "Mirroring".to_string(), - "save_state_mirroring".to_string(), - "Mirroring: Flips save states in the left-right direction across the stage center" - .to_string(), - true, - &(menu.save_state_mirroring as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Auto Save States".to_string(), - "save_state_autoload".to_string(), - "Auto Save States: Load save state when any fighter dies".to_string(), - true, - &(menu.save_state_autoload as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Save Dmg (CPU)".to_string(), - "save_damage_cpu".to_string(), - "Save Damage: Should save states retain CPU damage".to_string(), - true, - &(menu.save_damage_cpu.bits()), - ); - save_state_tab.add_submenu_with_slider::( - "Dmg Range (CPU)".to_string(), - "save_damage_limits_cpu".to_string(), - "Limits on random damage to apply to the CPU when loading a save state".to_string(), - &(menu.save_damage_limits_cpu.0 as u32), - &(menu.save_damage_limits_cpu.1 as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Save Dmg (Player)".to_string(), - "save_damage_player".to_string(), - "Save Damage: Should save states retain player damage".to_string(), - true, - &(menu.save_damage_player.bits() as u32), - ); - save_state_tab.add_submenu_with_slider::( - "Dmg Range (Player)".to_string(), - "save_damage_limits_player".to_string(), - "Limits on random damage to apply to the player when loading a save state".to_string(), - &(menu.save_damage_limits_player.0 as u32), - &(menu.save_damage_limits_player.1 as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Enable Save States".to_string(), - "save_state_enable".to_string(), - "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.".to_string(), - true, - &(menu.save_state_enable as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Save State Slot".to_string(), - "save_state_slot".to_string(), - "Save State Slot: Save and load states from different slots.".to_string(), - true, - &(menu.save_state_slot.bits() as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Randomize Slots".to_string(), - "randomize_slots".to_string(), - "Randomize Slots: Slots to randomize when loading save state.".to_string(), - false, - &(menu.randomize_slots.bits() as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Character Item".to_string(), - "character_item".to_string(), - "Character Item: The item to give to the player's fighter when loading a save state" - .to_string(), - true, - &(menu.character_item as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Buff Options".to_string(), - "buff_state".to_string(), - "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state" - .to_string(), - false, - &(menu.buff_state.bits()), - ); - save_state_tab.add_submenu_with_toggles::( - "Save State Playback".to_string(), - "save_state_playback".to_string(), - "Save State Playback: Choose which slots to playback input recording upon loading a save state".to_string(), - false, - &(menu.save_state_playback.bits() as u32), - ); overall_menu.tabs.push(save_state_tab); - let mut misc_tab = Tab { - tab_id: "misc".to_string(), - tab_title: "Misc Settings".to_string(), - tab_submenus: Vec::new(), + // Miscellaneous Tab + let mut misc_tab_submenus: Vec = Vec::new(); + misc_tab_submenus.push(to_submenu_frame_advantage()); + misc_tab_submenus.push(to_submenu_hitbox_vis()); + misc_tab_submenus.push(to_submenu_input_display()); + misc_tab_submenus.push(to_submenu_input_display_status()); + misc_tab_submenus.push(to_submenu_input_delay()); + misc_tab_submenus.push(to_submenu_stage_hazards()); + misc_tab_submenus.push(to_submenu_hud()); + misc_tab_submenus.push(to_submenu_update_policy()); + let misc_tab = Tab { + id: "misc", + title: "Misc Settings", + submenus: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, misc_tab_submenus), }; - misc_tab.add_submenu_with_toggles::( - "Frame Advantage".to_string(), - "frame_advantage".to_string(), - "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable".to_string(), - true, - &(menu.frame_advantage as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Hitbox Visualization".to_string(), - "hitbox_vis".to_string(), - "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)".to_string(), - true, - &(menu.hitbox_vis as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Input Display".to_string(), - "input_display".to_string(), - "Input Display: Log inputs in a queue on the left of the screen".to_string(), - true, - &(menu.input_display as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Input Display Status".to_string(), - "input_display_status".to_string(), - "Input Display Status: Group input logs by status in which they occurred".to_string(), - true, - &(menu.input_display_status as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Input Delay".to_string(), - "input_delay".to_string(), - "Input Delay: Frames to delay player inputs by".to_string(), - true, - &(menu.input_delay.bits()), - ); - misc_tab.add_submenu_with_toggles::( - "Stage Hazards".to_string(), - "stage_hazards".to_string(), - "Stage Hazards: Turn stage hazards on/off".to_string(), - true, - &(menu.stage_hazards as u32), - ); - misc_tab.add_submenu_with_toggles::( - "HUD".to_string(), - "hud".to_string(), - "HUD: Show/hide elements of the UI".to_string(), - true, - &(menu.hud as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Auto-Update".to_string(), - "update_policy".to_string(), - "Auto-Update: What type of Training Modpack updates to automatically apply. (Console Only!)" - .to_string(), - true, - &(menu.update_policy as u32), - ); overall_menu.tabs.push(misc_tab); - let mut input_tab = Tab { - tab_id: "input".to_string(), - tab_title: "Input Recording".to_string(), - tab_submenus: Vec::new(), - }; - input_tab.add_submenu_with_toggles::( - "Recording Slot".to_string(), - "recording_slot".to_string(), - "Recording Slot: Choose which slot to record into".to_string(), - true, - &(menu.recording_slot as u32), - ); - input_tab.add_submenu_with_toggles::( - "Recording Trigger".to_string(), - "record_trigger".to_string(), - format!("Recording Trigger: Whether to begin recording via button combination ({}) or upon loading a Save State", menu.input_record.combination_string()), - false, - &(menu.record_trigger.bits() as u32), - ); - input_tab.add_submenu_with_toggles::( - "Recording Duration".to_string(), - "recording_duration".to_string(), - "Recording Duration: Number of frames to record for in the current slot".to_string(), - true, - &(menu.recording_duration as u32), - ); - input_tab.add_submenu_with_toggles::( - "Recording Crop".to_string(), - "recording_crop".to_string(), - "Recording Crop: Remove neutral input frames at the end of your recording".to_string(), - true, - &(menu.recording_crop as u32), - ); - input_tab.add_submenu_with_toggles::( - "Playback Button Slots".to_string(), - "playback_button_slots".to_string(), - format!("Playback Button Slots: Choose which slots to playback input recording upon pressing button combination ({})", menu.input_playback.combination_string()), - false, - &(menu.playback_button_slots.bits() as u32), - ); - input_tab.add_submenu_with_toggles::( - "Playback Hitstun Timing".to_string(), - "hitstun_playback".to_string(), - "Playback Hitstun Timing: When to begin playing back inputs when a hitstun mash trigger occurs".to_string(), - true, - &(menu.hitstun_playback as u32), - ); - input_tab.add_submenu_with_toggles::( - "Playback Mash Interrupt".to_string(), - "playback_mash".to_string(), - "Playback Mash Interrupt: End input playback when a mash trigger occurs".to_string(), - true, - &(menu.playback_mash as u32), - ); - input_tab.add_submenu_with_toggles::( - "Playback Loop".to_string(), - "playback_loop".to_string(), - "Playback Loop: Repeat triggered input playbacks indefinitely".to_string(), - true, - &(menu.playback_loop as u32), - ); - overall_menu.tabs.push(input_tab); + // Ensure that a tab is always selected + if overall_menu.tabs.get_selected().is_none() { + overall_menu.tabs.state.select(Some(0)); + } overall_menu } diff --git a/training_mod_consts/src/options.rs b/training_mod_consts/src/options.rs index cfaf8e3..ce198ec 100644 --- a/training_mod_consts/src/options.rs +++ b/training_mod_consts/src/options.rs @@ -1,94 +1,64 @@ +use byteflags::*; use core::f64::consts::PI; use serde::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; #[cfg(feature = "smash")] use smash::lib::lua_const::*; -use std::fmt; -use strum::IntoEnumIterator; -use strum_macros::EnumIter; -const fn num_bits() -> u32 { - (std::mem::size_of::() * 8) as u32 -} - -fn log_2(x: u32) -> u32 { - if x == 0 { - 0 - } else { - num_bits::() - x.leading_zeros() - 1 +#[macro_export] +macro_rules! impl_toggletrait { + ( + $e:ty, + $title:literal, + $id:literal, + $help_text:literal, + $single:literal, + $max:expr, + ) => { + paste! { + fn []<'a>() -> SubMenu<'a> { + let submenu_type = if $single { SubMenuType::ToggleSingle } else { SubMenuType::ToggleMultiple }; + let value = 0; + let max: u8 = $max; + let toggles_vec: Vec = <$e>::ALL_NAMES + .iter() + .map(|title| Toggle { title, value, max }) + .collect(); + SubMenu { + title: $title, + id: $id, + help_text: $help_text, + submenu_type: submenu_type, + toggles: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, toggles_vec), + slider: None + } + } + } } } -pub trait ToggleTrait { - fn to_toggle_vals() -> Vec; - fn to_toggle_strings() -> Vec; -} - -pub trait SliderTrait { - fn get_limits() -> (u32, u32); -} - -// bitflag helper function macro #[macro_export] -macro_rules! extra_bitflag_impls { - ($e:ty) => { - impl $e { - pub fn to_vec(&self) -> Vec::<$e> { - let mut vec = Vec::<$e>::new(); - let mut field = <$e>::from_bits_truncate(self.bits); - while !field.is_empty() { - let flag = <$e>::from_bits(1u32 << field.bits.trailing_zeros()).unwrap(); - field -= flag; - vec.push(flag); +macro_rules! impl_slidertrait { + ( + $e:ty, + $title:literal, + $id:literal, + $help_text:literal, + ) => { + paste! { + fn []<'a>() -> SubMenu<'a> { + let slider = StatefulSlider { + lower: 0, + upper: 150, + ..StatefulSlider::new() + }; + SubMenu { + title: $title, + id: $id, + help_text: $help_text, + submenu_type: SubMenuType::Slider, + toggles: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, Vec::new()), + slider: Some(slider) } - return vec; - } - - pub fn to_index(&self) -> u32 { - if self.bits == 0 { - 0 - } else { - self.bits.trailing_zeros() - } - } - - pub fn get_random(&self) -> $e { - let options = self.to_vec(); - match options.len() { - 0 => { - return <$e>::empty(); - } - 1 => { - return options[0]; - } - _ => { - return *random_option(&options); - } - } - } - - pub fn combination_string(&self) -> String { - // Avoid infinite recursion lol - if self.to_vec().len() <= 1 { - return "".to_string(); - } - - self.to_vec() - .iter() - .map(|item| item.to_string()) - .intersperse(" + ".to_owned()) - .collect::() - } - } - impl ToggleTrait for $e { - fn to_toggle_vals() -> Vec { - let all_options = <$e>::all().to_vec(); - all_options.iter().map(|i| i.bits() as u32).collect() - } - - fn to_toggle_strings() -> Vec { - let all_options = <$e>::all().to_vec(); - all_options.iter().map(|i| i.to_string()).collect() } } } @@ -127,19 +97,19 @@ pub fn random_option(arg: &[T]) -> &T { */ // DI / Left stick -bitflags! { - pub struct Direction : u32 { - const OUT = 0x1; - const UP_OUT = 0x2; - const UP = 0x4; - const UP_IN = 0x8; - const IN = 0x10; - const DOWN_IN = 0x20; - const DOWN = 0x40; - const DOWN_OUT = 0x80; - const NEUTRAL = 0x100; - const LEFT = 0x200; - const RIGHT = 0x400; +byteflags! { + pub struct Direction { + pub OUT = "Out", + pub UP_OUT = "Up Out", + pub UP = "Up", + pub UP_IN = "Up In", + pub IN = "In", + pub DOWN_IN = "Down In", + pub DOWN = "Down", + pub DOWN_OUT = "Down Out", + pub NEUTRAL = "Neutral", + pub LEFT = "Left", + pub RIGHT = "Right", } } @@ -147,71 +117,47 @@ impl Direction { pub fn into_angle(self) -> Option { let index = self.into_index(); - if index == 0 { + if index == 0.0 { None } else { - Some((index as i32 - 1) as f64 * PI / 4.0) + Some((index - 1.0) * PI / 4.0) } } - fn into_index(self) -> i32 { + fn into_index(self) -> f64 { + if self == Direction::empty() { + return 0.0; + }; match self { - Direction::OUT => 1, - Direction::UP_OUT => 2, - Direction::UP => 3, - Direction::UP_IN => 4, - Direction::IN => 5, - Direction::DOWN_IN => 6, - Direction::DOWN => 7, - Direction::DOWN_OUT => 8, - Direction::NEUTRAL => 0, - Direction::LEFT => 5, - Direction::RIGHT => 1, - _ => 0, + Direction::OUT => 1.0, + Direction::UP_OUT => 2.0, + Direction::UP => 3.0, + Direction::UP_IN => 4.0, + Direction::IN => 5.0, + Direction::DOWN_IN => 6.0, + Direction::DOWN => 7.0, + Direction::DOWN_OUT => 8.0, + Direction::NEUTRAL => 0.0, + Direction::LEFT => 5.0, + Direction::RIGHT => 1.0, + _ => panic!("Invalid value in Direction::into_index: {}", self), } } } -impl fmt::Display for Direction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - Direction::OUT => "Away", - Direction::UP_OUT => "Up and Away", - Direction::UP => "Up", - Direction::UP_IN => "Up and In", - Direction::IN => "In", - Direction::DOWN_IN => "Down and In", - Direction::DOWN => "Down", - Direction::DOWN_OUT => "Down and Away", - Direction::NEUTRAL => "Neutral", - Direction::LEFT => "Left", - Direction::RIGHT => "Right", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {Direction} -impl_serde_for_bitflags!(Direction); - // Ledge Option -bitflags! { - pub struct LedgeOption : u32 +byteflags! { + pub struct LedgeOption { - const NEUTRAL = 0x1; - const ROLL = 0x2; - const JUMP = 0x4; - const ATTACK = 0x8; - const WAIT = 0x10; - const PLAYBACK_1 = 0x20; - const PLAYBACK_2 = 0x40; - const PLAYBACK_3 = 0x80; - const PLAYBACK_4 = 0x100; - const PLAYBACK_5 = 0x200; + pub NEUTRAL = "Neutral Getup", + pub ROLL = "Roll", + pub JUMP = "Jump", + pub ATTACK = "Getup Attack", + pub WAIT = "Wait", + pub PLAYBACK_1 = "Playback Slot 1", + pub PLAYBACK_2 = "Playback Slot 2", + pub PLAYBACK_3 = "Playback Slot 3", + pub PLAYBACK_4 = "Playback Slot 4", + pub PLAYBACK_5 = "Playback Slot 5", } } @@ -262,248 +208,110 @@ impl LedgeOption { pub const fn default() -> LedgeOption { // Neutral,Roll,Jump,Attack (everything except wait) - LedgeOption::NEUTRAL - .union(LedgeOption::ROLL) - .union(LedgeOption::JUMP) - .union(LedgeOption::ATTACK) + LedgeOption { + NEUTRAL: 1, + ROLL: 1, + JUMP: 1, + ATTACK: 1, + ..LedgeOption::empty() + } } } -impl fmt::Display for LedgeOption { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - LedgeOption::NEUTRAL => "Neutral Getup", - LedgeOption::ROLL => "Roll", - LedgeOption::JUMP => "Jump", - LedgeOption::ATTACK => "Getup Attack", - LedgeOption::WAIT => "Wait", - LedgeOption::PLAYBACK_1 => "Playback Slot 1", - LedgeOption::PLAYBACK_2 => "Playback Slot 2", - LedgeOption::PLAYBACK_3 => "Playback Slot 3", - LedgeOption::PLAYBACK_4 => "Playback Slot 4", - LedgeOption::PLAYBACK_5 => "Playback Slot 5", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {LedgeOption} -impl_serde_for_bitflags!(LedgeOption); - // Tech options -bitflags! { - pub struct TechFlags : u32 { - const NO_TECH = 0x1; - const ROLL_F = 0x2; - const ROLL_B = 0x4; - const IN_PLACE = 0x8; +byteflags! { + pub struct TechFlags { + pub NO_TECH = "No Tech", + pub ROLL_F = "Roll Forwards", + pub ROLL_B = "Roll Backwards", + pub IN_PLACE = "Tech In Place", } } -impl fmt::Display for TechFlags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - TechFlags::NO_TECH => "No Tech", - TechFlags::ROLL_F => "Roll Forwards", - TechFlags::ROLL_B => "Roll Backwards", - TechFlags::IN_PLACE => "Tech In Place", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {TechFlags} -impl_serde_for_bitflags!(TechFlags); - // Missed Tech Options -bitflags! { - pub struct MissTechFlags : u32 { - const GETUP = 0x1; - const ATTACK = 0x2; - const ROLL_F = 0x4; - const ROLL_B = 0x8; +byteflags! { + pub struct MissTechFlags { + pub GETUP = "Neutral Getup", + pub ATTACK = "Getup Attack", + pub ROLL_F = "Roll Forwards", + pub ROLL_B = "Roll Backwards", } } -impl fmt::Display for MissTechFlags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - MissTechFlags::GETUP => "Neutral Getup", - MissTechFlags::ATTACK => "Getup Attack", - MissTechFlags::ROLL_F => "Roll Forwards", - MissTechFlags::ROLL_B => "Roll Backwards", - _ => combination_string.as_str(), - } - ) +byteflags! { + pub struct Shield { + pub NONE = "None", + pub INFINITE = "Infinite", + pub HOLD = "Hold", + pub CONSTANT = "Constant", } } -extra_bitflag_impls! {MissTechFlags} -impl_serde_for_bitflags!(MissTechFlags); - -/// Shield States -#[repr(i32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum Shield { - None = 0x0, - Infinite = 0x1, - Hold = 0x2, - Constant = 0x4, -} - -impl fmt::Display for Shield { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - Shield::None => "None", - Shield::Infinite => "Infinite", - Shield::Hold => "Hold", - Shield::Constant => "Constant", - } - ) +byteflags! { + pub struct SaveStateMirroring { + pub NONE = "None", + pub ALTERNATE = "Alternate", + pub RANDOM = "Random", } } -impl ToggleTrait for Shield { - fn to_toggle_vals() -> Vec { - Shield::iter().map(|i| i as u32).collect() +byteflags! { + pub struct OnOff { + pub ON = "On", + pub OFF = "Off", } - fn to_toggle_strings() -> Vec { - Shield::iter().map(|i| i.to_string()).collect() - } -} - -// Save State Mirroring -#[repr(i32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum SaveStateMirroring { - None = 0x0, - Alternate = 0x1, - Random = 0x2, -} - -impl fmt::Display for SaveStateMirroring { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - SaveStateMirroring::None => "None", - SaveStateMirroring::Alternate => "Alternate", - SaveStateMirroring::Random => "Random", - } - ) - } -} - -impl ToggleTrait for SaveStateMirroring { - fn to_toggle_vals() -> Vec { - SaveStateMirroring::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - SaveStateMirroring::iter().map(|i| i.to_string()).collect() - } -} - -#[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] -pub enum OnOff { - Off = 0, - On = 1, } impl OnOff { pub fn from_val(val: u32) -> Option { match val { - 1 => Some(OnOff::On), - 0 => Some(OnOff::Off), + 1 => Some(OnOff::ON), + 0 => Some(OnOff::OFF), _ => None, } } pub fn as_bool(self) -> bool { match self { - OnOff::Off => false, - OnOff::On => true, + OnOff::OFF => false, + OnOff::ON => true, + _ => panic!("Invalid value in OnOff::as_bool: {}", self), } } } -impl fmt::Display for OnOff { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - OnOff::Off => "Off", - OnOff::On => "On", - } - ) - } -} - -impl ToggleTrait for OnOff { - fn to_toggle_vals() -> Vec { - vec![0, 1] - } - fn to_toggle_strings() -> Vec { - vec!["Off".to_string(), "On".to_string()] - } -} - -bitflags! { - pub struct Action : u32 { - const AIR_DODGE = 0x1; - const JUMP = 0x2; - const SHIELD = 0x4; - const SPOT_DODGE = 0x8; - const ROLL_F = 0x10; - const ROLL_B = 0x20; - const NAIR = 0x40; - const FAIR = 0x80; - const BAIR = 0x100; - const UAIR = 0x200; - const DAIR = 0x400; - const NEUTRAL_B = 0x800; - const SIDE_B = 0x1000; - const UP_B = 0x2000; - const DOWN_B = 0x4000; - const F_SMASH = 0x8000; - const U_SMASH = 0x10000; - const D_SMASH = 0x20000; - const JAB = 0x40000; - const F_TILT = 0x80000; - const U_TILT = 0x0010_0000; - const D_TILT = 0x0020_0000; - const GRAB = 0x0040_0000; - const DASH = 0x0080_0000; - const DASH_ATTACK = 0x0100_0000; - const PLAYBACK_1 = 0x0200_0000; - const PLAYBACK_2 = 0x0400_0000; - const PLAYBACK_3 = 0x0800_0000; - const PLAYBACK_4 = 0x1000_0000; - const PLAYBACK_5 = 0x2000_0000; +byteflags! { + pub struct Action { + pub AIR_DODGE = "Air Dodge", + pub JUMP = "Jump", + pub SHIELD = "Shield", + pub SPOT_DODGE = "Spot Dodge", + pub ROLL_F = "Roll Forwards", + pub ROLL_B = "Roll Backwards", + pub NAIR = "Neutral Air", + pub FAIR = "Forward Air", + pub BAIR = "Back Air", + pub UAIR = "Up Air", + pub DAIR = "Down Air", + pub NEUTRAL_B = "Neutral Special", + pub SIDE_B = "Side Special", + pub UP_B = "Up Special", + pub DOWN_B = "Down Special", + pub F_SMASH = "Forward Smash", + pub U_SMASH = "Up Smash", + pub D_SMASH = "Down Smash", + pub JAB = "Jab", + pub F_TILT = "Forward Tilt", + pub U_TILT = "Up Tilt", + pub D_TILT = "Down Tilt", + pub GRAB = "Grab", + pub DASH = "Dash", + pub DASH_ATTACK = "Dash Attack", + pub PLAYBACK_1 = "Playback Slot 1", + pub PLAYBACK_2 = "Playback Slot 2", + pub PLAYBACK_3 = "Playback Slot 3", + pub PLAYBACK_4 = "Playback Slot 4", + pub PLAYBACK_5 = "Playback Slot 5", } } @@ -547,196 +355,269 @@ impl Action { } } } - -impl fmt::Display for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - Action::AIR_DODGE => "Airdodge", - Action::JUMP => "Jump", - Action::SHIELD => "Shield", - Action::SPOT_DODGE => "Spotdodge", - Action::ROLL_F => "Roll Forwards", - Action::ROLL_B => "Roll Backwards", - Action::NAIR => "Neutral Aerial", - Action::FAIR => "Forward Aerial", - Action::BAIR => "Backward Aerial", - Action::UAIR => "Up Aerial", - Action::DAIR => "Down Aerial", - Action::NEUTRAL_B => "Neutral Special", - Action::SIDE_B => "Side Special", - Action::UP_B => "Up Special", - Action::DOWN_B => "Down Special", - Action::F_SMASH => "Forward Smash", - Action::U_SMASH => "Up Smash", - Action::D_SMASH => "Down Smash", - Action::JAB => "Jab", - Action::F_TILT => "Forward Tilt", - Action::U_TILT => "Up Tilt", - Action::D_TILT => "Down Tilt", - Action::GRAB => "Grab", - Action::DASH => "Dash", - Action::DASH_ATTACK => "Dash Attack", - Action::PLAYBACK_1 => "Playback Slot 1", - Action::PLAYBACK_2 => "Playback Slot 2", - Action::PLAYBACK_3 => "Playback Slot 3", - Action::PLAYBACK_4 => "Playback Slot 4", - Action::PLAYBACK_5 => "Playback Slot 5", - _ => combination_string.as_str(), - } - ) +byteflags! { + pub struct AttackAngle { + pub NEUTRAL = "Neutral", + pub UP = "Up", + pub DOWN = "Down", } } -extra_bitflag_impls! {Action} -impl_serde_for_bitflags!(Action); - -bitflags! { - pub struct AttackAngle : u32 { - const NEUTRAL = 0x1; - const UP = 0x2; - const DOWN = 0x4; - } -} - -impl fmt::Display for AttackAngle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - AttackAngle::NEUTRAL => "Neutral", - AttackAngle::UP => "Up", - AttackAngle::DOWN => "Down", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {AttackAngle} -impl_serde_for_bitflags!(AttackAngle); - -bitflags! { - pub struct Delay : u32 { - const D0 = 0x1; - const D1 = 0x2; - const D2 = 0x4; - const D3 = 0x8; - const D4 = 0x10; - const D5 = 0x20; - const D6 = 0x40; - const D7 = 0x80; - const D8 = 0x100; - const D9 = 0x200; - const D10 = 0x400; - const D11 = 0x800; - const D12 = 0x1000; - const D13 = 0x2000; - const D14 = 0x4000; - const D15 = 0x8000; - const D16 = 0x10000; - const D17 = 0x20000; - const D18 = 0x40000; - const D19 = 0x80000; - const D20 = 0x0010_0000; - const D21 = 0x0020_0000; - const D22 = 0x0040_0000; - const D23 = 0x0080_0000; - const D24 = 0x0100_0000; - const D25 = 0x0200_0000; - const D26 = 0x0400_0000; - const D27 = 0x0800_0000; - const D28 = 0x1000_0000; - const D29 = 0x2000_0000; - const D30 = 0x4000_0000; +byteflags! { + pub struct Delay { + pub D0 = "0", + pub D1 = "1", + pub D2 = "2", + pub D3 = "3", + pub D4 = "4", + pub D5 = "5", + pub D6 = "6", + pub D7 = "7", + pub D8 = "8", + pub D9 = "9", + pub D10 = "10", + pub D11 = "11", + pub D12 = "12", + pub D13 = "13", + pub D14 = "14", + pub D15 = "15", + pub D16 = "16", + pub D17 = "17", + pub D18 = "18", + pub D19 = "19", + pub D20 = "20", + pub D21 = "21", + pub D22 = "22", + pub D23 = "23", + pub D24 = "24", + pub D25 = "25", + pub D26 = "26", + pub D27 = "27", + pub D28 = "28", + pub D29 = "29", + pub D30 = "30", } } impl Delay { pub fn into_delay(&self) -> u32 { - self.to_index() - } -} - -// Throw Option -bitflags! { - pub struct ThrowOption : u32 - { - const NONE = 0x1; - const FORWARD = 0x2; - const BACKWARD = 0x4; - const UP = 0x8; - const DOWN = 0x10; - } -} - -impl ThrowOption { - pub fn into_cmd(self) -> Option { - #[cfg(feature = "smash")] - { - Some(match self { - ThrowOption::NONE => 0, - ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F, - ThrowOption::BACKWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_B, - ThrowOption::UP => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_HI, - ThrowOption::DOWN => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_LW, - _ => return None, - }) + if *self == Delay::empty() { + return 0; + }; + match *self { + Delay::D0 => 0, + Delay::D1 => 1, + Delay::D2 => 2, + Delay::D3 => 3, + Delay::D4 => 4, + Delay::D5 => 5, + Delay::D6 => 6, + Delay::D7 => 7, + Delay::D8 => 8, + Delay::D9 => 9, + Delay::D10 => 10, + Delay::D11 => 11, + Delay::D12 => 12, + Delay::D13 => 13, + Delay::D14 => 14, + Delay::D15 => 15, + Delay::D16 => 16, + Delay::D17 => 17, + Delay::D18 => 18, + Delay::D19 => 19, + Delay::D20 => 20, + Delay::D21 => 21, + Delay::D22 => 22, + Delay::D23 => 23, + Delay::D24 => 24, + Delay::D25 => 25, + Delay::D26 => 26, + Delay::D27 => 27, + Delay::D28 => 28, + Delay::D29 => 29, + Delay::D30 => 30, + _ => panic!("Invalid value in Delay::into_delay: {}", self), } - - #[cfg(not(feature = "smash"))] - None } } -impl fmt::Display for ThrowOption { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - ThrowOption::NONE => "None", - ThrowOption::FORWARD => "Forward Throw", - ThrowOption::BACKWARD => "Back Throw", - ThrowOption::UP => "Up Throw", - ThrowOption::DOWN => "Down Throw", - _ => combination_string.as_str(), - } - ) +byteflags! { + pub struct MedDelay { + pub D0 = "0", + pub D5 = "5", + pub D10 = "10", + pub D15 = "15", + pub D20 = "20", + pub D25 = "25", + pub D30 = "30", + pub D35 = "35", + pub D40 = "40", + pub D45 = "45", + pub D50 = "50", + pub D55 = "55", + pub D60 = "60", + pub D65 = "65", + pub D70 = "70", + pub D75 = "75", + pub D80 = "80", + pub D85 = "85", + pub D90 = "90", + pub D95 = "95", + pub D100 = "100", + pub D105 = "105", + pub D110 = "110", + pub D115 = "115", + pub D120 = "120", + pub D125 = "125", + pub D130 = "130", + pub D135 = "135", + pub D140 = "140", + pub D145 = "145", + pub D150 = "150", } } -extra_bitflag_impls! {ThrowOption} -impl_serde_for_bitflags!(ThrowOption); +impl MedDelay { + pub fn into_meddelay(&self) -> u32 { + if *self == MedDelay::empty() { + return 0; + }; + match *self { + MedDelay::D0 => 0, + MedDelay::D5 => 5, + MedDelay::D10 => 10, + MedDelay::D15 => 15, + MedDelay::D20 => 20, + MedDelay::D25 => 25, + MedDelay::D30 => 30, + MedDelay::D35 => 35, + MedDelay::D40 => 40, + MedDelay::D45 => 45, + MedDelay::D50 => 50, + MedDelay::D55 => 55, + MedDelay::D60 => 60, + MedDelay::D65 => 65, + MedDelay::D70 => 70, + MedDelay::D75 => 75, + MedDelay::D80 => 80, + MedDelay::D85 => 85, + MedDelay::D90 => 90, + MedDelay::D95 => 95, + MedDelay::D100 => 100, + MedDelay::D105 => 105, + MedDelay::D110 => 110, + MedDelay::D115 => 115, + MedDelay::D120 => 120, + MedDelay::D125 => 125, + MedDelay::D130 => 130, + MedDelay::D135 => 135, + MedDelay::D140 => 140, + MedDelay::D145 => 145, + MedDelay::D150 => 150, + _ => panic!("Invalid value in MedDelay::into_meddelay: {}", self), + } + } +} -// Buff Option -bitflags! { - pub struct BuffOption : u32 +byteflags! { + pub struct LongDelay { + pub D0 = "0", + pub D10 = "10", + pub D20 = "20", + pub D30 = "30", + pub D40 = "40", + pub D50 = "50", + pub D60 = "60", + pub D70 = "70", + pub D80 = "80", + pub D90 = "90", + pub D100 = "100", + pub D110 = "110", + pub D120 = "120", + pub D130 = "130", + pub D140 = "140", + pub D150 = "150", + pub D160 = "160", + pub D170 = "170", + pub D180 = "180", + pub D190 = "190", + pub D200 = "200", + pub D210 = "210", + pub D220 = "220", + pub D230 = "230", + pub D240 = "240", + pub D250 = "250", + pub D260 = "260", + pub D270 = "270", + pub D280 = "280", + pub D290 = "290", + pub D300 = "300", + } +} + +impl LongDelay { + pub fn into_longdelay(&self) -> u32 { + if *self == LongDelay::empty() { + return 0; + }; + match *self { + LongDelay::D0 => 0, + LongDelay::D10 => 10, + LongDelay::D20 => 20, + LongDelay::D30 => 30, + LongDelay::D40 => 40, + LongDelay::D50 => 50, + LongDelay::D60 => 60, + LongDelay::D70 => 70, + LongDelay::D80 => 80, + LongDelay::D90 => 90, + LongDelay::D100 => 100, + LongDelay::D110 => 110, + LongDelay::D120 => 120, + LongDelay::D130 => 130, + LongDelay::D140 => 140, + LongDelay::D150 => 150, + LongDelay::D160 => 160, + LongDelay::D170 => 170, + LongDelay::D180 => 180, + LongDelay::D190 => 190, + LongDelay::D200 => 200, + LongDelay::D210 => 210, + LongDelay::D220 => 220, + LongDelay::D230 => 230, + LongDelay::D240 => 240, + LongDelay::D250 => 250, + LongDelay::D260 => 260, + LongDelay::D270 => 270, + LongDelay::D280 => 280, + LongDelay::D290 => 290, + LongDelay::D300 => 300, + _ => panic!("Invalid value in LongDelay::into_longdelay: {}", self), + } + } +} + +byteflags! { + pub struct BuffOption { - const ACCELERATLE = 0x1; - const OOMPH = 0x2; - const PSYCHE = 0x4; - const BOUNCE = 0x8; - const ARSENE = 0x10; - const BREATHING = 0x20; - const LIMIT = 0x40; - const KO = 0x80; - const WING = 0x100; - const MONAD_JUMP = 0x200; - const MONAD_SPEED = 0x400; - const MONAD_SHIELD = 0x800; - const MONAD_BUSTER = 0x1000; - const MONAD_SMASH = 0x2000; - const POWER_DRAGON = 0x4000; - const WAFT_MINI = 0x8000; - const WAFT_HALF = 0x10000; - const WAFT_FULL = 0x20000; + pub ACCELERATLE = "Acceleratle", + pub OOMPH = "Oomph", + pub PSYCHE = "Psyche Up", + pub BOUNCE = "Bounce", + pub ARSENE = "Arsene", + pub BREATHING = "Deep Breathing", + pub LIMIT = "Limit", + pub KO = "KO Punch", + pub WING = "1-Winged Angel", + pub MONAD_JUMP = "Jump", + pub MONAD_SPEED = "Speed", + pub MONAD_SHIELD = "Shield", + pub MONAD_BUSTER = "Buster", + pub MONAD_SMASH = "Smash", + pub POWER_DRAGON = "Power Dragon", + pub WAFT_MINI = "Mini Waft", + pub WAFT_HALF = "Half Waft", + pub WAFT_FULL = "Full Waft", } } @@ -773,1005 +654,392 @@ impl BuffOption { pub fn hero_buffs(self) -> BuffOption { // Return a struct with only Hero's selected buffs - let hero_buffs_bitflags = BuffOption::ACCELERATLE + let hero_buffs_byteflags = BuffOption::ACCELERATLE .union(BuffOption::OOMPH) .union(BuffOption::BOUNCE) .union(BuffOption::PSYCHE); - self.intersection(hero_buffs_bitflags) + self.left_intersection(hero_buffs_byteflags) } pub fn shulk_buffs(self) -> BuffOption { // Return a struct with only Shulk's selected arts - let shulk_buffs_bitflags = BuffOption::MONAD_JUMP + let shulk_buffs_byteflags = BuffOption::MONAD_JUMP .union(BuffOption::MONAD_SPEED) .union(BuffOption::MONAD_SHIELD) .union(BuffOption::MONAD_BUSTER) .union(BuffOption::MONAD_SMASH); - self.intersection(shulk_buffs_bitflags) + self.left_intersection(shulk_buffs_byteflags) } pub fn wario_buffs(self) -> BuffOption { - let wario_buffs_bitflags = BuffOption::WAFT_MINI + let wario_buffs_byteflags = BuffOption::WAFT_MINI .union(BuffOption::WAFT_HALF) .union(BuffOption::WAFT_FULL); - self.intersection(wario_buffs_bitflags) + self.left_intersection(wario_buffs_byteflags) } } -impl fmt::Display for BuffOption { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - BuffOption::ACCELERATLE => "Acceleratle", - BuffOption::OOMPH => "Oomph", - BuffOption::BOUNCE => "Bounce", - BuffOption::PSYCHE => "Psyche Up", - BuffOption::BREATHING => "Deep Breathing", - BuffOption::ARSENE => "Arsene", - BuffOption::LIMIT => "Limit Break", - BuffOption::KO => "KO Punch", - BuffOption::WING => "1-Winged Angel", - BuffOption::MONAD_JUMP => "Jump", - BuffOption::MONAD_SPEED => "Speed", - BuffOption::MONAD_SHIELD => "Shield", - BuffOption::MONAD_BUSTER => "Buster", - BuffOption::MONAD_SMASH => "Smash", - BuffOption::POWER_DRAGON => "Power Dragon", - BuffOption::WAFT_MINI => "Mini Waft", - BuffOption::WAFT_HALF => "Half Waft", - BuffOption::WAFT_FULL => "Full Waft", - _ => combination_string.as_str(), - } - ) +byteflags! { + pub struct ThrowOption + { + NONE = "None", + FORWARD = "Forward Throw", + BACKWARD = "Backward Throw", + UP = "Up Throw", + DOWN = "Down Throw", } } -extra_bitflag_impls! {BuffOption} -impl_serde_for_bitflags!(BuffOption); +impl ThrowOption { + pub fn into_cmd(self) -> Option { + #[cfg(feature = "smash")] + { + Some(match self { + ThrowOption::NONE => 0, + ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F, + ThrowOption::BACKWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_B, + ThrowOption::UP => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_HI, + ThrowOption::DOWN => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_LW, + _ => return None, + }) + } -impl fmt::Display for Delay { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - Delay::D0 => "0", - Delay::D1 => "1", - Delay::D2 => "2", - Delay::D3 => "3", - Delay::D4 => "4", - Delay::D5 => "5", - Delay::D6 => "6", - Delay::D7 => "7", - Delay::D8 => "8", - Delay::D9 => "9", - Delay::D10 => "10", - Delay::D11 => "11", - Delay::D12 => "12", - Delay::D13 => "13", - Delay::D14 => "14", - Delay::D15 => "15", - Delay::D16 => "16", - Delay::D17 => "17", - Delay::D18 => "18", - Delay::D19 => "19", - Delay::D20 => "20", - Delay::D21 => "21", - Delay::D22 => "22", - Delay::D23 => "23", - Delay::D24 => "24", - Delay::D25 => "25", - Delay::D26 => "26", - Delay::D27 => "27", - Delay::D28 => "28", - Delay::D29 => "29", - Delay::D30 => "30", - _ => combination_string.as_str(), - } - ) + #[cfg(not(feature = "smash"))] + None } } -extra_bitflag_impls! {Delay} -impl_serde_for_bitflags!(Delay); - -bitflags! { - pub struct MedDelay : u32 { - const D0 = 0x1; - const D5 = 0x2; - const D10 = 0x4; - const D15 = 0x8; - const D20 = 0x10; - const D25 = 0x20; - const D30 = 0x40; - const D35 = 0x80; - const D40 = 0x100; - const D45 = 0x200; - const D50 = 0x400; - const D55 = 0x800; - const D60 = 0x1000; - const D65 = 0x2000; - const D70 = 0x4000; - const D75 = 0x8000; - const D80 = 0x10000; - const D85 = 0x20000; - const D90 = 0x40000; - const D95 = 0x80000; - const D100 = 0x0010_0000; - const D105 = 0x0020_0000; - const D110 = 0x0040_0000; - const D115 = 0x0080_0000; - const D120 = 0x0100_0000; - const D125 = 0x0200_0000; - const D130 = 0x0400_0000; - const D135 = 0x0800_0000; - const D140 = 0x1000_0000; - const D145 = 0x2000_0000; - const D150 = 0x4000_0000; +// TODO!() Is this redundant with OnOff? +byteflags! { + pub struct BoolFlag { + pub TRUE = "True", + pub FALSE = "False", } } -impl MedDelay { - pub fn into_meddelay(&self) -> u32 { - self.to_index() * 5 - } -} - -impl fmt::Display for MedDelay { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - MedDelay::D0 => "0", - MedDelay::D5 => "5", - MedDelay::D10 => "10", - MedDelay::D15 => "15", - MedDelay::D20 => "20", - MedDelay::D25 => "25", - MedDelay::D30 => "30", - MedDelay::D35 => "35", - MedDelay::D40 => "40", - MedDelay::D45 => "45", - MedDelay::D50 => "50", - MedDelay::D55 => "55", - MedDelay::D60 => "60", - MedDelay::D65 => "65", - MedDelay::D70 => "70", - MedDelay::D75 => "75", - MedDelay::D80 => "80", - MedDelay::D85 => "85", - MedDelay::D90 => "90", - MedDelay::D95 => "95", - MedDelay::D100 => "100", - MedDelay::D105 => "105", - MedDelay::D110 => "110", - MedDelay::D115 => "115", - MedDelay::D120 => "120", - MedDelay::D125 => "125", - MedDelay::D130 => "130", - MedDelay::D135 => "135", - MedDelay::D140 => "140", - MedDelay::D145 => "145", - MedDelay::D150 => "150", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {MedDelay} -impl_serde_for_bitflags!(MedDelay); - -bitflags! { - pub struct LongDelay : u32 { - const D0 = 0x1; - const D10 = 0x2; - const D20 = 0x4; - const D30 = 0x8; - const D40 = 0x10; - const D50 = 0x20; - const D60 = 0x40; - const D70 = 0x80; - const D80 = 0x100; - const D90 = 0x200; - const D100 = 0x400; - const D110 = 0x800; - const D120 = 0x1000; - const D130 = 0x2000; - const D140 = 0x4000; - const D150 = 0x8000; - const D160 = 0x10000; - const D170 = 0x20000; - const D180 = 0x40000; - const D190 = 0x80000; - const D200 = 0x0010_0000; - const D210 = 0x0020_0000; - const D220 = 0x0040_0000; - const D230 = 0x0080_0000; - const D240 = 0x0100_0000; - const D250 = 0x0200_0000; - const D260 = 0x0400_0000; - const D270 = 0x0800_0000; - const D280 = 0x1000_0000; - const D290 = 0x2000_0000; - const D300 = 0x4000_0000; - } -} - -impl LongDelay { - pub fn into_longdelay(&self) -> u32 { - self.to_index() * 10 - } -} - -impl fmt::Display for LongDelay { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - LongDelay::D0 => "0", - LongDelay::D10 => "10", - LongDelay::D20 => "20", - LongDelay::D30 => "30", - LongDelay::D40 => "40", - LongDelay::D50 => "50", - LongDelay::D60 => "60", - LongDelay::D70 => "70", - LongDelay::D80 => "80", - LongDelay::D90 => "90", - LongDelay::D100 => "100", - LongDelay::D110 => "110", - LongDelay::D120 => "120", - LongDelay::D130 => "130", - LongDelay::D140 => "140", - LongDelay::D150 => "150", - LongDelay::D160 => "160", - LongDelay::D170 => "170", - LongDelay::D180 => "180", - LongDelay::D190 => "190", - LongDelay::D200 => "200", - LongDelay::D210 => "210", - LongDelay::D220 => "220", - LongDelay::D230 => "230", - LongDelay::D240 => "240", - LongDelay::D250 => "250", - LongDelay::D260 => "260", - LongDelay::D270 => "270", - LongDelay::D280 => "280", - LongDelay::D290 => "290", - LongDelay::D300 => "300", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {LongDelay} -impl_serde_for_bitflags!(LongDelay); - -bitflags! { - pub struct BoolFlag : u32 { - const TRUE = 0x1; - const FALSE = 0x2; - } -} - -extra_bitflag_impls! {BoolFlag} -impl_serde_for_bitflags!(BoolFlag); - impl BoolFlag { pub fn into_bool(self) -> bool { matches!(self, BoolFlag::TRUE) } } -impl fmt::Display for BoolFlag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - BoolFlag::TRUE => "True", - BoolFlag::FALSE => "False", - _ => combination_string.as_str(), - } - ) +byteflags! { + pub struct SdiFrequency { + pub NONE = "None", + pub NORMAL = "Normal", + pub MEDIUM = "Medium", + pub HIGH = "High", } } -#[repr(u32)] -#[derive( - Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum SdiFrequency { - None = 0, - Normal = 1, - Medium = 2, - High = 4, -} - impl SdiFrequency { pub fn into_u32(self) -> u32 { match self { - SdiFrequency::None => u32::MAX, - SdiFrequency::Normal => 8, - SdiFrequency::Medium => 6, - SdiFrequency::High => 4, + SdiFrequency::NONE => u32::MAX, + SdiFrequency::NORMAL => 8, + SdiFrequency::MEDIUM => 6, + SdiFrequency::HIGH => 4, + _ => panic!("Invalid value in SdiFrequency::into_u32: {}", self), } } } -impl fmt::Display for SdiFrequency { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - SdiFrequency::None => "None", - SdiFrequency::Normal => "Normal", - SdiFrequency::Medium => "Medium", - SdiFrequency::High => "High", - } - ) +byteflags! { + pub struct ClatterFrequency { + pub NONE = "None", + pub NORMAL = "Normal", + pub MEDIUM = "Medium", + pub HIGH = "High", } } -impl ToggleTrait for SdiFrequency { - fn to_toggle_vals() -> Vec { - SdiFrequency::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - SdiFrequency::iter().map(|i| i.to_string()).collect() - } -} - -#[repr(u32)] -#[derive( - Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum ClatterFrequency { - None = 0, - Normal = 1, - Medium = 2, - High = 4, -} - impl ClatterFrequency { pub fn into_u32(self) -> u32 { match self { - ClatterFrequency::None => u32::MAX, - ClatterFrequency::Normal => 8, - ClatterFrequency::Medium => 5, - ClatterFrequency::High => 2, + ClatterFrequency::NONE => u32::MAX, + ClatterFrequency::NORMAL => 8, + ClatterFrequency::MEDIUM => 5, + ClatterFrequency::HIGH => 2, + _ => panic!("Invalid value in ClatterFrequency::into_u32: {}", self), } } } -impl fmt::Display for ClatterFrequency { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - ClatterFrequency::None => "None", - ClatterFrequency::Normal => "Normal", - ClatterFrequency::Medium => "Medium", - ClatterFrequency::High => "High", - } - ) +byteflags! { + pub struct CharacterItem { + pub NONE = "None", + pub PLAYER_VARIATION_1 = "Player 1st Var.", + pub PLAYER_VARIATION_2 = "Player 2nd Var.", + pub PLAYER_VARIATION_3 = "Player 3rd Var.", + pub PLAYER_VARIATION_4 = "Player 4th Var.", + pub PLAYER_VARIATION_5 = "Player 5th Var.", + pub PLAYER_VARIATION_6 = "Player 6th Var.", + pub PLAYER_VARIATION_7 = "Player 7th Var.", + pub PLAYER_VARIATION_8 = "Player 8th Var.", + pub CPU_VARIATION_1 = "CPU 1st Var.", + pub CPU_VARIATION_2 = "CPU 2nd Var.", + pub CPU_VARIATION_3 = "CPU 3rd Var.", + pub CPU_VARIATION_4 = "CPU 4th Var.", + pub CPU_VARIATION_5 = "CPU 5th Var.", + pub CPU_VARIATION_6 = "CPU 6th Var.", + pub CPU_VARIATION_7 = "CPU 7th Var.", + pub CPU_VARIATION_8 = "CPU 8th Var.", } } -impl ToggleTrait for ClatterFrequency { - fn to_toggle_vals() -> Vec { - ClatterFrequency::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - ClatterFrequency::iter().map(|i| i.to_string()).collect() - } -} - -/// Item Selections -#[repr(i32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum CharacterItem { - None = 0, - PlayerVariation1 = 0x1, - PlayerVariation2 = 0x2, - PlayerVariation3 = 0x4, - PlayerVariation4 = 0x8, - PlayerVariation5 = 0x10, - PlayerVariation6 = 0x20, - PlayerVariation7 = 0x40, - PlayerVariation8 = 0x80, - CpuVariation1 = 0x100, - CpuVariation2 = 0x200, - CpuVariation3 = 0x400, - CpuVariation4 = 0x800, - CpuVariation5 = 0x1000, - CpuVariation6 = 0x2000, - CpuVariation7 = 0x4000, - CpuVariation8 = 0x8000, -} - impl CharacterItem { - pub fn as_idx(self) -> u32 { - log_2(self as i32 as u32) + pub fn as_idx(&self) -> usize { + match *self { + CharacterItem::NONE => 0, + CharacterItem::PLAYER_VARIATION_1 => 1, + CharacterItem::PLAYER_VARIATION_2 => 2, + CharacterItem::PLAYER_VARIATION_3 => 3, + CharacterItem::PLAYER_VARIATION_4 => 4, + CharacterItem::PLAYER_VARIATION_5 => 5, + CharacterItem::PLAYER_VARIATION_6 => 6, + CharacterItem::PLAYER_VARIATION_7 => 7, + CharacterItem::PLAYER_VARIATION_8 => 8, + CharacterItem::CPU_VARIATION_1 => 9, + CharacterItem::CPU_VARIATION_2 => 10, + CharacterItem::CPU_VARIATION_3 => 11, + CharacterItem::CPU_VARIATION_4 => 12, + CharacterItem::CPU_VARIATION_5 => 13, + CharacterItem::CPU_VARIATION_6 => 14, + CharacterItem::CPU_VARIATION_7 => 15, + CharacterItem::CPU_VARIATION_8 => 16, + _ => panic!("Invalid value in CharacterItem::as_idx: {}", self), + } } } -impl fmt::Display for CharacterItem { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - CharacterItem::PlayerVariation1 => "Player 1st Var.", - CharacterItem::PlayerVariation2 => "Player 2nd Var.", - CharacterItem::PlayerVariation3 => "Player 3rd Var.", - CharacterItem::PlayerVariation4 => "Player 4th Var.", - CharacterItem::PlayerVariation5 => "Player 5th Var.", - CharacterItem::PlayerVariation6 => "Player 6th Var.", - CharacterItem::PlayerVariation7 => "Player 7th Var.", - CharacterItem::PlayerVariation8 => "Player 8th Var.", - CharacterItem::CpuVariation1 => "CPU 1st Var.", - CharacterItem::CpuVariation2 => "CPU 2nd Var.", - CharacterItem::CpuVariation3 => "CPU 3rd Var.", - CharacterItem::CpuVariation4 => "CPU 4th Var.", - CharacterItem::CpuVariation5 => "CPU 5th Var.", - CharacterItem::CpuVariation6 => "CPU 6th Var.", - CharacterItem::CpuVariation7 => "CPU 7th Var.", - CharacterItem::CpuVariation8 => "CPU 8th Var.", - CharacterItem::None => "None", - } - ) - } -} - -impl ToggleTrait for CharacterItem { - fn to_toggle_vals() -> Vec { - CharacterItem::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - CharacterItem::iter().map(|i| i.to_string()).collect() - } -} - -bitflags! { - pub struct MashTrigger : u32 { - const HIT = 0b0000_0000_0000_0000_0001; - const SHIELDSTUN = 0b0000_0000_0000_0000_0010; - const PARRY = 0b0000_0000_0000_0000_0100; - const TUMBLE = 0b0000_0000_0000_0000_1000; - const LANDING = 0b0000_0000_0000_0001_0000; - const TRUMP = 0b0000_0000_0000_0010_0000; - const FOOTSTOOL = 0b0000_0000_0000_0100_0000; - const CLATTER = 0b0000_0000_0000_1000_0000; - const LEDGE = 0b0000_0000_0001_0000_0000; - const TECH = 0b0000_0000_0010_0000_0000; - const MISTECH = 0b0000_0000_0100_0000_0000; - const GROUNDED = 0b0000_0000_1000_0000_0000; - const AIRBORNE = 0b0000_0001_0000_0000_0000; - const DISTANCE_CLOSE = 0b0000_0010_0000_0000_0000; - const DISTANCE_MID = 0b0000_0100_0000_0000_0000; - const DISTANCE_FAR = 0b0000_1000_0000_0000_0000; - const ALWAYS = 0b0001_0000_0000_0000_0000; +byteflags! { + pub struct MashTrigger { + pub HIT = "Hitstun", + pub SHIELDSTUN = "Shieldstun", + pub PARRY = "Parry", + pub TUMBLE = "Tumble", + pub LANDING = "Landing", + pub TRUMP = "Ledge Trump", + pub FOOTSTOOL = "Footstool", + pub CLATTER = "Clatter", + pub LEDGE = "Ledge Option", + pub TECH = "Tech Option", + pub MISTECH = "Mistech Option", + pub GROUNDED = "Grounded", + pub AIRBORNE = "Airborne", + pub DISTANCE_CLOSE = "Distance: Close", + pub DISTANCE_MID = "Distance: Mid", + pub DISTANCE_FAR = "Distance: Far", + pub ALWAYS = "Always", } } impl MashTrigger { pub const fn default() -> MashTrigger { // Hit, block, clatter - MashTrigger::HIT - .union(MashTrigger::TUMBLE) - .union(MashTrigger::SHIELDSTUN) - .union(MashTrigger::CLATTER) + MashTrigger { + HIT: 1, + TUMBLE: 1, + SHIELDSTUN: 1, + CLATTER: 1, + ..MashTrigger::empty() + } } } -impl fmt::Display for MashTrigger { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - MashTrigger::HIT => "Hitstun", - MashTrigger::SHIELDSTUN => "Shieldstun", - MashTrigger::PARRY => "Parry", - MashTrigger::TUMBLE => "Tumble", - MashTrigger::LANDING => "Landing", - MashTrigger::TRUMP => "Ledge Trump", - MashTrigger::FOOTSTOOL => "Footstool", - MashTrigger::CLATTER => "Clatter", - MashTrigger::LEDGE => "Ledge Option", - MashTrigger::TECH => "Tech Option", - MashTrigger::MISTECH => "Mistech Option", - MashTrigger::GROUNDED => "Grounded", - MashTrigger::AIRBORNE => "Airborne", - MashTrigger::DISTANCE_CLOSE => "Distance: Close", - MashTrigger::DISTANCE_MID => "Distance: Mid", - MashTrigger::DISTANCE_FAR => "Distance: Far", - MashTrigger::ALWAYS => "Always", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {MashTrigger} -impl_serde_for_bitflags!(MashTrigger); - #[derive(Clone, Copy, Serialize, Deserialize, Debug)] pub struct DamagePercent(pub u32, pub u32); -impl SliderTrait for DamagePercent { - fn get_limits() -> (u32, u32) { - (0, 150) - } -} - impl DamagePercent { pub const fn default() -> DamagePercent { DamagePercent(0, 150) } } -bitflags! { - pub struct SaveDamage : u32 - { - const DEFAULT = 0b001; - const SAVED = 0b010; - const RANDOM = 0b100; +byteflags! { + pub struct SaveDamage { + pub DEFAULT = "Default", + pub SAVED = "Save State", + pub RANDOM = "Random Value", } } -impl fmt::Display for SaveDamage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - SaveDamage::DEFAULT => "Default", - SaveDamage::SAVED => "Save State", - SaveDamage::RANDOM => "Random Value", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {SaveDamage} -impl_serde_for_bitflags!(SaveDamage); - -// Save State Slots -bitflags! { - pub struct SaveStateSlot : u32 +byteflags! { + pub struct SaveStateSlot { - const S1 = 0x1; - const S2 = 0x2; - const S3 = 0x4; - const S4 = 0x8; - const S5 = 0x10; + pub S1 = "Slot 1", + pub S2 = "Slot 2", + pub S3 = "Slot 3", + pub S4 = "Slot 4", + pub S5 = "Slot 5", } } impl SaveStateSlot { - pub fn into_idx(self) -> Option { - Some(match self { - SaveStateSlot::S1 => 0, - SaveStateSlot::S2 => 1, - SaveStateSlot::S3 => 2, - SaveStateSlot::S4 => 3, - SaveStateSlot::S5 => 4, - _ => return None, - }) - } - - pub fn as_idx(self) -> usize { - match self { - SaveStateSlot::S1 => 0, - SaveStateSlot::S2 => 1, - SaveStateSlot::S3 => 2, - SaveStateSlot::S4 => 3, - SaveStateSlot::S5 => 4, - _ => 0, + pub fn into_idx(&self) -> Option { + match *self { + SaveStateSlot::S1 => Some(0), + SaveStateSlot::S2 => Some(1), + SaveStateSlot::S3 => Some(2), + SaveStateSlot::S4 => Some(3), + SaveStateSlot::S5 => Some(4), + _ => panic!("Invalid value in SaveStateSlot::into_idx: {}", self), } } } -impl fmt::Display for SaveStateSlot { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - SaveStateSlot::S1 => "1", - SaveStateSlot::S2 => "2", - SaveStateSlot::S3 => "3", - SaveStateSlot::S4 => "4", - SaveStateSlot::S5 => "5", - _ => combination_string.as_str(), - } - ) +byteflags! { + pub struct RecordSlot { + pub S1 = "Slot 1", + pub S2 = "Slot 2", + pub S3 = "Slot 3", + pub S4 = "Slot 4", + pub S5 = "Slot 5", } } -extra_bitflag_impls! {SaveStateSlot} -impl_serde_for_bitflags!(SaveStateSlot); - -// Input Recording Slot -#[repr(u32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum RecordSlot { - S1 = 0x1, - S2 = 0x2, - S3 = 0x4, - S4 = 0x8, - S5 = 0x10, -} - impl RecordSlot { - pub fn into_idx(self) -> usize { - match self { - RecordSlot::S1 => 0, - RecordSlot::S2 => 1, - RecordSlot::S3 => 2, - RecordSlot::S4 => 3, - RecordSlot::S5 => 4, + pub fn into_idx(&self) -> Option { + match *self { + RecordSlot::S1 => Some(0), + RecordSlot::S2 => Some(1), + RecordSlot::S3 => Some(2), + RecordSlot::S4 => Some(3), + RecordSlot::S5 => Some(4), + _ => panic!("Invalid value in RecordSlot::into_idx: {}", self), } } } -impl fmt::Display for RecordSlot { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - RecordSlot::S1 => "Slot One", - RecordSlot::S2 => "Slot Two", - RecordSlot::S3 => "Slot Three", - RecordSlot::S4 => "Slot Four", - RecordSlot::S5 => "Slot Five", - } - ) - } -} - -impl ToggleTrait for RecordSlot { - fn to_toggle_vals() -> Vec { - RecordSlot::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - RecordSlot::iter().map(|i| i.to_string()).collect() - } -} - -// Input Playback Slot -bitflags! { - pub struct PlaybackSlot : u32 - { - const S1 = 0x1; - const S2 = 0x2; - const S3 = 0x4; - const S4 = 0x8; - const S5 = 0x10; +byteflags! { + pub struct PlaybackSlot { + pub S1 = "Slot 1", + pub S2 = "Slot 2", + pub S3 = "Slot 3", + pub S4 = "Slot 4", + pub S5 = "Slot 5", } } impl PlaybackSlot { - pub fn into_idx(self) -> Option { - Some(match self { - PlaybackSlot::S1 => 0, - PlaybackSlot::S2 => 1, - PlaybackSlot::S3 => 2, - PlaybackSlot::S4 => 3, - PlaybackSlot::S5 => 4, - _ => return None, - }) + pub fn into_idx(&self) -> Option { + match *self { + PlaybackSlot::S1 => Some(0), + PlaybackSlot::S2 => Some(1), + PlaybackSlot::S3 => Some(2), + PlaybackSlot::S4 => Some(3), + PlaybackSlot::S5 => Some(4), + _ => panic!("Invalid value in PlaybackSlot::into_idx: {}", self), + } } } -impl fmt::Display for PlaybackSlot { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - PlaybackSlot::S1 => "Slot One", - PlaybackSlot::S2 => "Slot Two", - PlaybackSlot::S3 => "Slot Three", - PlaybackSlot::S4 => "Slot Four", - PlaybackSlot::S5 => "Slot Five", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {PlaybackSlot} -impl_serde_for_bitflags!(PlaybackSlot); - // If doing input recording out of hitstun, when does playback begin after? -#[repr(u32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum HitstunPlayback { - // Should these start at 0? All of my new menu structs need some review, I'm just doing whatever atm - Hitstun = 0x1, - Hitstop = 0x2, - Instant = 0x4, -} - -impl ToggleTrait for HitstunPlayback { - fn to_toggle_vals() -> Vec { - HitstunPlayback::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - HitstunPlayback::iter().map(|i| i.to_string()).collect() +byteflags! { + pub struct HitstunPlayback { + pub HITSTUN = "As Hitstun Ends", + pub HITSTOP = "As Hitstop Ends", + pub INSTANT = "As Hitstop Begins", } } -impl fmt::Display for HitstunPlayback { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - HitstunPlayback::Hitstun => "As Hitstun Ends", - HitstunPlayback::Hitstop => "As Hitstop Ends", - HitstunPlayback::Instant => "As Hitstop Begins", - } - ) +byteflags! { + pub struct RecordTrigger { + pub COMMAND = "Button Combination", + pub SAVESTATE = "Save State Load", } } -// Input Recording Trigger Type -bitflags! { - pub struct RecordTrigger : u32 - { - const COMMAND = 0x1; - const SAVESTATE = 0x2; +byteflags! { + pub struct RecordingDuration { + pub F60 = "60", + pub F90 = "90", + pub F120 = "120", + pub F150 = "150", + pub F180 = "180", + pub F210 = "210", + pub F240 = "240", + pub F270 = "270", + pub F300 = "300", + pub F330 = "330", + pub F360 = "360", + pub F390 = "390", + pub F420 = "420", + pub F450 = "450", + pub F480 = "480", + pub F510 = "510", + pub F540 = "540", + pub F570 = "570", + pub F600 = "600", } } -impl fmt::Display for RecordTrigger { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - RecordTrigger::COMMAND => "Button Combination", - RecordTrigger::SAVESTATE => "Save State Load", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {RecordTrigger} -impl_serde_for_bitflags!(RecordTrigger); - -#[repr(u32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum RecordingDuration { - F60 = 0x1, - F90 = 0x2, - F120 = 0x4, - F150 = 0x8, - F180 = 0x10, - F210 = 0x20, - F240 = 0x40, - F270 = 0x80, - F300 = 0x100, - F330 = 0x200, - F360 = 0x400, - F390 = 0x800, - F420 = 0x1000, - F450 = 0x2000, - F480 = 0x4000, - F510 = 0x8000, - F540 = 0x10000, - F570 = 0x20000, - F600 = 0x40000, -} - impl RecordingDuration { - pub fn into_frames(self) -> usize { - (log_2(self as u32) as usize * 30) + 60 + pub fn into_frames(&self) -> usize { + match *self { + RecordingDuration::F60 => 60, + RecordingDuration::F90 => 90, + RecordingDuration::F120 => 120, + RecordingDuration::F150 => 150, + RecordingDuration::F180 => 180, + RecordingDuration::F210 => 210, + RecordingDuration::F240 => 240, + RecordingDuration::F270 => 270, + RecordingDuration::F300 => 300, + RecordingDuration::F330 => 330, + RecordingDuration::F360 => 360, + RecordingDuration::F390 => 390, + RecordingDuration::F420 => 420, + RecordingDuration::F450 => 450, + RecordingDuration::F480 => 480, + RecordingDuration::F510 => 510, + RecordingDuration::F540 => 540, + RecordingDuration::F570 => 570, + RecordingDuration::F600 => 600, + _ => panic!("Invalid value in RecordingDuration::into_frames: {}", self), + } } } -impl ToggleTrait for RecordingDuration { - fn to_toggle_vals() -> Vec { - RecordingDuration::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - RecordingDuration::iter().map(|i| i.to_string()).collect() +byteflags! { + pub struct ButtonConfig { + pub A = "A", + pub B = "B", + pub X = "X", + pub Y = "Y", + pub L = "Pro L", + pub R = "Pro R; GCC Z", + pub ZL = "Pro ZL; GCC L", + pub ZR = "Pro ZR; GCC R", + pub DPAD_UP = "DPad Up", + pub DPAD_DOWN = "DPad Down", + pub DPAD_LEFT = "DPad Left", + pub DPAD_RIGHT = "DPad Right", + pub PLUS = "Plus", + pub MINUS = "Minus", + pub LSTICK = "Left Stick Press", + pub RSTICK = "Right Stick Press", } } -impl fmt::Display for RecordingDuration { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use RecordingDuration::*; - write!( - f, - "{}", - match self { - F60 => "60", - F90 => "90", - F120 => "120", - F150 => "150", - F180 => "180", - F210 => "210", - F240 => "240", - F270 => "270", - F300 => "300", - F330 => "330", - F360 => "360", - F390 => "390", - F420 => "420", - F450 => "450", - F480 => "480", - F510 => "510", - F540 => "540", - F570 => "570", - F600 => "600", - } - ) +byteflags! { + pub struct UpdatePolicy { + pub STABLE = "Stable", + pub BETA = "Beta", + pub DISABLED = "Disabled", } } -bitflags! { - pub struct ButtonConfig : u32 { - const A = 0b0000_0000_0000_0000_0001; - const B = 0b0000_0000_0000_0000_0010; - const X = 0b0000_0000_0000_0000_0100; - const Y = 0b0000_0000_0000_0000_1000; - const L = 0b0000_0000_0000_0001_0000; - const R = 0b0000_0000_0000_0010_0000; - const ZL = 0b0000_0000_0000_0100_0000; - const ZR = 0b0000_0000_0000_1000_0000; - const DPAD_UP = 0b0000_0000_0001_0000_0000; - const DPAD_DOWN = 0b0000_0000_0010_0000_0000; - const DPAD_LEFT = 0b0000_0000_0100_0000_0000; - const DPAD_RIGHT = 0b0000_0000_1000_0000_0000; - const PLUS = 0b0000_0001_0000_0000_0000; - const MINUS = 0b0000_0010_0000_0000_0000; - const LSTICK = 0b0000_0100_0000_0000_0000; - const RSTICK = 0b0000_1000_0000_0000_0000; - } -} - -impl fmt::Display for ButtonConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let combination_string = self.combination_string(); - write!( - f, - "{}", - match *self { - ButtonConfig::A => "A", - ButtonConfig::B => "B", - ButtonConfig::X => "X", - ButtonConfig::Y => "Y", - ButtonConfig::L => "Pro L", - ButtonConfig::R => "Pro R; GCC Z", - ButtonConfig::ZL => "Pro ZL; GCC L", - ButtonConfig::ZR => "Pro ZR; GCC R", - ButtonConfig::DPAD_UP => "DPad Up", - ButtonConfig::DPAD_DOWN => "DPad Down", - ButtonConfig::DPAD_LEFT => "DPad Left", - ButtonConfig::DPAD_RIGHT => "DPad Right", - ButtonConfig::PLUS => "Plus", - ButtonConfig::MINUS => "Minus", - ButtonConfig::LSTICK => "Left Stick Press", - ButtonConfig::RSTICK => "Right Stick Press", - _ => combination_string.as_str(), - } - ) - } -} - -extra_bitflag_impls! {ButtonConfig} -impl_serde_for_bitflags!(ButtonConfig); - -#[repr(u32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum UpdatePolicy { - Stable, - Beta, - Disabled, -} - impl UpdatePolicy { pub const fn default() -> UpdatePolicy { - UpdatePolicy::Stable + UpdatePolicy::STABLE } } -impl fmt::Display for UpdatePolicy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - UpdatePolicy::Stable => "Stable", - UpdatePolicy::Beta => "Beta", - UpdatePolicy::Disabled => "Disabled", - } - ) - } -} - -impl ToggleTrait for UpdatePolicy { - fn to_toggle_vals() -> Vec { - UpdatePolicy::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - UpdatePolicy::iter().map(|i| i.to_string()).collect() - } -} - -#[repr(u32)] -#[derive( - Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, -)] -pub enum InputDisplay { - None, - Smash, - Raw, -} - -impl fmt::Display for InputDisplay { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - InputDisplay::None => "None", - InputDisplay::Smash => "Smash Inputs", - InputDisplay::Raw => "Raw Inputs", - } - ) - } -} - -impl ToggleTrait for InputDisplay { - fn to_toggle_vals() -> Vec { - InputDisplay::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec { - InputDisplay::iter().map(|i| i.to_string()).collect() +byteflags! { + pub struct InputDisplay { + pub NONE = "None", + pub SMASH = "Smash Inputs", + pub RAW = "Raw Inputs", } } diff --git a/training_mod_tui/Cargo.toml b/training_mod_tui/Cargo.toml index 3bcfada..4ff5130 100644 --- a/training_mod_tui/Cargo.toml +++ b/training_mod_tui/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "training_mod_tui" version = "0.1.0" -edition = "2018" +edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +doctest = false [dependencies] -tui = { version = "0.16.0", default-features = false } -unicode-width = "0.1.9" -training_mod_consts = { path = "../training_mod_consts", default-features = false} -serde_json = "1.0.79" -bitflags = "1.2.1" +itertools = "0.11.0" +ratatui = { git = "https://github.com/tonogdlp/ratatui.git", branch = "single-cell", default-features = false} +serde = { version = "1.0.188", features = ["derive"] } +serde_json = "1.0.106" crossterm = { version = "0.22.1", optional = true } [features] default = [] -has_terminal = ["crossterm", "tui/crossterm"] \ No newline at end of file +has_terminal = ["crossterm", "ratatui/crossterm"] \ No newline at end of file diff --git a/training_mod_tui/src/containers/app.rs b/training_mod_tui/src/containers/app.rs new file mode 100644 index 0000000..7d6dfab --- /dev/null +++ b/training_mod_tui/src/containers/app.rs @@ -0,0 +1,390 @@ +use serde::ser::{SerializeMap, Serializer}; +use serde::Serialize; +use std::collections::HashMap; + +use crate::{InputControl, StatefulList, SubMenu, SubMenuType, Tab}; + +#[derive(PartialEq, Serialize, Clone, Copy)] +pub enum AppPage { + SUBMENU, + TOGGLE, + SLIDER, + CONFIRMATION, + CLOSE, +} + +#[derive(PartialEq)] +pub enum ConfirmationState { + HoverNo, + HoverYes, +} + +impl ConfirmationState { + pub fn switch(&self) -> ConfirmationState { + match self { + ConfirmationState::HoverNo => ConfirmationState::HoverYes, + ConfirmationState::HoverYes => ConfirmationState::HoverNo, + } + } +} + +// Menu structure is: +// App > +// │ +// └─ Tab > +// │ +// └─ Submenu +// │ +// ├─ StatefulTable +// │ +// │ OR +// │ +// └─ Option + +pub struct App<'a> { + pub tabs: StatefulList>, + pub page: AppPage, + pub serialized_settings: String, + pub serialized_default_settings: String, + pub confirmation_state: ConfirmationState, + pub confirmation_return_page: AppPage, +} + +impl<'a> App<'a> { + pub fn new() -> App<'a> { + App { + tabs: StatefulList::new(), + page: AppPage::SUBMENU, + serialized_settings: String::new(), + serialized_default_settings: String::new(), + confirmation_state: ConfirmationState::HoverNo, + confirmation_return_page: AppPage::SUBMENU, + } + } + + pub fn current_settings_to_json(&self) -> String { + serde_json::to_string(&self).expect("Could not serialize the menu to JSON!") + } + + pub fn get_serialized_settings_with_defaults(&self) -> String { + format!( + "{{\"menu\":{}, \"defaults_menu\":{}}}", + self.serialized_settings, self.serialized_default_settings + ) + } + + pub fn save_settings(&mut self) { + self.serialized_settings = self.current_settings_to_json(); + } + + pub fn save_default_settings(&mut self) { + self.serialized_default_settings = self.current_settings_to_json(); + } + + pub fn load_defaults(&mut self) { + // TODO!() is there a way to do this without cloning? + let json = self.serialized_default_settings.clone(); + self.update_all_from_json(&json); + } + + pub fn load_defaults_for_current_submenu(&mut self) { + let submenu_id = self.selected_submenu().id; + let json = self.serialized_default_settings.clone(); + self.update_one_from_json(&json, submenu_id); + } + + pub fn update_all_from_json(&mut self, json: &str) { + let all_settings: HashMap> = + serde_json::from_str(json).expect("Could not parse the json!"); + for tab in self.tabs.iter_mut() { + for submenu_opt in tab.submenus.iter_mut() { + if let Some(submenu) = submenu_opt { + if let Some(val) = all_settings.get(submenu.id) { + submenu.update_from_vec(val.clone()); + } + } + } + } + self.save_settings(); + } + + #[allow(unused_labels)] + pub fn update_one_from_json(&mut self, json: &str, submenu_id: &str) { + let all_settings: HashMap> = + serde_json::from_str(json).expect("Could not parse the json!"); + if let Some(val) = all_settings.get(submenu_id) { + // No need to iterate through all the submenus if the id doesn't exist in the hashmap + 'tabs_scope: for tab in self.tabs.iter_mut() { + 'submenus_scope: for submenu_opt in tab.submenus.iter_mut() { + if let Some(submenu) = submenu_opt { + if submenu.id == submenu_id { + submenu.update_from_vec(val.clone()); + break 'tabs_scope; + } + } + } + } + } + self.save_settings(); + } + + pub fn confirm(&mut self) -> bool { + self.confirmation_state == ConfirmationState::HoverYes + } + + pub fn return_from_confirmation(&mut self) { + self.confirmation_state = ConfirmationState::HoverNo; + self.page = self.confirmation_return_page; + } + + pub fn selected_tab(&mut self) -> &mut Tab<'a> { + self.tabs.get_selected().expect("No tab selected!") + } + + pub fn selected_submenu(&mut self) -> &mut SubMenu<'a> { + self.selected_tab() + .submenus + .get_selected() + .expect("No submenu selected!") + } + + pub fn should_show_clear_keyhelp(&mut self) -> bool { + // Only show the "Clear Toggle" keyhelp if all of the following are true + // 1. app.page is TOGGLE, + // 2. selected_submenu.submenu_type is ToggleMultiple + // 3. the toggle can be set to values greater than 1 (i.e. its not a boolean toggle) + if self.page != AppPage::TOGGLE { + return false; + } + let submenu = self.selected_submenu(); + match submenu.submenu_type { + SubMenuType::ToggleMultiple => submenu.selected_toggle().max > 1, + _ => false, + } + } +} + +impl<'a> Serialize for App<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Serializes as a mapping between submenu titles and values + // Need to iterate through tabs to avoid making a list of mappings + let len: usize = self.tabs.iter().map(|tab| tab.len()).sum(); + let mut map = serializer.serialize_map(Some(len))?; + for tab in self.tabs.iter() { + for submenu in tab.submenus.iter() { + map.serialize_entry(submenu.id, submenu)?; + } + } + map.end() + } +} + +impl<'a> InputControl for App<'a> { + fn on_a(&mut self) { + match self.page { + AppPage::SUBMENU => { + self.page = match self.selected_submenu().submenu_type { + SubMenuType::ToggleSingle => AppPage::TOGGLE, + SubMenuType::ToggleMultiple => AppPage::TOGGLE, + SubMenuType::Slider => AppPage::SLIDER, + }; + self.selected_tab().on_a() + } + AppPage::TOGGLE => self.selected_submenu().on_a(), + AppPage::SLIDER => self.selected_submenu().on_a(), + AppPage::CONFIRMATION => { + // For resetting defaults + // TODO: Is this the right place for this logic? + if self.confirm() { + match self.confirmation_return_page { + AppPage::SUBMENU => { + // Reset ALL settings to default + self.load_defaults(); + } + AppPage::TOGGLE | AppPage::SLIDER => { + // Reset current submenu to default + self.load_defaults_for_current_submenu(); + } + _ => {} + } + } + self.return_from_confirmation(); + } + AppPage::CLOSE => {} + } + self.save_settings(); // A button can make changes, update the serialized settings + } + fn on_b(&mut self) { + match self.page { + AppPage::SUBMENU => { + // Exit the app + self.page = AppPage::CLOSE; + } + AppPage::TOGGLE => { + // Return to the list of submenus + self.page = AppPage::SUBMENU; + } + AppPage::SLIDER => { + // Return to the list of submenus if we don't have a slider handle selected + let slider = self + .selected_submenu() + .slider + .as_mut() + .expect("No slider selected!"); + if !slider.is_handle_selected() { + self.page = AppPage::SUBMENU; + } else { + self.selected_submenu().on_b(); + } + } + AppPage::CONFIRMATION => { + // Return to the list of submenus + self.return_from_confirmation(); + } + AppPage::CLOSE => {} + } + self.save_settings(); // B button can make changes, update the serialized settings + } + fn on_x(&mut self) { + self.save_default_settings(); + } + fn on_y(&mut self) { + // Clear current toggle, for toggles w/ weighted selections + match self.page { + AppPage::TOGGLE => self.selected_submenu().on_y(), + _ => {} + } + } + fn on_up(&mut self) { + match self.page { + AppPage::SUBMENU => self.tabs.get_selected().expect("No tab selected!").on_up(), + AppPage::TOGGLE => self + .tabs + .get_selected() + .expect("No tab selected!") + .submenus + .get_selected() + .expect("No submenu selected!") + .on_up(), + AppPage::SLIDER => self + .tabs + .get_selected() + .expect("No tab selected!") + .submenus + .get_selected() + .expect("No submenu selected!") + .on_up(), + AppPage::CONFIRMATION => {} + AppPage::CLOSE => {} + } + } + fn on_down(&mut self) { + match self.page { + AppPage::SUBMENU => self + .tabs + .get_selected() + .expect("No tab selected!") + .on_down(), + AppPage::TOGGLE => self + .tabs + .get_selected() + .expect("No tab selected!") + .submenus + .get_selected() + .expect("No submenu selected!") + .on_down(), + AppPage::SLIDER => self + .tabs + .get_selected() + .expect("No tab selected!") + .submenus + .get_selected() + .expect("No submenu selected!") + .on_down(), + AppPage::CONFIRMATION => {} + AppPage::CLOSE => {} + } + } + fn on_left(&mut self) { + match self.page { + AppPage::SUBMENU => self + .tabs + .get_selected() + .expect("No tab selected!") + .on_left(), + AppPage::TOGGLE => self + .tabs + .get_selected() + .expect("No tab selected!") + .submenus + .get_selected() + .expect("No submenu selected!") + .on_left(), + AppPage::SLIDER => self + .tabs + .get_selected() + .expect("No tab selected!") + .submenus + .get_selected() + .expect("No submenu selected!") + .on_left(), + AppPage::CONFIRMATION => self.confirmation_state = self.confirmation_state.switch(), + AppPage::CLOSE => {} + } + } + fn on_right(&mut self) { + match self.page { + AppPage::SUBMENU => self + .tabs + .get_selected() + .expect("No tab selected!") + .on_right(), + AppPage::TOGGLE => self + .tabs + .get_selected() + .expect("No tab selected!") + .submenus + .get_selected() + .expect("No submenu selected!") + .on_right(), + AppPage::SLIDER => self + .tabs + .get_selected() + .expect("No tab selected!") + .submenus + .get_selected() + .expect("No submenu selected!") + .on_right(), + AppPage::CONFIRMATION => self.confirmation_state = self.confirmation_state.switch(), + AppPage::CLOSE => {} + } + } + fn on_start(&mut self) { + // Close menu + self.page = AppPage::CLOSE; + } + fn on_l(&mut self) {} + fn on_r(&mut self) { + // Reset settings to default + // See App::on_a() for the logic + self.confirmation_return_page = self.page; + self.page = AppPage::CONFIRMATION; + } + fn on_zl(&mut self) { + match self.page { + AppPage::SUBMENU => { + self.tabs.previous(); + } + _ => {} + } + } + fn on_zr(&mut self) { + match self.page { + AppPage::SUBMENU => self.tabs.next(), + _ => {} + } + } +} diff --git a/training_mod_tui/src/containers/mod.rs b/training_mod_tui/src/containers/mod.rs new file mode 100644 index 0000000..de697a0 --- /dev/null +++ b/training_mod_tui/src/containers/mod.rs @@ -0,0 +1,24 @@ +mod app; +mod submenu; +mod tab; +mod toggle; +pub use app::*; +pub use submenu::*; +pub use tab::*; +pub use toggle::*; + +pub trait InputControl { + fn on_a(&mut self); + fn on_b(&mut self); + fn on_x(&mut self); + fn on_y(&mut self); + fn on_up(&mut self); + fn on_down(&mut self); + fn on_left(&mut self); + fn on_right(&mut self); + fn on_start(&mut self); + fn on_l(&mut self); + fn on_r(&mut self); + fn on_zl(&mut self); + fn on_zr(&mut self); +} diff --git a/training_mod_tui/src/containers/submenu.rs b/training_mod_tui/src/containers/submenu.rs new file mode 100644 index 0000000..18e46a0 --- /dev/null +++ b/training_mod_tui/src/containers/submenu.rs @@ -0,0 +1,168 @@ +use serde::ser::Serializer; +use serde::Serialize; + +use crate::{InputControl, StatefulSlider, StatefulTable, Toggle}; + +#[derive(Clone)] +pub struct SubMenu<'a> { + pub title: &'a str, + pub id: &'a str, + pub help_text: &'a str, + pub submenu_type: SubMenuType, + pub toggles: StatefulTable>, + pub slider: Option, +} + +impl<'a> Serialize for SubMenu<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self.submenu_type { + SubMenuType::ToggleMultiple | SubMenuType::ToggleSingle => { + self.toggles.serialize(serializer) + } + SubMenuType::Slider => self.slider.serialize(serializer), + } + } +} + +impl<'a> InputControl for SubMenu<'a> { + fn on_a(&mut self) { + match self.submenu_type { + SubMenuType::ToggleSingle => { + // Set all values to 0 first before incrementing the selected toggle + // This ensure that exactly one toggle has a nonzero value + for ind in 0..self.toggles.len() { + self.toggles.get_by_idx_mut(ind).unwrap().value = 0; + } + self.selected_toggle().increment(); + } + SubMenuType::ToggleMultiple => self.selected_toggle().increment(), + SubMenuType::Slider => { + let slider = self.slider.as_mut().expect("No slider selected!"); + slider.select_deselect(); + } + } + } + fn on_b(&mut self) { + match self.submenu_type { + SubMenuType::ToggleSingle => {} + SubMenuType::ToggleMultiple => {} + SubMenuType::Slider => { + let slider = self.slider.as_mut().expect("No slider selected!"); + if slider.is_handle_selected() { + slider.deselect() + } + } + } + } + fn on_x(&mut self) {} + fn on_y(&mut self) { + match self.submenu_type { + SubMenuType::ToggleMultiple => { + let toggle = self.selected_toggle(); + if toggle.max > 1 { + toggle.value = 0; + } + } + _ => {} + } + } + fn on_up(&mut self) { + match self.submenu_type { + SubMenuType::ToggleSingle => self.toggles.prev_row_checked(), + SubMenuType::ToggleMultiple => self.toggles.prev_row_checked(), + SubMenuType::Slider => { + let slider = self.slider.as_mut().expect("No slider selected!"); + if slider.is_handle_selected() { + slider.increment_selected_fast(); + } + } + } + } + fn on_down(&mut self) { + match self.submenu_type { + SubMenuType::ToggleSingle => self.toggles.next_row_checked(), + SubMenuType::ToggleMultiple => self.toggles.next_row_checked(), + SubMenuType::Slider => { + let slider = self.slider.as_mut().expect("No slider selected!"); + if slider.is_handle_selected() { + slider.decrement_selected_fast(); + } + } + } + } + fn on_left(&mut self) { + match self.submenu_type { + SubMenuType::ToggleSingle => self.toggles.prev_col_checked(), + SubMenuType::ToggleMultiple => self.toggles.prev_col_checked(), + SubMenuType::Slider => { + let slider = self.slider.as_mut().expect("No slider selected!"); + if slider.is_handle_selected() { + slider.decrement_selected_slow(); + } else { + slider.switch_hover(); + } + } + } + } + fn on_right(&mut self) { + match self.submenu_type { + SubMenuType::ToggleSingle => self.toggles.next_col_checked(), + SubMenuType::ToggleMultiple => self.toggles.next_col_checked(), + SubMenuType::Slider => { + let slider = self.slider.as_mut().expect("No slider selected!"); + if slider.is_handle_selected() { + slider.increment_selected_slow(); + } else { + slider.switch_hover(); + } + } + } + } + fn on_start(&mut self) {} + fn on_l(&mut self) {} + fn on_r(&mut self) {} + fn on_zl(&mut self) {} + fn on_zr(&mut self) {} +} + +impl<'a> SubMenu<'a> { + pub fn selected_toggle(&mut self) -> &mut Toggle<'a> { + self.toggles.get_selected().expect("No toggle selected!") + } + + pub fn update_from_vec(&mut self, values: Vec) { + match self.submenu_type { + SubMenuType::ToggleSingle | SubMenuType::ToggleMultiple => { + for (idx, value) in values.iter().enumerate() { + if let Some(toggle) = self.toggles.get_by_idx_mut(idx) { + toggle.value = *value; + } + } + } + SubMenuType::Slider => { + assert_eq!( + values.len(), + 2, + "Exactly two values need to be passed to submenu.set() for slider!" + ); + if let Some(s) = self.slider { + self.slider = Some(StatefulSlider { + lower: values[0].into(), + upper: values[1].into(), + ..s + }); + } + } + } + } +} + +#[derive(Clone, Copy, Serialize)] +pub enum SubMenuType { + ToggleSingle, + ToggleMultiple, + Slider, +} diff --git a/training_mod_tui/src/containers/tab.rs b/training_mod_tui/src/containers/tab.rs new file mode 100644 index 0000000..763df7b --- /dev/null +++ b/training_mod_tui/src/containers/tab.rs @@ -0,0 +1,54 @@ +use serde::ser::{SerializeMap, Serializer}; +use serde::Serialize; + +use crate::{InputControl, StatefulTable, SubMenu}; + +#[derive(Clone)] +pub struct Tab<'a> { + pub title: &'a str, + pub id: &'a str, + pub submenus: StatefulTable>, +} + +impl<'a> Tab<'a> { + pub fn len(&self) -> usize { + self.submenus.len() + } +} + +impl<'a> Serialize for Tab<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(self.submenus.len()))?; + for submenu in self.submenus.as_vec().iter() { + map.serialize_entry(&submenu.title, &submenu)?; + } + map.end() + } +} + +impl<'a> InputControl for Tab<'a> { + fn on_a(&mut self) {} + fn on_b(&mut self) {} + fn on_x(&mut self) {} + fn on_y(&mut self) {} + fn on_up(&mut self) { + self.submenus.prev_row_checked() + } + fn on_down(&mut self) { + self.submenus.next_row_checked() + } + fn on_left(&mut self) { + self.submenus.prev_col_checked() + } + fn on_right(&mut self) { + self.submenus.next_col_checked() + } + fn on_start(&mut self) {} + fn on_l(&mut self) {} + fn on_r(&mut self) {} + fn on_zl(&mut self) {} + fn on_zr(&mut self) {} +} diff --git a/training_mod_tui/src/containers/toggle.rs b/training_mod_tui/src/containers/toggle.rs new file mode 100644 index 0000000..19b24b2 --- /dev/null +++ b/training_mod_tui/src/containers/toggle.rs @@ -0,0 +1,36 @@ +use serde::ser::Serializer; +use serde::Serialize; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Toggle<'a> { + pub title: &'a str, + pub value: u8, + pub max: u8, +} + +impl<'a> Serialize for Toggle<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8(self.value) + } +} + +impl<'a> Toggle<'a> { + pub fn increment(&mut self) { + if self.value == self.max { + self.value = 0; + } else { + self.value += 1; + } + } + + pub fn decrement(&mut self) { + if self.value == 0 { + self.value = self.max; + } else { + self.value -= 1; + } + } +} diff --git a/training_mod_tui/src/gauge.rs b/training_mod_tui/src/gauge.rs deleted file mode 100644 index cb9f462..0000000 --- a/training_mod_tui/src/gauge.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub enum GaugeState { - MinHover, - MaxHover, - MinSelected, - MaxSelected, - None, -} - -pub struct DoubleEndedGauge { - pub state: GaugeState, - pub selected_min: u32, - pub selected_max: u32, - pub abs_min: u32, - pub abs_max: u32, -} - -impl DoubleEndedGauge { - pub fn new() -> DoubleEndedGauge { - DoubleEndedGauge { - state: GaugeState::None, - selected_min: 0, - selected_max: 150, - abs_min: 0, - abs_max: 150, - } - } -} diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs index 0ca02a1..5446433 100644 --- a/training_mod_tui/src/lib.rs +++ b/training_mod_tui/src/lib.rs @@ -1,911 +1,7 @@ -use training_mod_consts::{ - ui_menu, MenuJsonStruct, Slider, SubMenu, SubMenuType, Toggle, TrainingModpackMenu, UiMenu, -}; -use tui::{ - backend::Backend, - layout::{Constraint, Corner, Direction, Layout, Rect}, - style::{Modifier, Style}, - text::{Span, Spans}, - widgets::{Block, LineGauge, List, ListItem, ListState, Paragraph, Tabs}, - Frame, -}; +mod containers; +pub use containers::*; +mod structures; +pub use structures::*; -use serde_json::{json, Map}; -use std::collections::HashMap; -pub use tui::{backend::TestBackend, style::Color, Terminal}; - -pub mod gauge; -mod list; - -use crate::gauge::{DoubleEndedGauge, GaugeState}; -use crate::list::{MultiStatefulList, StatefulList}; - -static NX_TUI_WIDTH: u16 = 240; -// Number of lists per page -pub const NUM_LISTS: usize = 4; - -#[derive(PartialEq)] -pub enum AppPage { - SUBMENU, - TOGGLE, - SLIDER, - CONFIRMATION, -} - -/// We should hold a list of SubMenus. -/// The currently selected SubMenu should also have an associated list with necessary information. -/// We can convert the option types (Toggle, OnOff, Slider) to lists -pub struct App { - pub tabs: StatefulList, - pub menu_items: HashMap>, - pub selected_sub_menu_toggles: MultiStatefulList, - pub selected_sub_menu_slider: DoubleEndedGauge, - pub page: AppPage, - pub default_menu: (UiMenu, String), -} - -impl<'a> App { - pub fn new(menu: UiMenu, default_menu: (UiMenu, String)) -> App { - let mut menu_items_stateful = HashMap::new(); - menu.tabs.iter().for_each(|tab| { - menu_items_stateful.insert( - tab.tab_title.clone(), - MultiStatefulList::with_items(tab.tab_submenus.clone(), NUM_LISTS), - ); - }); - - let mut app = App { - tabs: StatefulList::with_items( - menu.tabs.iter().map(|tab| tab.tab_title.clone()).collect(), - ), - menu_items: menu_items_stateful, - selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0), - selected_sub_menu_slider: DoubleEndedGauge::new(), - page: AppPage::SUBMENU, - default_menu: default_menu, - }; - app.set_sub_menu_items(); - app - } - - /// Takes the currently selected tab/submenu and clones the options into - /// self.selected_sub_menu_toggles and self.selected_sub_menu_slider - pub fn set_sub_menu_items(&mut self) { - let (list_section, list_idx) = self - .menu_items - .get(self.tab_selected()) - .unwrap() - .idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state); - let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists - [list_section] - .items - .get(list_idx) - .unwrap(); - - let toggles = selected_sub_menu.toggles.clone(); - let slider = selected_sub_menu.slider.clone(); - match SubMenuType::from_string(&self.sub_menu_selected()._type) { - SubMenuType::TOGGLE => { - self.selected_sub_menu_toggles = MultiStatefulList::with_items( - toggles, - if selected_sub_menu.toggles.len() >= NUM_LISTS { - NUM_LISTS - } else { - selected_sub_menu.toggles.len() - }, - ) - } - SubMenuType::SLIDER => { - let slider = slider.unwrap(); - self.selected_sub_menu_slider = DoubleEndedGauge { - state: GaugeState::None, - selected_min: slider.selected_min, - selected_max: slider.selected_max, - abs_min: slider.abs_min, - abs_max: slider.abs_max, - } - } - }; - } - - /// Returns the id of the currently selected tab - pub fn tab_selected(&self) -> &str { - self.tabs - .items - .get(self.tabs.state.selected().unwrap()) - .unwrap() - } - - /// Returns the currently selected SubMenu struct - /// - /// { - /// submenu_title: String, - /// submenu_id: String, - /// help_text: String, - /// is_single_option: bool, - /// toggles: Vec>, - /// slider: Option, - /// _type: String, - /// } - fn sub_menu_selected(&self) -> &SubMenu { - let (list_section, list_idx) = self - .menu_items - .get(self.tab_selected()) - .unwrap() - .idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state); - self.menu_items.get(self.tab_selected()).unwrap().lists[list_section] - .items - .get(list_idx) - .unwrap() - } - - /// A "next()" function which differs per submenu type - /// Toggles: calls next() - /// Slider: Swaps between MinHover and MaxHover - pub fn sub_menu_next(&mut self) { - match SubMenuType::from_string(&self.sub_menu_selected()._type) { - SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(), - SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { - GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover, - GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover, - _ => {} - }, - } - } - - /// A "next_list()" function which differs per submenu type - /// Toggles: Calls next_list() - /// Slider: - /// * Swaps between MinHover and MaxHover - /// * Increments the selected_min/max if possible - pub fn sub_menu_next_list(&mut self) { - match SubMenuType::from_string(&self.sub_menu_selected()._type) { - SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(), - SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { - GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover, - GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover, - GaugeState::MinSelected => { - if self.selected_sub_menu_slider.selected_min - < self.selected_sub_menu_slider.selected_max - { - self.selected_sub_menu_slider.selected_min += 1; - } - } - GaugeState::MaxSelected => { - if self.selected_sub_menu_slider.selected_max - < self.selected_sub_menu_slider.abs_max - { - self.selected_sub_menu_slider.selected_max += 1; - } - } - GaugeState::None => {} - }, - } - } - - /// A "previous()" function which differs per submenu type - /// Toggles: calls previous() - /// Slider: Swaps between MinHover and MaxHover - pub fn sub_menu_previous(&mut self) { - match SubMenuType::from_string(&self.sub_menu_selected()._type) { - SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(), - SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { - GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover, - GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover, - _ => {} - }, - } - } - - /// A "previous_list()" function which differs per submenu type - /// Toggles: Calls previous_list() - /// Slider: - /// * Swaps between MinHover and MaxHover - /// * Decrements the selected_min/max if possible - pub fn sub_menu_previous_list(&mut self) { - match SubMenuType::from_string(&self.sub_menu_selected()._type) { - SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(), - SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { - GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover, - GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover, - GaugeState::MinSelected => { - if self.selected_sub_menu_slider.selected_min - > self.selected_sub_menu_slider.abs_min - { - self.selected_sub_menu_slider.selected_min -= 1; - } - } - GaugeState::MaxSelected => { - if self.selected_sub_menu_slider.selected_max - > self.selected_sub_menu_slider.selected_min - { - self.selected_sub_menu_slider.selected_max -= 1; - } - } - GaugeState::None => {} - }, - } - } - - /// Returns information about the currently selected submenu - /// - /// 0: Submenu Title - /// 1: Submenu Help Text - /// 2: Vec(toggle checked, title) for toggles, Vec(nothing) for slider - /// 3: ListState for toggles, ListState::new() for slider - /// TODO: Refactor return type into a nice struct - pub fn sub_menu_strs_and_states( - &self, - ) -> (String, String, Vec<(Vec<(bool, String)>, ListState)>) { - ( - self.sub_menu_selected().submenu_title.clone(), - self.sub_menu_selected().help_text.clone(), - match SubMenuType::from_string(&self.sub_menu_selected()._type) { - SubMenuType::TOGGLE => self - .selected_sub_menu_toggles - .lists - .iter() - .map(|toggle_list| { - ( - toggle_list - .items - .iter() - .map(|toggle| (toggle.checked, toggle.toggle_title.clone())) - .collect(), - toggle_list.state.clone(), - ) - }) - .collect(), - SubMenuType::SLIDER => { - vec![(vec![], ListState::default())] - } - }, - ) - } - - /// Returns information about the currently selected slider - /// 0: Title - /// 1: Help text - /// 2: Reference to self.selected_sub_menu_slider - /// TODO: Refactor return type into a nice struct - pub fn sub_menu_strs_for_slider(&self) -> (String, String, &DoubleEndedGauge) { - let slider = match SubMenuType::from_string(&self.sub_menu_selected()._type) { - SubMenuType::SLIDER => &self.selected_sub_menu_slider, - _ => { - panic!("Slider not selected!"); - } - }; - ( - self.sub_menu_selected().submenu_title.clone(), - self.sub_menu_selected().help_text.clone(), - slider, - ) - } - - /// Different behavior depending on the current menu location - /// Submenu list: Enters toggle or slider submenu - /// Toggle submenu: Toggles the selected submenu toggle in self.selected_sub_menu_toggles and in the actual SubMenu struct - /// Slider submenu: Swaps hover/selected state. Updates the actual SubMenu struct if going from Selected -> Hover - pub fn on_a(&mut self) { - let tab_selected = self - .tabs - .items - .get(self.tabs.state.selected().unwrap()) - .unwrap(); - let (list_section, list_idx) = self - .menu_items - .get(tab_selected) - .unwrap() - .idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state); - let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists[list_section] - .items - .get_mut(list_idx) - .unwrap(); - if self.page == AppPage::SUBMENU { - match SubMenuType::from_string(&selected_sub_menu._type) { - // Need to change the slider state to MinHover so the slider shows up initially - SubMenuType::SLIDER => { - self.page = AppPage::SLIDER; - self.selected_sub_menu_slider.state = GaugeState::MinHover; - } - SubMenuType::TOGGLE => self.page = AppPage::TOGGLE, - } - } else { - match SubMenuType::from_string(&selected_sub_menu._type) { - SubMenuType::TOGGLE => { - let is_single_option = selected_sub_menu.is_single_option; - let state = self.selected_sub_menu_toggles.state; - // Change the toggles in self.selected_sub_menu_toggles (for display) - self.selected_sub_menu_toggles - .lists - .iter_mut() - .map(|list| (list.state.selected(), &mut list.items)) - .for_each(|(state, toggle_list)| { - toggle_list.iter_mut().enumerate().for_each(|(i, o)| { - if state.is_some() && i == state.unwrap() { - if !o.checked { - o.checked = true; - } else { - if is_single_option { - return; - } - o.checked = false; - } - } else if is_single_option { - o.checked = false; - } - }) - }); - // Actually change the toggle values in the SubMenu struct - selected_sub_menu - .toggles - .iter_mut() - .enumerate() - .for_each(|(i, o)| { - if i == state { - if !o.checked { - o.checked = true; - } else { - if is_single_option { - return; - } - o.checked = false; - } - } else if is_single_option { - o.checked = false; - } - }); - } - SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { - GaugeState::MinHover => { - self.selected_sub_menu_slider.state = GaugeState::MinSelected; - } - GaugeState::MaxHover => { - self.selected_sub_menu_slider.state = GaugeState::MaxSelected; - } - GaugeState::MinSelected => { - self.selected_sub_menu_slider.state = GaugeState::MinHover; - selected_sub_menu.slider = Some(Slider { - selected_min: self.selected_sub_menu_slider.selected_min, - selected_max: self.selected_sub_menu_slider.selected_max, - abs_min: self.selected_sub_menu_slider.abs_min, - abs_max: self.selected_sub_menu_slider.abs_max, - }); - } - GaugeState::MaxSelected => { - self.selected_sub_menu_slider.state = GaugeState::MaxHover; - selected_sub_menu.slider = Some(Slider { - selected_min: self.selected_sub_menu_slider.selected_min, - selected_max: self.selected_sub_menu_slider.selected_max, - abs_min: self.selected_sub_menu_slider.abs_min, - abs_max: self.selected_sub_menu_slider.abs_max, - }); - } - GaugeState::None => { - self.selected_sub_menu_slider.state = GaugeState::MinHover; - } - }, - } - } - } - - /// Different behavior depending on the current menu location - /// Submenu selection: None - /// Toggle submenu: Sets page to submenu selection - /// Slider submenu: If in a selected state, then commit changes and change to hover. Else set page to submenu selection - pub fn on_b(&mut self) { - let tab_selected = self - .tabs - .items - .get(self.tabs.state.selected().unwrap()) - .unwrap(); - let (list_section, list_idx) = self - .menu_items - .get(tab_selected) - .unwrap() - .idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state); - let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists[list_section] - .items - .get_mut(list_idx) - .unwrap(); - match SubMenuType::from_string(&selected_sub_menu._type) { - SubMenuType::SLIDER => match self.selected_sub_menu_slider.state { - GaugeState::MinSelected => { - self.selected_sub_menu_slider.state = GaugeState::MinHover; - selected_sub_menu.slider = Some(Slider { - selected_min: self.selected_sub_menu_slider.selected_min, - selected_max: self.selected_sub_menu_slider.selected_max, - abs_min: self.selected_sub_menu_slider.abs_min, - abs_max: self.selected_sub_menu_slider.abs_max, - }); - // Don't go back to the outer list - return; - } - GaugeState::MaxSelected => { - self.selected_sub_menu_slider.state = GaugeState::MaxHover; - selected_sub_menu.slider = Some(Slider { - selected_min: self.selected_sub_menu_slider.selected_min, - selected_max: self.selected_sub_menu_slider.selected_max, - abs_min: self.selected_sub_menu_slider.abs_min, - abs_max: self.selected_sub_menu_slider.abs_max, - }); - // Don't go back to the outer list - return; - } - _ => {} - }, - _ => {} - } - self.page = AppPage::SUBMENU; - self.set_sub_menu_items(); - } - - /// Save defaults command - pub fn save_defaults(&mut self) { - if self.page == AppPage::SUBMENU { - let json = self.to_json(); - unsafe { - self.default_menu = ( - ui_menu(serde_json::from_str::(&json).unwrap()), - json, - ); - } - } - } - - /// Reset current submenu to defaults - pub fn reset_current_submenu(&mut self) { - if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { - let json = self.to_json(); - let mut json_value = serde_json::from_str::(&json).unwrap(); - let selected_sub_menu = self.sub_menu_selected(); - let id = selected_sub_menu.submenu_id.clone(); - let default_json_value = - serde_json::from_str::(&self.default_menu.1).unwrap(); - *json_value.get_mut(id.as_str()).unwrap() = - default_json_value.get(id.as_str()).unwrap().clone(); - let new_menu = serde_json::from_value::(json_value).unwrap(); - *self = App::new(unsafe { ui_menu(new_menu) }, self.default_menu.clone()); - } - } - - /// Reset all menus to defaults - pub fn reset_all_submenus(&mut self) { - *self = App::new(self.default_menu.0.clone(), self.default_menu.clone()); - } - - pub fn previous_tab(&mut self) { - if self.page == AppPage::SUBMENU { - self.tabs.previous(); - self.set_sub_menu_items(); - } - } - - pub fn next_tab(&mut self) { - if self.page == AppPage::SUBMENU { - self.tabs.next(); - self.set_sub_menu_items(); - } - } - - pub fn on_up(&mut self) { - if self.page == AppPage::SUBMENU { - self.menu_items - .get_mut( - self.tabs - .items - .get(self.tabs.state.selected().unwrap()) - .unwrap(), - ) - .unwrap() - .previous(); - self.set_sub_menu_items(); - } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { - self.sub_menu_previous(); - } - } - - pub fn on_down(&mut self) { - if self.page == AppPage::SUBMENU { - self.menu_items - .get_mut( - self.tabs - .items - .get(self.tabs.state.selected().unwrap()) - .unwrap(), - ) - .unwrap() - .next(); - self.set_sub_menu_items(); - } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { - self.sub_menu_next(); - } - } - - pub fn on_left(&mut self) { - if self.page == AppPage::SUBMENU { - self.menu_items - .get_mut( - self.tabs - .items - .get(self.tabs.state.selected().unwrap()) - .unwrap(), - ) - .unwrap() - .previous_list(); - self.set_sub_menu_items(); - } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { - self.sub_menu_previous_list(); - } - } - - pub fn on_right(&mut self) { - if self.page == AppPage::SUBMENU { - self.menu_items - .get_mut( - self.tabs - .items - .get(self.tabs.state.selected().unwrap()) - .unwrap(), - ) - .unwrap() - .next_list(); - self.set_sub_menu_items(); - } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { - self.sub_menu_next_list(); - } - } - - /// Returns JSON representation of current menu settings - pub fn to_json(&self) -> String { - let mut settings = Map::new(); - for key in self.menu_items.keys() { - for list in &self.menu_items.get(key).unwrap().lists { - for sub_menu in &list.items { - if !sub_menu.toggles.is_empty() { - let val: u32 = sub_menu - .toggles - .iter() - .filter(|t| t.checked) - .map(|t| t.toggle_value) - .sum(); - settings.insert(sub_menu.submenu_id.to_string(), json!(val)); - } else if sub_menu.slider.is_some() { - let s: &Slider = sub_menu.slider.as_ref().unwrap(); - let val: Vec = vec![s.selected_min, s.selected_max]; - settings.insert(sub_menu.submenu_id.to_string(), json!(val)); - } else { - panic!("Could not collect settings for {:?}", sub_menu.submenu_id); - } - } - } - } - serde_json::to_string(&settings).unwrap() - } - - /// Returns the current menu selections and the default menu selections. - pub fn get_menu_selections(&self) -> String { - serde_json::to_string(&MenuJsonStruct { - menu: serde_json::from_str(self.to_json().as_str()).unwrap(), - defaults_menu: serde_json::from_str(self.default_menu.1.clone().as_str()).unwrap(), - }) - .unwrap() - } - - pub fn submenu_ids(&self) -> Vec { - return self - .menu_items - .values() - .flat_map(|multi_stateful_list| { - multi_stateful_list - .lists - .iter() - .flat_map(|sub_stateful_list| { - sub_stateful_list - .items - .iter() - .map(|submenu| submenu.submenu_id.clone()) - }) - }) - .collect::>(); - } -} - -fn render_submenu_page( - f: &mut Frame, - app: &mut App, - list_chunks: Vec, - help_chunk: Rect, -) { - let tab_selected = app.tab_selected(); - let mut item_help = None; - for (list_section, stateful_list) in app - .menu_items - .get(tab_selected) - .unwrap() - .lists - .iter() - .enumerate() - { - let items: Vec = stateful_list - .items - .iter() - .map(|i| { - let lines = vec![Spans::from(if stateful_list.state.selected().is_some() { - i.submenu_title.clone() - } else { - format!(" {}", i.submenu_title.clone()) - })]; - ListItem::new(lines).style(Style::default().fg(Color::White)) - }) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .title(if list_section == 0 { "Options" } else { "" }) - .style(Style::default().fg(Color::LightRed)), - ) - .highlight_style( - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">> "); - - let mut state = stateful_list.state.clone(); - if state.selected().is_some() { - item_help = Some( - stateful_list.items[state.selected().unwrap()] - .help_text - .clone(), - ); - } - - f.render_stateful_widget(list, list_chunks[list_section], &mut state); - } - - let help_paragraph = Paragraph::new( - item_help.unwrap_or("".to_string()).replace('\"', "") - + "\nZL/ZR: Next tab | X: Save Defaults | R: Reset All Menus", - ) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(help_paragraph, help_chunk); -} - -pub fn render_toggle_page( - f: &mut Frame, - app: &mut App, - list_chunks: Vec, - help_chunk: Rect, -) { - let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states(); - for list_section in 0..sub_menu_str_lists.len() { - let sub_menu_str = sub_menu_str_lists[list_section].0.clone(); - let sub_menu_state = &mut sub_menu_str_lists[list_section].1; - let values_items: Vec = sub_menu_str - .iter() - .map(|s| { - ListItem::new(vec![Spans::from(if s.0 { - format!("X {}", s.1) - } else { - format!(" {}", s.1) - })]) - }) - .collect(); - - let values_list = List::new(values_items) - .block(Block::default().title(if list_section == 0 { - title.clone() - } else { - "".to_string() - })) - .start_corner(Corner::TopLeft) - .highlight_style( - Style::default() - .fg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">> "); - f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state); - } - let help_paragraph = Paragraph::new(help_text.replace('\"', "") + "\nL: Reset Current Menu") - .style(Style::default().fg(Color::Cyan)); - f.render_widget(help_paragraph, help_chunk); -} - -pub fn render_slider_page( - f: &mut Frame, - app: &mut App, - vertical_chunk: Rect, - help_chunk: Rect, -) { - let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider(); - let abs_min = gauge_vals.abs_min; - let abs_max = gauge_vals.abs_max; - let selected_min = gauge_vals.selected_min; - let selected_max = gauge_vals.selected_max; - let lbl_ratio = 0.95; // Needed so that the upper limit label is visible - let constraints = [ - Constraint::Ratio( - (lbl_ratio * (selected_min - abs_min) as f32) as u32, - abs_max - abs_min, - ), - Constraint::Ratio( - (lbl_ratio * (selected_max - selected_min) as f32) as u32, - abs_max - abs_min, - ), - Constraint::Ratio( - (lbl_ratio * (abs_max - selected_max) as f32) as u32, - abs_max - abs_min, - ), - Constraint::Min(3), // For upper limit label - ]; - let gauge_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints(constraints) - .split(vertical_chunk); - - let slider_lbls = [abs_min, selected_min, selected_max, abs_max]; - for (idx, lbl) in slider_lbls.iter().enumerate() { - let mut line_set = tui::symbols::line::NORMAL; - line_set.horizontal = "-"; - let mut gauge = LineGauge::default() - .ratio(1.0) - .label(format!("{}", lbl)) - .style(Style::default().fg(Color::White)) - .line_set(line_set) - .gauge_style(Style::default().fg(Color::White).bg(Color::Black)); - if idx == 1 { - // Slider between selected_min and selected_max - match gauge_vals.state { - GaugeState::MinHover => gauge = gauge.style(Style::default().fg(Color::Red)), - GaugeState::MinSelected => gauge = gauge.style(Style::default().fg(Color::Green)), - _ => {} - } - gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black)); - } else if idx == 2 { - // Slider between selected_max and abs_max - match gauge_vals.state { - GaugeState::MaxHover => gauge = gauge.style(Style::default().fg(Color::Red)), - GaugeState::MaxSelected => gauge = gauge.style(Style::default().fg(Color::Green)), - _ => {} - } - } else if idx == 3 { - // Slider for abs_max - // We only want the label to show, so set the line character to " " - let mut line_set = tui::symbols::line::NORMAL; - line_set.horizontal = " "; - gauge = gauge.line_set(line_set); - - // For some reason, the selected_max slider displays on top - // So we need to change the abs_max slider styling to match - // If the selected_max is close enough to the abs_max - if (selected_max as f32 / abs_max as f32) > 0.95 { - gauge = gauge.style(match gauge_vals.state { - GaugeState::MaxHover => Style::default().fg(Color::Red), - GaugeState::MaxSelected => Style::default().fg(Color::Green), - _ => Style::default(), - }) - } - } - f.render_widget(gauge, gauge_chunks[idx]); - } - - let help_paragraph = Paragraph::new(help_text.replace('\"', "") + "\nL: Reset Current Menu") - .style(Style::default().fg(Color::Cyan)); - f.render_widget(help_paragraph, help_chunk); -} - -/// Run -pub fn ui(f: &mut Frame, app: &mut App) { - let app_tabs = &app.tabs; - let tab_selected = app_tabs.state.selected().unwrap(); - let mut span_selected = Spans::default(); - - let titles: Vec = app_tabs - .items - .iter() - .cloned() - .enumerate() - .map(|(idx, tab)| { - if idx == tab_selected { - span_selected = Spans::from(format!("> {}", tab)); - Spans::from(format!("> {}", tab)) - } else { - Spans::from(format!(" {}", tab)) - } - }) - .collect(); - // There is only enough room to display 3 tabs of text - // So lets replace tabs not near the selected with "..." - let all_windows: Vec<&[Spans]> = titles - .windows(3) - .filter(|w| w.contains(&titles[tab_selected])) - .collect(); - let first_window = all_windows[0]; - let mut titles: Vec = titles - .iter() - .cloned() - .map( - // Converts all tabs not in the window to "..." - |t| { - if first_window.contains(&t) { - t - } else { - Spans::from("...".to_owned()) - } - }, - ) - .collect(); - // Don't keep consecutive "..." tabs - titles.dedup(); - // Now that the size of the titles vector has changed, need to re-locate the selected tab - let tab_selected_deduped: usize = titles - .iter() - .cloned() - .position(|span| span == span_selected) - .unwrap_or(0); - - let tabs = Tabs::new(titles) - .block(Block::default().title(Spans::from(Span::styled( - "Ultimate Training Modpack Menu", - Style::default().fg(Color::LightRed), - )))) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().fg(Color::Yellow)) - .divider("|") - .select(tab_selected_deduped); - - let vertical_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(2), - Constraint::Max(10), - Constraint::Length(2), - ] - .as_ref(), - ) - .split(f.size()); - - // Prevent overflow by adding a length constraint of NX_TUI_WIDTH - // Need to add a second constraint since the .expand_to_fill() method - // is not publicly exposed, and the attribute defaults to true. - // https://github.com/fdehau/tui-rs/blob/v0.19.0/src/layout.rs#L121 - let vertical_chunks: Vec = vertical_chunks - .iter() - .map(|chunk| { - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Length(NX_TUI_WIDTH), // Width of the TUI terminal - Constraint::Min(0), // Fill the remainder margin - ] - .as_ref(), - ) - .split(*chunk)[0] - }) - .collect(); - - let list_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints( - (0..NUM_LISTS) - .into_iter() - .map(|_idx| Constraint::Percentage((100.0 / NUM_LISTS as f32) as u16)) - .collect::>() - .as_ref(), - ) - .split(vertical_chunks[1]); - - f.render_widget(tabs, vertical_chunks[0]); - - match app.page { - AppPage::SUBMENU => render_submenu_page(f, app, list_chunks, vertical_chunks[2]), - AppPage::SLIDER => render_slider_page(f, app, vertical_chunks[1], vertical_chunks[2]), - AppPage::TOGGLE => render_toggle_page(f, app, list_chunks, vertical_chunks[2]), - AppPage::CONFIRMATION => todo!(), - } -} +pub const NX_SUBMENU_ROWS: usize = 8; +pub const NX_SUBMENU_COLUMNS: usize = 4; diff --git a/training_mod_tui/src/list.rs b/training_mod_tui/src/list.rs deleted file mode 100644 index 6742095..0000000 --- a/training_mod_tui/src/list.rs +++ /dev/null @@ -1,209 +0,0 @@ -use tui::widgets::ListState; - -pub struct MultiStatefulList { - pub lists: Vec>, - pub state: usize, - pub total_len: usize, -} - -impl MultiStatefulList { - pub fn selected_list_item(&mut self) -> &mut T { - let (list_section, list_idx) = self.idx_to_list_idx(self.state); - &mut self.lists[list_section].items[list_idx] - } - - pub fn idx_to_list_idx(&self, idx: usize) -> (usize, usize) { - self.idx_to_list_idx_opt(idx).unwrap_or((0, 0)) - } - - pub fn idx_to_list_idx_opt(&self, idx: usize) -> Option<(usize, usize)> { - for list_section in 0..self.lists.len() { - let list_section_min_idx = - (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * list_section; - let list_section_max_idx = std::cmp::min( - (self.total_len as f32 / self.lists.len() as f32).ceil() as usize - * (list_section + 1), - self.total_len, - ); - if (list_section_min_idx..list_section_max_idx).contains(&idx) { - return Some((list_section, idx - list_section_min_idx)); - } - } - - None - } - - fn list_idx_to_idx(&self, list_idx: (usize, usize)) -> usize { - let list_section = list_idx.0; - let mut list_idx = list_idx.1; - for list_section in 0..list_section { - list_idx += self.lists[list_section].items.len(); - } - list_idx - } - - pub fn with_items(items: Vec, num_lists: usize) -> MultiStatefulList { - let lists = (0..num_lists) - .map(|list_section| { - // Try to evenly chunk - let list_size = (items.len() as f32 / num_lists as f32).ceil() as usize; - let list_section_min_idx = std::cmp::min(list_size * list_section, items.len()); - let list_section_max_idx = - std::cmp::min(list_size * (list_section + 1), items.len()); - let mut state = ListState::default(); - if list_section == 0 { - // Enforce state as first of list - state.select(Some(0)); - } - StatefulList { - state, - items: items[list_section_min_idx..list_section_max_idx].to_vec(), - } - }) - .collect(); - let total_len = items.len(); - MultiStatefulList { - lists, - total_len, - state: 0, - } - } - - pub fn next(&mut self) { - let (list_section, _) = self.idx_to_list_idx(self.state); - let (next_list_section, next_list_idx) = self.idx_to_list_idx(self.state + 1); - - if list_section != next_list_section { - self.lists[list_section].unselect(); - } - let state = if self.state + 1 >= self.total_len { - (0, 0) - } else { - (next_list_section, next_list_idx) - }; - - self.lists[state.0].state.select(Some(state.1)); - self.state = self.list_idx_to_idx(state); - } - - pub fn previous(&mut self) { - let (list_section, _) = self.idx_to_list_idx(self.state); - let mut last_list_section = self.lists.len() - 1; - let mut last_list_size = self.lists[last_list_section].items.len(); - - while last_list_size == 0 { - last_list_section -= 1; - last_list_size = self.lists[last_list_section].items.len(); - } - - let last_list_idx = last_list_size - 1; - - self.lists[list_section].unselect(); - let state = if self.state == 0 { - (last_list_section, last_list_idx) - } else { - let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1); - (prev_list_section, prev_list_idx) - }; - - self.lists[state.0].state.select(Some(state.1)); - self.state = self.list_idx_to_idx(state); - } - - pub fn next_list(&mut self) { - let (list_section, list_idx) = self.idx_to_list_idx(self.state); - let mut next_list_section = (list_section + 1) % self.lists.len(); - let mut next_list_len = self.lists[next_list_section].items.len(); - while next_list_len == 0 { - next_list_section = (next_list_section + 1) % self.lists.len(); - next_list_len = self.lists[next_list_section].items.len(); - } - let next_list_idx = if list_idx > next_list_len - 1 { - next_list_len - 1 - } else { - list_idx - }; - - if list_section != next_list_section { - self.lists[list_section].unselect(); - } - let state = (next_list_section, next_list_idx); - - self.lists[state.0].state.select(Some(state.1)); - self.state = self.list_idx_to_idx(state); - } - - pub fn previous_list(&mut self) { - let (list_section, list_idx) = self.idx_to_list_idx(self.state); - let mut prev_list_section = if list_section == 0 { - self.lists.len() - 1 - } else { - list_section - 1 - }; - - let mut prev_list_len = self.lists[prev_list_section].items.len(); - while prev_list_len == 0 { - prev_list_section -= 1; - prev_list_len = self.lists[prev_list_section].items.len(); - } - let prev_list_idx = if list_idx > prev_list_len - 1 { - prev_list_len - 1 - } else { - list_idx - }; - - if list_section != prev_list_section { - self.lists[list_section].unselect(); - } - let state = (prev_list_section, prev_list_idx); - - self.lists[state.0].state.select(Some(state.1)); - self.state = self.list_idx_to_idx(state); - } -} - -pub struct StatefulList { - pub state: ListState, - pub items: Vec, -} - -impl StatefulList { - pub fn with_items(items: Vec) -> StatefulList { - let mut state = ListState::default(); - // Enforce state as first of list - state.select(Some(0)); - StatefulList { state, items } - } - - pub fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.items.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - self.items.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn unselect(&mut self) { - self.state.select(None); - } -} diff --git a/training_mod_tui/src/main.rs b/training_mod_tui/src/main.rs deleted file mode 100644 index e1ecd1a..0000000 --- a/training_mod_tui/src/main.rs +++ /dev/null @@ -1,360 +0,0 @@ -#[cfg(feature = "has_terminal")] -use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; - -use std::error::Error; -#[cfg(feature = "has_terminal")] -use std::{ - io, - time::{Duration, Instant}, -}; -#[cfg(feature = "has_terminal")] -use tui::backend::CrosstermBackend; -use tui::Terminal; - -use training_mod_consts::*; - -fn test_backend_setup( - ui_menu: UiMenu, - menu_defaults: (UiMenu, String), -) -> Result< - ( - Terminal, - training_mod_tui::App, - ), - Box, -> { - let app = training_mod_tui::App::new(ui_menu, menu_defaults); - let backend = tui::backend::TestBackend::new(120, 15); - let terminal = Terminal::new(backend)?; - let mut state = tui::widgets::ListState::default(); - state.select(Some(1)); - - Ok((terminal, app)) -} - -#[test] -fn test_set_airdodge() -> Result<(), Box> { - let menu; - let mut prev_menu; - let menu_defaults; - unsafe { - prev_menu = MENU.clone(); - menu = ui_menu(MENU); - menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap()); - } - - let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?; - // Enter Mash Section - app.next_tab(); - // Enter Mash Toggles - app.on_a(); - // Set Mash Airdodge - app.on_a(); - let menu_json = app.get_menu_selections(); - let menu_struct = serde_json::from_str::(&menu_json).unwrap(); - let menu = menu_struct.menu; - let _ = menu_struct.defaults_menu; - prev_menu.mash_state.toggle(Action::AIR_DODGE); - assert_eq!( - serde_json::to_string(&prev_menu).unwrap(), - serde_json::to_string(&menu).unwrap() - ); - - Ok(()) -} - -#[test] -fn test_ensure_menu_retains_selections() -> Result<(), Box> { - let menu; - let prev_menu; - let menu_defaults; - unsafe { - prev_menu = MENU.clone(); - menu = ui_menu(MENU); - menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap()); - } - - let (_terminal, app) = test_backend_setup(menu, menu_defaults)?; - let menu_json = app.get_menu_selections(); - let menu_struct = serde_json::from_str::(&menu_json).unwrap(); - let menu = menu_struct.menu; - let _ = menu_struct.defaults_menu; - // At this point, we didn't change the menu at all; we should still see all the same options. - assert_eq!( - serde_json::to_string(&prev_menu).unwrap(), - serde_json::to_string(&menu).unwrap() - ); - - Ok(()) -} - -#[test] -fn test_save_and_reset_defaults() -> Result<(), Box> { - let menu; - let mut prev_menu; - let menu_defaults; - unsafe { - prev_menu = MENU.clone(); - menu = ui_menu(MENU); - menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap()); - } - - let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?; - - // Enter Mash Section - app.next_tab(); - // Enter Mash Toggles - app.on_a(); - // Set Mash Airdodge - app.on_a(); - // Return to submenu selection - app.on_b(); - // Save Defaults - app.save_defaults(); - // Enter Mash Toggles again - app.on_a(); - // Unset Mash Airdodge - app.on_a(); - - let menu_json = app.get_menu_selections(); - let menu_struct = serde_json::from_str::(&menu_json).unwrap(); - let menu = menu_struct.menu; - let defaults_menu = menu_struct.defaults_menu; - - assert_eq!( - serde_json::to_string(&prev_menu).unwrap(), - serde_json::to_string(&menu).unwrap(), - "The menu should be the same as how we started" - ); - prev_menu.mash_state.toggle(Action::AIR_DODGE); - assert_eq!( - serde_json::to_string(&prev_menu).unwrap(), - serde_json::to_string(&defaults_menu).unwrap(), - "The defaults menu should have Mash Airdodge" - ); - - // Reset current menu alone to defaults - app.reset_current_submenu(); - let menu_json = app.get_menu_selections(); - let menu_struct = serde_json::from_str::(&menu_json).unwrap(); - let menu = menu_struct.menu; - let _ = menu_struct.defaults_menu; - - assert_eq!( - serde_json::to_string(&prev_menu).unwrap(), - serde_json::to_string(&menu).unwrap(), - "The menu should have Mash Airdodge" - ); - - // Enter Mash Section - app.next_tab(); - // Enter Mash Toggles - app.on_a(); - // Unset Mash Airdodge - app.on_a(); - // Return to submenu selection - app.on_b(); - // Go down to Followup Toggles - app.on_down(); - // Enter Followup Toggles - app.on_a(); - // Go down and set Jump - app.on_down(); - app.on_a(); - // Return to submenu selection - app.on_b(); - // Save defaults - app.save_defaults(); - // Go back in and unset Jump - app.on_a(); - app.on_down(); - app.on_a(); - // Return to submenu selection - app.on_b(); - // Reset all to defaults - app.reset_all_submenus(); - let menu_json = app.get_menu_selections(); - let menu_struct = serde_json::from_str::(&menu_json).unwrap(); - let menu = menu_struct.menu; - let _ = menu_struct.defaults_menu; - - prev_menu.mash_state.toggle(Action::AIR_DODGE); - prev_menu.follow_up.toggle(Action::JUMP); - assert_eq!( - serde_json::to_string(&prev_menu).unwrap(), - serde_json::to_string(&menu).unwrap(), - "The menu should have Mash Airdodge off and Followup Jump on" - ); - - Ok(()) -} - -fn _get_frame_buffer( - mut terminal: Terminal, - mut app: training_mod_tui::App, -) -> Result> { - let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?; - let mut full_frame_buffer = String::new(); - for (i, cell) in frame_res.buffer.content().iter().enumerate() { - full_frame_buffer.push_str(&cell.symbol); - if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 { - full_frame_buffer.push_str("\n"); - } - } - full_frame_buffer.push_str("\n"); - - Ok(full_frame_buffer) -} - -#[test] -fn test_toggle_naming() -> Result<(), Box> { - let menu; - let mut prev_menu; - let menu_defaults; - unsafe { - prev_menu = MENU.clone(); - menu = ui_menu(MENU); - menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap()); - } - - let (mut terminal, mut app) = test_backend_setup(menu, menu_defaults)?; - // Enter Mash Section - app.next_tab(); - // Enter Mash Toggles - app.on_a(); - // Set Mash Airdodge - app.on_a(); - - let frame_buffer = _get_frame_buffer(terminal, app)?; - assert!(frame_buffer.contains("Airdodge")); - - Ok(()) -} - -fn main() -> Result<(), Box> { - let args: Vec = std::env::args().collect(); - let inputs = args.get(1); - let menu; - let menu_defaults; - - unsafe { - menu = ui_menu(MENU); - menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap()); - } - - #[cfg(not(feature = "has_terminal"))] - { - let (mut terminal, mut app) = test_backend_setup(menu, menu_defaults)?; - if inputs.is_some() { - inputs - .unwrap() - .split(",") - .for_each(|input| match input.to_uppercase().as_str() { - "X" => app.save_defaults(), - "Y" => app.reset_current_submenu(), - "Z" => app.reset_all_submenus(), - "L" => app.previous_tab(), - "R" => app.next_tab(), - "A" => app.on_a(), - "B" => app.on_b(), - "UP" => app.on_up(), - "DOWN" => app.on_down(), - "LEFT" => app.on_left(), - "RIGHT" => app.on_right(), - _ => {} - }) - } - let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?; - let menu_json = app.get_menu_selections(); - - for (i, cell) in frame_res.buffer.content().iter().enumerate() { - print!("{}", cell.symbol); - if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 { - println!(); - } - } - println!(); - - println!("Menu:\n{menu_json}"); - } - - #[cfg(feature = "has_terminal")] - { - let app = training_mod_tui::App::new(menu, menu_defaults); - - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let tick_rate = Duration::from_millis(250); - let res = run_app(&mut terminal, app, tick_rate); - - // restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{:?}", err) - } else { - println!("JSONs: {:#?}", res.as_ref().unwrap()); - unsafe { - let menu = serde_json::from_str::(&res.as_ref().unwrap()).unwrap(); - println!("menu: {:#?}", menu); - } - } - } - - Ok(()) -} - -#[cfg(feature = "has_terminal")] -fn run_app( - terminal: &mut Terminal, - mut app: training_mod_tui::App, - tick_rate: Duration, -) -> io::Result { - let mut last_tick = Instant::now(); - loop { - terminal.draw(|f| training_mod_tui::ui(f, &mut app).clone())?; - let menu_json = app.get_menu_selections(); - - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - - if crossterm::event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') => return Ok(menu_json), - KeyCode::Char('x') => app.save_defaults(), - KeyCode::Char('p') => app.reset_current_submenu(), - KeyCode::Char('o') => app.reset_all_submenus(), - KeyCode::Char('r') => app.next_tab(), - KeyCode::Char('l') => app.previous_tab(), - KeyCode::Left => app.on_left(), - KeyCode::Right => app.on_right(), - KeyCode::Down => app.on_down(), - KeyCode::Up => app.on_up(), - KeyCode::Enter => app.on_a(), - KeyCode::Backspace => app.on_b(), - _ => {} - } - } - } - if last_tick.elapsed() >= tick_rate { - last_tick = Instant::now(); - } - } -} diff --git a/training_mod_tui/src/structures/mod.rs b/training_mod_tui/src/structures/mod.rs new file mode 100644 index 0000000..d464c84 --- /dev/null +++ b/training_mod_tui/src/structures/mod.rs @@ -0,0 +1,6 @@ +mod stateful_list; +mod stateful_slider; +mod stateful_table; +pub use stateful_list::*; +pub use stateful_slider::*; +pub use stateful_table::*; diff --git a/training_mod_tui/src/structures/stateful_list.rs b/training_mod_tui/src/structures/stateful_list.rs new file mode 100644 index 0000000..fbc47bf --- /dev/null +++ b/training_mod_tui/src/structures/stateful_list.rs @@ -0,0 +1,124 @@ +use ratatui::widgets::ListState; +use serde::ser::{Serialize, SerializeSeq, Serializer}; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct StatefulList { + pub state: ListState, + pub items: Vec, +} + +impl IntoIterator for StatefulList { + type Item = T; + type IntoIter = std::vec::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} + +impl StatefulList { + pub fn new() -> StatefulList { + StatefulList { + state: ListState::default(), + items: Vec::new(), + } + } + + pub fn with_items(items: Vec) -> StatefulList { + let mut state = ListState::default(); + // Enforce state as first of list + state.select(Some(0)); + StatefulList { state, items } + } + + pub fn push(&mut self, item: T) { + self.items.push(item); + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn unselect(&mut self) { + self.state.select(None); + } + + pub fn get_selected(&mut self) -> Option<&mut T> { + if let Some(selected_index) = self.state.selected() { + Some(&mut self.items[selected_index]) + } else { + None + } + } + + pub fn get_before_selected(&mut self) -> Option<&mut T> { + let len = self.items.len(); + if let Some(selected_index) = self.state.selected() { + if selected_index == 0 { + Some(&mut self.items[len - 1]) + } else { + Some(&mut self.items[selected_index - 1]) + } + } else { + None + } + } + + pub fn get_after_selected(&mut self) -> Option<&mut T> { + let len = self.items.len(); + if let Some(selected_index) = self.state.selected() { + if selected_index == len - 1 { + Some(&mut self.items[0]) + } else { + Some(&mut self.items[selected_index + 1]) + } + } else { + None + } + } +} + +impl Serialize for StatefulList { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.items.len()))?; + for e in self.items.iter() { + seq.serialize_element(e)?; + } + seq.end() + } +} + +impl StatefulList { + pub fn iter(&self) -> impl Iterator + '_ { + self.items.iter() + } + pub fn iter_mut(&mut self) -> std::slice::IterMut { + self.items.iter_mut() + } +} diff --git a/training_mod_tui/src/structures/stateful_slider.rs b/training_mod_tui/src/structures/stateful_slider.rs new file mode 100644 index 0000000..fa47ee2 --- /dev/null +++ b/training_mod_tui/src/structures/stateful_slider.rs @@ -0,0 +1,146 @@ +use serde::{Serialize, Serializer}; + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum SliderState { + LowerHover, + UpperHover, + LowerSelected, + UpperSelected, + None, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct StatefulSlider { + pub state: SliderState, + pub lower: u32, + pub upper: u32, + pub min: u32, + pub max: u32, + pub incr_amount_slow: u32, + pub incr_amount_fast: u32, +} + +impl StatefulSlider { + pub fn new() -> StatefulSlider { + StatefulSlider { + state: SliderState::LowerHover, + lower: 0, + upper: 150, + min: 0, + max: 150, + incr_amount_slow: 1, + incr_amount_fast: 10, + } + } + + pub fn increment_selected_slow(&mut self) { + match self.state { + SliderState::LowerSelected => { + self.lower = self + .lower + .saturating_add(self.incr_amount_slow) + .min(self.upper); // Don't allow lower > upper + } + SliderState::UpperSelected => { + self.upper = self + .upper + .saturating_add(self.incr_amount_slow) + .min(self.max); // Don't allow upper > max + } + _ => {} + } + } + + pub fn increment_selected_fast(&mut self) { + match self.state { + SliderState::LowerSelected => { + self.lower = self + .lower + .saturating_add(self.incr_amount_fast) + .min(self.upper); // Don't allow lower > upper + } + SliderState::UpperSelected => { + self.upper = self + .upper + .saturating_add(self.incr_amount_fast) + .min(self.max); // Don't allow upper > max + } + _ => {} + } + } + + pub fn decrement_selected_slow(&mut self) { + match self.state { + SliderState::LowerSelected => { + self.lower = self + .lower + .saturating_sub(self.incr_amount_slow) + .max(self.min); // Don't allow lower < min + } + SliderState::UpperSelected => { + self.upper = self + .upper + .saturating_sub(self.incr_amount_slow) + .max(self.lower); // Don't allow upper < lower + } + _ => {} + } + } + + pub fn decrement_selected_fast(&mut self) { + match self.state { + SliderState::LowerSelected => { + self.lower = self + .lower + .saturating_sub(self.incr_amount_fast) + .max(self.min); // Don't allow lower < min + } + SliderState::UpperSelected => { + self.upper = self + .upper + .saturating_sub(self.incr_amount_fast) + .max(self.lower); // Don't allow upper < lower + } + _ => {} + } + } + + pub fn select_deselect(&mut self) { + self.state = match self.state { + SliderState::LowerHover => SliderState::LowerSelected, + SliderState::LowerSelected => SliderState::LowerHover, + SliderState::UpperHover => SliderState::UpperSelected, + SliderState::UpperSelected => SliderState::UpperHover, + SliderState::None => SliderState::None, + } + } + + pub fn deselect(&mut self) { + self.state = match self.state { + SliderState::LowerSelected => SliderState::LowerHover, + SliderState::UpperSelected => SliderState::UpperHover, + _ => self.state, + } + } + + pub fn switch_hover(&mut self) { + self.state = match self.state { + SliderState::LowerHover => SliderState::UpperHover, + SliderState::UpperHover => SliderState::LowerHover, + _ => self.state, + } + } + + pub fn is_handle_selected(&mut self) -> bool { + self.state == SliderState::LowerSelected || self.state == SliderState::UpperSelected + } +} + +impl Serialize for StatefulSlider { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + [self.lower, self.upper].serialize(serializer) + } +} diff --git a/training_mod_tui/src/structures/stateful_table.rs b/training_mod_tui/src/structures/stateful_table.rs new file mode 100644 index 0000000..de20f03 --- /dev/null +++ b/training_mod_tui/src/structures/stateful_table.rs @@ -0,0 +1,274 @@ +use ratatui::widgets::*; +use serde::{Serialize, Serializer}; + +/// Allows a snake-filled table of arbitrary size +/// The final row does not need to be filled +/// [ a , b , c , d ] +/// [ e, f, g, h, i ] +/// [ j, k ] + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct StatefulTable { + pub state: TableState, + pub items: Vec>>, + pub rows: usize, + pub cols: usize, +} + +// Size-related functions +impl StatefulTable { + pub fn len(&self) -> usize { + self.items + .iter() + .map(|row| { + row.iter() + .map(|item| if item.is_some() { 1 } else { 0 }) + .sum::() + }) + .sum() + } + pub fn full_len(&self) -> usize { + self.rows * self.cols + } + pub fn as_vec(&self) -> Vec { + let mut v = Vec::with_capacity(self.len()); + for row in self.items.iter() { + for item in row.iter() { + if let Some(i) = item { + v.push(i.clone()); + } + } + } + v + } +} + +// Initalization Functions +impl StatefulTable { + pub fn new(rows: usize, cols: usize) -> Self { + Self { + state: TableState::default().with_selected(Some(TableSelection::default())), + items: vec![vec![None; cols]; rows], + rows: rows, + cols: cols, + } + } + pub fn with_items(rows: usize, cols: usize, v: Vec) -> Self { + let mut table: Self = Self::new(rows, cols); + if v.len() > rows * cols { + panic!( + "Cannot create StatefulTable; too many items for size {}x{}: {}", + rows, + cols, + v.len() + ); + } else { + for (i, item) in v.iter().enumerate() { + table.items[i.div_euclid(cols)][i.rem_euclid(cols)] = Some(item.clone()); + } + table + } + } +} + +// State Functions +impl StatefulTable { + pub fn select(&mut self, row: usize, col: usize) { + assert!(col < self.cols); + assert!(row < self.rows); + self.state.select(Some(TableSelection::Cell { row, col })); + } + + pub fn get_selected(&mut self) -> Option<&mut T> { + self.items[self.state.selected_row().unwrap()][self.state.selected_col().unwrap()].as_mut() + } + + pub fn get(&self, row: usize, column: usize) -> Option<&T> { + if row >= self.rows || column >= self.cols { + None + } else { + self.items[row][column].as_ref() + } + } + + pub fn get_mut(&mut self, row: usize, column: usize) -> Option<&mut T> { + if row >= self.rows || column >= self.cols { + None + } else { + self.items[row][column].as_mut() + } + } + + pub fn get_by_idx(&self, idx: usize) -> Option<&T> { + let row = idx.div_euclid(self.cols); + let col = idx.rem_euclid(self.cols); + self.get(row, col) + } + pub fn get_by_idx_mut(&mut self, idx: usize) -> Option<&mut T> { + let row = idx.div_euclid(self.cols); + let col = idx.rem_euclid(self.cols); + self.get_mut(row, col) + } + + pub fn next_row(&mut self) { + let next_row = match self.state.selected_row() { + Some(row) => { + if row == self.items.len() - 1 { + 0 + } else { + row + 1 + } + } + + None => 0, + }; + self.state.select_row(Some(next_row)); + } + + pub fn next_row_checked(&mut self) { + self.next_row(); + if self.get_selected().is_none() { + self.next_row_checked(); + } + } + + pub fn prev_row(&mut self) { + let prev_row = match self.state.selected_row() { + Some(row) => { + if row == 0 { + self.items.len() - 1 + } else { + row - 1 + } + } + None => 0, + }; + + self.state.select_row(Some(prev_row)); + } + + pub fn prev_row_checked(&mut self) { + self.prev_row(); + if self.get_selected().is_none() { + self.prev_row_checked(); + } + } + + pub fn next_col(&mut self) { + // Assumes that all rows are the same width + let next_col = match self.state.selected_col() { + Some(col) => { + if col == self.items[0].len() - 1 { + 0 + } else { + col + 1 + } + } + None => 0, + }; + self.state.select_col(Some(next_col)); + } + + pub fn next_col_checked(&mut self) { + self.next_col(); + if self.get_selected().is_none() { + self.next_col_checked(); + } + } + + pub fn prev_col(&mut self) { + let prev_col = match self.state.selected_col() { + Some(col) => { + if col == 0 { + self.items[0].len() - 1 + } else { + col - 1 + } + } + None => 0, + }; + self.state.select_col(Some(prev_col)); + } + + pub fn prev_col_checked(&mut self) { + self.prev_col(); + self.carriage_return(); + } + + /// If the selected cell is None, move selection to the left until you get Some. + /// No-op if the selected cell is Some. + /// For example, a 2x3 table with 4 elements would shift the selection from 1,2 to 1,0 + /// + /// [ a , b , c ] + /// [ d , e , [ ]] + /// + /// | + /// V + /// + /// [ a , b , c ] + /// [[d], , ] + pub fn carriage_return(&mut self) { + assert!( + self.items[self.state.selected_row().unwrap()] + .iter() + .any(|x| x.is_some()), + "Carriage return called on an empty row!" + ); + if self.get_selected().is_none() { + self.prev_col(); + self.carriage_return(); + } + } +} + +impl Serialize for StatefulTable { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let flat: Vec = self.as_vec(); + flat.serialize(serializer) + } +} + +// Implement .iter() for StatefulTable +pub struct StatefulTableIterator<'a, T: Clone + Serialize> { + stateful_table: &'a StatefulTable, + index: usize, +} + +impl<'a, T: Clone + Serialize> Iterator for StatefulTableIterator<'a, T> { + type Item = &'a T; + fn next(&mut self) -> Option { + self.index += 1; + self.stateful_table.get_by_idx(self.index - 1) + } +} + +impl StatefulTable { + pub fn iter(&self) -> StatefulTableIterator { + StatefulTableIterator { + stateful_table: self, + index: 0, + } + } +} + +pub struct StatefulTableIteratorMut<'a, T: Clone + Serialize> { + inner: std::iter::Flatten>>>, +} + +impl<'a, T: Clone + Serialize> Iterator for StatefulTableIteratorMut<'a, T> { + type Item = &'a mut Option; + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl<'a, T: Clone + Serialize + 'a> StatefulTable { + pub fn iter_mut(&'a mut self) -> StatefulTableIteratorMut { + StatefulTableIteratorMut { + inner: self.items.iter_mut().flatten(), + } + } +} diff --git a/training_mod_tui/tests/test_stateful_list.rs b/training_mod_tui/tests/test_stateful_list.rs new file mode 100644 index 0000000..c766198 --- /dev/null +++ b/training_mod_tui/tests/test_stateful_list.rs @@ -0,0 +1,182 @@ +use ratatui::widgets::ListState; +use training_mod_tui_2::StatefulList; + +fn initialize_list(selected: Option) -> StatefulList { + StatefulList { + state: initialize_state(selected), + items: vec![10, 20, 30, 40], + } +} + +fn initialize_state(selected: Option) -> ListState { + let mut state = ListState::default(); + state.select(selected); + state +} + +#[test] +fn stateful_list_test_new() { + let l = initialize_list(None); + assert_eq!( + l, + StatefulList { + state: ListState::default(), + items: vec![10, 20, 30, 40], + } + ); +} + +#[test] +fn stateful_list_with_items() { + let l = initialize_list(Some(0)); + let m = StatefulList::::with_items(vec![10, 20, 30, 40]); + assert_eq!(l, m); +} + +#[test] +fn stateful_list_next() { + let mut l = initialize_list(Some(0)); + let mut state = ListState::default(); + state.select(Some(0)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 10)); + l.next(); + state.select(Some(1)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 20)); + l.next(); + state.select(Some(2)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 30)); + l.next(); + state.select(Some(3)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 40)); + l.next(); + state.select(Some(0)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 10)); +} + +#[test] +fn stateful_list_prev() { + let mut l = initialize_list(Some(0)); + let mut state = ListState::default(); + state.select(Some(0)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 10)); + l.previous(); + state.select(Some(3)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 40)); + l.previous(); + state.select(Some(2)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 30)); + l.previous(); + state.select(Some(1)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 20)); + l.previous(); + state.select(Some(0)); + assert_eq!(l.state, state); + assert_eq!(l.get_selected(), Some(&mut 10)); +} + +#[test] +fn stateful_list_unselect() { + let mut l = initialize_list(Some(0)); + let state = ListState::default(); + l.unselect(); + assert_eq!(l.state, state); + l.unselect(); + assert_eq!(l.state, state); +} + +#[test] +fn stateful_list_get_selected() { + let mut l = initialize_list(None); + assert_eq!(l.get_selected(), None); + l.state.select(Some(0)); + assert_eq!(l.get_selected(), Some(&mut 10)); + l.state.select(Some(1)); + assert_eq!(l.get_selected(), Some(&mut 20)); + l.state.select(Some(2)); + assert_eq!(l.get_selected(), Some(&mut 30)); + l.state.select(Some(3)); + assert_eq!(l.get_selected(), Some(&mut 40)); +} + +#[test] +fn stateful_list_get_before_selected() { + let mut l = initialize_list(None); + assert_eq!(l.get_before_selected(), None); + l.state.select(Some(0)); + assert_eq!(l.get_before_selected(), Some(&mut 40)); + l.state.select(Some(1)); + assert_eq!(l.get_before_selected(), Some(&mut 10)); + l.state.select(Some(2)); + assert_eq!(l.get_before_selected(), Some(&mut 20)); + l.state.select(Some(3)); + assert_eq!(l.get_before_selected(), Some(&mut 30)); +} + +#[test] +fn stateful_list_get_after_selected() { + let mut l = initialize_list(None); + assert_eq!(l.get_after_selected(), None); + l.state.select(Some(0)); + assert_eq!(l.get_after_selected(), Some(&mut 20)); + l.state.select(Some(1)); + assert_eq!(l.get_after_selected(), Some(&mut 30)); + l.state.select(Some(2)); + assert_eq!(l.get_after_selected(), Some(&mut 40)); + l.state.select(Some(3)); + assert_eq!(l.get_after_selected(), Some(&mut 10)); +} + +#[test] +fn stateful_list_serialize() { + let l = initialize_list(Some(2)); + let l_json = serde_json::to_string(&l).unwrap(); + assert_eq!(&l_json, "[10,20,30,40]"); +} + +#[test] +fn stateful_list_iter() { + let l = initialize_list(Some(2)); + let mut l_iter = l.iter(); + assert_eq!(l_iter.next(), Some(&10)); + assert_eq!(l_iter.next(), Some(&20)); + assert_eq!(l_iter.next(), Some(&30)); + assert_eq!(l_iter.next(), Some(&40)); + assert_eq!(l_iter.next(), None); + assert_eq!(l_iter.next(), None); +} + +#[test] +fn stateful_list_iter_mut() { + let mut l = initialize_list(Some(2)); + let mut l_iter_mut = l.iter_mut(); + assert_eq!(l_iter_mut.next(), Some(&mut 10)); + assert_eq!(l_iter_mut.next(), Some(&mut 20)); + assert_eq!(l_iter_mut.next(), Some(&mut 30)); + assert_eq!(l_iter_mut.next(), Some(&mut 40)); + assert_eq!(l_iter_mut.next(), None); + assert_eq!(l_iter_mut.next(), None); +} + +#[test] +fn stateful_list_push() { + let mut l = initialize_list(None); + l.push(5); + l.push(6); + l.push(7); + assert_eq!( + l, + StatefulList { + state: ListState::default(), + items: vec![10, 20, 30, 40, 5, 6, 7], + } + ); +} diff --git a/training_mod_tui/tests/test_stateful_slider.rs b/training_mod_tui/tests/test_stateful_slider.rs new file mode 100644 index 0000000..8acbed0 --- /dev/null +++ b/training_mod_tui/tests/test_stateful_slider.rs @@ -0,0 +1,288 @@ +use training_mod_tui_2::{SliderState, StatefulSlider}; + +fn initialize_slider(state: SliderState) -> StatefulSlider { + StatefulSlider { + state: state, + lower: 0, + upper: 150, + min: 0, + max: 150, + incr_amount_slow: 1, + incr_amount_fast: 10, + } +} + +#[test] +fn stateful_slider_new() { + let s = initialize_slider(SliderState::LowerHover); + assert_eq!(s, StatefulSlider::new()); +} + +#[test] +fn stateful_slider_increment_selected_slow() { + let mut s = initialize_slider(SliderState::LowerHover); + s.upper = 50; + + // Check LowerHover: no increment + s.increment_selected_slow(); + assert_eq!(s.lower, 0); + + // Check LowerSelected: lower increments + s.state = SliderState::LowerSelected; + s.increment_selected_slow(); + assert_eq!(s.lower, 1); + + // Check LowerSelected: lower can't go above upper + s.lower = s.upper; + s.increment_selected_slow(); + assert_eq!(s.lower, s.upper); + + // Check UpperHover: no increment + s.lower = 5; + s.upper = 50; + s.state = SliderState::UpperHover; + s.increment_selected_slow(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, 50); + + // Check UpperSelected: upper increments + s.state = SliderState::UpperSelected; + s.increment_selected_slow(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, 51); + + // Check UpperSelected: upper can't go above max + s.upper = s.max; + s.increment_selected_slow(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, s.max); +} + +#[test] +fn stateful_slider_increment_selected_fast() { + let mut s = initialize_slider(SliderState::LowerHover); + s.upper = 50; + + // Check LowerHover: no increment + s.increment_selected_fast(); + assert_eq!(s.lower, 0); + + // Check LowerSelected: lower increments + s.state = SliderState::LowerSelected; + s.increment_selected_fast(); + assert_eq!(s.lower, 10); + + // Check LowerSelected: lower can't go above upper + s.lower = s.upper; + s.increment_selected_fast(); + assert_eq!(s.lower, s.upper); + + // Check UpperHover: no increment + s.lower = 5; + s.upper = 50; + s.state = SliderState::UpperHover; + s.increment_selected_fast(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, 50); + + // Check UpperSelected: upper increments + s.state = SliderState::UpperSelected; + s.increment_selected_fast(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, 60); + + // Check UpperSelected: upper can't go above max + s.upper = s.max; + s.increment_selected_fast(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, s.max); +} + +#[test] +fn stateful_slider_decrement_selected_slow() { + let mut s = initialize_slider(SliderState::LowerHover); + s.min = 4; + s.lower = 5; + s.upper = 50; + + // Check LowerHover: no decrement + s.decrement_selected_slow(); + assert_eq!(s.lower, 5); + + // Check LowerSelected: lower decrements + s.state = SliderState::LowerSelected; + s.decrement_selected_slow(); + assert_eq!(s.lower, 4); + + // Check LowerSelected: lower can't go below min + s.lower = s.min; + s.decrement_selected_slow(); + assert_eq!(s.lower, s.min); + + // Check LowerSelected: lower can't go below 0 + s.min = 0; + s.lower = 0; + s.decrement_selected_slow(); + assert_eq!(s.lower, 0); + + // Check UpperHover: no decrement + s.lower = 5; + s.upper = 50; + s.state = SliderState::UpperHover; + s.decrement_selected_slow(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, 50); + + // Check UpperSelected: upper decrements + s.state = SliderState::UpperSelected; + s.decrement_selected_slow(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, 49); + + // Check UpperSelected: upper can't go below lower + s.upper = s.lower; + s.decrement_selected_slow(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, s.lower); +} + +#[test] +fn stateful_slider_decrement_selected_fast() { + let mut s = initialize_slider(SliderState::LowerHover); + s.min = 20; + s.lower = 35; + s.upper = 50; + + // Check LowerHover: no decrement + s.decrement_selected_fast(); + assert_eq!(s.lower, 35); + + // Check LowerSelected: lower decrements + s.state = SliderState::LowerSelected; + s.decrement_selected_fast(); + assert_eq!(s.lower, 25); + + // Check LowerSelected: lower can't go below min + s.lower = s.min; + s.decrement_selected_fast(); + assert_eq!(s.lower, s.min); + + // Check LowerSelected: lower can't go below 0 + s.min = 0; + s.lower = 0; + s.decrement_selected_fast(); + assert_eq!(s.lower, 0); + + // Check UpperHover: no decrement + s.lower = 5; + s.upper = 50; + s.state = SliderState::UpperHover; + s.decrement_selected_fast(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, 50); + + // Check UpperSelected: upper decrements + s.state = SliderState::UpperSelected; + s.decrement_selected_fast(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, 40); + + // Check UpperSelected: upper can't go below lower + s.upper = s.lower; + s.decrement_selected_fast(); + assert_eq!(s.lower, 5); + assert_eq!(s.upper, s.lower); +} + +#[test] +fn stateful_slider_select_deselect() { + let mut s = initialize_slider(SliderState::LowerHover); + assert_eq!(s.state, SliderState::LowerHover); + s.select_deselect(); + assert_eq!(s.state, SliderState::LowerSelected); + s.select_deselect(); + assert_eq!(s.state, SliderState::LowerHover); + s.state = SliderState::UpperHover; + s.select_deselect(); + assert_eq!(s.state, SliderState::UpperSelected); + s.select_deselect(); + assert_eq!(s.state, SliderState::UpperHover); + s.state = SliderState::None; + s.select_deselect(); + assert_eq!(s.state, SliderState::None); +} + +#[test] +fn stateful_slider_deselect() { + let mut s = initialize_slider(SliderState::LowerHover); + s.deselect(); + assert_eq!(s.state, SliderState::LowerHover); + + s.state = SliderState::LowerSelected; + s.deselect(); + assert_eq!(s.state, SliderState::LowerHover); + + s.state = SliderState::UpperHover; + s.deselect(); + assert_eq!(s.state, SliderState::UpperHover); + + s.state = SliderState::UpperSelected; + s.deselect(); + assert_eq!(s.state, SliderState::UpperHover); + + s.state = SliderState::None; + s.deselect(); + assert_eq!(s.state, SliderState::None); +} + +#[test] +fn stateful_slider_switch_hover() { + let mut s = initialize_slider(SliderState::LowerHover); + s.switch_hover(); + assert_eq!(s.state, SliderState::UpperHover); + + s.state = SliderState::LowerSelected; + s.switch_hover(); + assert_eq!(s.state, SliderState::LowerSelected); + + s.state = SliderState::UpperHover; + s.switch_hover(); + assert_eq!(s.state, SliderState::LowerHover); + + s.state = SliderState::UpperSelected; + s.switch_hover(); + assert_eq!(s.state, SliderState::UpperSelected); + + s.state = SliderState::None; + s.switch_hover(); + assert_eq!(s.state, SliderState::None); +} + +#[test] +fn stateful_slider_is_handle_selected() { + let mut s = initialize_slider(SliderState::LowerHover); + assert_eq!(s.is_handle_selected(), false); + + s.state = SliderState::LowerSelected; + assert_eq!(s.is_handle_selected(), true); + + s.state = SliderState::UpperHover; + assert_eq!(s.is_handle_selected(), false); + + s.state = SliderState::UpperSelected; + assert_eq!(s.is_handle_selected(), true); + + s.state = SliderState::None; + assert_eq!(s.is_handle_selected(), false); +} + +#[test] +fn stateful_slider_serialize() { + let mut s = initialize_slider(SliderState::LowerHover); + let s_json = serde_json::to_string(&s).unwrap(); + assert_eq!(&s_json, "[0,150]"); + s.lower = 25; + s.upper = 75; + let s_json = serde_json::to_string(&s).unwrap(); + assert_eq!(&s_json, "[25,75]"); +} diff --git a/training_mod_tui/tests/test_stateful_table.rs b/training_mod_tui/tests/test_stateful_table.rs new file mode 100644 index 0000000..3e4ad3b --- /dev/null +++ b/training_mod_tui/tests/test_stateful_table.rs @@ -0,0 +1,342 @@ +use ratatui::widgets::{TableSelection, TableState}; +use training_mod_tui_2::StatefulTable; + +fn initialize_table(row: usize, col: usize) -> StatefulTable { + let mut s = StatefulTable::with_items(2, 3, vec![0, 1, 2, 3, 4]); + s.select(row, col); + s +} + +fn tablestate_with(row: usize, col: usize) -> TableState { + TableState::default().with_selected(Some(TableSelection::Cell { row, col })) +} + +#[test] +fn stateful_table_next_col_full() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.next_col(); + assert_eq!(t.get_selected(), Some(&mut 1)); + t.next_col(); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.next_col(); + assert_eq!(t.get_selected(), Some(&mut 0)); +} + +#[test] +fn stateful_table_next_col_checked_full() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.next_col_checked(); + assert_eq!(t.get_selected(), Some(&mut 1)); + t.next_col_checked(); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.next_col_checked(); + assert_eq!(t.get_selected(), Some(&mut 0)); +} + +#[test] +fn stateful_table_prev_col_full() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.prev_col(); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.prev_col(); + assert_eq!(t.get_selected(), Some(&mut 1)); + t.prev_col(); + assert_eq!(t.get_selected(), Some(&mut 0)); +} + +#[test] +fn stateful_table_prev_col_checked_full() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.prev_col_checked(); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.prev_col_checked(); + assert_eq!(t.get_selected(), Some(&mut 1)); + t.prev_col_checked(); + assert_eq!(t.get_selected(), Some(&mut 0)); +} + +#[test] +fn stateful_table_next_col_short() { + let mut t = initialize_table(1, 0); + assert_eq!(t.get_selected(), Some(&mut 3)); + t.next_col(); + assert_eq!(t.get_selected(), Some(&mut 4)); + t.next_col(); + assert_eq!(t.get_selected(), None); + t.next_col(); + assert_eq!(t.get_selected(), Some(&mut 3)); +} + +#[test] +fn stateful_table_next_col_checked_short() { + let mut t = initialize_table(1, 0); + assert_eq!(t.get_selected(), Some(&mut 3)); + t.next_col_checked(); + assert_eq!(t.get_selected(), Some(&mut 4)); + t.next_col_checked(); + assert_eq!(t.get_selected(), Some(&mut 3)); +} + +#[test] +fn stateful_table_prev_col_short() { + let mut t = initialize_table(1, 0); + assert_eq!(t.get_selected(), Some(&mut 3)); + t.prev_col(); + assert_eq!(t.get_selected(), None); + t.prev_col(); + assert_eq!(t.get_selected(), Some(&mut 4)); + t.prev_col(); + assert_eq!(t.get_selected(), Some(&mut 3)); +} + +#[test] +fn stateful_table_carriage_return_none() { + let mut t = initialize_table(1, 2); + t.carriage_return(); + assert_eq!(t.state, tablestate_with(1, 1)); +} + +#[test] +fn stateful_table_carriage_return_some() { + let mut t = initialize_table(1, 1); + t.carriage_return(); + assert_eq!(t.state, tablestate_with(1, 1)); +} + +#[test] +fn stateful_table_table_with_items() { + let items: Vec = vec![0, 1, 2, 3, 4]; + let t: StatefulTable = StatefulTable::with_items(2, 3, items); + let u = initialize_table(0, 0); + assert_eq!(t, u); +} + +#[test] +fn stateful_table_get_selected() { + let mut t = initialize_table(1, 1); + assert_eq!(t.get_selected(), Some(&mut 4)); +} + +#[test] +fn stateful_table_get() { + let t = initialize_table(1, 1); + assert_eq!(t.get(0, 0), Some(&0)); + assert_eq!(t.get(0, 1), Some(&1)); + assert_eq!(t.get(0, 2), Some(&2)); + assert_eq!(t.get(1, 0), Some(&3)); + assert_eq!(t.get(1, 1), Some(&4)); + assert_eq!(t.get(1, 2), None); + assert_eq!(t.get(10, 0), None); + assert_eq!(t.get(0, 10), None); +} + +#[test] +fn stateful_table_get_by_idx() { + let t = initialize_table(1, 1); + assert_eq!(t.get_by_idx(0), Some(&0)); + assert_eq!(t.get_by_idx(1), Some(&1)); + assert_eq!(t.get_by_idx(2), Some(&2)); + assert_eq!(t.get_by_idx(3), Some(&3)); + assert_eq!(t.get_by_idx(4), Some(&4)); + assert_eq!(t.get_by_idx(5), None); + assert_eq!(t.get_by_idx(50), None); +} + +#[test] +fn stateful_table_len() { + let t = initialize_table(1, 1); + assert_eq!(t.len(), 5); +} + +#[test] +fn stateful_table_full_len() { + let t = initialize_table(0, 0); + assert_eq!(t.full_len(), 6); +} + +#[test] +fn stateful_table_serialize() { + let t = initialize_table(1, 1); + let t_ser = serde_json::to_string(&t).unwrap(); + assert_eq!(&t_ser, "[0,1,2,3,4]"); +} + +#[test] +fn stateful_table_new() { + let t: StatefulTable = StatefulTable::new(2, 3); + let u: StatefulTable = StatefulTable::with_items(2, 3, vec![]); + let v: StatefulTable = StatefulTable { + state: tablestate_with(0, 0), + items: vec![vec![None; 3]; 2], + rows: 2, + cols: 3, + }; + assert_eq!(t, u); + assert_eq!(t, v); +} + +#[test] +fn stateful_table_with_items() { + let t: StatefulTable = StatefulTable::with_items(2, 3, vec![1, 2]); + let u: StatefulTable = StatefulTable { + state: tablestate_with(0, 0), + items: vec![vec![Some(1), Some(2), None], vec![None; 3]], + rows: 2, + cols: 3, + }; + assert_eq!(t, u); +} + +#[test] +fn stateful_table_select() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.select(0, 1); + assert_eq!(t.get_selected(), Some(&mut 1)); + t.select(0, 2); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.select(1, 0); + assert_eq!(t.get_selected(), Some(&mut 3)); + t.select(1, 1); + assert_eq!(t.get_selected(), Some(&mut 4)); + t.select(1, 2); + assert_eq!(t.get_selected(), None); +} + +#[test] +fn stateful_table_get_mut() { + let mut t = initialize_table(1, 1); + assert_eq!(t.get_mut(0, 0), Some(&mut 0)); + assert_eq!(t.get_mut(0, 1), Some(&mut 1)); + assert_eq!(t.get_mut(0, 2), Some(&mut 2)); + assert_eq!(t.get_mut(1, 0), Some(&mut 3)); + assert_eq!(t.get_mut(1, 1), Some(&mut 4)); + assert_eq!(t.get_mut(1, 2), None); + assert_eq!(t.get_mut(10, 0), None); + assert_eq!(t.get_mut(0, 10), None); +} + +#[test] +fn stateful_table_get_by_idx_mut() { + let mut t = initialize_table(1, 1); + assert_eq!(t.get_by_idx_mut(0), Some(&mut 0)); + assert_eq!(t.get_by_idx_mut(1), Some(&mut 1)); + assert_eq!(t.get_by_idx_mut(2), Some(&mut 2)); + assert_eq!(t.get_by_idx_mut(3), Some(&mut 3)); + assert_eq!(t.get_by_idx_mut(4), Some(&mut 4)); + assert_eq!(t.get_by_idx_mut(5), None); + assert_eq!(t.get_by_idx_mut(50), None); +} + +#[test] +fn stateful_table_next_row_full() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.next_row(); + assert_eq!(t.get_selected(), Some(&mut 3)); + t.next_row(); + assert_eq!(t.get_selected(), Some(&mut 0)); +} + +#[test] +fn stateful_table_next_row_short() { + let mut t = initialize_table(0, 2); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.next_row(); + assert_eq!(t.get_selected(), None); + t.next_row(); + assert_eq!(t.get_selected(), Some(&mut 2)); +} + +#[test] +fn stateful_table_next_row_checked_full() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.next_row_checked(); + assert_eq!(t.get_selected(), Some(&mut 3)); + t.next_row_checked(); + assert_eq!(t.get_selected(), Some(&mut 0)); +} + +#[test] +fn stateful_table_next_row_checked_short() { + let mut t = initialize_table(0, 2); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.next_row_checked(); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.next_row_checked(); + assert_eq!(t.get_selected(), Some(&mut 2)); +} + +#[test] +fn stateful_table_prev_row_full() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.prev_row(); + assert_eq!(t.get_selected(), Some(&mut 3)); + t.prev_row(); + assert_eq!(t.get_selected(), Some(&mut 0)); +} + +#[test] +fn stateful_table_prev_row_short() { + let mut t = initialize_table(0, 2); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.prev_row(); + assert_eq!(t.get_selected(), None); + t.prev_row(); + assert_eq!(t.get_selected(), Some(&mut 2)); +} + +#[test] +fn stateful_table_prev_row_checked_full() { + let mut t = initialize_table(0, 0); + assert_eq!(t.get_selected(), Some(&mut 0)); + t.prev_row_checked(); + assert_eq!(t.get_selected(), Some(&mut 3)); + t.prev_row_checked(); + assert_eq!(t.get_selected(), Some(&mut 0)); +} + +#[test] +fn stateful_table_prev_row_checked_short() { + let mut t = initialize_table(0, 2); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.prev_row_checked(); + assert_eq!(t.get_selected(), Some(&mut 2)); + t.prev_row_checked(); + assert_eq!(t.get_selected(), Some(&mut 2)); +} + +#[test] +fn stateful_table_iter() { + let t = initialize_table(0, 0); + let mut t_iter = t.iter(); + assert_eq!(t_iter.next(), Some(&0)); + assert_eq!(t_iter.next(), Some(&1)); + assert_eq!(t_iter.next(), Some(&2)); + assert_eq!(t_iter.next(), Some(&3)); + assert_eq!(t_iter.next(), Some(&4)); + assert_eq!(t_iter.next(), None); + assert_eq!(t_iter.next(), None); +} + +#[test] +fn stateful_table_iter_mut() { + let mut t = initialize_table(0, 0); + for item in t.iter_mut().filter(|item| item.is_some()) { + *item = Some(item.unwrap() + 10); + } + let mut t_iter = t.iter(); + assert_eq!(t_iter.next(), Some(&10)); + assert_eq!(t_iter.next(), Some(&11)); + assert_eq!(t_iter.next(), Some(&12)); + assert_eq!(t_iter.next(), Some(&13)); + assert_eq!(t_iter.next(), Some(&14)); + assert_eq!(t_iter.next(), None); + assert_eq!(t_iter.next(), None); +} diff --git a/training_mod_tui/tests/test_submenu.rs b/training_mod_tui/tests/test_submenu.rs new file mode 100644 index 0000000..a453d92 --- /dev/null +++ b/training_mod_tui/tests/test_submenu.rs @@ -0,0 +1,502 @@ +use ratatui::widgets::{TableSelection, TableState}; +use training_mod_tui_2::*; + +fn make_toggle<'a>(v: u8) -> Toggle<'a> { + Toggle { + title: "Title", + value: v, + max: 4, + } +} + +fn make_toggle_table_multiple<'a>( + rows: usize, + cols: usize, + num: usize, +) -> StatefulTable> { + // [ (0) 1 2 ] + // [ 3 ] + let v: Vec = (0..num).map(|v| make_toggle(v as u8)).collect(); + StatefulTable::with_items(rows, cols, v) +} + +fn make_toggle_table_single<'a>(rows: usize, cols: usize, num: usize) -> StatefulTable> { + // [ (1) 0 0 ] + // [ 0 ] + let v: Vec = (0..num).map(|_| make_toggle(0)).collect(); + let mut t = StatefulTable::with_items(rows, cols, v); + t.items[0][0] = Some(make_toggle(1)); + t +} + +fn initialize_submenu<'a>(submenu_type: SubMenuType) -> SubMenu<'a> { + match submenu_type { + SubMenuType::ToggleSingle => SubMenu { + title: "Single Option Menu", + id: "single_option", + help_text: "A Single Option", + submenu_type: submenu_type, + toggles: make_toggle_table_single(2, 3, 4), + slider: None, + }, + SubMenuType::ToggleMultiple => SubMenu { + title: "Multi Option Menu", + id: "multi_option", + help_text: "Multiple Options", + submenu_type: submenu_type, + toggles: make_toggle_table_multiple(2, 3, 4), + slider: None, + }, + SubMenuType::Slider => SubMenu { + title: "Slider Menu", + id: "slider", + help_text: "A Double-ended Slider", + submenu_type: submenu_type, + toggles: make_toggle_table_multiple(0, 0, 0), + slider: Some(StatefulSlider::new()), + }, + SubMenuType::None => { + panic!() + } + } +} + +#[test] +fn submenu_serialize() { + let submenu = initialize_submenu(SubMenuType::ToggleSingle); + let json = serde_json::to_string(&submenu).unwrap(); + assert_eq!(&json, "[1,0,0,0]"); + + let submenu = initialize_submenu(SubMenuType::ToggleMultiple); + let json = serde_json::to_string(&submenu).unwrap(); + assert_eq!(&json, "[0,1,2,3]"); + + let submenu = initialize_submenu(SubMenuType::Slider); + let json = serde_json::to_string(&submenu).unwrap(); + assert_eq!(&json, "[0,150]"); +} + +#[test] +fn submenu_selected_toggle() { + let mut submenu = initialize_submenu(SubMenuType::ToggleSingle); + let mut t = make_toggle(1); + assert_eq!(submenu.selected_toggle(), &mut t); + t = make_toggle(0); + submenu.toggles.next_col(); + assert_eq!(submenu.selected_toggle(), &mut t); + + let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple); + let mut t = make_toggle(0); + assert_eq!(submenu.selected_toggle(), &mut t); + t = make_toggle(1); + submenu.toggles.next_col(); + assert_eq!(submenu.selected_toggle(), &mut t); + t = make_toggle(2); + submenu.toggles.next_col(); + assert_eq!(submenu.selected_toggle(), &mut t); +} + +#[test] +fn submenu_update_from_vec() { + let mut submenu = initialize_submenu(SubMenuType::ToggleSingle); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); + submenu.update_from_vec(vec![0, 0, 1, 0]); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); + + let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(3))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); + submenu.update_from_vec(vec![1, 1, 0, 4]); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(4))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); + + let mut submenu = initialize_submenu(SubMenuType::Slider); + let mut slider = StatefulSlider::new(); + assert_eq!(submenu.slider, Some(slider)); + slider.lower = 5; + submenu.update_from_vec(vec![5, 150]); + assert_eq!(submenu.slider, Some(slider)); + slider.upper = 75; + submenu.update_from_vec(vec![5, 75]); + assert_eq!(submenu.slider, Some(slider)); +} + +#[test] +fn submenu_single_on_a() { + let mut submenu = initialize_submenu(SubMenuType::ToggleSingle); + submenu.toggles.select(1, 0); + submenu.on_a(); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); + submenu.on_a(); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); + submenu.toggles.select(0, 0); + submenu.on_a(); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); +} + +#[test] +fn submenu_multiple_on_a() { + let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple); + submenu.toggles.select(1, 0); + submenu.on_a(); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(4))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); + submenu.on_a(); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); + submenu.toggles.select(0, 0); + submenu.on_a(); + assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1))); + assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2))); + assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0))); + assert_eq!(submenu.toggles.items[1][1], None); + assert_eq!(submenu.toggles.items[1][2], None); +} + +#[test] +fn submenu_slider_on_a() { + let mut submenu = initialize_submenu(SubMenuType::Slider); + assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover); + submenu.on_a(); + assert_eq!(submenu.slider.unwrap().state, SliderState::LowerSelected); + submenu.on_a(); + assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover); + submenu.slider = Some(StatefulSlider { + state: SliderState::UpperHover, + ..submenu.slider.unwrap() + }); + assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover); + submenu.on_a(); + assert_eq!(submenu.slider.unwrap().state, SliderState::UpperSelected); + submenu.on_a(); + assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover); +} + +#[test] +fn submenu_slider_on_b_selected() { + let mut submenu = initialize_submenu(SubMenuType::Slider); + submenu.slider = Some(StatefulSlider { + state: SliderState::LowerSelected, + ..submenu.slider.unwrap() + }); + submenu.on_b(); + assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover); + submenu.on_b(); + assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover); + submenu.slider = Some(StatefulSlider { + state: SliderState::UpperSelected, + ..submenu.slider.unwrap() + }); + submenu.on_b(); + assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover); + submenu.on_b(); + assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover); +} + +#[test] +fn submenu_single_on_up() { + let mut submenu = initialize_submenu(SubMenuType::ToggleSingle); + let mut state = TableState::default(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_up(); + state.select(Some(TableSelection::Cell { row: 1, col: 0 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_up(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + + submenu.toggles.select(0, 2); + state.select(Some(TableSelection::Cell { row: 0, col: 2 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_up(); + assert_eq!(submenu.toggles.state, state); +} + +#[test] +fn submenu_multiple_on_up() { + let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple); + let mut state = TableState::default(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_up(); + state.select(Some(TableSelection::Cell { row: 1, col: 0 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_up(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + + submenu.toggles.select(0, 2); + state.select(Some(TableSelection::Cell { row: 0, col: 2 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_up(); + assert_eq!(submenu.toggles.state, state); +} +#[test] +fn submenu_single_on_down() { + let mut submenu = initialize_submenu(SubMenuType::ToggleSingle); + let mut state = TableState::default(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_down(); + state.select(Some(TableSelection::Cell { row: 1, col: 0 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_down(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + + submenu.toggles.select(0, 2); + state.select(Some(TableSelection::Cell { row: 0, col: 2 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_down(); + assert_eq!(submenu.toggles.state, state); +} + +#[test] +fn submenu_multiple_on_down() { + let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple); + let mut state = TableState::default(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_down(); + state.select(Some(TableSelection::Cell { row: 1, col: 0 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_down(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + + submenu.toggles.select(0, 2); + state.select(Some(TableSelection::Cell { row: 0, col: 2 })); + assert_eq!(submenu.toggles.state, state); + submenu.on_down(); + assert_eq!(submenu.toggles.state, state); +} + +#[test] +fn submenu_single_on_left() { + let mut submenu = initialize_submenu(SubMenuType::ToggleSingle); + let mut state = TableState::default(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1))); + submenu.on_left(); + state.select(Some(TableSelection::Cell { row: 0, col: 2 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); + submenu.on_left(); + state.select(Some(TableSelection::Cell { row: 0, col: 1 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); + + submenu.toggles.select(1, 0); + submenu.on_left(); + state.select(Some(TableSelection::Cell { row: 1, col: 0 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); + submenu.on_left(); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); +} + +#[test] +fn submenu_multiple_on_left() { + let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple); + let mut state = TableState::default(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); + submenu.on_left(); + state.select(Some(TableSelection::Cell { row: 0, col: 2 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(2))); + submenu.on_left(); + state.select(Some(TableSelection::Cell { row: 0, col: 1 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1))); + + submenu.toggles.select(1, 0); + submenu.on_left(); + state.select(Some(TableSelection::Cell { row: 1, col: 0 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3))); + submenu.on_left(); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3))); +} + +#[test] +fn submenu_slider_on_left() { + let mut submenu = initialize_submenu(SubMenuType::Slider); + let mut state = SliderState::LowerHover; + assert_eq!(submenu.slider.unwrap().state, state); + state = SliderState::UpperHover; + submenu.on_left(); + assert_eq!(submenu.slider.unwrap().state, state); + + submenu.slider = Some(StatefulSlider { + state: SliderState::LowerSelected, + lower: 1, + ..submenu.slider.unwrap() + }); + state = SliderState::LowerSelected; + submenu.on_left(); + assert_eq!(submenu.slider.unwrap().state, state); + assert_eq!(submenu.slider.unwrap().lower, 0); + assert_eq!(submenu.slider.unwrap().upper, 150); + submenu.on_left(); + assert_eq!(submenu.slider.unwrap().state, state); + assert_eq!(submenu.slider.unwrap().lower, 0); + assert_eq!(submenu.slider.unwrap().upper, 150); + + submenu.slider = Some(StatefulSlider { + state: SliderState::UpperSelected, + lower: 99, + upper: 100, + ..submenu.slider.unwrap() + }); + state = SliderState::UpperSelected; + submenu.on_left(); + assert_eq!(submenu.slider.unwrap().state, state); + assert_eq!(submenu.slider.unwrap().lower, 99); + assert_eq!(submenu.slider.unwrap().upper, 99); + submenu.on_left(); + assert_eq!(submenu.slider.unwrap().state, state); + assert_eq!(submenu.slider.unwrap().lower, 99); + assert_eq!(submenu.slider.unwrap().upper, 99); +} + +#[test] +fn submenu_single_on_right() { + let mut submenu = initialize_submenu(SubMenuType::ToggleSingle); + let mut state = TableState::default(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1))); + submenu.on_right(); + state.select(Some(TableSelection::Cell { row: 0, col: 1 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); + submenu.on_right(); + state.select(Some(TableSelection::Cell { row: 0, col: 2 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); + + submenu.toggles.select(1, 0); + submenu.on_right(); + state.select(Some(TableSelection::Cell { row: 1, col: 0 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); + submenu.on_right(); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); +} + +#[test] +fn submenu_multiple_on_right() { + let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple); + let mut state = TableState::default(); + state.select(Some(TableSelection::Cell { row: 0, col: 0 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0))); + submenu.on_right(); + state.select(Some(TableSelection::Cell { row: 0, col: 1 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1))); + submenu.on_right(); + state.select(Some(TableSelection::Cell { row: 0, col: 2 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(2))); + + submenu.toggles.select(1, 0); + submenu.on_right(); + state.select(Some(TableSelection::Cell { row: 1, col: 0 })); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3))); + submenu.on_right(); + assert_eq!(submenu.toggles.state, state); + assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3))); +} + +#[test] +fn submenu_slider_on_right() { + let mut submenu = initialize_submenu(SubMenuType::Slider); + let mut state = SliderState::LowerHover; + assert_eq!(submenu.slider.unwrap().state, state); + state = SliderState::UpperHover; + submenu.on_right(); + assert_eq!(submenu.slider.unwrap().state, state); + + submenu.slider = Some(StatefulSlider { + state: SliderState::LowerSelected, + lower: 10, + upper: 11, + ..submenu.slider.unwrap() + }); + state = SliderState::LowerSelected; + submenu.on_right(); + assert_eq!(submenu.slider.unwrap().state, state); + assert_eq!(submenu.slider.unwrap().lower, 11); + assert_eq!(submenu.slider.unwrap().upper, 11); + submenu.on_right(); + assert_eq!(submenu.slider.unwrap().state, state); + assert_eq!(submenu.slider.unwrap().lower, 11); + assert_eq!(submenu.slider.unwrap().upper, 11); + + submenu.slider = Some(StatefulSlider { + state: SliderState::UpperSelected, + lower: 100, + upper: 149, + ..submenu.slider.unwrap() + }); + state = SliderState::UpperSelected; + submenu.on_right(); + assert_eq!(submenu.slider.unwrap().state, state); + assert_eq!(submenu.slider.unwrap().lower, 100); + assert_eq!(submenu.slider.unwrap().upper, 150); + submenu.on_right(); + assert_eq!(submenu.slider.unwrap().state, state); + assert_eq!(submenu.slider.unwrap().lower, 100); + assert_eq!(submenu.slider.unwrap().upper, 150); +} diff --git a/training_mod_tui/tests/test_toggle.rs b/training_mod_tui/tests/test_toggle.rs new file mode 100644 index 0000000..adb07f8 --- /dev/null +++ b/training_mod_tui/tests/test_toggle.rs @@ -0,0 +1,44 @@ +use training_mod_tui_2::Toggle; + +#[test] +fn toggle_serialize() { + let t = Toggle { + title: "Title", + value: 5, + max: 10, + }; + let json = serde_json::to_string(&t).unwrap(); + assert_eq!(json, "5"); +} + +#[test] +fn toggle_increment() { + let mut t = Toggle { + title: "Title", + value: 5, + max: 10, + }; + t.increment(); + assert_eq!(t.value, 6); + t.value = 9; + t.increment(); + assert_eq!(t.value, 10); + t.increment(); + assert_eq!(t.value, 0); +} + +#[test] +fn toggle_decrement() { + let mut t = Toggle { + title: "Title", + value: 5, + max: 10, + }; + t.decrement(); + assert_eq!(t.value, 4); + t.value = 1; + t.decrement(); + assert_eq!(t.value, 0); + t.decrement(); + assert_eq!(t.value, 10); +} diff --git a/training_mod_tui/training_mod_tui.iml b/training_mod_tui/training_mod_tui.iml deleted file mode 100644 index 2fecef3..0000000 --- a/training_mod_tui/training_mod_tui.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file