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<Buttons> { + // Reimplemented for bitflags + let mut vec = Vec::<Buttons>::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::<MenuJsonStruct>(&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::<BufReader<_>, 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<training_mod_tui::App> = 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<training_mod_tui::App<'static>> = Mutex::new({ + info!("Initialized lazy_static: QUICK_MENU_APP"); + unsafe { create_app() } + }); pub static ref P1_CONTROLLER_STYLE: Mutex<ControllerStyle> = Mutex::new(ControllerStyle::default()); static ref DIRECTION_HOLD_FRAMES: Mutex<HashMap<DirectionButton, u32>> = { @@ -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<String> = 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<String> = 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<L2CValue> = (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 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 delta 22497 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 OJ@^2#?FC!ySxNu~CVHm; diff --git a/src/training/buff.rs b/src/training/buff.rs index 55e2277..d5f04e3 100644 --- a/src/training/buff.rs +++ b/src/training/buff.rs @@ -10,8 +10,8 @@ use crate::training::handle_add_limit; use once_cell::sync::Lazy; -static mut BUFF_REMAINING_PLAYER: i32 = 0; -static mut BUFF_REMAINING_CPU: i32 = 0; +static mut BUFF_REMAINING_PLAYER: usize = 0; +static mut BUFF_REMAINING_CPU: usize = 0; static mut IS_BUFFING_PLAYER: bool = false; static mut IS_BUFFING_CPU: bool = false; @@ -46,7 +46,10 @@ pub unsafe fn is_buffing_any() -> 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<BuffOption> = 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<usize> = - Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset)); -pub static OVERALL_FRAME_COUNTER: Lazy<usize> = - Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset)); - -pub const NUM_LOGS: usize = 15; -pub static mut DRAW_LOG_BASE_IDX: Lazy<Mutex<usize>> = 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::<VecDeque<(&str, ResColor)>>() - } - - 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<T>(array: &mut [T], value: T, index: usize) { - array[index..].rotate_right(1); - array[index] = value; -} - -fn insert_in_front<T>(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<usize> = + Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset)); +pub static OVERALL_FRAME_COUNTER: Lazy<usize> = + Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset)); + +pub const NUM_LOGS: usize = 15; +pub static mut DRAW_LOG_BASE_IDX: Lazy<Mutex<usize>> = 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::<VecDeque<(&str, ResColor)>>() + } + + 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<T>(array: &mut [T], value: T, index: usize) { + array[index..].rotate_right(1); + array[index] = value; +} + +fn insert_in_front<T>(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<usize>) -> 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<Action> { unsafe { let mut override_action: Option<Action> = 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<SaveStateSlots> = Mutex::new(load_from_file()); + static ref SAVE_STATE_SLOTS : Mutex<SaveStateSlots> = 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<f32> { 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<Toggle>, - pub slider: Option<Slider>, - 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<T: ToggleTrait>( - 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<S: SliderTrait>( - 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<SubMenu>, -} - -impl Tab { - pub fn add_submenu_with_toggles<T: ToggleTrait>( - &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::<T>( - submenu_title.to_string(), - submenu_id.to_string(), - help_text.to_string(), - is_single_option, - initial_value, - )); - } - - pub fn add_submenu_with_slider<S: SliderTrait>( - &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::<S>( - 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<Tab>, -} - -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<SubMenu> = 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::<OnOff>( - "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::<ButtonConfig>( - "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::<ButtonConfig>( - "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::<ButtonConfig>( - "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::<ButtonConfig>( - "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::<Action>( - "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::<Action>( - "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::<MashTrigger>( - "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::<AttackAngle>( - "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::<ThrowOption>( - "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::<MedDelay>( - "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::<MedDelay>( - "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::<BoolFlag>( - "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::<BoolFlag>( - "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::<Delay>( - "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::<BoolFlag>( - "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::<Delay>( - "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::<Delay>( - "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::<Delay>( - "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<SubMenu> = 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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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::<Action>( - "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<SubMenu> = 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::<Direction>( - "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::<Direction>( - "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::<Direction>( - "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::<SdiFrequency>( - "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::<ClatterFrequency>( - "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::<LedgeOption>( - "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::<LongDelay>( - "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::<TechFlags>( - "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::<MissTechFlags>( - "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>( - "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::<Direction>( - "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::<OnOff>( - "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::<OnOff>( - "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::<OnOff>( - "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<SubMenu> = 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<SubMenu> = 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<SubMenu> = 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::<SaveStateMirroring>( - "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::<OnOff>( - "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::<SaveDamage>( - "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::<DamagePercent>( - "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::<SaveDamage>( - "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::<DamagePercent>( - "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::<OnOff>( - "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::<SaveStateSlot>( - "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::<SaveStateSlot>( - "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::<CharacterItem>( - "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::<BuffOption>( - "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::<PlaybackSlot>( - "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<SubMenu> = 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::<OnOff>( - "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::<OnOff>( - "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::<InputDisplay>( - "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::<OnOff>( - "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::<Delay>( - "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::<OnOff>( - "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::<OnOff>( - "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::<UpdatePolicy>( - "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::<RecordSlot>( - "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::<RecordTrigger>( - "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::<RecordingDuration>( - "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::<OnOff>( - "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::<PlaybackSlot>( - "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::<HitstunPlayback>( - "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::<OnOff>( - "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::<OnOff>( - "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<T>() -> u32 { - (std::mem::size_of::<T>() * 8) as u32 -} - -fn log_2(x: u32) -> u32 { - if x == 0 { - 0 - } else { - num_bits::<u32>() - 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 [<to_submenu_ $id>]<'a>() -> SubMenu<'a> { + let submenu_type = if $single { SubMenuType::ToggleSingle } else { SubMenuType::ToggleMultiple }; + let value = 0; + let max: u8 = $max; + let toggles_vec: Vec<Toggle> = <$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<u32>; - fn to_toggle_strings() -> Vec<String>; -} - -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 [<to_submenu_ $id>]<'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::<String>() - } - } - impl ToggleTrait for $e { - fn to_toggle_vals() -> Vec<u32> { - let all_options = <$e>::all().to_vec(); - all_options.iter().map(|i| i.bits() as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - let all_options = <$e>::all().to_vec(); - all_options.iter().map(|i| i.to_string()).collect() } } } @@ -127,19 +97,19 @@ pub fn random_option<T>(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<f64> { 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<u32> { - Shield::iter().map(|i| i as u32).collect() +byteflags! { + pub struct OnOff { + pub ON = "On", + pub OFF = "Off", } - fn to_toggle_strings() -> Vec<String> { - 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<u32> { - SaveStateMirroring::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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<Self> { 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<u32> { - vec![0, 1] - } - fn to_toggle_strings() -> Vec<String> { - 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<i32> { - #[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<i32> { + #[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<u32> { - SdiFrequency::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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<u32> { - ClatterFrequency::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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<u32> { - CharacterItem::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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<usize> { - 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<usize> { + 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<usize> { + 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<u32> { - RecordSlot::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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<usize> { - 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<usize> { + 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<u32> { - HitstunPlayback::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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<u32> { - RecordingDuration::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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<u32> { - UpdatePolicy::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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<u32> { - InputDisplay::iter().map(|i| i as u32).collect() - } - - fn to_toggle_strings() -> Vec<String> { - 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 <StatefulTable<Tab>> +// │ +// └─ Tab <StatefulTable<Submenu>> +// │ +// └─ Submenu <Struct> +// │ +// ├─ StatefulTable<Toggle> +// │ +// │ OR +// │ +// └─ Option<Slider> + +pub struct App<'a> { + pub tabs: StatefulList<Tab<'a>>, + 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<String, Vec<u8>> = + 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<String, Vec<u8>> = + 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + 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<Toggle<'a>>, + pub slider: Option<StatefulSlider>, +} + +impl<'a> Serialize for SubMenu<'a> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + 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<u8>) { + 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<SubMenu<'a>>, +} + +impl<'a> Tab<'a> { + pub fn len(&self) -> usize { + self.submenus.len() + } +} + +impl<'a> Serialize for Tab<'a> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + 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<String>, - pub menu_items: HashMap<String, MultiStatefulList<SubMenu>>, - pub selected_sub_menu_toggles: MultiStatefulList<Toggle>, - 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<Toggle<'a>>, - /// slider: Option<Slider>, - /// _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::<TrainingModpackMenu>(&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::<serde_json::Value>(&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::<serde_json::Value>(&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::<TrainingModpackMenu>(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<u32> = 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<String> { - 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::<Vec<String>>(); - } -} - -fn render_submenu_page<B: Backend>( - f: &mut Frame<B>, - app: &mut App, - list_chunks: Vec<Rect>, - 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<ListItem> = 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<B: Backend>( - f: &mut Frame<B>, - app: &mut App, - list_chunks: Vec<Rect>, - 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<ListItem> = 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<B: Backend>( - f: &mut Frame<B>, - 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<B: Backend>(f: &mut Frame<B>, 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<Spans> = 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<Spans> = 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<Rect> = 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::<Vec<Constraint>>() - .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<T> { - pub lists: Vec<StatefulList<T>>, - pub state: usize, - pub total_len: usize, -} - -impl<T: Clone> MultiStatefulList<T> { - 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<T>, num_lists: usize) -> MultiStatefulList<T> { - 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<T> { - pub state: ListState, - pub items: Vec<T>, -} - -impl<T> StatefulList<T> { - pub fn with_items(items: Vec<T>) -> StatefulList<T> { - 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::TestBackend>, - training_mod_tui::App, - ), - Box<dyn Error>, -> { - 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<dyn Error>> { - 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::<MenuJsonStruct>(&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<dyn Error>> { - 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::<MenuJsonStruct>(&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<dyn Error>> { - 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::<MenuJsonStruct>(&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::<MenuJsonStruct>(&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::<MenuJsonStruct>(&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<training_mod_tui::TestBackend>, - mut app: training_mod_tui::App, -) -> Result<String, Box<dyn Error>> { - 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<dyn Error>> { - 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<dyn Error>> { - let args: Vec<String> = 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::<MenuJsonStruct>(&res.as_ref().unwrap()).unwrap(); - println!("menu: {:#?}", menu); - } - } - } - - Ok(()) -} - -#[cfg(feature = "has_terminal")] -fn run_app<B: tui::backend::Backend>( - terminal: &mut Terminal<B>, - mut app: training_mod_tui::App, - tick_rate: Duration, -) -> io::Result<String> { - 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<T: Serialize> { + pub state: ListState, + pub items: Vec<T>, +} + +impl<T: Serialize> IntoIterator for StatefulList<T> { + type Item = T; + type IntoIter = std::vec::IntoIter<Self::Item>; + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} + +impl<T: Serialize> StatefulList<T> { + pub fn new() -> StatefulList<T> { + StatefulList { + state: ListState::default(), + items: Vec::new(), + } + } + + pub fn with_items(items: Vec<T>) -> StatefulList<T> { + 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<T: Serialize> Serialize for StatefulList<T> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + 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<T: Serialize> StatefulList<T> { + pub fn iter(&self) -> impl Iterator<Item = &T> + '_ { + self.items.iter() + } + pub fn iter_mut(&mut self) -> std::slice::IterMut<T> { + 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + 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<T: Clone + Serialize> { + pub state: TableState, + pub items: Vec<Vec<Option<T>>>, + pub rows: usize, + pub cols: usize, +} + +// Size-related functions +impl<T: Clone + Serialize> StatefulTable<T> { + pub fn len(&self) -> usize { + self.items + .iter() + .map(|row| { + row.iter() + .map(|item| if item.is_some() { 1 } else { 0 }) + .sum::<usize>() + }) + .sum() + } + pub fn full_len(&self) -> usize { + self.rows * self.cols + } + pub fn as_vec(&self) -> Vec<T> { + 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<T: Clone + Serialize> StatefulTable<T> { + 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<T>) -> 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<T: Clone + Serialize> StatefulTable<T> { + 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<T: Clone + Serialize> Serialize for StatefulTable<T> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let flat: Vec<T> = self.as_vec(); + flat.serialize(serializer) + } +} + +// Implement .iter() for StatefulTable +pub struct StatefulTableIterator<'a, T: Clone + Serialize> { + stateful_table: &'a StatefulTable<T>, + index: usize, +} + +impl<'a, T: Clone + Serialize> Iterator for StatefulTableIterator<'a, T> { + type Item = &'a T; + fn next(&mut self) -> Option<Self::Item> { + self.index += 1; + self.stateful_table.get_by_idx(self.index - 1) + } +} + +impl<T: Clone + Serialize> StatefulTable<T> { + pub fn iter(&self) -> StatefulTableIterator<T> { + StatefulTableIterator { + stateful_table: self, + index: 0, + } + } +} + +pub struct StatefulTableIteratorMut<'a, T: Clone + Serialize> { + inner: std::iter::Flatten<std::slice::IterMut<'a, Vec<Option<T>>>>, +} + +impl<'a, T: Clone + Serialize> Iterator for StatefulTableIteratorMut<'a, T> { + type Item = &'a mut Option<T>; + fn next(&mut self) -> Option<Self::Item> { + self.inner.next() + } +} + +impl<'a, T: Clone + Serialize + 'a> StatefulTable<T> { + pub fn iter_mut(&'a mut self) -> StatefulTableIteratorMut<T> { + 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<usize>) -> StatefulList<u8> { + StatefulList { + state: initialize_state(selected), + items: vec![10, 20, 30, 40], + } +} + +fn initialize_state(selected: Option<usize>) -> 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::<u8>::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<u8> { + 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<u8> = vec![0, 1, 2, 3, 4]; + let t: StatefulTable<u8> = 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<u8> = StatefulTable::new(2, 3); + let u: StatefulTable<u8> = StatefulTable::with_items(2, 3, vec![]); + let v: StatefulTable<u8> = 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<u8> = StatefulTable::with_items(2, 3, vec![1, 2]); + let u: StatefulTable<u8> = 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<Toggle<'a>> { + // [ (0) 1 2 ] + // [ 3 ] + let v: Vec<Toggle> = (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<Toggle<'a>> { + // [ (1) 0 0 ] + // [ 0 ] + let v: Vec<Toggle> = (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 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module type="RUST_MODULE" version="4"> - <component name="NewModuleRootManager" inherit-compiler-output="true"> - <exclude-output /> - <content url="file://$MODULE_DIR$"> - <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> - <excludeFolder url="file://$MODULE_DIR$/target" /> - </content> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - </component> -</module> \ No newline at end of file