From d5c0d636a0a1117b78ef4deec3bda5601fd8d45c Mon Sep 17 00:00:00 2001 From: jugeeya Date: Thu, 2 Feb 2023 15:01:11 -0800 Subject: [PATCH] UI Code Refactor; Notifications; Save Defaults for Quick Menu (#461) * Initial refactor * Full refactor * Depend only on pane creator flags * Small refactor * Small refactors; notification support * Don't push event for every quick menu change * Backend for defaults almost done * Run tests on CI * Finish save + reset defaults without confirmation * Added slider menu UI --------- Co-authored-by: xhudaman --- .github/workflows/rust.yml | 5 +- src/common/menu.rs | 88 +- src/lib.rs | 7 +- src/training/character_specific/items.rs | 1 + src/training/combo.rs | 15 + src/training/mod.rs | 3 +- src/training/ui/damage.rs | 146 +++ src/training/ui/display.rs | 147 +++ src/training/ui/menu.rs | 973 +++++++++++++++++ src/training/ui/mod.rs | 168 +++ src/training/ui/notifications.rs | 69 ++ src/training/ui_hacks.rs | 1206 ---------------------- training_mod_consts/src/lib.rs | 92 +- training_mod_tui/src/lib.rs | 499 +++++---- training_mod_tui/src/main.rs | 193 +++- 15 files changed, 2077 insertions(+), 1535 deletions(-) create mode 100644 src/training/ui/damage.rs create mode 100644 src/training/ui/display.rs create mode 100644 src/training/ui/menu.rs create mode 100644 src/training/ui/mod.rs create mode 100644 src/training/ui/notifications.rs delete mode 100644 src/training/ui_hacks.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2371f48..7bfe276 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,7 +9,7 @@ on: jobs: checker: - name: Check, Clippy + name: Check, Clippy, Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -29,6 +29,9 @@ jobs: run: cargo +nightly check --target=x86_64-unknown-linux-gnu - name: Clippy run: cargo +nightly clippy --all-targets --all-features --target=x86_64-unknown-linux-gnu + - name: TUI Test + working-directory: training_mod_tui + run: cargo +nightly test plugin: name: Plugin NRO runs-on: ubuntu-latest diff --git a/src/common/menu.rs b/src/common/menu.rs index b66eb5e..fe7b437 100644 --- a/src/common/menu.rs +++ b/src/common/menu.rs @@ -10,7 +10,7 @@ use skyline::nn::web::WebSessionBootMode; use skyline_web::{Background, BootDisplay, WebSession, Webpage}; use std::fs; use std::path::Path; -use training_mod_consts::{MenuJsonStruct, TrainingModpackMenu}; +use training_mod_consts::MenuJsonStruct; static mut FRAME_COUNTER_INDEX: usize = 0; pub static mut QUICK_MENU_FRAME_COUNTER_INDEX: usize = 0; @@ -48,7 +48,7 @@ pub unsafe fn menu_condition(module_accessor: &mut smash::app::BattleObjectModul pub unsafe fn write_web_menu_file() { let tpl = Template::new(include_str!("../templates/menu.html")).unwrap(); - let overall_menu = get_menu(); + let overall_menu = ui_menu(MENU); let data = tpl.render(&overall_menu); @@ -67,7 +67,6 @@ const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.json"; pub unsafe fn set_menu_from_json(message: &str) { let web_response = serde_json::from_str::(message); - let tui_response = serde_json::from_str::(message); info!("Received menu message: {message}"); if let Ok(message_json) = web_response { // Includes both MENU and DEFAULTS_MENU @@ -78,18 +77,7 @@ pub unsafe fn set_menu_from_json(message: &str) { MENU_CONF_PATH, serde_json::to_string_pretty(&message_json).unwrap(), ) - .expect("Failed to write menu settings file from web response"); - } else if let Ok(message_json) = tui_response { - // Only includes MENU - // From TUI - MENU = message_json; - - let conf = MenuJsonStruct { - menu: MENU, - defaults_menu: DEFAULTS_MENU, - }; - std::fs::write(MENU_CONF_PATH, serde_json::to_string_pretty(&conf).unwrap()) - .expect("Failed to write menu settings file from quick menu response"); + .expect("Failed to write menu settings file"); } else { skyline::error::show_error( 0x70, @@ -105,7 +93,6 @@ pub unsafe fn set_menu_from_json(message: &str) { ); MENU.quick_menu = OnOff::On; } - EVENT_QUEUE.push(Event::menu_open(message.to_string())); } pub fn spawn_menu() { @@ -127,7 +114,9 @@ pub fn spawn_menu() { } } else { let mut app = QUICK_MENU_APP.lock(); - *app = training_mod_tui::App::new(get_menu()); + *app = training_mod_tui::App::new( + ui_menu(MENU), + (ui_menu(DEFAULTS_MENU), serde_json::to_string(&DEFAULTS_MENU).unwrap())); drop(app); QUICK_MENU_ACTIVE = true; } @@ -137,6 +126,9 @@ pub fn spawn_menu() { pub struct ButtonPresses { pub a: ButtonPress, pub b: ButtonPress, + pub x: ButtonPress, + pub r: ButtonPress, + pub l: ButtonPress, pub zr: ButtonPress, pub zl: ButtonPress, pub left: ButtonPress, @@ -183,6 +175,21 @@ pub static mut BUTTON_PRESSES: ButtonPresses = ButtonPresses { is_pressed: false, lockout_frames: 0, }, + x: ButtonPress { + prev_frame_is_pressed: false, + is_pressed: false, + lockout_frames: 0, + }, + r: ButtonPress { + prev_frame_is_pressed: false, + is_pressed: false, + lockout_frames: 0, + }, + l: ButtonPress { + prev_frame_is_pressed: false, + is_pressed: false, + lockout_frames: 0, + }, zr: ButtonPress { prev_frame_is_pressed: false, is_pressed: false, @@ -240,6 +247,15 @@ pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32 if (*state).Buttons & (1 << 1) > 0 { BUTTON_PRESSES.b.is_pressed = true; } + if (*state).Buttons & (1 << 2) > 0 { + BUTTON_PRESSES.x.is_pressed = true; + } + if (*state).Buttons & (1 << 6) > 0 { + BUTTON_PRESSES.l.is_pressed = true; + } + if (*state).Buttons & (1 << 7) > 0 { + BUTTON_PRESSES.r.is_pressed = true; + } if (*state).Buttons & (1 << 8) > 0 { BUTTON_PRESSES.zl.is_pressed = true; } @@ -270,18 +286,20 @@ pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32 use lazy_static::lazy_static; use parking_lot::Mutex; +use training_mod_tui::AppPage; lazy_static! { pub static ref QUICK_MENU_APP: Mutex> = - Mutex::new(training_mod_tui::App::new(unsafe { get_menu() })); + Mutex::new(training_mod_tui::App::new( + unsafe { ui_menu(MENU) }, + unsafe { (ui_menu(DEFAULTS_MENU), serde_json::to_string(&DEFAULTS_MENU).unwrap())} + ) + ); } pub unsafe fn quick_menu_loop() { loop { std::thread::sleep(std::time::Duration::from_secs(10)); - let backend = training_mod_tui::TestBackend::new(75, 15); - let mut terminal = training_mod_tui::Terminal::new(backend).unwrap(); - let mut json_response = String::new(); let button_presses = &mut BUTTON_PRESSES; let mut received_input = true; loop { @@ -299,24 +317,37 @@ pub unsafe fn quick_menu_loop() { let b_press = &mut button_presses.b; b_press.read_press().then(|| { received_input = true; - if !app.outer_list { + if app.page != AppPage::SUBMENU { app.on_b() } else if frame_counter::get_frame_count(QUICK_MENU_FRAME_COUNTER_INDEX) == 0 - && !json_response.is_empty() { // Leave menu. QUICK_MENU_ACTIVE = false; - set_menu_from_json(&json_response); + let menu_json = app.get_menu_selections(); + set_menu_from_json(&menu_json); + EVENT_QUEUE.push(Event::menu_open(menu_json.to_string())); } }); - button_presses.zl.read_press().then(|| { + button_presses.x.read_press().then(|| { + app.on_x(); + received_input = true; + }); + button_presses.l.read_press().then(|| { app.on_l(); received_input = true; }); - button_presses.zr.read_press().then(|| { + button_presses.r.read_press().then(|| { app.on_r(); received_input = true; }); + button_presses.zl.read_press().then(|| { + app.on_zl(); + received_input = true; + }); + button_presses.zr.read_press().then(|| { + app.on_zr(); + received_input = true; + }); button_presses.left.read_press().then(|| { app.on_left(); received_input = true; @@ -335,10 +366,8 @@ pub unsafe fn quick_menu_loop() { }); if received_input { - terminal - .draw(|f| json_response = training_mod_tui::ui(f, app)) - .unwrap(); received_input = false; + set_menu_from_json(&app.get_menu_selections()); } } } @@ -360,6 +389,7 @@ unsafe fn spawn_web_session(session: WebSession) { session.exit(); session.wait_for_exit(); set_menu_from_json(&message_recv); + EVENT_QUEUE.push(Event::menu_open(message_recv.to_string())); } unsafe fn new_web_session(hidden: bool) -> WebSession { diff --git a/src/lib.rs b/src/lib.rs index 867410b..12421c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ use crate::menu::quick_menu_loop; #[cfg(feature = "web_session_preload")] use crate::menu::web_session_loop; use training_mod_consts::{MenuJsonStruct, OnOff}; +use crate::training::ui::notifications::notification; fn nro_main(nro: &NroInfo<'_>) { if nro.module.isLoaded { @@ -81,10 +82,12 @@ pub fn main() { info!("Initialized."); unsafe { EVENT_QUEUE.push(Event::smash_open()); + notification("Training Modpack", "Welcome!", 60); + notification("Open Menu", "Special + Uptaunt", 120); + notification("Save State", "Grab + Downtaunt", 120); + notification("Load State", "Grab + Uptaunt", 120); } - training::ui_hacks::install_hooks(); - hitbox_visualizer::hitbox_visualization(); hazard_manager::hazard_manager(); training::training_mods(); diff --git a/src/training/character_specific/items.rs b/src/training/character_specific/items.rs index 72e0b28..45db648 100644 --- a/src/training/character_specific/items.rs +++ b/src/training/character_specific/items.rs @@ -509,6 +509,7 @@ daikon_replace!(DAISY, daisy, 1); // GenerateArticleForTarget for Peach/Diddy(/Link?) item creation static GAFT_OFFSET: usize = 0x03d40a0; + #[skyline::hook(offset = GAFT_OFFSET)] pub unsafe fn handle_generate_article_for_target( article_module_accessor: *mut app::BattleObjectModuleAccessor, diff --git a/src/training/combo.rs b/src/training/combo.rs index 934e008..8a63aea 100644 --- a/src/training/combo.rs +++ b/src/training/combo.rs @@ -1,8 +1,10 @@ +use skyline::nn::ui2d::ResColor; use crate::common::consts::FighterId; use crate::common::*; use crate::training::*; pub static mut FRAME_ADVANTAGE: i32 = 0; +static mut FRAME_ADVANTAGE_STR: String = String::new(); static mut PLAYER_ACTIONABLE: bool = false; static mut CPU_ACTIONABLE: bool = false; static mut PLAYER_ACTIVE_FRAME: u32 = 0; @@ -47,6 +49,19 @@ unsafe fn is_actionable(module_accessor: *mut app::BattleObjectModuleAccessor) - fn update_frame_advantage(new_frame_adv: i32) { unsafe { FRAME_ADVANTAGE = new_frame_adv; + FRAME_ADVANTAGE_STR = String::new(); + FRAME_ADVANTAGE_STR.push_str(&format!("{}", FRAME_ADVANTAGE)); + ui::notifications::clear_notifications("Frame Advantage"); + ui::notifications::color_notification( + "Frame Advantage", + &FRAME_ADVANTAGE_STR, + 60, + match FRAME_ADVANTAGE { + x if x < 0 => ResColor{r: 200, g: 8, b: 8, a: 255}, + x if x == 0 => ResColor{r: 0, g: 0, b: 0, a: 255}, + _ => ResColor{r: 31, g: 198, b: 0, a: 255}, + } + ); } } diff --git a/src/training/mod.rs b/src/training/mod.rs index 6f59115..b9a2480 100644 --- a/src/training/mod.rs +++ b/src/training/mod.rs @@ -24,7 +24,7 @@ pub mod sdi; pub mod shield; pub mod tech; pub mod throw; -pub mod ui_hacks; +pub mod ui; mod air_dodge_direction; mod attack_angle; @@ -570,4 +570,5 @@ pub fn training_mods() { buff::init(); items::init(); tech::init(); + ui::init(); } diff --git a/src/training/ui/damage.rs b/src/training/ui/damage.rs new file mode 100644 index 0000000..e20e60e --- /dev/null +++ b/src/training/ui/damage.rs @@ -0,0 +1,146 @@ +use crate::common::{get_player_dmg_digits, is_ready_go, is_training_mode}; +use crate::consts::FighterId; +use skyline::nn::ui2d::*; +use smash::ui2d::SmashPane; + +pub unsafe fn iterate_anim_list( + anim_transform_node: &mut AnimTransformNode, + layout_name: Option<&str>, +) { + let mut curr = anim_transform_node as *mut AnimTransformNode; + let mut _anim_idx = 0; + while !curr.is_null() { + // Only if valid + if curr != (*curr).next { + let anim_transform = (curr as *mut u64).add(2) as *mut AnimTransform; + + parse_anim_transform(anim_transform.as_mut().unwrap(), layout_name); + } + + curr = (*curr).next; + _anim_idx += 1; + if curr == anim_transform_node as *mut AnimTransformNode || curr == (*curr).next { + break; + } + } +} + +pub unsafe fn parse_anim_transform(anim_transform: &mut AnimTransform, layout_name: Option<&str>) { + let res_animation_block_data_start = anim_transform.res_animation_block as u64; + let res_animation_block = &*anim_transform.res_animation_block; + let mut anim_cont_offsets = (res_animation_block_data_start + + res_animation_block.anim_cont_offsets_offset as u64) + as *const u32; + for _anim_cont_idx in 0..res_animation_block.anim_cont_count { + let anim_cont_offset = *anim_cont_offsets; + let res_animation_cont = (res_animation_block_data_start + anim_cont_offset as u64) + as *const ResAnimationContent; + + let name = skyline::try_from_c_str((*res_animation_cont).name.as_ptr()) + .unwrap_or("UNKNOWN".to_string()); + let anim_type = (*res_animation_cont).anim_content_type; + + // AnimContentType 1 == MATERIAL + if name.starts_with("set_dmg_num") && anim_type == 1 { + if let Some(layout_name) = layout_name { + let (hundreds, tens, ones, dec) = get_player_dmg_digits(match layout_name { + "p1" => FighterId::Player, + "p2" => FighterId::CPU, + _ => panic!("Unknown layout name: {}", layout_name), + }); + + if name == "set_dmg_num_3" { + anim_transform.frame = hundreds as f32; + } + if name == "set_dmg_num_2" { + anim_transform.frame = tens as f32; + } + if name == "set_dmg_num_1" { + anim_transform.frame = ones as f32; + } + if name == "set_dmg_num_dec" { + anim_transform.frame = dec as f32; + } + } + } + + anim_cont_offsets = anim_cont_offsets.add(1); + } +} + +pub unsafe fn draw(root_pane: &mut Pane, layout_name: &str) { + // Update percentage display as soon as possible on death + if is_training_mode() && is_ready_go() && layout_name == "info_melee" { + for player_name in &["p1", "p2"] { + if let Some(parent) = root_pane.find_pane_by_name_recursive(player_name) { + let _p1_layout_name = skyline::from_c_str((*parent.as_parts().layout).layout_name); + let anim_list = &mut (*parent.as_parts().layout).anim_trans_list; + + let mut has_altered_anim_list = false; + let (hundreds, tens, _, _) = get_player_dmg_digits(match *player_name { + "p1" => FighterId::Player, + "p2" => FighterId::CPU, + _ => panic!("Unknown player name: {}", player_name), + }); + + for dmg_num_s in &[ + "set_dmg_num_3", + "dig_3", + "dig_3_anim", + "set_dmg_num_2", + "dig_2", + "dig_2_anim", + "set_dmg_num_1", + "dig_1", + "dig_1_anim", + "set_dmg_num_p", + "dig_dec", + "dig_dec_anim_00", + "set_dmg_num_dec", + "dig_dec_anim_01", + "dig_0_anim", + "set_dmg_p", + ] { + if let Some(dmg_num) = parent.find_pane_by_name_recursive(dmg_num_s) { + if (dmg_num_s.contains('3') && hundreds == 0) + || (dmg_num_s.contains('2') && hundreds == 0 && tens == 0) + { + continue; + } + + if *dmg_num_s == "set_dmg_p" { + dmg_num.pos_y = 0.0; + } else if *dmg_num_s == "set_dmg_num_p" { + dmg_num.pos_y = -4.0; + } else if *dmg_num_s == "dig_dec" { + dmg_num.pos_y = -16.0; + } else { + dmg_num.pos_y = 0.0; + } + + if dmg_num.alpha != 255 || dmg_num.global_alpha != 255 { + dmg_num.set_visible(true); + if !has_altered_anim_list { + iterate_anim_list(anim_list, Some(player_name)); + has_altered_anim_list = true; + } + } + } + } + + for death_explosion_s in &[ + "set_fxui_dead1", + "set_fxui_dead2", + "set_fxui_dead3", + "set_fxui_fire", + ] { + if let Some(death_explosion) = + parent.find_pane_by_name_recursive(death_explosion_s) + { + death_explosion.set_visible(false); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/training/ui/display.rs b/src/training/ui/display.rs new file mode 100644 index 0000000..4650dcc --- /dev/null +++ b/src/training/ui/display.rs @@ -0,0 +1,147 @@ +use crate::training::ui; +use skyline::nn::ui2d::*; +use smash::ui2d::{SmashPane, SmashTextBox}; + +pub static NUM_DISPLAY_PANES: usize = 1; + +macro_rules! display_parent_fmt { + ($x:ident) => { + format!("trMod_disp_{}", $x).as_str() + }; +} + +macro_rules! display_pic_fmt { + ($x:ident) => { + format!("trMod_disp_{}_base", $x).as_str() + }; +} + +macro_rules! display_header_fmt { + ($x:ident) => { + format!("trMod_disp_{}_header", $x).as_str() + }; +} + +macro_rules! display_txt_fmt { + ($x:ident) => { + format!("trMod_disp_{}_txt", $x).as_str() + }; +} + +pub unsafe fn draw(root_pane: &mut Pane) { + let notification_idx = 0; + + let queue = &mut ui::notifications::QUEUE; + let notification = queue.first_mut(); + + if let Some(parent) = root_pane.find_pane_by_name_recursive(display_parent_fmt!(notification_idx)) { + parent.set_visible(notification.is_some()); + if notification.is_none() { + return; + } + } + + let notification = notification.unwrap(); + let header_txt = notification.header(); + let message = notification.message(); + let color = notification.color(); + let has_completed = notification.tick(); + if has_completed { + queue.remove(0); + } + + if let Some(header) = root_pane.find_pane_by_name_recursive(display_header_fmt!(notification_idx)) { + header.as_textbox().set_text_string(header_txt); + } + + if let Some(text) = root_pane.find_pane_by_name_recursive(display_txt_fmt!(notification_idx)) { + let text = text.as_textbox(); + text.set_text_string(message); + text.set_color(color.r, color.g, color.b, color.a); + } +} + + +pub static BUILD_PIC_BASE: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + (0..NUM_DISPLAY_PANES).for_each(|idx| { + let block = block as *mut ResPictureWithTex<1>; + let mut pic_block = *block; + pic_block.set_name(display_pic_fmt!(idx)); + pic_block.set_pos(ResVec3::default()); + let pic_pane = build!(pic_block, ResPictureWithTex<1>, kind, Picture); + pic_pane.detach(); + + // pic is loaded first, we can create our parent pane here. + let disp_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']); + let mut disp_pane_block = ResPane::new(display_parent_fmt!(idx)); + disp_pane_block.set_pos(ResVec3::new(806.0, -50.0 - (idx as f32 * 110.0), 0.0)); + let disp_pane = build!(disp_pane_block, ResPane, disp_pane_kind, Pane); + disp_pane.detach(); + root_pane.append_child(disp_pane); + disp_pane.append_child(pic_pane); + }); +}; + +pub static BUILD_PANE_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + (0..NUM_DISPLAY_PANES).for_each(|idx| { + let disp_pane = root_pane + .find_pane_by_name(display_parent_fmt!(idx), true) + .unwrap(); + + let block = block as *mut ResTextBox; + let mut text_block = *block; + text_block.set_name(display_txt_fmt!(idx)); + text_block.set_pos(ResVec3::new(-10.0, -25.0, 0.0)); + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane.set_text_string(format!("Pane {idx}!").as_str()); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + text_pane.set_default_material_colors(); + text_pane.detach(); + disp_pane.append_child(text_pane); + }); +}; + +pub static BUILD_HEADER_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + (0..NUM_DISPLAY_PANES).for_each(|idx| { + let disp_pane = root_pane + .find_pane_by_name(display_parent_fmt!(idx), true) + .unwrap(); + + let block = block as *mut ResTextBox; + let mut header_block = *block; + header_block.set_name(display_header_fmt!(idx)); + header_block.set_pos(ResVec3::new(0.0, 25.0, 0.0)); + let header_pane = build!(header_block, ResTextBox, kind, TextBox); + header_pane.set_text_string(format!("Header {idx}").as_str()); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + header_pane.set_default_material_colors(); + // Header should be white text + header_pane.set_color(255, 255, 255, 255); + header_pane.detach(); + disp_pane.append_child(header_pane); + }); +}; \ No newline at end of file diff --git a/src/training/ui/menu.rs b/src/training/ui/menu.rs new file mode 100644 index 0000000..b5d2380 --- /dev/null +++ b/src/training/ui/menu.rs @@ -0,0 +1,973 @@ +use crate::{common::menu::QUICK_MENU_ACTIVE}; +use skyline::nn::ui2d::*; +use smash::ui2d::{SmashPane, SmashTextBox}; +use training_mod_tui::AppPage; +use training_mod_tui::gauge::GaugeState; +use crate::training::ui; + +pub static NUM_MENU_TEXT_OPTIONS: usize = 27; +pub static NUM_MENU_TEXT_SLIDERS: usize = 2; +pub static NUM_MENU_TABS: usize = 3; + +pub static mut HAS_SORTED_MENU_CHILDREN: bool = false; + +const BG_LEFT_ON_WHITE_COLOR: ResColor = ResColor { + r: 0, + g: 28, + b: 118, + a: 255, +}; + +const BG_LEFT_ON_BLACK_COLOR: ResColor = ResColor { + r: 0, + g: 22, + b: 112, + a: 0, +}; + +const BG_LEFT_OFF_WHITE_COLOR: ResColor = ResColor { + r: 8, + g: 13, + b: 17, + a: 255, +}; + +const BG_LEFT_OFF_BLACK_COLOR: ResColor = ResColor { + r: 5, + g: 10, + b: 14, + a: 0, +}; + +const BG_LEFT_SELECTED_BLACK_COLOR: ResColor = ResColor { + r: 240, + g: 154, + b: 7, + a: 0, +}; + +const BG_LEFT_SELECTED_WHITE_COLOR: ResColor = ResColor { + r: 255, + g: 166, + b: 7, + a: 255, +}; + +const BLACK: ResColor = ResColor { + r: 0, + g: 0, + b: 0, + a: 255, +}; + +macro_rules! menu_text_name_fmt { + ($x:ident, $y:ident) => { + format!("trMod_menu_opt_{}_{}", $x, $y).as_str() + }; +} + +macro_rules! menu_text_check_fmt { + ($x:ident, $y:ident) => { + format!("trMod_menu_check_{}_{}", $x, $y).as_str() + }; +} + +macro_rules! menu_text_bg_left_fmt { + ($x:ident, $y:ident) => { + format!("trMod_menu_bg_left_{}_{}", $x, $y).as_str() + }; +} + +macro_rules! menu_text_bg_back_fmt { + ($x:ident, $y:ident) => { + format!("trMod_menu_bg_back_{}_{}", $x, $y).as_str() + }; +} + +macro_rules! menu_text_slider_fmt { + ($x:ident) => { + format!("trMod_menu_slider_{}", $x).as_str() + }; +} + +macro_rules! menu_slider_label_fmt { + ($x:ident) => { + format!("trMod_menu_slider_{}_lbl", $x).as_str() + }; +} + +// Sort all panes in under menu pane such that text and check options +// are last +pub unsafe fn all_menu_panes_sorted(root_pane: &Pane) -> Vec<&mut Pane> { + let mut panes = (0..NUM_MENU_TEXT_OPTIONS) + .flat_map(|idx| { + let x = idx % 3; + let y = idx / 3; + [ + root_pane + .find_pane_by_name_recursive(menu_text_name_fmt!(x, y)) + .unwrap(), + root_pane + .find_pane_by_name_recursive(menu_text_check_fmt!(x, y)) + .unwrap(), + root_pane + .find_pane_by_name_recursive(menu_text_bg_left_fmt!(x, y)) + .unwrap(), + root_pane + .find_pane_by_name_recursive(menu_text_bg_back_fmt!(x, y)) + .unwrap(), + ] + }) + .collect::>(); + + panes.append( + &mut (0..NUM_MENU_TEXT_SLIDERS) + .map(|idx| { + root_pane + .find_pane_by_name_recursive(menu_text_slider_fmt!(idx)) + .unwrap() + }) + .collect::>(), + ); + + panes.append( + &mut (0..NUM_MENU_TEXT_SLIDERS) + .map(|idx| { + root_pane + .find_pane_by_name_recursive(menu_slider_label_fmt!(idx)) + .unwrap() + }) + .collect::>(), + ); + + panes.sort_by(|a, _| { + if a.get_name().contains("opt") || a.get_name().contains("check") { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Less + } + }); + + panes +} + +pub unsafe fn draw(root_pane: &mut Pane) { + // Update menu display + // Grabbing lock as read-only, essentially + let app = &*crate::common::menu::QUICK_MENU_APP.data_ptr(); + + if let Some(quit_button) = root_pane.find_pane_by_name_recursive("btn_finish") { + // Normally at (-804, 640) + // Comes down to (-804, 514) + if QUICK_MENU_ACTIVE { + quit_button.pos_y = 514.0; + } + + for quit_txt_s in &["set_txt_00", "set_txt_01"] { + if let Some(quit_txt) = quit_button.find_pane_by_name_recursive(quit_txt_s) { + quit_txt.as_textbox().set_text_string(if QUICK_MENU_ACTIVE { + "Modpack Menu" + } else { + // Awkward. We should get the o.g. translation for non-english games + // Or create our own textbox here so we don't step on their toes. + "Quit Training" + }); + } + } + } + + let menu_pane = root_pane.find_pane_by_name_recursive("trMod_menu").unwrap(); + menu_pane.set_visible(QUICK_MENU_ACTIVE); + + if !HAS_SORTED_MENU_CHILDREN { + let sorted_panes = all_menu_panes_sorted(root_pane); + // Place in sorted order such that backings are behind, etc. + sorted_panes.iter().for_each(|p| menu_pane.remove_child(p)); + sorted_panes.iter().for_each(|p| menu_pane.append_child(p)); + + HAS_SORTED_MENU_CHILDREN = true; + } + + // Make all invisible first + (0..NUM_MENU_TEXT_OPTIONS).for_each(|idx| { + let x = idx % 3; + let y = idx / 3; + root_pane + .find_pane_by_name_recursive(menu_text_name_fmt!(x, y)) + .map(|text| text.set_visible(false)); + root_pane + .find_pane_by_name_recursive(menu_text_check_fmt!(x, y)) + .map(|text| text.set_visible(false)); + root_pane + .find_pane_by_name_recursive(menu_text_bg_left_fmt!(x, y)) + .map(|text| text.set_visible(false)); + root_pane + .find_pane_by_name_recursive(menu_text_bg_back_fmt!(x, y)) + .map(|text| text.set_visible(false)); + }); + (0..NUM_MENU_TEXT_SLIDERS).for_each(|idx| { + root_pane + .find_pane_by_name_recursive(menu_text_slider_fmt!(idx)) + .map(|text| text.set_visible(false)); + + root_pane + .find_pane_by_name_recursive(menu_slider_label_fmt!(idx)) + .map(|text| text.set_visible(false)); + }); + + root_pane + .find_pane_by_name_recursive("slider_menu") + .map(|pane| pane.set_visible(false)); + + 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]); + + (0..NUM_MENU_TABS).for_each(|idx| { + root_pane + .find_pane_by_name_recursive(format!("trMod_menu_tab_{idx}").as_str()) + .map(|text| text.as_textbox().set_text_string(tab_titles[idx])); + }); + + if app.page == AppPage::SUBMENU { + let tab_selected = app.tab_selected(); + let tab = app.menu_items.get(tab_selected).unwrap(); + + (0..NUM_MENU_TEXT_OPTIONS) + // Valid options in this submenu + .filter_map(|idx| tab.idx_to_list_idx_opt(idx)) + .map(|(list_section, list_idx)| { + ( + list_section, + list_idx, + root_pane + .find_pane_by_name_recursive(menu_text_name_fmt!( + list_section, + list_idx + )) + .unwrap(), + root_pane + .find_pane_by_name_recursive(menu_text_bg_left_fmt!( + list_section, + list_idx + )) + .unwrap(), + root_pane + .find_pane_by_name_recursive(menu_text_bg_back_fmt!( + list_section, + list_idx + )) + .unwrap(), + ) + }) + .for_each(|(list_section, list_idx, text, bg_left, bg_back)| { + 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(); + let text = text.as_textbox(); + text.set_text_string(submenu.submenu_title); + text.set_visible(true); + let bg_left_material = &mut *bg_left.as_picture().material; + if is_selected { + if let Some(footer) = + root_pane.find_pane_by_name_recursive("trMod_menu_footer_txt") + { + footer.as_textbox().set_text_string(submenu.help_text); + } + bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); + } else { + bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); + } + + bg_left.set_visible(true); + bg_back.set_visible(true); + }); + } else if matches!(app.selected_sub_menu_slider.state, GaugeState::None) { + 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(|(idx, (checked, name))| { + let is_selected = sub_menu_state.selected().filter(|s| *s == idx).is_some(); + if let Some(text) = root_pane + .find_pane_by_name_recursive(menu_text_name_fmt!(list_section, idx)) + { + let text = text.as_textbox(); + text.set_text_string(name); + text.set_visible(true); + } + + if let Some(bg_left) = root_pane + .find_pane_by_name_recursive(menu_text_bg_left_fmt!(list_section, idx)) + { + let bg_left_material = &mut *bg_left.as_picture().material; + if is_selected { + bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); + } else { + bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); + } + bg_left.set_visible(true); + } + + if let Some(bg_back) = root_pane + .find_pane_by_name_recursive(menu_text_bg_back_fmt!(list_section, idx)) + { + bg_back.set_visible(true); + } + + if let Some(check) = root_pane + .find_pane_by_name_recursive(menu_text_check_fmt!(list_section, idx)) + { + if *checked { + let check = check.as_textbox(); + + check.set_text_string("+"); + check.set_visible(true); + } + } + }); + }); + } else { + 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; + + if let Some(pane) = root_pane.find_pane_by_name_recursive("slider_menu") { + pane.set_visible(true); + } + + if let Some(text) = root_pane.find_pane_by_name_recursive("slider_title") { + let text = text.as_textbox(); + text.set_text_string(&format!("{_title}")); + } + + (0..NUM_MENU_TEXT_SLIDERS).for_each(|index| { + if let Some(text_pane) = root_pane.find_pane_by_name_recursive( + format!("trMod_menu_slider_{}_lbl", index).as_str(), + ) { + let text_pane = text_pane.as_textbox(); + text_pane.set_visible(true); + + match index { + 0 => { + text_pane.set_text_string("Min"); + + match gauge_vals.state { + GaugeState::MinHover | GaugeState::MinSelected => { + text_pane.m_bits |= 1 << TextBoxFlag::ShadowEnabled as u8; + text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8); + text_pane.set_color(255, 255, 255, 255); + } + _ => { + text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::ShadowEnabled as u8); + text_pane.m_bits |= 1 << TextBoxFlag::InvisibleBorderEnabled as u8; + text_pane.set_color(85, 89, 92, 255); + } + } + } + 1 => { + text_pane.set_text_string("Max"); + + match gauge_vals.state { + GaugeState::MaxHover | GaugeState::MaxSelected => { + text_pane.m_bits |= 1 << TextBoxFlag::ShadowEnabled as u8; + text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8); + text_pane.set_color(255, 255, 255, 255); + } + _ => { + text_pane.m_bits |= 1 << TextBoxFlag::InvisibleBorderEnabled as u8; + text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::ShadowEnabled as u8); + text_pane.set_color(85, 89, 92, 255); + } + } + } + _ => panic!("Unexpected slider label index {}!", index), + } + } + + if let Some(text_pane) = root_pane + .find_pane_by_name_recursive(format!("trMod_menu_slider_{}", index).as_str()) + { + let text_pane = text_pane.as_textbox(); + text_pane.set_visible(true); + + match index { + 0 => text_pane.set_text_string(&format!("{selected_min}")), + 1 => text_pane.set_text_string(&format!("{selected_max}")), + _ => panic!("Unexpected slider label index {}!", index), + } + } + + if let Some(bg_left) = root_pane + .find_pane_by_name_recursive(format!("slider_btn_fg_{}", index).as_str()) + { + let bg_left_material = &mut *bg_left.as_picture().material; + + match index { + 0 => match gauge_vals.state { + GaugeState::MinHover => { + bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); + } + GaugeState::MinSelected => { + bg_left_material.set_white_res_color(BG_LEFT_SELECTED_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_SELECTED_BLACK_COLOR); + } + _ => { + bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); + } + }, + 1 => match gauge_vals.state { + GaugeState::MaxHover => { + bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); + } + GaugeState::MaxSelected => { + bg_left_material.set_white_res_color(BG_LEFT_SELECTED_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_SELECTED_BLACK_COLOR); + } + _ => { + bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); + bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); + } + }, + _ => panic!("Unexpected slider label index {}!", index), + } + bg_left.set_visible(true); + } + }); + } +} + +pub static mut MENU_PANE_PTR: u64 = 0; +const MENU_POS : ResVec3 = ResVec3 { + x: -360.0, + y: 440.0, + z: 0.0 +}; + +pub static BUILD_CONTAINER_PANE: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, _block, parts_build_data_set, build_arg_set, build_res_set, _kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + // Let's create our parent display pane here. + let menu_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']); + let mut menu_pane_block = ResPane::new("trMod_menu"); + // Overall menu pane @ 0,0 to reason about positions globally + menu_pane_block.set_pos(ResVec3::default()); + let menu_pane = build!(menu_pane_block, ResPane, menu_pane_kind, Pane); + menu_pane.detach(); + + root_pane.append_child(menu_pane); + if MENU_PANE_PTR != menu_pane as *mut Pane as u64 { + MENU_PANE_PTR = menu_pane as *mut Pane as u64; + HAS_SORTED_MENU_CHILDREN = false; + } + + ui::reset_creation(); +}; + +pub static BUILD_FOOTER_BG: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); + let block = block as *mut ResPictureWithTex<1>; + // For menu backing + let mut pic_menu_block = *block; + pic_menu_block.set_name("trMod_menu_footer_bg"); + let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<1>, kind, Picture); + pic_menu_pane.detach(); + + menu_pane.append_child(pic_menu_pane); +}; + +pub static BUILD_FOOTER_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); + + let block = block as *mut ResTextBox; + let mut text_block = *block; + text_block.set_name("trMod_menu_footer_txt"); + + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane.set_text_string("Footer!"); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + text_pane.set_default_material_colors(); + text_pane.set_color(255, 255, 255, 255); + text_pane.detach(); + + menu_pane.append_child(text_pane); +}; + +pub static BUILD_TAB_TXTS: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + (0..NUM_MENU_TABS).for_each(|txt_idx| { + let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); + + let block = block as *mut ResTextBox; + let mut text_block = *block; + text_block.enable_shadow(); + text_block.text_alignment(TextAlignment::Center); + + let x = txt_idx; + text_block.set_name(format!("trMod_menu_tab_{x}").as_str()); + + let mut x_offset = x as f32 * 300.0; + // Center current tab since we don't have a help key + if x == 1 { + x_offset -= 25.0; + } + text_block.set_pos(ResVec3::new( + MENU_POS.x - 25.0 + x_offset, + MENU_POS.y + 75.0, + 0.0, + )); + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane.set_text_string(format!("Tab {txt_idx}!").as_str()); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + text_pane.set_default_material_colors(); + text_pane.set_color(255, 255, 255, 255); + if txt_idx == 1 { + text_pane.set_color(255, 255, 0, 255); + } + text_pane.detach(); + menu_pane.append_child(text_pane); + + let mut help_block = *block; + // Font Idx 2 = nintendo64 which contains nice symbols + help_block.font_idx = 2; + + let x = txt_idx; + help_block.set_name(format!("trMod_menu_tab_help_{x}").as_str()); + + let x_offset = x as f32 * 300.0; + help_block.set_pos(ResVec3::new( + MENU_POS.x - 250.0 + x_offset, + MENU_POS.y + 75.0, + 0.0, + )); + let help_pane = build!(help_block, ResTextBox, kind, TextBox); + help_pane.set_text_string("abcd"); + let it = help_pane.m_text_buf as *mut u16; + match txt_idx { + // Left Tab: ZL + 0 => { + *it = 0xE0E6; + *(it.add(1)) = 0x0; + help_pane.m_text_len = 1; + } + 1 => { + *it = 0x0; + help_pane.m_text_len = 0; + } + // Right Tab: ZR + 2 => { + *it = 0xE0E7; + *(it.add(1)) = 0x0; + help_pane.m_text_len = 1; + } + _ => {} + } + + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + help_pane.set_default_material_colors(); + help_pane.set_color(255, 255, 255, 255); + help_pane.detach(); + menu_pane.append_child(help_pane); + }); +}; + +pub static BUILD_OPT_TXTS: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + (0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| { + let x = txt_idx % 3; + let y = txt_idx / 3; + + let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); + + let block = block as *mut ResTextBox; + let mut text_block = *block; + text_block.enable_shadow(); + text_block.text_alignment(TextAlignment::Center); + + text_block.set_name(menu_text_name_fmt!(x, y)); + + let x_offset = x as f32 * 500.0; + let y_offset = y as f32 * 85.0; + text_block.set_pos(ResVec3::new( + MENU_POS.x - 480.0 + x_offset, + MENU_POS.y - 50.0 - y_offset, + 0.0, + )); + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane.set_text_string(format!("Opt {txt_idx}!").as_str()); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + text_pane.set_default_material_colors(); + text_pane.set_color(255, 255, 255, 255); + text_pane.detach(); + menu_pane.append_child(text_pane); + + let mut check_block = *block; + // Font Idx 2 = nintendo64 which contains nice symbols + check_block.font_idx = 2; + + check_block.set_name(menu_text_check_fmt!(x, y)); + check_block.set_pos(ResVec3::new( + MENU_POS.x - 375.0 + x_offset, + MENU_POS.y - 50.0 - y_offset, + 0.0, + )); + let check_pane = build!(check_block, ResTextBox, kind, TextBox); + check_pane.set_text_string(format!("Check {txt_idx}!").as_str()); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + check_pane.set_default_material_colors(); + check_pane.set_color(0, 0, 0, 255); + check_pane.detach(); + menu_pane.append_child(check_pane); + }); +}; + +pub static BUILD_SLIDER_CONTAINER_PANE: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + let slider_root_name = "slider_menu"; + let slider_container_name = "slider_ui_container"; + + let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); + let slider_ui_root_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']); + let mut slider_ui_root_block = ResPane::new(slider_root_name); + + slider_ui_root_block.set_pos(ResVec3::default()); + + let slider_ui_root = build!( + slider_ui_root_block, + ResPane, + slider_ui_root_pane_kind, + Pane + ); + + slider_ui_root.detach(); + menu_pane.append_child(slider_ui_root); + + let block = block as *mut ResPictureWithTex<1>; + + let mut picture_block = *block; + + picture_block.set_name(slider_container_name); + picture_block.set_size(ResVec2::new(675.0, 300.0)); + picture_block.set_pos(ResVec3::new(-530.0, 180.0, 0.0)); + picture_block.tex_coords = [ + [ResVec2::new(0.0, 0.0)], + [ResVec2::new(1.0, 0.0)], + [ResVec2::new(0.0, 1.5)], + [ResVec2::new(1.0, 1.5)], + ]; + + let picture_pane = build!(picture_block, ResPictureWithTex<1>, kind, Picture); + picture_pane.detach(); + slider_ui_root.append_child(picture_pane); +}; + +pub static BUILD_SLIDER_HEADER_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + let slider_root_name = "slider_menu"; + let container_pane = root_pane.find_pane_by_name(slider_root_name, true).unwrap(); + + let block = block as *mut ResTextBox; + let mut title_block = *block; + + title_block.set_name("slider_title"); + title_block.set_pos(ResVec3::new(-530.0, 285.0, 0.0)); + title_block.set_size(ResVec2::new(550.0, 100.0)); + title_block.font_size = ResVec2::new(50.0, 100.0); + + let title_pane = build!(title_block, ResTextBox, kind, TextBox); + + title_pane.set_text_string(format!("Slider Title").as_str()); + + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + title_pane.set_default_material_colors(); + + // Header should be white text + title_pane.set_color(255, 255, 255, 255); + title_pane.detach(); + container_pane.append_child(title_pane); +}; + +pub static BUILD_SLIDER_TXTS: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + let slider_root_name = "slider_menu"; + let slider_container_name = "slider_ui_container"; + + (0..NUM_MENU_TEXT_SLIDERS).for_each(|idx| { + let x = idx % 2; + + let label_x_offset = x as f32 * 345.0; + + let slider_root_pane = root_pane.find_pane_by_name(slider_root_name, true).unwrap(); + let slider_container = root_pane + .find_pane_by_name(slider_container_name, true) + .unwrap(); + + let block = block as *mut ResTextBox; + + let mut text_block = *block; + + text_block.text_alignment(TextAlignment::Center); + + text_block.set_name(menu_text_slider_fmt!(idx)); + + let value_x_offset = x as f32 * 345.0; + + text_block.set_pos(ResVec3::new( + slider_root_pane.pos_x - 675.0 + value_x_offset, + slider_root_pane.pos_y + (slider_container.size_y * 0.458), + 0.0, + )); + + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane.set_text_string(format!("Slider opt {idx}!").as_str()); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + text_pane.set_default_material_colors(); + text_pane.set_color(0, 0, 0, 255); + text_pane.detach(); + slider_root_pane.append_child(text_pane); + + let mut label_block = *block; + + label_block.text_alignment(TextAlignment::Center); + label_block.set_name(menu_slider_label_fmt!(idx)); + label_block.set_pos(ResVec3::new( + slider_root_pane.pos_x - 750.0 + label_x_offset, + slider_root_pane.pos_y + slider_container.size_y * 0.458 + 5.0, + 0.0, + )); + label_block.font_size = ResVec2::new(25.0, 50.0); + + // Aligns text to the center horizontally + label_block.text_position = 4; + + label_block.shadow_offset = ResVec2::new(4.0, -3.0); + label_block.shadow_cols = [BLACK, BLACK]; + label_block.shadow_scale = ResVec2::new(1.0, 1.0); + + let label_pane = build!(label_block, ResTextBox, kind, TextBox); + + label_pane.set_text_string(format!("Slider opt {idx}!").as_str()); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + label_pane.set_default_material_colors(); + label_pane.set_color(85, 89, 92, 255); + // Turns on text outline + label_pane.m_bits = label_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8); + label_pane.detach(); + + slider_root_pane.append_child(label_pane); + + }); +}; + +pub static BUILD_BG_LEFTS: ui::PaneCreationCallback = |_, _, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + (0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| { + let x = txt_idx % 3; + let y = txt_idx / 3; + + let x_offset = x as f32 * 500.0; + let y_offset = y as f32 * 85.0; + + let block = block as *mut ResPictureWithTex<2>; + let mut pic_menu_block = *block; + pic_menu_block.set_name(menu_text_bg_left_fmt!(x, y)); + pic_menu_block.picture.scale_x /= 1.5; + pic_menu_block.picture.set_pos(ResVec3::new( + MENU_POS.x - 400.0 - 195.0 + x_offset, + MENU_POS.y - 50.0 - y_offset, + 0.0, + )); + let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<2>, kind, Picture); + pic_menu_pane.detach(); + if MENU_PANE_PTR != 0 { + (*(MENU_PANE_PTR as *mut Pane)).append_child(pic_menu_pane); + } + }); + + (0..NUM_MENU_TEXT_SLIDERS).for_each(|index| { + let x = index % 2; + + if MENU_PANE_PTR != 0 { + let slider_root = (*(MENU_PANE_PTR as *mut Pane)) + .find_pane_by_name("slider_menu", true) + .unwrap(); + let slider_bg = (*(MENU_PANE_PTR as *mut Pane)) + .find_pane_by_name("slider_ui_container", true) + .unwrap(); + let x_offset = x as f32 * 345.0; + + let block = block as *mut ResPictureWithTex<2>; + let mut pic_menu_block = *block; + + pic_menu_block.set_name(format!("slider_btn_fg_{}", index).as_str()); + + pic_menu_block.picture.scale_x /= 1.85; + pic_menu_block.picture.scale_y /= 1.25; + + pic_menu_block.set_pos(ResVec3::new( + slider_root.pos_x - 842.5 + x_offset, + slider_root.pos_y + slider_bg.size_y * 0.458, + 0.0, + )); + + let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<2>, kind, Picture); + pic_menu_pane.detach(); + + slider_root.append_child(pic_menu_pane); + } + }); +}; + +pub static BUILD_BG_BACKS: ui::PaneCreationCallback = |_, _, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe { + macro_rules! build { + ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { + paste::paste! { + &mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ) + } + }; + } + + (0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| { + let x = txt_idx % 3; + let y = txt_idx / 3; + + let x_offset = x as f32 * 500.0; + let y_offset = y as f32 * 85.0; + + let block = block as *mut ResWindowWithTexCoordsAndFrames<1, 4>; + + let mut bg_block = *block; + bg_block.set_name(menu_text_bg_back_fmt!(x, y)); + bg_block.scale_x /= 2.0; + bg_block.set_pos(ResVec3::new( + MENU_POS.x - 400.0 + x_offset, + MENU_POS.y - 50.0 - y_offset, + 0.0, + )); + let bg_pane = build!(bg_block, ResWindowWithTexCoordsAndFrames<1,4>, kind, Window); + bg_pane.detach(); + if MENU_PANE_PTR != 0 { + (*(MENU_PANE_PTR as *mut Pane)).append_child(bg_pane); + } + }); + + (0..NUM_MENU_TEXT_SLIDERS).for_each(|index| { + let x = index % 2; + + if MENU_PANE_PTR != 0 { + let slider_root = (*(MENU_PANE_PTR as *mut Pane)) + .find_pane_by_name("slider_menu", true) + .unwrap(); + let slider_bg = (*(MENU_PANE_PTR as *mut Pane)) + .find_pane_by_name("slider_ui_container", true) + .unwrap(); + + let size_y = 90.0; + + let x_offset = x as f32 * 345.0; + + let block = block as *mut ResWindowWithTexCoordsAndFrames<1, 4>; + let mut bg_block = *block; + + bg_block.set_name(format!("slider_item_btn_{}", index).as_str()); + bg_block.scale_x /= 2.0; + + bg_block.set_size(ResVec2::new(605.0, size_y)); + + bg_block.set_pos(ResVec3::new( + slider_root.pos_x - 700.0 + x_offset, + slider_root.pos_y + slider_bg.size_y * 0.458, + 0.0, + )); + + let bg_pane = build!(bg_block, ResWindowWithTexCoordsAndFrames<1,4>, kind, Window); + bg_pane.detach(); + + slider_root.append_child(bg_pane); + } + }); +}; \ No newline at end of file diff --git a/src/training/ui/mod.rs b/src/training/ui/mod.rs new file mode 100644 index 0000000..e319127 --- /dev/null +++ b/src/training/ui/mod.rs @@ -0,0 +1,168 @@ +use crate::common::{is_ready_go, is_training_mode}; +use skyline::nn::ui2d::*; +use training_mod_consts::{OnOff, MENU}; +use std::collections::HashMap; +use parking_lot::Mutex; +use skyline::libc::c_void; + +mod damage; +mod display; +mod menu; +pub mod notifications; + +type PaneCreationCallback = for<'a, 'b> unsafe fn(&'a str, &'b mut Pane, + extern "C" fn(*mut Layout, *mut u8, *const u8, *mut ResPane, *const u8, *const u8, *const u8, u32) -> *mut Pane, + *mut Layout, *mut u8, *const u8, *mut ResPane, + *const u8, *const u8, *const u8, u32); + +lazy_static::lazy_static! { + static ref PANE_CREATED: Mutex + >> = Mutex::new(HashMap::from([ + ( + (String::from("info_training"), String::from("pic_numbase_01")), + vec![ + (false, menu::BUILD_CONTAINER_PANE), + (false, display::BUILD_PIC_BASE), + (false, menu::BUILD_SLIDER_CONTAINER_PANE), + ] + ), + ( + (String::from("info_training"), String::from("pic_help_bg_00")), + vec![(false, menu::BUILD_FOOTER_BG)] + ), + ( + (String::from("info_training"), String::from("set_txt_help_00")), + vec![(false, menu::BUILD_FOOTER_TXT)] + ), + ( + (String::from("info_training"), String::from("set_txt_num_01")), + vec![ + (false, menu::BUILD_TAB_TXTS), + (false, menu::BUILD_OPT_TXTS), + (false, menu::BUILD_SLIDER_TXTS), + (false, display::BUILD_PANE_TXT), + ] + ), + ( + (String::from("info_training"), String::from("txt_cap_01")), + vec![ + (false, display::BUILD_HEADER_TXT), + (false, menu::BUILD_SLIDER_HEADER_TXT), + ] + ), + ( + (String::from("info_training_btn0_00_item"), String::from("icn_bg_main")), + vec![(false, menu::BUILD_BG_LEFTS)] + ), + ( + (String::from("info_training_btn0_00_item"), String::from("btn_bg")), + vec![(false, menu::BUILD_BG_BACKS)] + ), + ])); +} + +pub unsafe fn reset_creation() { + let pane_created = &mut *PANE_CREATED.data_ptr(); + pane_created.iter_mut().for_each(|(_identifier, creators)| { + creators.iter_mut().for_each(|(created, _callback)| { + *created = false; + }) + }) +} + +#[skyline::hook(offset = 0x4b620)] +pub unsafe fn handle_draw(layout: *mut Layout, draw_info: u64, cmd_buffer: u64) { + let layout_name = &skyline::from_c_str((*layout).layout_name); + let root_pane = &mut *(*layout).root_pane; + + // Set HUD to invisible if HUD is toggled off + if is_training_mode() && is_ready_go() && layout_name != "info_training" { + // 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); + } + + damage::draw(root_pane, layout_name); + + if layout_name == "info_training" { + display::draw(root_pane); + menu::draw(root_pane); + } + + original!()(layout, draw_info, cmd_buffer); +} + +#[skyline::hook(offset = 0x493a0)] +pub unsafe fn layout_build_parts_impl( + layout: *mut Layout, + out_build_result_information: *mut u8, + device: *const u8, + block: *mut ResPane, + parts_build_data_set: *const u8, + build_arg_set: *const u8, + build_res_set: *const u8, + kind: u32, +) -> *mut Pane { + let layout_name = &skyline::from_c_str((*layout).layout_name); + let root_pane = &mut *(*layout).root_pane; + + let block_name = (*block).get_name(); + let identifier = (layout_name.to_string(), block_name); + let pane_created = &mut *PANE_CREATED.data_ptr(); + let panes = pane_created.get_mut(&identifier); + if let Some(panes) = panes { + panes.iter_mut().for_each(|(has_created, callback)| { + if !*has_created { + callback(layout_name, + root_pane, + original!(), + layout, + out_build_result_information, + device, + block, + parts_build_data_set, + build_arg_set, + build_res_set, + kind + ); + + // Special case: Menu init should always occur + if ("info_training".to_string(), "pic_numbase_01".to_string()) != identifier { + *has_created = true; + } + } + }); + } + + original!()( + layout, + out_build_result_information, + device, + block, + parts_build_data_set, + build_arg_set, + build_res_set, + kind, + ) +} + + +#[skyline::hook(offset = 0x32cace0)] +pub unsafe fn handle_load_layout_files( + meta_layout_root: *mut c_void, + loading_list: *mut c_void, + layout_arc_hash: *const u32, + param_4: i32 +) -> u64 { + println!("Layout.arc hash: {:x}", *layout_arc_hash); + original!()(meta_layout_root, loading_list, layout_arc_hash, param_4) +} + +pub fn init() { + skyline::install_hooks!( + handle_draw, + layout_build_parts_impl, + // handle_load_layout_files + ); +} diff --git a/src/training/ui/notifications.rs b/src/training/ui/notifications.rs new file mode 100644 index 0000000..ada9b2a --- /dev/null +++ b/src/training/ui/notifications.rs @@ -0,0 +1,69 @@ +use skyline::nn::ui2d::ResColor; + +pub static mut QUEUE: Vec> = vec![]; + +#[derive(Copy, Clone)] +pub struct Notification<'a> { + header: &'a str, + message: &'a str, + length: u32, + color: ResColor +} + +impl<'a> Notification<'a> { + pub fn new(header: &'a str, message: &'a str, length: u32, color: ResColor) -> Notification<'a> { + Notification { + header, + message, + length, + color + } + } + + // Returns: has_completed + pub fn tick(&mut self) -> bool { + if self.length <= 1 { + return true; + } + self.length -= 1; + false + } + + pub fn header(self) -> &'a str { + self.header + } + + pub fn message(self) -> &'a str { + self.message + } + + pub fn color(self) -> ResColor { + self.color + } +} + +pub fn notification(header: &'static str, message: &'static str, len: u32) { + unsafe { + let queue = &mut QUEUE; + queue.push(Notification::new(header, message, len, ResColor { + r: 0, + g: 0, + b: 0, + a: 255 + })); + } +} + +pub fn color_notification(header: &'static str, message: &'static str, len: u32, color: ResColor) { + unsafe { + let queue = &mut QUEUE; + queue.push(Notification::new(header, message, len, color)); + } +} + +pub fn clear_notifications(header: &'static str) { + unsafe { + let queue = &mut QUEUE; + queue.retain(|notif| notif.header != header); + } +} \ No newline at end of file diff --git a/src/training/ui_hacks.rs b/src/training/ui_hacks.rs deleted file mode 100644 index f984abc..0000000 --- a/src/training/ui_hacks.rs +++ /dev/null @@ -1,1206 +0,0 @@ -use crate::common::{get_player_dmg_digits, is_ready_go, is_training_mode}; -use crate::consts::FighterId; -use crate::{common::menu::QUICK_MENU_ACTIVE, training::combo::FRAME_ADVANTAGE}; -use skyline::nn::ui2d::*; -use smash::ui2d::{SmashPane, SmashTextBox}; -use training_mod_consts::{OnOff, MENU}; -use training_mod_tui::gauge::GaugeState; - -pub unsafe fn iterate_anim_list( - anim_transform_node: &mut AnimTransformNode, - layout_name: Option<&str>, -) { - let mut curr = anim_transform_node as *mut AnimTransformNode; - let mut _anim_idx = 0; - while !curr.is_null() { - // Only if valid - if curr != (*curr).next { - let anim_transform = (curr as *mut u64).add(2) as *mut AnimTransform; - - parse_anim_transform(anim_transform.as_mut().unwrap(), layout_name); - } - - curr = (*curr).next; - _anim_idx += 1; - if curr == anim_transform_node as *mut AnimTransformNode || curr == (*curr).next { - break; - } - } -} - -pub unsafe fn parse_anim_transform(anim_transform: &mut AnimTransform, layout_name: Option<&str>) { - let res_animation_block_data_start = anim_transform.res_animation_block as u64; - let res_animation_block = &*anim_transform.res_animation_block; - let mut anim_cont_offsets = (res_animation_block_data_start - + res_animation_block.anim_cont_offsets_offset as u64) - as *const u32; - for _anim_cont_idx in 0..res_animation_block.anim_cont_count { - let anim_cont_offset = *anim_cont_offsets; - let res_animation_cont = (res_animation_block_data_start + anim_cont_offset as u64) - as *const ResAnimationContent; - - let name = skyline::try_from_c_str((*res_animation_cont).name.as_ptr()) - .unwrap_or("UNKNOWN".to_string()); - let anim_type = (*res_animation_cont).anim_content_type; - - // AnimContentType 1 == MATERIAL - if name.starts_with("set_dmg_num") && anim_type == 1 { - if let Some(layout_name) = layout_name { - let (hundreds, tens, ones, dec) = get_player_dmg_digits(match layout_name { - "p1" => FighterId::Player, - "p2" => FighterId::CPU, - _ => panic!("Unknown layout name: {}", layout_name), - }); - - if name == "set_dmg_num_3" { - anim_transform.frame = hundreds as f32; - } - if name == "set_dmg_num_2" { - anim_transform.frame = tens as f32; - } - if name == "set_dmg_num_1" { - anim_transform.frame = ones as f32; - } - if name == "set_dmg_num_dec" { - anim_transform.frame = dec as f32; - } - } - } - - anim_cont_offsets = anim_cont_offsets.add(1); - } -} - -pub static NUM_DISPLAY_PANES: usize = 1; -pub static NUM_MENU_TEXT_OPTIONS: usize = 27; -pub static NUM_MENU_TEXT_SLIDERS: usize = 2; -pub static NUM_MENU_TABS: usize = 3; - -pub static mut HAS_SORTED_MENU_CHILDREN: bool = false; - -const BG_LEFT_ON_WHITE_COLOR: ResColor = ResColor { - r: 0, - g: 28, - b: 118, - a: 255, -}; - -const BG_LEFT_ON_BLACK_COLOR: ResColor = ResColor { - r: 0, - g: 22, - b: 112, - a: 0, -}; - -const BG_LEFT_OFF_WHITE_COLOR: ResColor = ResColor { - r: 8, - g: 13, - b: 17, - a: 255, -}; - -const BG_LEFT_OFF_BLACK_COLOR: ResColor = ResColor { - r: 5, - g: 10, - b: 14, - a: 0, -}; - -const BG_LEFT_SELECTED_BLACK_COLOR: ResColor = ResColor { - r: 240, - g: 154, - b: 7, - a: 0, -}; - -const BG_LEFT_SELECTED_WHITE_COLOR: ResColor = ResColor { - r: 255, - g: 166, - b: 7, - a: 255, -}; - -const BLACK: ResColor = ResColor { - r: 0, - g: 0, - b: 0, - a: 255, -}; - -macro_rules! menu_text_name_fmt { - ($x:ident, $y:ident) => { - format!("trMod_menu_opt_{}_{}", $x, $y).as_str() - }; -} - -macro_rules! menu_text_check_fmt { - ($x:ident, $y:ident) => { - format!("trMod_menu_check_{}_{}", $x, $y).as_str() - }; -} - -macro_rules! menu_text_bg_left_fmt { - ($x:ident, $y:ident) => { - format!("trMod_menu_bg_left_{}_{}", $x, $y).as_str() - }; -} - -macro_rules! menu_text_bg_back_fmt { - ($x:ident, $y:ident) => { - format!("trMod_menu_bg_back_{}_{}", $x, $y).as_str() - }; -} - -macro_rules! menu_text_slider_fmt { - ($x:ident) => { - format!("trMod_menu_slider_{}", $x).as_str() - }; -} - -macro_rules! menu_slider_label_fmt { - ($x:ident) => { - format!("trMod_menu_slider_{}_lbl", $x).as_str() - }; -} - -// Sort all panes in under menu pane such that text and check options -// are last -pub unsafe fn all_menu_panes_sorted(root_pane: &Pane) -> Vec<&mut Pane> { - let mut panes = (0..NUM_MENU_TEXT_OPTIONS) - .flat_map(|idx| { - let x = idx % 3; - let y = idx / 3; - [ - root_pane - .find_pane_by_name_recursive(menu_text_name_fmt!(x, y)) - .unwrap(), - root_pane - .find_pane_by_name_recursive(menu_text_check_fmt!(x, y)) - .unwrap(), - root_pane - .find_pane_by_name_recursive(menu_text_bg_left_fmt!(x, y)) - .unwrap(), - root_pane - .find_pane_by_name_recursive(menu_text_bg_back_fmt!(x, y)) - .unwrap(), - ] - }) - .collect::>(); - - panes.append( - &mut (0..NUM_MENU_TEXT_SLIDERS) - .map(|idx| { - root_pane - .find_pane_by_name_recursive(menu_text_slider_fmt!(idx)) - .unwrap() - }) - .collect::>(), - ); - - panes.append( - &mut (0..NUM_MENU_TEXT_SLIDERS) - .map(|idx| { - root_pane - .find_pane_by_name_recursive(menu_slider_label_fmt!(idx)) - .unwrap() - }) - .collect::>(), - ); - - panes.sort_by(|a, _| { - if a.get_name().contains("opt") || a.get_name().contains("check") { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Less - } - }); - - panes -} - -#[skyline::hook(offset = 0x4b620)] -pub unsafe fn handle_draw(layout: *mut Layout, draw_info: u64, cmd_buffer: u64) { - let layout_name = skyline::from_c_str((*layout).layout_name); - let root_pane = &mut *(*layout).root_pane; - - if is_training_mode() && is_ready_go() && layout_name != "info_training" { - root_pane.flags |= 1 << PaneFlag::InfluencedAlpha as u8; - root_pane.set_visible(MENU.hud == OnOff::On); - } - - // Update percentage display as soon as possible on death - if is_training_mode() && is_ready_go() && layout_name == "info_melee" { - for player_name in &["p1", "p2"] { - if let Some(parent) = root_pane.find_pane_by_name_recursive(player_name) { - let _p1_layout_name = skyline::from_c_str((*parent.as_parts().layout).layout_name); - let anim_list = &mut (*parent.as_parts().layout).anim_trans_list; - - let mut has_altered_anim_list = false; - let (hundreds, tens, _, _) = get_player_dmg_digits(match *player_name { - "p1" => FighterId::Player, - "p2" => FighterId::CPU, - _ => panic!("Unknown player name: {}", player_name), - }); - - for dmg_num_s in &[ - "set_dmg_num_3", - "dig_3", - "dig_3_anim", - "set_dmg_num_2", - "dig_2", - "dig_2_anim", - "set_dmg_num_1", - "dig_1", - "dig_1_anim", - "set_dmg_num_p", - "dig_dec", - "dig_dec_anim_00", - "set_dmg_num_dec", - "dig_dec_anim_01", - "dig_0_anim", - "set_dmg_p", - ] { - if let Some(dmg_num) = parent.find_pane_by_name_recursive(dmg_num_s) { - if (dmg_num_s.contains('3') && hundreds == 0) - || (dmg_num_s.contains('2') && hundreds == 0 && tens == 0) - { - continue; - } - - if *dmg_num_s == "set_dmg_p" { - dmg_num.pos_y = 0.0; - } else if *dmg_num_s == "set_dmg_num_p" { - dmg_num.pos_y = -4.0; - } else if *dmg_num_s == "dig_dec" { - dmg_num.pos_y = -16.0; - } else { - dmg_num.pos_y = 0.0; - } - - if dmg_num.alpha != 255 || dmg_num.global_alpha != 255 { - dmg_num.set_visible(true); - if !has_altered_anim_list { - iterate_anim_list(anim_list, Some(player_name)); - has_altered_anim_list = true; - } - } - } - } - - for death_explosion_s in &[ - "set_fxui_dead1", - "set_fxui_dead2", - "set_fxui_dead3", - "set_fxui_fire", - ] { - if let Some(death_explosion) = - parent.find_pane_by_name_recursive(death_explosion_s) - { - death_explosion.set_visible(false); - } - } - } - } - } - - // Update training mod displays - if layout_name == "info_training" { - // Update frame advantage - if let Some(parent) = root_pane.find_pane_by_name_recursive("trMod_disp_0") { - parent.set_visible(crate::common::MENU.frame_advantage == OnOff::On); - } - - if let Some(header) = root_pane.find_pane_by_name_recursive("trMod_disp_0_header") { - header.as_textbox().set_text_string("Frame Advantage"); - } - - if let Some(text) = root_pane.find_pane_by_name_recursive("trMod_disp_0_txt") { - let text = text.as_textbox(); - text.set_text_string(format!("{FRAME_ADVANTAGE}").as_str()); - match FRAME_ADVANTAGE { - x if x < 0 => text.set_color(200, 8, 8, 255), - x if x == 0 => text.set_color(0, 0, 0, 255), - _ => text.set_color(31, 198, 0, 255), - }; - } - - // Update menu display - // Grabbing lock as read-only, essentially - let app = &*crate::common::menu::QUICK_MENU_APP.data_ptr(); - - if let Some(quit_button) = root_pane.find_pane_by_name_recursive("btn_finish") { - // Normally at (-804, 640) - // Comes down to (-804, 514) - if QUICK_MENU_ACTIVE { - quit_button.pos_y = 514.0; - } - - for quit_txt_s in &["set_txt_00", "set_txt_01"] { - if let Some(quit_txt) = quit_button.find_pane_by_name_recursive(quit_txt_s) { - quit_txt.as_textbox().set_text_string(if QUICK_MENU_ACTIVE { - "Modpack Menu" - } else { - // Awkward. We should get the o.g. translation for non-english games - // Or create our own textbox here so we don't step on their toes. - "Quit Training" - }); - } - } - } - - let menu_pane = root_pane.find_pane_by_name_recursive("trMod_menu").unwrap(); - menu_pane.set_visible(QUICK_MENU_ACTIVE); - - if !HAS_SORTED_MENU_CHILDREN { - let sorted_panes = all_menu_panes_sorted(root_pane); - // Place in sorted order such that backings are behind, etc. - sorted_panes.iter().for_each(|p| menu_pane.remove_child(p)); - sorted_panes.iter().for_each(|p| menu_pane.append_child(p)); - - HAS_SORTED_MENU_CHILDREN = true; - } - - // Make all invisible first - (0..NUM_MENU_TEXT_OPTIONS).for_each(|idx| { - let x = idx % 3; - let y = idx / 3; - root_pane - .find_pane_by_name_recursive(menu_text_name_fmt!(x, y)) - .map(|text| text.set_visible(false)); - root_pane - .find_pane_by_name_recursive(menu_text_check_fmt!(x, y)) - .map(|text| text.set_visible(false)); - root_pane - .find_pane_by_name_recursive(menu_text_bg_left_fmt!(x, y)) - .map(|text| text.set_visible(false)); - root_pane - .find_pane_by_name_recursive(menu_text_bg_back_fmt!(x, y)) - .map(|text| text.set_visible(false)); - }); - (0..NUM_MENU_TEXT_SLIDERS).for_each(|idx| { - root_pane - .find_pane_by_name_recursive(menu_text_slider_fmt!(idx)) - .map(|text| text.set_visible(false)); - - root_pane - .find_pane_by_name_recursive(format!("trMod_menu_slider_{}_lbl", idx).as_str()) - .map(|text| text.set_visible(false)); - }); - - root_pane - .find_pane_by_name_recursive("slider_menu") - .map(|pane| pane.set_visible(false)); - - 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]); - - (0..NUM_MENU_TABS).for_each(|idx| { - root_pane - .find_pane_by_name_recursive(format!("trMod_menu_tab_{idx}").as_str()) - .map(|text| text.as_textbox().set_text_string(tab_titles[idx])); - }); - - if app.outer_list { - let tab_selected = app.tab_selected(); - let tab = app.menu_items.get(tab_selected).unwrap(); - - (0..NUM_MENU_TEXT_OPTIONS) - // Valid options in this submenu - .filter_map(|idx| tab.idx_to_list_idx_opt(idx)) - .map(|(list_section, list_idx)| { - ( - list_section, - list_idx, - root_pane - .find_pane_by_name_recursive(menu_text_name_fmt!( - list_section, - list_idx - )) - .unwrap(), - root_pane - .find_pane_by_name_recursive(menu_text_bg_left_fmt!( - list_section, - list_idx - )) - .unwrap(), - root_pane - .find_pane_by_name_recursive(menu_text_bg_back_fmt!( - list_section, - list_idx - )) - .unwrap(), - ) - }) - .for_each(|(list_section, list_idx, text, bg_left, bg_back)| { - 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(); - let text = text.as_textbox(); - text.set_text_string(submenu.submenu_title); - text.set_visible(true); - let bg_left_material = &mut *bg_left.as_picture().material; - if is_selected { - if let Some(footer) = - root_pane.find_pane_by_name_recursive("trMod_menu_footer_txt") - { - footer.as_textbox().set_text_string(submenu.help_text); - } - bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); - } else { - bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); - } - - bg_left.set_visible(true); - bg_back.set_visible(true); - }); - } else if matches!(app.selected_sub_menu_slider.state, GaugeState::None) { - 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(|(idx, (checked, name))| { - let is_selected = sub_menu_state.selected().filter(|s| *s == idx).is_some(); - if let Some(text) = root_pane - .find_pane_by_name_recursive(menu_text_name_fmt!(list_section, idx)) - { - let text = text.as_textbox(); - text.set_text_string(name); - text.set_visible(true); - } - - if let Some(bg_left) = root_pane - .find_pane_by_name_recursive(menu_text_bg_left_fmt!(list_section, idx)) - { - let bg_left_material = &mut *bg_left.as_picture().material; - if is_selected { - bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); - } else { - bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); - } - bg_left.set_visible(true); - } - - if let Some(bg_back) = root_pane - .find_pane_by_name_recursive(menu_text_bg_back_fmt!(list_section, idx)) - { - bg_back.set_visible(true); - } - - if let Some(check) = root_pane - .find_pane_by_name_recursive(menu_text_check_fmt!(list_section, idx)) - { - if *checked { - let check = check.as_textbox(); - - check.set_text_string("+"); - check.set_visible(true); - } - } - }); - }); - } else { - 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; - - if let Some(pane) = root_pane.find_pane_by_name_recursive("slider_menu") { - pane.set_visible(true); - } - - if let Some(text) = root_pane.find_pane_by_name_recursive("slider_title") { - let text = text.as_textbox(); - text.set_text_string(&format!("{_title}")); - } - - (0..NUM_MENU_TEXT_SLIDERS).for_each(|index| { - if let Some(text_pane) = root_pane.find_pane_by_name_recursive( - format!("trMod_menu_slider_{}_lbl", index).as_str(), - ) { - let text_pane = text_pane.as_textbox(); - text_pane.set_visible(true); - - match index { - 0 => { - text_pane.set_text_string("Min"); - - match gauge_vals.state { - GaugeState::MinHover | GaugeState::MinSelected => { - text_pane.m_bits |= 1 << TextBoxFlag::ShadowEnabled as u8; - text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8); - text_pane.set_color(255, 255, 255, 255); - } - _ => { - text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::ShadowEnabled as u8); - text_pane.m_bits |= 1 << TextBoxFlag::InvisibleBorderEnabled as u8; - text_pane.set_color(85, 89, 92, 255); - } - } - } - 1 => { - text_pane.set_text_string("Max"); - - match gauge_vals.state { - GaugeState::MaxHover | GaugeState::MaxSelected => { - text_pane.m_bits |= 1 << TextBoxFlag::ShadowEnabled as u8; - text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8); - text_pane.set_color(255, 255, 255, 255); - } - _ => { - text_pane.m_bits |= 1 << TextBoxFlag::InvisibleBorderEnabled as u8; - text_pane.m_bits = text_pane.m_bits & !(1 << TextBoxFlag::ShadowEnabled as u8); - text_pane.set_color(85, 89, 92, 255); - } - } - } - _ => panic!("Unexpected slider label index {}!", index), - } - } - - if let Some(text_pane) = root_pane - .find_pane_by_name_recursive(format!("trMod_menu_slider_{}", index).as_str()) - { - let text_pane = text_pane.as_textbox(); - text_pane.set_visible(true); - - match index { - 0 => text_pane.set_text_string(&format!("{selected_min}")), - 1 => text_pane.set_text_string(&format!("{selected_max}")), - _ => panic!("Unexpected slider label index {}!", index), - } - } - - if let Some(bg_left) = root_pane - .find_pane_by_name_recursive(format!("slider_btn_fg_{}", index).as_str()) - { - let bg_left_material = &mut *bg_left.as_picture().material; - - match index { - 0 => match gauge_vals.state { - GaugeState::MinHover => { - bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); - } - GaugeState::MinSelected => { - bg_left_material.set_white_res_color(BG_LEFT_SELECTED_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_SELECTED_BLACK_COLOR); - } - _ => { - bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); - } - }, - 1 => match gauge_vals.state { - GaugeState::MaxHover => { - bg_left_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); - } - GaugeState::MaxSelected => { - bg_left_material.set_white_res_color(BG_LEFT_SELECTED_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_SELECTED_BLACK_COLOR); - } - _ => { - bg_left_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR); - bg_left_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR); - } - }, - _ => panic!("Unexpected slider label index {}!", index), - } - bg_left.set_visible(true); - } - }); - } - } - - original!()(layout, draw_info, cmd_buffer); -} - -pub static mut MENU_PANE_PTR: u64 = 0; -pub static mut HAS_CREATED_OPT_BG: bool = false; -pub static mut HAS_CREATED_OPT_BG_BACK: bool = false; -pub static mut HAS_CREATED_SLIDER_BG: bool = false; -pub static mut HAS_CREATED_SLIDER_BG_BACK: bool = false; - -#[skyline::hook(offset = 0x493a0)] -pub unsafe fn layout_build_parts_impl( - layout: *mut Layout, - out_build_result_information: *mut u8, - device: *const u8, - data: *mut u8, - parts_build_data_set: *const u8, - build_arg_set: *const u8, - build_res_set: *const u8, - kind: u32, -) -> *mut Pane { - let layout_name = skyline::from_c_str((*layout).layout_name); - let _kind_str: String = kind.to_le_bytes().map(|b| b as char).iter().collect(); - - macro_rules! build { - ($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => { - paste::paste! { - &mut *(original!()( - layout, - out_build_result_information, - device, - &mut $block as *mut $resTyp as *mut u8, - parts_build_data_set, - build_arg_set, - build_res_set, - $kind, - ) as *mut $typ) - } - }; - } - - let root_pane = &mut *(*layout).root_pane; - let block = data as *mut ResPane; - let menu_pos = ResVec3::new(-360.0, 440.0, 0.0); - - if layout_name == "info_training_btn0_00_item" { - if !HAS_CREATED_OPT_BG && (*block).name_matches("icn_bg_main") { - (0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| { - let x = txt_idx % 3; - let y = txt_idx / 3; - - let x_offset = x as f32 * 500.0; - let y_offset = y as f32 * 85.0; - - let block = block as *mut ResPictureWithTex<2>; - let mut pic_menu_block = *block; - pic_menu_block.set_name(menu_text_bg_left_fmt!(x, y)); - pic_menu_block.picture.scale_x /= 1.5; - pic_menu_block.picture.set_pos(ResVec3::new( - menu_pos.x - 400.0 - 195.0 + x_offset, - menu_pos.y - 50.0 - y_offset, - 0.0, - )); - let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<2>, kind, Picture); - pic_menu_pane.detach(); - if MENU_PANE_PTR != 0 { - (*(MENU_PANE_PTR as *mut Pane)).append_child(pic_menu_pane); - HAS_CREATED_OPT_BG = true; - } - }); - } - - if !HAS_CREATED_OPT_BG_BACK && (*block).name_matches("btn_bg") { - (0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| { - let x = txt_idx % 3; - let y = txt_idx / 3; - - let x_offset = x as f32 * 500.0; - let y_offset = y as f32 * 85.0; - - let block = block as *mut ResWindowWithTexCoordsAndFrames<1, 4>; - - let mut bg_block = *block; - bg_block.set_name(menu_text_bg_back_fmt!(x, y)); - bg_block.scale_x /= 2.0; - bg_block.set_pos(ResVec3::new( - menu_pos.x - 400.0 + x_offset, - menu_pos.y - 50.0 - y_offset, - 0.0, - )); - let bg_pane = build!(bg_block, ResWindowWithTexCoordsAndFrames<1,4>, kind, Window); - bg_pane.detach(); - if MENU_PANE_PTR != 0 { - (*(MENU_PANE_PTR as *mut Pane)).append_child(bg_pane); - HAS_CREATED_OPT_BG_BACK = true; - } - }); - } - - if !HAS_CREATED_SLIDER_BG && (*block).name_matches("icn_bg_main") { - (0..NUM_MENU_TEXT_SLIDERS).for_each(|index| { - let x = index % 2; - - if MENU_PANE_PTR != 0 { - let slider_root = (*(MENU_PANE_PTR as *mut Pane)) - .find_pane_by_name("slider_menu", true) - .unwrap(); - let slider_bg = (*(MENU_PANE_PTR as *mut Pane)) - .find_pane_by_name("slider_ui_container", true) - .unwrap(); - let x_offset = x as f32 * 345.0; - - let block = block as *mut ResPictureWithTex<2>; - let mut pic_menu_block = *block; - - pic_menu_block.set_name(format!("slider_btn_fg_{}", index).as_str()); - - pic_menu_block.picture.scale_x /= 1.85; - pic_menu_block.picture.scale_y /= 1.25; - - pic_menu_block.set_pos(ResVec3::new( - slider_root.pos_x - 842.5 + x_offset, - slider_root.pos_y + slider_bg.size_y * 0.458, - 0.0, - )); - - let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<2>, kind, Picture); - pic_menu_pane.detach(); - - slider_root.append_child(pic_menu_pane); - HAS_CREATED_SLIDER_BG = true; - } - }); - } - - if !HAS_CREATED_SLIDER_BG_BACK && (*block).name_matches("btn_bg") { - (0..NUM_MENU_TEXT_SLIDERS).for_each(|index| { - let x = index % 2; - - if MENU_PANE_PTR != 0 { - let slider_root = (*(MENU_PANE_PTR as *mut Pane)) - .find_pane_by_name("slider_menu", true) - .unwrap(); - let slider_bg = (*(MENU_PANE_PTR as *mut Pane)) - .find_pane_by_name("slider_ui_container", true) - .unwrap(); - - let size_y = 90.0; - - let x_offset = x as f32 * 345.0; - - let block = block as *mut ResWindowWithTexCoordsAndFrames<1, 4>; - let mut bg_block = *block; - - bg_block.set_name(format!("slider_item_btn_{}", index).as_str()); - bg_block.scale_x /= 2.0; - - bg_block.set_size(ResVec2::new(605.0, size_y)); - - bg_block.set_pos(ResVec3::new( - slider_root.pos_x - 700.0 + x_offset, - slider_root.pos_y + slider_bg.size_y * 0.458, - 0.0, - )); - - let bg_pane = build!(bg_block, ResWindowWithTexCoordsAndFrames<1,4>, kind, Window); - bg_pane.detach(); - - slider_root.append_child(bg_pane); - HAS_CREATED_SLIDER_BG_BACK = true; - } - }); - } - } - - if layout_name != "info_training" { - return original!()( - layout, - out_build_result_information, - device, - data, - parts_build_data_set, - build_arg_set, - build_res_set, - kind, - ); - } - - // Menu creation - if (*block).name_matches("pic_numbase_01") { - // pic is loaded first, we can create our parent pane here. - let menu_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']); - let mut menu_pane_block = ResPane::new("trMod_menu"); - // Overall menu pane @ 0,0 to reason about positions globally - menu_pane_block.set_pos(ResVec3::default()); - let menu_pane = build!(menu_pane_block, ResPane, menu_pane_kind, Pane); - menu_pane.detach(); - root_pane.append_child(menu_pane); - if MENU_PANE_PTR != menu_pane as *mut Pane as u64 { - MENU_PANE_PTR = menu_pane as *mut Pane as u64; - HAS_CREATED_OPT_BG = false; - HAS_CREATED_OPT_BG_BACK = false; - HAS_SORTED_MENU_CHILDREN = false; - HAS_CREATED_SLIDER_BG = false; - HAS_CREATED_SLIDER_BG_BACK = false; - } - } - - // Menu footer background - if (*block).name_matches("pic_help_bg_00") { - let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); - let block = block as *mut ResPictureWithTex<1>; - // For menu backing - let mut pic_menu_block = *block; - pic_menu_block.set_name("trMod_menu_footer_bg"); - let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<1>, kind, Picture); - pic_menu_pane.detach(); - - menu_pane.append_child(pic_menu_pane); - } - - // Menu footer text - if (*block).name_matches("set_txt_help_00") { - let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); - - let block = data as *mut ResTextBox; - let mut text_block = *block; - text_block.set_name("trMod_menu_footer_txt"); - - let text_pane = build!(text_block, ResTextBox, kind, TextBox); - text_pane.set_text_string("Footer!"); - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - text_pane.set_default_material_colors(); - text_pane.set_color(255, 255, 255, 255); - text_pane.detach(); - menu_pane.append_child(text_pane); - } - - (0..NUM_MENU_TABS).for_each(|txt_idx| { - if (*block).name_matches("set_txt_num_01") { - let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); - - let block = data as *mut ResTextBox; - let mut text_block = *block; - text_block.enable_shadow(); - text_block.text_alignment(TextAlignment::Center); - - let x = txt_idx; - text_block.set_name(format!("trMod_menu_tab_{x}").as_str()); - - let mut x_offset = x as f32 * 300.0; - // Center current tab since we don't have a help key - if x == 1 { - x_offset -= 25.0; - } - text_block.set_pos(ResVec3::new( - menu_pos.x - 25.0 + x_offset, - menu_pos.y + 75.0, - 0.0, - )); - let text_pane = build!(text_block, ResTextBox, kind, TextBox); - text_pane.set_text_string(format!("Tab {txt_idx}!").as_str()); - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - text_pane.set_default_material_colors(); - text_pane.set_color(255, 255, 255, 255); - if txt_idx == 1 { - text_pane.set_color(255, 255, 0, 255); - } - text_pane.detach(); - menu_pane.append_child(text_pane); - - let mut help_block = *block; - // Font Idx 2 = nintendo64 which contains nice symbols - help_block.font_idx = 2; - - let x = txt_idx; - help_block.set_name(format!("trMod_menu_tab_help_{x}").as_str()); - - let x_offset = x as f32 * 300.0; - help_block.set_pos(ResVec3::new( - menu_pos.x - 250.0 + x_offset, - menu_pos.y + 75.0, - 0.0, - )); - let help_pane = build!(help_block, ResTextBox, kind, TextBox); - help_pane.set_text_string("abcd"); - let it = help_pane.m_text_buf as *mut u16; - match txt_idx { - // Left Tab: ZL - 0 => { - *it = 0xE0E6; - *(it.add(1)) = 0x0; - help_pane.m_text_len = 1; - } - 1 => { - *it = 0x0; - help_pane.m_text_len = 1; - } - // Right Tab: ZR - 2 => { - *it = 0xE0E7; - *(it.add(1)) = 0x0; - help_pane.m_text_len = 1; - } - _ => {} - } - - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - help_pane.set_default_material_colors(); - help_pane.set_color(255, 255, 255, 255); - help_pane.detach(); - menu_pane.append_child(help_pane); - } - }); - - (0..NUM_MENU_TEXT_OPTIONS).for_each(|txt_idx| { - let x = txt_idx % 3; - let y = txt_idx / 3; - - if (*block).name_matches("set_txt_num_01") { - let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); - - let block = data as *mut ResTextBox; - let mut text_block = *block; - text_block.enable_shadow(); - text_block.text_alignment(TextAlignment::Center); - - text_block.set_name(menu_text_name_fmt!(x, y)); - - let x_offset = x as f32 * 500.0; - let y_offset = y as f32 * 85.0; - text_block.set_pos(ResVec3::new( - menu_pos.x - 480.0 + x_offset, - menu_pos.y - 50.0 - y_offset, - 0.0, - )); - let text_pane = build!(text_block, ResTextBox, kind, TextBox); - text_pane.set_text_string(format!("Opt {txt_idx}!").as_str()); - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - text_pane.set_default_material_colors(); - text_pane.set_color(255, 255, 255, 255); - text_pane.detach(); - menu_pane.append_child(text_pane); - - let mut check_block = *block; - // Font Idx 2 = nintendo64 which contains nice symbols - check_block.font_idx = 2; - - check_block.set_name(menu_text_check_fmt!(x, y)); - check_block.set_pos(ResVec3::new( - menu_pos.x - 375.0 + x_offset, - menu_pos.y - 50.0 - y_offset, - 0.0, - )); - let check_pane = build!(check_block, ResTextBox, kind, TextBox); - check_pane.set_text_string(format!("Check {txt_idx}!").as_str()); - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - check_pane.set_default_material_colors(); - check_pane.set_color(0, 0, 0, 255); - check_pane.detach(); - menu_pane.append_child(check_pane); - } - }); - - // Slider visualization - - // UI Backing - let slider_root_name = "slider_menu"; - let slider_container_name = "slider_ui_container"; - - if (*block).name_matches("pic_numbase_01") { - let menu_pane = root_pane.find_pane_by_name("trMod_menu", true).unwrap(); - let slider_ui_root_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']); - let mut slider_ui_root_block = ResPane::new(slider_root_name); - - slider_ui_root_block.set_pos(ResVec3::default()); - - let slider_ui_root = build!( - slider_ui_root_block, - ResPane, - slider_ui_root_pane_kind, - Pane - ); - - slider_ui_root.detach(); - menu_pane.append_child(slider_ui_root); - - let block = data as *mut ResPictureWithTex<1>; - - let mut picture_block = *block; - - picture_block.set_name(slider_container_name); - picture_block.set_size(ResVec2::new(675.0, 300.0)); - picture_block.set_pos(ResVec3::new(-530.0, 180.0, 0.0)); - picture_block.tex_coords = [ - [ResVec2::new(0.0, 0.0)], - [ResVec2::new(1.0, 0.0)], - [ResVec2::new(0.0, 1.5)], - [ResVec2::new(1.0, 1.5)], - ]; - - let picture_pane = build!(picture_block, ResPictureWithTex<1>, kind, Picture); - picture_pane.detach(); - slider_ui_root.append_child(picture_pane); - } - - if (*block).name_matches("txt_cap_01") { - let container_pane = root_pane.find_pane_by_name(slider_root_name, true).unwrap(); - - let block = data as *mut ResTextBox; - let mut title_block = *block; - - title_block.set_name("slider_title"); - title_block.set_pos(ResVec3::new(-530.0, 285.0, 0.0)); - title_block.set_size(ResVec2::new(550.0, 100.0)); - title_block.font_size = ResVec2::new(50.0, 100.0); - - let title_pane = build!(title_block, ResTextBox, kind, TextBox); - - title_pane.set_text_string(format!("Slider Title").as_str()); - - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - title_pane.set_default_material_colors(); - - // Header should be white text - title_pane.set_color(255, 255, 255, 255); - title_pane.detach(); - container_pane.append_child(title_pane); - } - - (0..NUM_MENU_TEXT_SLIDERS).for_each(|idx| { - let x = idx % 2; - - let label_x_offset = x as f32 * 345.0; - - if (*block).name_matches("set_txt_num_01") { - let slider_root_pane = root_pane.find_pane_by_name(slider_root_name, true).unwrap(); - let slider_container = root_pane - .find_pane_by_name(slider_container_name, true) - .unwrap(); - - let block = data as *mut ResTextBox; - - let mut text_block = *block; - - text_block.enable_shadow(); - text_block.text_alignment(TextAlignment::Center); - - text_block.set_name(menu_text_slider_fmt!(idx)); - - let value_x_offset = x as f32 * 345.0; - - text_block.set_pos(ResVec3::new( - slider_root_pane.pos_x - 675.0 + value_x_offset, - slider_root_pane.pos_y + (slider_container.size_y * 0.458), - 0.0, - )); - - let text_pane = build!(text_block, ResTextBox, kind, TextBox); - text_pane.set_text_string(format!("Slider opt {idx}!").as_str()); - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - text_pane.set_default_material_colors(); - text_pane.set_color(0, 0, 0, 255); - text_pane.detach(); - slider_root_pane.append_child(text_pane); - - let mut label_block = *block; - - label_block.text_alignment(TextAlignment::Center); - label_block.set_name(menu_slider_label_fmt!(idx)); - label_block.set_pos(ResVec3::new( - slider_root_pane.pos_x - 750.0 + label_x_offset, - slider_root_pane.pos_y + slider_container.size_y * 0.458 + 5.0, - 0.0, - )); - label_block.font_size = ResVec2::new(25.0, 50.0); - - // Aligns text to the center horizontally - label_block.text_position = 4; - - label_block.shadow_offset = ResVec2::new(4.0, -3.0); - label_block.shadow_cols = [BLACK, BLACK]; - label_block.shadow_scale = ResVec2::new(1.0, 1.0); - - let label_pane = build!(label_block, ResTextBox, kind, TextBox); - - label_pane.set_text_string(format!("Slider opt {idx}!").as_str()); - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - label_pane.set_default_material_colors(); - label_pane.set_color(85, 89, 92, 255); - // Turns on text outline - label_pane.m_bits = label_pane.m_bits & !(1 << TextBoxFlag::InvisibleBorderEnabled as u8); - label_pane.detach(); - - slider_root_pane.append_child(label_pane); - } - }); - - // Display panes - (0..NUM_DISPLAY_PANES).for_each(|idx| { - let mod_prefix = "trMod_disp_"; - let parent_name = format!("{mod_prefix}{idx}"); - let pic_name = format!("{mod_prefix}{idx}_base"); - let header_name = format!("{mod_prefix}{idx}_header"); - let txt_name = format!("{mod_prefix}{idx}_txt"); - - if (*block).name_matches("pic_numbase_01") { - let block = block as *mut ResPictureWithTex<1>; - let mut pic_block = *block; - pic_block.set_name(pic_name.as_str()); - pic_block.set_pos(ResVec3::default()); - let pic_pane = build!(pic_block, ResPictureWithTex<1>, kind, Picture); - pic_pane.detach(); - - // pic is loaded first, we can create our parent pane here. - let disp_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']); - let mut disp_pane_block = ResPane::new(parent_name.as_str()); - disp_pane_block.set_pos(ResVec3::new(806.0, 390.0 - (idx as f32 * 110.0), 0.0)); - let disp_pane = build!(disp_pane_block, ResPane, disp_pane_kind, Pane); - disp_pane.detach(); - root_pane.append_child(disp_pane); - disp_pane.append_child(pic_pane); - } - - if (*block).name_matches("set_txt_num_01") { - let disp_pane = root_pane - .find_pane_by_name(parent_name.as_str(), true) - .unwrap(); - - let block = data as *mut ResTextBox; - let mut text_block = *block; - text_block.set_name(txt_name.as_str()); - text_block.set_pos(ResVec3::new(-10.0, -25.0, 0.0)); - let text_pane = build!(text_block, ResTextBox, kind, TextBox); - text_pane.set_text_string(format!("Pane {idx}!").as_str()); - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - text_pane.set_default_material_colors(); - text_pane.detach(); - disp_pane.append_child(text_pane); - } - - if (*block).name_matches("txt_cap_01") { - let disp_pane = root_pane - .find_pane_by_name(parent_name.as_str(), true) - .unwrap(); - - let block = data as *mut ResTextBox; - let mut header_block = *block; - header_block.set_name(header_name.as_str()); - header_block.set_pos(ResVec3::new(0.0, 25.0, 0.0)); - let header_pane = build!(header_block, ResTextBox, kind, TextBox); - header_pane.set_text_string(format!("Header {idx}").as_str()); - // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - header_pane.set_default_material_colors(); - // Header should be white text - header_pane.set_color(255, 255, 255, 255); - header_pane.detach(); - disp_pane.append_child(header_pane); - } - }); - - original!()( - layout, - out_build_result_information, - device, - data, - parts_build_data_set, - build_arg_set, - build_res_set, - kind, - ) -} - -pub fn install_hooks() { - skyline::install_hooks!(handle_draw, layout_build_parts_impl,); -} diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index 3662e25..f027a46 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -1329,7 +1329,7 @@ impl<'a> SubMenu<'a> { } } -#[derive(Content, Serialize)] +#[derive(Content, Serialize, Clone)] pub struct Tab<'a> { pub tab_id: &'a str, pub tab_title: &'a str, @@ -1372,12 +1372,12 @@ impl<'a> Tab<'a> { } } -#[derive(Content, Serialize)] +#[derive(Content, Serialize, Clone)] pub struct UiMenu<'a> { pub tabs: Vec>, } -pub unsafe fn get_menu() -> UiMenu<'static> { +pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu<'static> { let mut overall_menu = UiMenu { tabs: Vec::new() }; let mut mash_tab = Tab { @@ -1390,98 +1390,98 @@ pub unsafe fn get_menu() -> UiMenu<'static> { "mash_state", "Mash Toggles: Actions to be performed as soon as possible", false, - &(MENU.mash_state.bits as u32), + &(menu.mash_state.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Followup Toggles", "follow_up", "Followup Toggles: Actions to be performed after the Mash option", false, - &(MENU.follow_up.bits as u32), + &(menu.follow_up.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Mash Triggers", "mash_triggers", "Mash triggers: When the Mash Option will be performed", false, - &(MENU.mash_triggers.bits as u32), + &(menu.mash_triggers.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Attack Angle", "attack_angle", "Attack Angle: For attacks that can be angled, such as some forward tilts", false, - &(MENU.attack_angle.bits as u32), + &(menu.attack_angle.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Throw Options", "throw_state", "Throw Options: Throw to be performed when a grab is landed", false, - &(MENU.throw_state.bits as u32), + &(menu.throw_state.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Throw Delay", "throw_delay", "Throw Delay: How many frames to delay the throw option", false, - &(MENU.throw_delay.bits as u32), + &(menu.throw_delay.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Pummel Delay", "pummel_delay", "Pummel Delay: How many frames after a grab to wait before starting to pummel", false, - &(MENU.pummel_delay.bits as u32), + &(menu.pummel_delay.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Falling Aerials", "falling_aerials", "Falling Aerials: Should aerials be performed when rising or when falling", false, - &(MENU.falling_aerials.bits as u32), + &(menu.falling_aerials.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Full Hop", "full_hop", "Full Hop: Should the CPU perform a full hop or a short hop", false, - &(MENU.full_hop.bits as u32), + &(menu.full_hop.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Aerial Delay", "aerial_delay", "Aerial Delay: How long to delay a Mash aerial attack", false, - &(MENU.aerial_delay.bits as u32), + &(menu.aerial_delay.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Fast Fall", "fast_fall", "Fast Fall: Should the CPU fastfall during a jump", false, - &(MENU.fast_fall.bits as u32), + &(menu.fast_fall.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Fast Fall Delay", "fast_fall_delay", "Fast Fall Delay: How many frames the CPU should delay their fastfall", false, - &(MENU.fast_fall_delay.bits as u32), + &(menu.fast_fall_delay.bits as u32), ); mash_tab.add_submenu_with_toggles::( "OoS Offset", "oos_offset", "OoS Offset: How many times the CPU shield can be hit before performing a Mash option", false, - &(MENU.oos_offset.bits as u32), + &(menu.oos_offset.bits as u32), ); mash_tab.add_submenu_with_toggles::( "Reaction Time", "reaction_time", "Reaction Time: How many frames to delay before performing a mash option", false, - &(MENU.reaction_time.bits as u32), + &(menu.reaction_time.bits as u32), ); overall_menu.tabs.push(mash_tab); @@ -1495,77 +1495,77 @@ pub unsafe fn get_menu() -> UiMenu<'static> { "air_dodge_dir", "Airdodge Direction: Direction to angle airdodges", false, - &(MENU.air_dodge_dir.bits as u32), + &(menu.air_dodge_dir.bits as u32), ); defensive_tab.add_submenu_with_toggles::( "DI Direction", "di_state", "DI Direction: Direction to angle the directional influence during hitlag", false, - &(MENU.di_state.bits as u32), + &(menu.di_state.bits as u32), ); defensive_tab.add_submenu_with_toggles::( "SDI Direction", "sdi_state", "SDI Direction: Direction to angle the smash directional influence during hitlag", false, - &(MENU.sdi_state.bits as u32), + &(menu.sdi_state.bits as u32), ); defensive_tab.add_submenu_with_toggles::( "SDI Strength", "sdi_strength", "SDI Strength: Relative strength of the smash directional influence inputs", true, - &(MENU.sdi_strength as u32), + &(menu.sdi_strength as u32), ); defensive_tab.add_submenu_with_toggles::( "Clatter Strength", "clatter_strength", "Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.", true, - &(MENU.clatter_strength as u32), + &(menu.clatter_strength as u32), ); defensive_tab.add_submenu_with_toggles::( "Ledge Options", "ledge_state", "Ledge Options: Actions to be taken when on the ledge", false, - &(MENU.ledge_state.bits as u32), + &(menu.ledge_state.bits as u32), ); defensive_tab.add_submenu_with_toggles::( "Ledge Delay", "ledge_delay", "Ledge Delay: How many frames to delay the ledge option", false, - &(MENU.ledge_delay.bits as u32), + &(menu.ledge_delay.bits as u32), ); defensive_tab.add_submenu_with_toggles::( "Tech Options", "tech_state", "Tech Options: Actions to take when slammed into a hard surface", false, - &(MENU.tech_state.bits as u32), + &(menu.tech_state.bits as u32), ); defensive_tab.add_submenu_with_toggles::( "Mistech Options", "miss_tech_state", "Mistech Options: Actions to take after missing a tech", false, - &(MENU.miss_tech_state.bits as u32), + &(menu.miss_tech_state.bits as u32), ); defensive_tab.add_submenu_with_toggles::( "Shield Toggles", "shield_state", "Shield Toggles: CPU Shield Behavior", true, - &(MENU.shield_state as u32), + &(menu.shield_state as u32), ); defensive_tab.add_submenu_with_toggles::( "Shield Tilt", "shield_tilt", "Shield Tilt: Direction to tilt the shield", false, // TODO: Should this be true? - &(MENU.shield_tilt.bits as u32), + &(menu.shield_tilt.bits as u32), ); defensive_tab.add_submenu_with_toggles::( @@ -1573,7 +1573,7 @@ pub unsafe fn get_menu() -> UiMenu<'static> { "crouch", "Crouch: Should the CPU crouch when on the ground", true, - &(MENU.crouch as u32), + &(menu.crouch as u32), ); overall_menu.tabs.push(defensive_tab); @@ -1587,63 +1587,63 @@ pub unsafe fn get_menu() -> UiMenu<'static> { "save_state_mirroring", "Mirroring: Flips save states in the left-right direction across the stage center", true, - &(MENU.save_state_mirroring as u32), + &(menu.save_state_mirroring as u32), ); save_state_tab.add_submenu_with_toggles::( "Auto Save States", "save_state_autoload", "Auto Save States: Load save state when any fighter dies", true, - &(MENU.save_state_autoload as u32), + &(menu.save_state_autoload as u32), ); save_state_tab.add_submenu_with_toggles::( "Save Dmg (CPU)", "save_damage_cpu", "Save Damage: Should save states retain CPU damage", true, - &(MENU.save_damage_cpu.bits as u32), + &(menu.save_damage_cpu.bits as u32), ); save_state_tab.add_submenu_with_slider::( "Dmg Range (CPU)", "save_damage_limits_cpu", "Limits on random damage to apply to the CPU when loading a save state", - &(MENU.save_damage_limits_cpu.0 as u32), - &(MENU.save_damage_limits_cpu.1 as u32), + &(menu.save_damage_limits_cpu.0 as u32), + &(menu.save_damage_limits_cpu.1 as u32), ); save_state_tab.add_submenu_with_toggles::( "Save Dmg (Player)", "save_damage_player", "Save Damage: Should save states retain player damage", true, - &(MENU.save_damage_player.bits as u32), + &(menu.save_damage_player.bits as u32), ); save_state_tab.add_submenu_with_slider::( "Dmg Range (Player)", "save_damage_limits_player", "Limits on random damage to apply to the player when loading a save state", - &(MENU.save_damage_limits_player.0 as u32), - &(MENU.save_damage_limits_player.1 as u32), + &(menu.save_damage_limits_player.0 as u32), + &(menu.save_damage_limits_player.1 as u32), ); save_state_tab.add_submenu_with_toggles::( "Enable Save States", "save_state_enable", "Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt.", true, - &(MENU.save_state_enable as u32), + &(menu.save_state_enable as u32), ); save_state_tab.add_submenu_with_toggles::( "Character Item", "character_item", "Character Item: CPU/Player item to hold when loading a save state", true, - &(MENU.character_item as u32), + &(menu.character_item as u32), ); save_state_tab.add_submenu_with_toggles::( "Buff Options", "buff_state", "Buff Options: Buff(s) to be applied to respective character when loading save states", false, - &(MENU.buff_state.bits as u32), + &(menu.buff_state.bits as u32), ); overall_menu.tabs.push(save_state_tab); @@ -1657,42 +1657,42 @@ pub unsafe fn get_menu() -> UiMenu<'static> { "frame_advantage", "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable", true, - &(MENU.frame_advantage as u32), + &(menu.frame_advantage as u32), ); misc_tab.add_submenu_with_toggles::( "Hitbox Visualization", "hitbox_vis", "Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects", true, - &(MENU.hitbox_vis as u32), + &(menu.hitbox_vis as u32), ); misc_tab.add_submenu_with_toggles::( "Input Delay", "input_delay", "Input Delay: Frames to delay player inputs by", true, - &(MENU.input_delay.bits as u32), + &(menu.input_delay.bits as u32), ); misc_tab.add_submenu_with_toggles::( "Stage Hazards", "stage_hazards", "Stage Hazards: Should stage hazards be present", true, - &(MENU.stage_hazards as u32), + &(menu.stage_hazards as u32), ); misc_tab.add_submenu_with_toggles::( "Quick Menu", "quick_menu", "Quick Menu: Should use quick or web menu", true, - &(MENU.quick_menu as u32), + &(menu.quick_menu as u32), ); misc_tab.add_submenu_with_toggles::( "HUD", "hud", "HUD: Turn UI on or off", true, - &(MENU.hud as u32), + &(menu.hud as u32), ); overall_menu.tabs.push(misc_tab); diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs index 6bc5221..32c884c 100644 --- a/training_mod_tui/src/lib.rs +++ b/training_mod_tui/src/lib.rs @@ -1,4 +1,4 @@ -use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu}; +use training_mod_consts::{MenuJsonStruct, Slider, SubMenu, SubMenuType, Toggle, UiMenu, ui_menu, TrainingModpackMenu}; use tui::{ backend::Backend, layout::{Constraint, Corner, Direction, Layout, Rect}, @@ -20,6 +20,14 @@ use crate::list::{MultiStatefulList, StatefulList}; static NX_TUI_WIDTH: u16 = 66; +#[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 @@ -28,11 +36,12 @@ pub struct App<'a> { pub menu_items: HashMap<&'a str, MultiStatefulList>>, pub selected_sub_menu_toggles: MultiStatefulList>, pub selected_sub_menu_slider: DoubleEndedGauge, - pub outer_list: bool, + pub page: AppPage, + pub default_menu: (UiMenu<'a>, String), } impl<'a> App<'a> { - pub fn new(menu: UiMenu<'a>) -> App<'a> { + pub fn new(menu: UiMenu<'a>, default_menu: (UiMenu<'a>, String)) -> App<'a> { let num_lists = 3; let mut menu_items_stateful = HashMap::new(); @@ -48,7 +57,8 @@ impl<'a> App<'a> { menu_items: menu_items_stateful, selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0), selected_sub_menu_slider: DoubleEndedGauge::new(), - outer_list: true, + page: AppPage::SUBMENU, + default_menu: default_menu }; app.set_sub_menu_items(); app @@ -269,7 +279,7 @@ impl<'a> App<'a> { } /// Different behavior depending on the current menu location - /// Outer list: Sets self.outer_list to false + /// 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) { @@ -288,14 +298,14 @@ impl<'a> App<'a> { .items .get_mut(list_idx) .unwrap(); - if self.outer_list { - self.outer_list = false; + if self.page == AppPage::SUBMENU { match SubMenuType::from_str(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_str(selected_sub_menu._type) { @@ -371,9 +381,9 @@ impl<'a> App<'a> { } /// Different behavior depending on the current menu location - /// Outer list: None - /// Toggle submenu: Sets self.outer_list to true - /// Slider submenu: If in a selected state, then commit changes and change to hover. Else set self.outer_list to true + /// 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 @@ -417,26 +427,55 @@ impl<'a> App<'a> { }, _ => {} } - self.outer_list = true; + self.page = AppPage::SUBMENU; self.set_sub_menu_items(); } + /// Save defaults command + pub fn on_x(&mut self) { + if self.page == AppPage::SUBMENU { + let json = self.to_json(); + unsafe { + self.default_menu = (ui_menu(serde_json::from_str::(&json).unwrap()), json); + } + } + } + + /// Reset current submenu to defaults pub fn on_l(&mut self) { - if self.outer_list { + if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { + let json = self.to_json(); + let mut json_value = serde_json::from_str::(&json).unwrap(); + let selected_sub_menu= self.sub_menu_selected(); + let id = selected_sub_menu.submenu_id; + let default_json_value = serde_json::from_str::(&self.default_menu.1).unwrap(); + *json_value.get_mut(id).unwrap() = default_json_value.get(id).unwrap().clone(); + let new_menu = serde_json::from_value::(json_value).unwrap(); + *self = App::new(unsafe { ui_menu(new_menu) }, self.default_menu.clone()); + } + } + + /// Reset all menus to defaults + pub fn on_r(&mut self) { + *self = App::new(self.default_menu.0.clone(), self.default_menu.clone()); + } + + pub fn on_zl(&mut self) { + if self.page == AppPage::SUBMENU { self.tabs.previous(); self.set_sub_menu_items(); } } - pub fn on_r(&mut self) { - if self.outer_list { + pub fn on_zr(&mut self) { + if self.page == AppPage::SUBMENU { self.tabs.next(); self.set_sub_menu_items(); } } pub fn on_up(&mut self) { - if self.outer_list { + if self.page == AppPage::SUBMENU { self.menu_items .get_mut( self.tabs @@ -447,13 +486,13 @@ impl<'a> App<'a> { .unwrap() .previous(); self.set_sub_menu_items(); - } else { + } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { self.sub_menu_previous(); } } pub fn on_down(&mut self) { - if self.outer_list { + if self.page == AppPage::SUBMENU { self.menu_items .get_mut( self.tabs @@ -464,13 +503,13 @@ impl<'a> App<'a> { .unwrap() .next(); self.set_sub_menu_items(); - } else { + } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { self.sub_menu_next(); } } pub fn on_left(&mut self) { - if self.outer_list { + if self.page == AppPage::SUBMENU { self.menu_items .get_mut( self.tabs @@ -481,13 +520,13 @@ impl<'a> App<'a> { .unwrap() .previous_list(); self.set_sub_menu_items(); - } else { + } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { self.sub_menu_previous_list(); } } pub fn on_right(&mut self) { - if self.outer_list { + if self.page == AppPage::SUBMENU { self.menu_items .get_mut( self.tabs @@ -498,13 +537,221 @@ impl<'a> App<'a> { .unwrap() .next_list(); self.set_sub_menu_items(); - } else { + } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER { self.sub_menu_next_list(); } } + + /// Returns JSON representation of current menu settings + pub fn to_json(&self) -> String { + let mut settings = Map::new(); + for key in self.menu_items.keys() { + for list in &self.menu_items.get(key).unwrap().lists { + for sub_menu in &list.items { + if !sub_menu.toggles.is_empty() { + let val: u32 = sub_menu + .toggles + .iter() + .filter(|t| t.checked) + .map(|t| t.toggle_value) + .sum(); + settings.insert(sub_menu.submenu_id.to_string(), json!(val)); + } else if sub_menu.slider.is_some() { + let s: &Slider = sub_menu.slider.as_ref().unwrap(); + let val: Vec = vec![s.selected_min, s.selected_max]; + settings.insert(sub_menu.submenu_id.to_string(), json!(val)); + } else { + panic!("Could not collect settings for {:?}", sub_menu.submenu_id); + } + } + } + } + serde_json::to_string(&settings).unwrap() + } + + + /// Returns the current menu selections and the default menu selections. + pub fn get_menu_selections(&self) -> String { + serde_json::to_string( + &MenuJsonStruct { + menu: serde_json::from_str(self.to_json().as_str()).unwrap(), + defaults_menu: serde_json::from_str(self.default_menu.1.clone().as_str()).unwrap(), + }).unwrap() + } } -pub fn ui(f: &mut Frame, app: &mut App) -> String { +fn render_submenu_page(f: &mut Frame, app: &mut App, list_chunks: Vec, help_chunk: Rect) { + let tab_selected = app.tab_selected(); + let mut item_help = None; + for (list_section, stateful_list) in app + .menu_items + .get(tab_selected) + .unwrap() + .lists + .iter() + .enumerate() + { + let items: Vec = stateful_list + .items + .iter() + .map(|i| { + let lines = vec![Spans::from(if stateful_list.state.selected().is_some() { + i.submenu_title.to_owned() + } else { + " ".to_owned() + i.submenu_title + })]; + 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); + } + + f.render_stateful_widget(list, list_chunks[list_section], &mut state); + } + + let help_paragraph = Paragraph::new( + item_help.unwrap_or("").replace('\"', "") + + "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab | X: Save Defaults", + ) + .style(Style::default().fg(Color::Cyan)); + f.render_widget(help_paragraph, help_chunk); +} + +pub fn render_toggle_page(f: &mut Frame, app: &mut App, list_chunks: Vec, help_chunk: Rect) { + let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states(); + for list_section in 0..sub_menu_str_lists.len() { + let sub_menu_str = sub_menu_str_lists[list_section].0.clone(); + let sub_menu_state = &mut sub_menu_str_lists[list_section].1; + let values_items: Vec = sub_menu_str + .iter() + .map(|s| { + ListItem::new(vec![Spans::from( + (if s.0 { "X " } else { " " }).to_owned() + s.1, + )]) + }) + .collect(); + + let values_list = List::new(values_items) + .block(Block::default().title(if list_section == 0 { title } else { "" })) + .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('\"', "") + "\nA: Select toggle | B: Exit submenu | X: Reset to defaults", + ) + .style(Style::default().fg(Color::Cyan)); + f.render_widget(help_paragraph, help_chunk); +} + + +pub fn render_slider_page(f: &mut Frame, app: &mut App, vertical_chunk: Rect, help_chunk: Rect) { + let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider(); + let abs_min = gauge_vals.abs_min; + let abs_max = gauge_vals.abs_max; + let selected_min = gauge_vals.selected_min; + let selected_max = gauge_vals.selected_max; + let lbl_ratio = 0.95; // Needed so that the upper limit label is visible + let constraints = [ + Constraint::Ratio((lbl_ratio * (selected_min-abs_min) as f32) as u32, abs_max-abs_min), + Constraint::Ratio((lbl_ratio * (selected_max-selected_min) as f32) as u32, abs_max-abs_min), + Constraint::Ratio((lbl_ratio * (abs_max-selected_max) as f32) as u32, abs_max-abs_min), + Constraint::Min(3), // For upper limit label + ]; + let gauge_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints) + .split(vertical_chunk); + + let slider_lbls = [ + abs_min, + selected_min, + selected_max, + abs_max, + ]; + for (idx, lbl) in slider_lbls.iter().enumerate() { + let mut line_set = tui::symbols::line::NORMAL; + line_set.horizontal = "-"; + let mut gauge = LineGauge::default() + .ratio(1.0) + .label(format!("{}", lbl)) + .style(Style::default().fg(Color::White)) + .line_set(line_set) + .gauge_style(Style::default().fg(Color::White).bg(Color::Black)); + if idx == 1 { + // Slider between selected_min and selected_max + match gauge_vals.state { + GaugeState::MinHover => { + gauge = gauge.style(Style::default().fg(Color::Red)) + } + GaugeState::MinSelected => { + gauge = gauge.style(Style::default().fg(Color::Green)) + } + _ => {} + } + gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black)); + } else if idx == 2 { + // Slider between selected_max and abs_max + match gauge_vals.state { + GaugeState::MaxHover => { + gauge = gauge.style(Style::default().fg(Color::Red)) + } + GaugeState::MaxSelected => { + gauge = gauge.style(Style::default().fg(Color::Green)) + } + _ => {} + } + } else if idx == 3 { + // Slider for abs_max + // We only want the label to show, so set the line character to " " + let mut line_set = tui::symbols::line::NORMAL; + line_set.horizontal = " "; + gauge = gauge.line_set(line_set); + + // For some reason, the selected_max slider displays on top + // So we need to change the abs_max slider styling to match + // If the selected_max is close enough to the abs_max + if (selected_max as f32 / abs_max as f32) > 0.95 { + gauge = gauge.style(match gauge_vals.state { + GaugeState::MaxHover => Style::default().fg(Color::Red), + GaugeState::MaxSelected => Style::default().fg(Color::Green), + _ => Style::default(), + }) + } + } + f.render_widget(gauge, gauge_chunks[idx]); + } + + let help_paragraph = Paragraph::new( + help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu | X: Reset to defaults", + ) + .style(Style::default().fg(Color::Cyan)); + f.render_widget(help_paragraph, help_chunk); +} + +/// Run +pub fn ui(f: &mut Frame, app: &mut App) { let app_tabs = &app.tabs; let tab_selected = app_tabs.state.selected().unwrap(); let mut span_selected = Spans::default(); @@ -611,202 +858,10 @@ pub fn ui(f: &mut Frame, app: &mut App) -> String { f.render_widget(tabs, vertical_chunks[0]); - if app.outer_list { - let tab_selected = app.tab_selected(); - let mut item_help = None; - for (list_section, stateful_list) in app - .menu_items - .get(tab_selected) - .unwrap() - .lists - .iter() - .enumerate() - { - let items: Vec = stateful_list - .items - .iter() - .map(|i| { - let lines = vec![Spans::from(if stateful_list.state.selected().is_some() { - i.submenu_title.to_owned() - } else { - " ".to_owned() + i.submenu_title - })]; - 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); - } - - f.render_stateful_widget(list, list_chunks[list_section], &mut state); - } - - // TODO: Add Save Defaults - let help_paragraph = Paragraph::new( - item_help.unwrap_or("").replace('\"', "") - + "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab", - ) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(help_paragraph, vertical_chunks[2]); - } else { - if matches!(app.selected_sub_menu_slider.state, GaugeState::None) { - let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states(); - for list_section in 0..sub_menu_str_lists.len() { - let sub_menu_str = sub_menu_str_lists[list_section].0.clone(); - let sub_menu_state = &mut sub_menu_str_lists[list_section].1; - let values_items: Vec = sub_menu_str - .iter() - .map(|s| { - ListItem::new(vec![Spans::from( - (if s.0 { "X " } else { " " }).to_owned() + s.1, - )]) - }) - .collect(); - - let values_list = List::new(values_items) - .block(Block::default().title(if list_section == 0 { title } else { "" })) - .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('\"', "") + "\nA: Select toggle | B: Exit submenu", - ) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(help_paragraph, vertical_chunks[2]); - } else { - 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_chunks[1]); - - 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('\"', "") + "\nA: Select toggle | B: Exit submenu", - ) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(help_paragraph, vertical_chunks[2]); - } + 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!() } - - // Collect settings - to_json(app) - - // TODO: Add saveDefaults -} - -pub fn to_json(app: &App) -> String { - let mut settings = Map::new(); - for key in app.menu_items.keys() { - for list in &app.menu_items.get(key).unwrap().lists { - for sub_menu in &list.items { - if !sub_menu.toggles.is_empty() { - let val: u32 = sub_menu - .toggles - .iter() - .filter(|t| t.checked) - .map(|t| t.toggle_value) - .sum(); - settings.insert(sub_menu.submenu_id.to_string(), json!(val)); - } else if sub_menu.slider.is_some() { - let s: &Slider = sub_menu.slider.as_ref().unwrap(); - let val: Vec = vec![s.selected_min, s.selected_max]; - settings.insert(sub_menu.submenu_id.to_string(), json!(val)); - } else { - panic!("Could not collect settings for {:?}", sub_menu.submenu_id); - } - } - } - } - serde_json::to_string(&settings).unwrap() -} +} \ No newline at end of file diff --git a/training_mod_tui/src/main.rs b/training_mod_tui/src/main.rs index 4cf9f31..1e72b63 100644 --- a/training_mod_tui/src/main.rs +++ b/training_mod_tui/src/main.rs @@ -17,10 +17,10 @@ use tui::Terminal; use training_mod_consts::*; -fn test_backend_setup(ui_menu: UiMenu) -> Result< - (Terminal, training_mod_tui::App), +fn test_backend_setup<'a>(ui_menu: UiMenu<'a>, menu_defaults: (UiMenu<'a>, String)) -> Result< + (Terminal, training_mod_tui::App<'a>), Box> { - let app = training_mod_tui::App::new(ui_menu); + let app = training_mod_tui::App::<'a>::new(ui_menu, menu_defaults); let backend = tui::backend::TestBackend::new(75, 15); let terminal = Terminal::new(backend)?; let mut state = tui::widgets::ListState::default(); @@ -30,26 +30,154 @@ fn test_backend_setup(ui_menu: UiMenu) -> Result< } #[test] -fn ensure_menu_retains_selections() -> Result<(), Box> { +fn test_set_airdodge() -> Result<(), Box> { let menu; - let prev_menu; + let mut prev_menu; + let menu_defaults; unsafe { - prev_menu = MENU; - menu = get_menu(); + 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)?; - let mut json_response = String::new(); - let _frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?; + let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?; + // Enter Mash Toggles + app.on_a(); + // Set Mash Airdodge + app.on_a(); + let menu_json = app.get_menu_selections(); + let menu_struct = serde_json::from_str::(&menu_json).unwrap(); + let menu = menu_struct.menu; + let _ = menu_struct.defaults_menu; + prev_menu.mash_state.toggle(Action::AIR_DODGE); + assert_eq!( + serde_json::to_string(&prev_menu).unwrap(), + serde_json::to_string(&menu).unwrap() + ); + + Ok(()) +} + +#[test] +fn test_ensure_menu_retains_selections() -> Result<(), Box> { + let menu; + let prev_menu; + let menu_defaults; unsafe { - MENU = serde_json::from_str::(&json_response).unwrap(); - // 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() - ); + prev_menu = MENU.clone(); + menu = ui_menu(MENU); + menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap()); } + let (_terminal, app) = test_backend_setup(menu, menu_defaults)?; + let menu_json = app.get_menu_selections(); + let menu_struct = serde_json::from_str::(&menu_json).unwrap(); + let menu = menu_struct.menu; + let _ = menu_struct.defaults_menu; + // At this point, we didn't change the menu at all; we should still see all the same options. + assert_eq!( + serde_json::to_string(&prev_menu).unwrap(), + serde_json::to_string(&menu).unwrap() + ); + + Ok(()) +} + +#[test] +fn test_save_and_reset_defaults() -> Result<(), Box> { + let menu; + let mut prev_menu; + let menu_defaults; + unsafe { + prev_menu = MENU.clone(); + menu = ui_menu(MENU); + menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap()); + } + + let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?; + + // Enter Mash Toggles + app.on_a(); + // Set Mash Airdodge + app.on_a(); + // Return to submenu selection + app.on_b(); + // Save Defaults + app.on_x(); + // Enter Mash Toggles again + app.on_a(); + // Unset Mash Airdodge + app.on_a(); + + let menu_json = app.get_menu_selections(); + let menu_struct = serde_json::from_str::(&menu_json).unwrap(); + let menu = menu_struct.menu; + let defaults_menu = menu_struct.defaults_menu; + + assert_eq!( + serde_json::to_string(&prev_menu).unwrap(), + serde_json::to_string(&menu).unwrap(), + "The menu should be the same as how we started" + ); + prev_menu.mash_state.toggle(Action::AIR_DODGE); + assert_eq!( + serde_json::to_string(&prev_menu).unwrap(), + serde_json::to_string(&defaults_menu).unwrap(), + "The defaults menu should have Mash Airdodge" + ); + + // Reset current menu alone to defaults + app.on_l(); + let menu_json = app.get_menu_selections(); + let menu_struct = serde_json::from_str::(&menu_json).unwrap(); + let menu = menu_struct.menu; + let _ = menu_struct.defaults_menu; + + assert_eq!( + serde_json::to_string(&prev_menu).unwrap(), + serde_json::to_string(&menu).unwrap(), + "The menu should have Mash Airdodge" + ); + + // Enter Mash 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.on_x(); + // 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.on_r(); + let menu_json = app.get_menu_selections(); + let menu_struct = serde_json::from_str::(&menu_json).unwrap(); + let menu = menu_struct.menu; + let _ = menu_struct.defaults_menu; + + prev_menu.mash_state.toggle(Action::AIR_DODGE); + prev_menu.follow_up.toggle(Action::JUMP); + assert_eq!( + serde_json::to_string(&prev_menu).unwrap(), + serde_json::to_string(&menu).unwrap(), + "The menu should have Mash Airdodge off and Followup Jump on" + ); + + Ok(()) } @@ -57,17 +185,23 @@ fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); let inputs = args.get(1); let menu; + let menu_defaults; + unsafe { - menu = get_menu(); + 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)?; + 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.on_x(), "L" => app.on_l(), "R" => app.on_r(), + "O" => app.on_zl(), + "P" => app.on_zr(), "A" => app.on_a(), "B" => app.on_b(), "UP" => app.on_up(), @@ -78,8 +212,8 @@ fn main() -> Result<(), Box> { } }) } - let mut json_response = String::new(); - let frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?; + 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); @@ -89,11 +223,11 @@ fn main() -> Result<(), Box> { } println!(); - println!("json_response:\n{}", json_response); + println!("Menu:\n{menu_json}"); } #[cfg(feature = "has_terminal")] { - let app = training_mod_tui::App::new(menu); + let app = training_mod_tui::App::new(menu, menu_defaults); // setup terminal enable_raw_mode()?; @@ -117,10 +251,10 @@ fn main() -> Result<(), Box> { if let Err(err) = res { println!("{:?}", err) } else { - println!("JSON: {}", res.as_ref().unwrap()); + println!("JSONs: {:#?}", res.as_ref().unwrap()); unsafe { - MENU = serde_json::from_str::(&res.as_ref().unwrap()).unwrap(); - println!("MENU: {:#?}", MENU); + let menu = serde_json::from_str::(&res.as_ref().unwrap()).unwrap(); + println!("menu: {:#?}", menu); } } } @@ -135,9 +269,9 @@ fn run_app( tick_rate: Duration, ) -> io::Result { let mut last_tick = Instant::now(); - let mut json_response = String::new(); loop { - terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app).clone())?; + 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()) @@ -146,7 +280,10 @@ fn run_app( if crossterm::event::poll(timeout)? { if let Event::Key(key) = event::read()? { match key.code { - KeyCode::Char('q') => return Ok(json_response), + KeyCode::Char('q') => return Ok(menu_json), + KeyCode::Char('x') => app.on_x(), + KeyCode::Char('p') => app.on_zr(), + KeyCode::Char('o') => app.on_zl(), KeyCode::Char('r') => app.on_r(), KeyCode::Char('l') => app.on_l(), KeyCode::Left => app.on_left(),