diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index abbf2dc..1bb9f2c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -113,7 +113,6 @@ jobs: cp libparam_hook.nro ${{env.SMASH_PLUGIN_DIR}}/libparam_hook.nro cp libnro_hook.nro ${{env.SMASH_PLUGIN_DIR}}/libnro_hook.nro cp libnn_hid_hook.nro ${{env.SMASH_PLUGIN_DIR}}/libnn_hid_hook.nro - mv static/libtraining_modpack_menu.nro ${{env.SMASH_PLUGIN_DIR}}/libtraining_modpack_menu.nro cp -r static/* ${{env.SMASH_WEB_DIR}} zip -r training_modpack_beta.zip atmosphere - name: Update Release diff --git a/README.md b/README.md index dbbf4ce..db9f278 100644 --- a/README.md +++ b/README.md @@ -262,8 +262,7 @@ SD Card Root ├── libnn_hid_hook.nro ├── libnro_hook.nro ├── libparam_hook.nro - ├── libtraining_modpack.nro - └── libtraining_modpack_menu.nro + └── libtraining_modpack.nro ``` To install a beta version of the modpack, follow the same procedure using the [latest beta release](https://github.com/jugeeya/UltimateTrainingModpack/tree/beta) on Github. Beta releases may have additional features and bugfixes, but are subject to change. diff --git a/src/common/menu.rs b/src/common/menu.rs index 5a90327..34b812e 100644 --- a/src/common/menu.rs +++ b/src/common/menu.rs @@ -2,7 +2,6 @@ use crate::common::*; use crate::events::{Event, EVENT_QUEUE}; use crate::training::frame_counter; -use owo_colors::OwoColorize; use ramhorns::Template; use skyline::info::get_program_id; use skyline::nn::hid::NpadGcState; @@ -11,7 +10,6 @@ use skyline_web::{Background, BootDisplay, WebSession, Webpage}; use std::fs; use std::path::Path; use training_mod_consts::{MenuJsonStruct, TrainingModpackMenu}; -use training_mod_tui::Color; static mut FRAME_COUNTER_INDEX: usize = 0; pub static mut QUICK_MENU_FRAME_COUNTER_INDEX: usize = 0; @@ -273,32 +271,18 @@ pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32 } } -extern "C" { - #[link_name = "render_text_to_screen"] - pub fn render_text_to_screen_cstr(str: *const skyline::libc::c_char); +use lazy_static::lazy_static; +use parking_lot::Mutex; - #[link_name = "set_should_display_text_to_screen"] - pub fn set_should_display_text_to_screen(toggle: bool); -} - -macro_rules! c_str { - ($l:tt) => { - [$l.as_bytes(), "\u{0}".as_bytes()].concat().as_ptr() - }; -} - -pub fn render_text_to_screen(s: &str) { - unsafe { - render_text_to_screen_cstr(c_str!(s)); - } +lazy_static! { + pub static ref QUICK_MENU_APP: Mutex> = + Mutex::new(training_mod_tui::App::new(unsafe { get_menu() })); } pub unsafe fn quick_menu_loop() { loop { std::thread::sleep(std::time::Duration::from_secs(10)); - let menu = get_menu(); - - let mut app = training_mod_tui::App::new(menu); + let mut app = QUICK_MENU_APP.lock(); let backend = training_mod_tui::TestBackend::new(75, 15); let mut terminal = training_mod_tui::Terminal::new(backend).unwrap(); @@ -357,47 +341,15 @@ pub unsafe fn quick_menu_loop() { has_slept_millis = 16; if !QUICK_MENU_ACTIVE { - app = training_mod_tui::App::new(get_menu()); - set_should_display_text_to_screen(false); continue; } if !received_input { continue; } - let mut view = String::new(); - - let frame_res = terminal + terminal .draw(|f| json_response = training_mod_tui::ui(f, &mut app)) .unwrap(); - use std::fmt::Write; - for (i, cell) in frame_res.buffer.content().iter().enumerate() { - match cell.fg { - Color::Black => write!(&mut view, "{}", &cell.symbol.black()), - Color::Blue => write!(&mut view, "{}", &cell.symbol.blue()), - Color::LightBlue => write!(&mut view, "{}", &cell.symbol.bright_blue()), - Color::Cyan => write!(&mut view, "{}", &cell.symbol.cyan()), - Color::LightCyan => write!(&mut view, "{}", &cell.symbol.cyan()), - Color::Red => write!(&mut view, "{}", &cell.symbol.red()), - Color::LightRed => write!(&mut view, "{}", &cell.symbol.bright_red()), - Color::LightGreen => write!(&mut view, "{}", &cell.symbol.bright_green()), - Color::Green => write!(&mut view, "{}", &cell.symbol.green()), - Color::Yellow => write!(&mut view, "{}", &cell.symbol.yellow()), - Color::LightYellow => write!(&mut view, "{}", &cell.symbol.bright_yellow()), - Color::Magenta => write!(&mut view, "{}", &cell.symbol.magenta()), - Color::LightMagenta => { - write!(&mut view, "{}", &cell.symbol.bright_magenta()) - } - _ => write!(&mut view, "{}", &cell.symbol), - } - .unwrap(); - if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 { - writeln!(&mut view).unwrap(); - } - } - writeln!(&mut view).unwrap(); - - render_text_to_screen(view.as_str()); received_input = false; } } diff --git a/src/common/mod.rs b/src/common/mod.rs index 48847a0..9ae493e 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -229,6 +229,16 @@ pub unsafe fn entry_count() -> i32 { FighterManager::entry_count(fighter_manager) } +pub unsafe fn get_player_dmg_digits(p: FighterId) -> (u8, u8, u8, u8) { + let module_accessor = get_module_accessor(p); + let dmg = DamageModule::damage(module_accessor, 0); + let hundreds = dmg as u16 / 100; + let tens = (dmg as u16 - hundreds * 100) / 10; + let ones = (dmg as u16) - (hundreds * 100) - (tens * 10); + let dec = ((dmg * 10.0) as u16) - (hundreds * 1000) - (tens * 100) - ones * 10; + (hundreds as u8, tens as u8, ones as u8, dec as u8) +} + pub unsafe fn get_fighter_distance() -> f32 { let player_module_accessor = get_module_accessor(FighterId::Player); let cpu_module_accessor = get_module_accessor(FighterId::CPU); diff --git a/src/static/libtraining_modpack_menu.nro b/src/static/libtraining_modpack_menu.nro deleted file mode 100644 index dbfa23b..0000000 Binary files a/src/static/libtraining_modpack_menu.nro and /dev/null differ diff --git a/src/training/ui/mod.rs b/src/training/ui/mod.rs index 2a8d67b..55342d3 100644 --- a/src/training/ui/mod.rs +++ b/src/training/ui/mod.rs @@ -1,10 +1,15 @@ #![allow(dead_code)] +use std::ops::{Deref, DerefMut}; + use bitfield_struct::bitfield; mod resources; pub use resources::*; +use crate::common::get_player_dmg_digits; +use crate::consts::FighterId; + macro_rules! c_str { ($l:tt) => { [$l.as_bytes(), "\u{0}".as_bytes()].concat().as_ptr() @@ -49,13 +54,13 @@ pub struct AnimTransform { } impl AnimTransform { - pub unsafe fn parse_anim_transform(&mut self) { + pub unsafe fn parse_anim_transform(&mut self, layout_name: Option<&str>) { let res_animation_block_data_start = (*self).res_animation_block as u64; let res_animation_block = &*(*self).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 { + 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; @@ -63,19 +68,28 @@ impl AnimTransform { 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; - let frame = (*self).frame; - println!( - "animTransform/resAnimationContent_{anim_cont_idx}: {name} of type {anim_type} on frame {frame}", - ); + // AnimContentType 1 == MATERIAL - if (name == "dig_3_anim" || name == "set_dmg_num_3") && anim_type == 1 { - (*self).frame = 4.0; - } - if (name == "dig_2_anim" || name == "set_dmg_num_2") && anim_type == 1 { - (*self).frame = 2.0; - } - if (name == "dig_1_anim" || name == "set_dmg_num_1") && anim_type == 1 { - (*self).frame = 8.0; + if layout_name.is_some() && name.starts_with("set_dmg_num") && anim_type == 1 { + let layout_name = layout_name.unwrap(); + 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" { + self.frame = hundreds as f32; + } + if name == "set_dmg_num_2" { + self.frame = tens as f32; + } + if name == "set_dmg_num_1" { + self.frame = ones as f32; + } + if name == "set_dmg_num_dec" { + self.frame = dec as f32; + } } anim_cont_offsets = anim_cont_offsets.add(1); @@ -91,14 +105,17 @@ pub struct AnimTransformNode { } impl AnimTransformNode { - pub unsafe fn iterate_anim_list(&mut self) { + pub unsafe fn iterate_anim_list(&mut self, layout_name: Option<&str>) { let mut curr = self 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; - anim_transform.as_mut().unwrap().parse_anim_transform(); + anim_transform + .as_mut() + .unwrap() + .parse_anim_transform(layout_name); } curr = (*curr).next; @@ -146,6 +163,20 @@ pub struct Pane { user_data: [skyline::libc::c_char; 9], } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub enum PaneFlag { + Visible, + InfluencedAlpha, + LocationAdjust, + UserAllocated, + IsGlobalMatrixDirty, + UserMatrix, + UserGlobalMatrix, + IsConstantBufferReady, + Max, +} + impl Pane { pub unsafe fn find_pane_by_name_recursive(&self, s: &str) -> Option<&mut Pane> { find_pane_by_name_recursive(self, c_str!(s)).as_mut() @@ -167,9 +198,32 @@ impl Pane { pane_append_child(self, child as *const Pane); } + /// Detach from current parent pane + pub unsafe fn detach(&self) { + pane_remove_child(self.parent, self as *const Pane); + } + pub unsafe fn as_parts(&mut self) -> *mut Parts { self as *mut Pane as *mut Parts } + + pub unsafe fn as_picture(&mut self) -> &mut Picture { + &mut *(self as *mut Pane as *mut Picture) + } + + pub unsafe fn as_textbox(&mut self) -> &mut TextBox { + &mut *(self as *mut Pane as *mut TextBox) + } + + pub unsafe fn set_visible(&mut self, visible: bool) { + if visible { + self.alpha = 255; + self.global_alpha = 255; + } else { + self.alpha = 0; + self.global_alpha = 0; + } + } } #[repr(C)] @@ -181,15 +235,43 @@ pub struct Parts { pub layout: *mut Layout, } +impl Deref for Parts { + type Target = Pane; + + fn deref(&self) -> &Self::Target { + &self.pane + } +} + +impl DerefMut for Parts { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.pane + } +} + #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct Picture { pub pane: Pane, - material: *mut u8, - vertex_colors: [[u8; 4]; 4], + pub material: *mut Material, + pub vertex_colors: [[u8; 4]; 4], shared_memory: *mut u8, } +impl Deref for Picture { + type Target = Pane; + + fn deref(&self) -> &Self::Target { + &self.pane + } +} + +impl DerefMut for Picture { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.pane + } +} + #[bitfield(u16)] pub struct TextBoxBits { #[bits(2)] @@ -216,7 +298,7 @@ pub struct TextBoxBits { pub struct TextBox { pub pane: Pane, // Actually a union - m_text_buf: *const skyline::libc::c_char, + pub m_text_buf: *mut skyline::libc::c_char, m_p_text_id: *const skyline::libc::c_char, m_text_colors: [[u8; 4]; 2], m_p_font: *const skyline::libc::c_void, @@ -229,12 +311,12 @@ pub struct TextBox { m_p_tag_processor: *const skyline::libc::c_char, m_text_buf_len: u16, - m_text_len: u16, + pub m_text_len: u16, m_bits: TextBoxBits, m_text_position: u8, - m_is_utf8: bool, + pub m_is_utf8: bool, m_italic_ratio: f32, @@ -271,6 +353,33 @@ impl TextBox { self.m_bits.set_is_ptdirty(1); } } + + pub unsafe fn set_material_white_color(&mut self, r: f32, g: f32, b: f32, a: f32) { + (*self.m_p_material).set_white_color(r, g, b, a); + } + + pub unsafe fn set_material_black_color(&mut self, r: f32, g: f32, b: f32, a: f32) { + (*self.m_p_material).set_black_color(r, g, b, a); + } + + pub unsafe fn set_default_material_colors(&mut self) { + self.set_material_white_color(255.0, 255.0, 255.0, 255.0); + self.set_material_black_color(0.0, 0.0, 0.0, 255.0); + } +} + +impl Deref for TextBox { + type Target = Pane; + + fn deref(&self) -> &Self::Target { + &self.pane + } +} + +impl DerefMut for TextBox { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.pane + } } #[repr(C)] @@ -313,14 +422,14 @@ pub enum MaterialFlags { #[derive(Debug)] pub struct Material { vtable: u64, - m_colors: MaterialColor, + pub m_colors: MaterialColor, // Actually a struct m_mem_cap: u32, // Actually a struct m_mem_count: u32, m_p_mem: *mut skyline::libc::c_void, m_p_shader_info: *const skyline::libc::c_void, - m_p_name: *const skyline::libc::c_char, + pub m_p_name: *const skyline::libc::c_char, m_vertex_shader_constant_buffer_offset: u32, m_pixel_shader_constant_buffer_offset: u32, m_p_user_shader_constant_buffer_information: *const skyline::libc::c_void, @@ -370,13 +479,10 @@ impl Material { } #[repr(C)] -#[derive(Debug)] -pub struct RawLayout { - pub anim_trans_list: AnimTransformNode, - pub root_pane: *const Pane, - group_container: u64, - layout_size: f64, - pub layout_name: *const skyline::libc::c_char, +#[derive(Debug, Copy, Clone)] +pub struct Window { + pub pane: Pane, + // TODO } #[derive(Debug, Copy, Clone)] @@ -398,7 +504,11 @@ pub struct GroupContainer {} #[derive(Debug)] pub struct Layout { vtable: u64, - pub raw_layout: RawLayout, + pub anim_trans_list: AnimTransformNode, + pub root_pane: *const Pane, + group_container: u64, + layout_size: f64, + pub layout_name: *const skyline::libc::c_char, } #[skyline::from_offset(0x59970)] diff --git a/src/training/ui/resources.rs b/src/training/ui/resources.rs index 236599c..490befa 100644 --- a/src/training/ui/resources.rs +++ b/src/training/ui/resources.rs @@ -6,12 +6,22 @@ pub struct ResVec2 { y: f32, } +impl ResVec2 { + pub fn default() -> ResVec2 { + ResVec2 { x: 0.0, y: 0.0 } + } + + pub fn new(x: f32, y: f32) -> ResVec2 { + ResVec2 { x, y } + } +} + #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct ResVec3 { - x: f32, - y: f32, - z: f32, + pub x: f32, + pub y: f32, + pub z: f32, } impl ResVec3 { @@ -103,6 +113,11 @@ impl ResPane { self.pos = pos; } + pub fn set_size(&mut self, size: ResVec2) { + self.size_x = size.x; + self.size_y = size.y; + } + pub fn name_matches(&self, other: &str) -> bool { self.name .iter() @@ -113,6 +128,37 @@ impl ResPane { } } +#[repr(C)] +#[derive(Debug, PartialEq)] +enum TextBoxFlag { + ShadowEnabled, + ForceAssignTextLength, + InvisibleBorderEnabled, + DoubleDrawnBorderEnabled, + PerCharacterTransformEnabled, + CenterCeilingEnabled, + LineWidthOffsetEnabled, + ExtendedTagEnabled, + PerCharacterTransformSplitByCharWidth, + PerCharacterTransformAutoShadowAlpha, + DrawFromRightToLeft, + PerCharacterTransformOriginToCenter, + KeepingFontScaleEnabled, + PerCharacterTransformFixSpace, + PerCharacterTransformSplitByCharWidthInsertSpaceEnabled, + MaxTextBoxFlag, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub enum TextAlignment { + Synchronous, + Left, + Center, + Right, + MaxTextAlignment, +} + #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct ResTextBox { @@ -120,7 +166,7 @@ pub struct ResTextBox { text_buf_bytes: u16, text_str_bytes: u16, material_idx: u16, - font_idx: u16, + pub font_idx: u16, text_position: u8, text_alignment: u8, text_box_flag: u16, @@ -148,6 +194,16 @@ pub struct ResTextBox { */ } +impl ResTextBox { + pub fn enable_shadow(&mut self) { + self.text_box_flag |= 0x1 << TextBoxFlag::ShadowEnabled as u8; + } + + pub fn text_alignment(&mut self, align: TextAlignment) { + self.text_alignment = align as u8; + } +} + #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct ResPicture { @@ -168,3 +224,109 @@ pub struct ResPictureWithTex { pub picture: ResPicture, tex_coords: [[ResVec2; TEX_COORD_COUNT]; 4], } + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ResParts { + pub pane: ResPane, + pub property_count: u32, + magnify: ResVec2, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct ResPartsProperty { + name: [skyline::libc::c_char; 24], + usage_flag: u8, + basic_usage_flag: u8, + material_usage_flag: u8, + system_ext_user_data_override_flag: u8, + property_offset: u32, + ext_user_data_offset: u32, + pane_basic_info_offset: u32, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ResPartsWithProperty { + pub parts: ResParts, + property_table: [ResPartsProperty; PROPERTY_COUNT], +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct ResWindowInflation { + left: i16, + right: i16, + top: i16, + bottom: i16, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ResWindowFrameSize { + left: u16, + right: u16, + top: u16, + bottom: u16, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ResWindowContent { + vtx_cols: [ResColor; 4], + material_idx: u16, + tex_coord_count: u8, + padding: [u8; 1], + /* Additional Info + nn::util::Float2 texCoords[texCoordCount][VERTEX_MAX]; + */ +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ResWindowContentWithTexCoords { + pub window_content: ResWindowContent, + // This has to be wrong. + // Should be [[ResVec2; TEX_COORD_COUNT]; 4]? + tex_coords: [[ResVec3; TEX_COORD_COUNT]; 1], +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ResWindowFrame { + material_idx: u16, + texture_flip: u8, + padding: [u8; 1], +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ResWindow { + pub pane: ResPane, + inflation: ResWindowInflation, + frame_size: ResWindowFrameSize, + frame_count: u8, + window_flags: u8, + padding: [u8; 2], + content_offset: u32, + frame_offset_table_offset: u32, + content: ResWindowContent, + /* Additional Info + + ResWindowContent content; + + detail::uint32_t frameOffsetTable[frameCount]; + ResWindowFrame frames; + + */ +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ResWindowWithTexCoordsAndFrames { + pub window: ResWindow, + content: ResWindowContentWithTexCoords, + frame_offset_table: [u32; FRAME_COUNT], + frames: [ResWindowFrame; FRAME_COUNT], +} diff --git a/src/training/ui_hacks.rs b/src/training/ui_hacks.rs index 8968dd3..53839c6 100644 --- a/src/training/ui_hacks.rs +++ b/src/training/ui_hacks.rs @@ -1,38 +1,298 @@ -use crate::training::combo::FRAME_ADVANTAGE; +use crate::common::get_player_dmg_digits; +use crate::common::MENU; +use crate::consts::FighterId; use crate::training::ui::*; -use training_mod_consts::OnOff; +use crate::{common::menu::QUICK_MENU_ACTIVE, training::combo::FRAME_ADVANTAGE}; +use training_mod_consts::{OnOff, SaveDamage}; +use training_mod_tui::gauge::GaugeState; + +pub static NUM_DISPLAY_PANES: usize = 1; +pub static NUM_MENU_TEXT_OPTIONS: usize = 27; +pub static NUM_MENU_TEXT_SLIDERS: usize = 4; +pub static NUM_MENU_TABS: usize = 3; #[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).raw_layout.layout_name); - let layout_root_pane = &*(*layout).raw_layout.root_pane; - let _anim_list = &mut (*layout).raw_layout.anim_trans_list; - // anim_list.iterate_anim_list(); + let layout_name = skyline::from_c_str((*layout).layout_name); + let root_pane = &*(*layout).root_pane; - if layout_name == "info_training" { - if let Some(parent) = layout_root_pane.find_pane_by_name_recursive("trMod_disp_0") { - if crate::common::MENU.frame_advantage == OnOff::On { - parent.alpha = 255; - parent.global_alpha = 255; - } else { - parent.alpha = 0; - parent.global_alpha = 0; + // Update percentage display as soon as possible on death, + // only if we have random save state damage active + if crate::common::is_training_mode() + && (MENU.save_damage_cpu == SaveDamage::RANDOM + || MENU.save_damage_player == SaveDamage::RANDOM) + && 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 { + anim_list.iterate_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); + } + } } } + } - if let Some(header) = layout_root_pane.find_pane_by_name_recursive("trMod_disp_0_header") { + // 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.set_text_string("Frame Advantage"); } - if let Some(text) = layout_root_pane.find_pane_by_name_recursive("trMod_disp_0_txt") { + if let Some(text) = root_pane.find_pane_by_name_recursive("trMod_disp_0_txt") { text.set_text_string(format!("{FRAME_ADVANTAGE}").as_str()); - let text = text as *mut Pane as *mut TextBox; + let text = text.as_textbox(); if FRAME_ADVANTAGE < 0 { - (*text).set_color(200, 8, 8, 255); + text.set_color(200, 8, 8, 255); } else if FRAME_ADVANTAGE == 0 { - (*text).set_color(0, 0, 0, 255); + text.set_color(0, 0, 0, 255); } else { - (*text).set_color(31, 198, 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; + } + if let Some(quit_txt) = quit_button.find_pane_by_name_recursive("set_txt_00") { + quit_txt.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); + + // 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(format!("trMod_menu_opt_{x}_{y}").as_str()) + .map(|text| text.set_visible(false)); + root_pane + .find_pane_by_name_recursive(format!("trMod_menu_check_{x}_{y}").as_str()) + .map(|text| text.set_visible(false)); + }); + (0..NUM_MENU_TEXT_SLIDERS).for_each(|idx| { + root_pane + .find_pane_by_name_recursive(&format!("trMod_menu_slider_{idx}").as_str()) + .map(|text| text.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.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( + &format!("trMod_menu_opt_{list_section}_{list_idx}").to_owned(), + ) + .unwrap(), + ) + }) + .for_each(|(list_section, list_idx, text)| { + 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(); + text.set_text_string(submenu.submenu_title); + text.set_visible(true); + let text = text.as_textbox(); + if is_selected { + text.set_color(0x27, 0x4E, 0x13, 255); + if let Some(footer) = root_pane + .find_pane_by_name_recursive(&format!("trMod_menu_footer_txt").as_str()) + { + footer.set_text_string(submenu.help_text); + } + } else { + text.set_color(0, 0, 0, 255); + } + }); + } 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; + 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( + format!("trMod_menu_opt_{list_section}_{idx}").as_str(), + ) { + let text = text.as_textbox(); + text.set_text_string(name); + if is_selected { + text.set_color(0x27, 0x4E, 0x13, 255); + } else { + text.set_color(0, 0, 0, 255); + } + text.set_visible(true); + } + + if let Some(check) = root_pane.find_pane_by_name_recursive( + format!("trMod_menu_check_{list_section}_{idx}").as_str(), + ) { + 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 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; + if let Some(text) = root_pane.find_pane_by_name_recursive("trMod_menu_slider_0") { + let text = text.as_textbox(); + text.set_visible(true); + text.set_text_string(&format!("{abs_min}")); + } + + if let Some(text) = root_pane.find_pane_by_name_recursive("trMod_menu_slider_1") { + let text = text.as_textbox(); + text.set_visible(true); + text.set_text_string(&format!("{selected_min}")); + match gauge_vals.state { + GaugeState::MinHover => text.set_color(200, 8, 8, 255), + GaugeState::MinSelected => text.set_color(8, 200, 8, 255), + _ => text.set_color(0, 0, 0, 255), + } + } + + if let Some(text) = root_pane.find_pane_by_name_recursive("trMod_menu_slider_2") { + let text = text.as_textbox(); + text.set_visible(true); + text.set_text_string(&format!("{selected_max}")); + match gauge_vals.state { + GaugeState::MaxHover => text.set_color(200, 8, 8, 255), + GaugeState::MaxSelected => text.set_color(8, 200, 8, 255), + _ => text.set_color(0, 0, 0, 255), + } + } + + if let Some(text) = root_pane.find_pane_by_name_recursive("trMod_menu_slider_3") { + let text = text.as_textbox(); + text.set_visible(true); + text.set_text_string(&format!("{abs_max}")); + } } } } @@ -51,9 +311,26 @@ pub unsafe fn layout_build_parts_impl( build_res_set: *const u8, kind: u32, ) -> *mut Pane { - let layout_name = skyline::from_c_str((*layout).raw_layout.layout_name); + 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) + } + }; + } + if layout_name != "info_training" { return original!()( layout, @@ -67,11 +344,265 @@ pub unsafe fn layout_build_parts_impl( ); } - let root_pane = (*layout).raw_layout.root_pane; - + let root_pane = &*(*layout).root_pane; let block = data as *mut ResPane; - let num_display_panes = 1; - (0..num_display_panes).for_each(|idx| { + let menu_pos = ResVec3::new(-360.0, 440.0, 0.0); + + // Menu creation + if (*block).name_matches("pic_numbase_01") { + let block = block as *mut ResPictureWithTex<1>; + // For menu backing + let mut pic_menu_block = (*block).clone(); + pic_menu_block.picture.pane.set_name("trMod_menu_base"); + pic_menu_block.picture.pane.set_pos(menu_pos); + pic_menu_block + .picture + .pane + .set_size(ResVec2::new(1400.0, 1600.0)); + let pic_menu_pane = build!(pic_menu_block, ResPictureWithTex<1>, kind, Picture); + pic_menu_pane.detach(); + + // 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); + menu_pane.append_child(pic_menu_pane); + } + + // Menu header + // TODO: Copy "Quit Training" window and text + 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; + + // Header + let mut text_block = (*block).clone(); + text_block.pane.size_x = text_block.pane.size_x * 2.0; + text_block.pane.set_name("trMod_menu_header"); + + text_block + .pane + .set_pos(ResVec3::new(menu_pos.x - 525.0, menu_pos.y + 75.0, 0.0)); + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane.pane.set_text_string("Modpack Menu"); + // Ensure Material Colors are not hardcoded so we can just use SetTextColor. + text_pane.set_default_material_colors(); + text_pane.set_color(200, 8, 8, 255); + text_pane.detach(); + menu_pane.append_child(text_pane); + } + + // 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).clone(); + pic_menu_block.picture.pane.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).clone(); + text_block + .pane + .set_name(format!("trMod_menu_footer_txt").as_str()); + + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane.pane.set_text_string(format!("Footer!").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); + } + + (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).clone(); + text_block.enable_shadow(); + text_block.text_alignment(TextAlignment::Center); + + let x = txt_idx; + text_block + .pane + .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.pane.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 + .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).clone(); + // Font Idx 2 = nintendo64 which contains nice symbols + help_block.font_idx = 2; + + let x = txt_idx; + help_block + .pane + .set_name(format!("trMod_menu_tab_help_{x}").as_str()); + + let x_offset = x as f32 * 300.0; + help_block.pane.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.pane.set_text_string(format!("abcd").as_str()); + 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).clone(); + text_block.enable_shadow(); + text_block.text_alignment(TextAlignment::Center); + + text_block + .pane + .set_name(format!("trMod_menu_opt_{x}_{y}").as_str()); + + let x_offset = x as f32 * 400.0; + let y_offset = y as f32 * 75.0; + text_block.pane.set_pos(ResVec3::new( + menu_pos.x - 450.0 + x_offset, + menu_pos.y - 50.0 - y_offset, + 0.0, + )); + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane + .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(0, 0, 0, 255); + text_pane.detach(); + menu_pane.append_child(text_pane); + + let mut check_block = (*block).clone(); + // Font Idx 2 = nintendo64 which contains nice symbols + check_block.font_idx = 2; + + check_block + .pane + .set_name(format!("trMod_menu_check_{x}_{y}").as_str()); + check_block.pane.set_pos(ResVec3::new( + menu_pos.x - 675.0 + x_offset, + menu_pos.y - 50.0 - y_offset, + 0.0, + )); + let check_pane = build!(check_block, ResTextBox, kind, TextBox); + check_pane + .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 + (0..NUM_MENU_TEXT_SLIDERS).for_each(|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).clone(); + text_block.enable_shadow(); + text_block.text_alignment(TextAlignment::Center); + + text_block + .pane + .set_name(format!("trMod_menu_slider_{idx}").as_str()); + + let x_offset = idx as f32 * 250.0; + text_block.pane.set_pos(ResVec3::new( + menu_pos.x - 450.0 + x_offset, + menu_pos.y - 150.0, + 0.0, + )); + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane + .pane + .set_text_string(format!("Slider {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(); + menu_pane.append_child(text_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"); @@ -83,39 +614,21 @@ pub unsafe fn layout_build_parts_impl( let mut pic_block = (*block).clone(); pic_block.picture.pane.set_name(pic_name.as_str()); pic_block.picture.pane.set_pos(ResVec3::default()); - let pic_pane = original!()( - layout, - out_build_result_information, - device, - &mut pic_block as *mut ResPictureWithTex<1> as *mut u8, - parts_build_data_set, - build_arg_set, - build_res_set, - kind, - ); - (*(*pic_pane).parent).remove_child(&*pic_pane); + 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 = original!()( - layout, - out_build_result_information, - device, - &mut disp_pane_block as *mut ResPane as *mut u8, - parts_build_data_set, - build_arg_set, - build_res_set, - disp_pane_kind, - ); - (*(*disp_pane).parent).remove_child(&*disp_pane); - (*root_pane).append_child(&*disp_pane); - (*disp_pane).append_child(&*pic_pane); + 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) + let disp_pane = root_pane .find_pane_by_name(parent_name.as_str(), true) .unwrap(); @@ -123,27 +636,18 @@ pub unsafe fn layout_build_parts_impl( let mut text_block = (*block).clone(); text_block.pane.set_name(txt_name.as_str()); text_block.pane.set_pos(ResVec3::new(-10.0, -25.0, 0.0)); - let text_pane = original!()( - layout, - out_build_result_information, - device, - &mut text_block as *mut ResTextBox as *mut u8, - parts_build_data_set, - build_arg_set, - build_res_set, - kind, - ); - (*text_pane).set_text_string(format!("Pane {idx}!").as_str()); + let text_pane = build!(text_block, ResTextBox, kind, TextBox); + text_pane + .pane + .set_text_string(format!("Pane {idx}!").as_str()); // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - (*((*(text_pane as *mut TextBox)).m_p_material)) - .set_white_color(255.0, 255.0, 255.0, 255.0); - (*((*(text_pane as *mut TextBox)).m_p_material)).set_black_color(0.0, 0.0, 0.0, 255.0); - (*(*text_pane).parent).remove_child(&*text_pane); - (*disp_pane).append_child(&*text_pane); + 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) + let disp_pane = root_pane .find_pane_by_name(parent_name.as_str(), true) .unwrap(); @@ -151,26 +655,16 @@ pub unsafe fn layout_build_parts_impl( let mut header_block = (*block).clone(); header_block.pane.set_name(header_name.as_str()); header_block.pane.set_pos(ResVec3::new(0.0, 25.0, 0.0)); - let header_pane = original!()( - layout, - out_build_result_information, - device, - &mut header_block as *mut ResTextBox as *mut u8, - parts_build_data_set, - build_arg_set, - build_res_set, - kind, - ); - (*header_pane).set_text_string(format!("Header {idx}").as_str()); + let header_pane = build!(header_block, ResTextBox, kind, TextBox); + header_pane + .pane + .set_text_string(format!("Header {idx}").as_str()); // Ensure Material Colors are not hardcoded so we can just use SetTextColor. - (*((*(header_pane as *mut TextBox)).m_p_material)) - .set_white_color(255.0, 255.0, 255.0, 255.0); - (*((*(header_pane as *mut TextBox)).m_p_material)) - .set_black_color(0.0, 0.0, 0.0, 255.0); + header_pane.set_default_material_colors(); // Header should be white text - (*(header_pane as *mut TextBox)).set_color(255, 255, 255, 255); - (*(*header_pane).parent).remove_child(&*header_pane); - (*disp_pane).append_child(&*header_pane); + header_pane.set_color(255, 255, 255, 255); + header_pane.detach(); + disp_pane.append_child(header_pane); } }); diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs index 0ba36b9..6bc5221 100644 --- a/training_mod_tui/src/lib.rs +++ b/training_mod_tui/src/lib.rs @@ -12,7 +12,7 @@ use std::collections::HashMap; use serde_json::{Map, json}; pub use tui::{backend::TestBackend, style::Color, Terminal}; -mod gauge; +pub mod gauge; mod list; use crate::gauge::{DoubleEndedGauge, GaugeState}; @@ -95,7 +95,7 @@ impl<'a> App<'a> { } /// Returns the id of the currently selected tab - fn tab_selected(&self) -> &str { + pub fn tab_selected(&self) -> &str { self.tabs .items .get(self.tabs.state.selected().unwrap()) @@ -221,7 +221,7 @@ impl<'a> App<'a> { /// 3: ListState for toggles, ListState::new() for slider /// TODO: Refactor return type into a nice struct pub fn sub_menu_strs_and_states( - &mut self, + &self, ) -> (&str, &str, Vec<(Vec<(bool, &str)>, ListState)>) { ( self.sub_menu_selected().submenu_title, @@ -254,7 +254,7 @@ impl<'a> App<'a> { /// 1: Help text /// 2: Reference to self.selected_sub_menu_slider /// TODO: Refactor return type into a nice struct - pub fn sub_menu_strs_for_slider(&mut self) -> (&str, &str, &DoubleEndedGauge) { + pub fn sub_menu_strs_for_slider(&self) -> (&str, &str, &DoubleEndedGauge) { let slider = match SubMenuType::from_str(self.sub_menu_selected()._type) { SubMenuType::SLIDER => &self.selected_sub_menu_slider, _ => { @@ -780,6 +780,12 @@ pub fn ui(f: &mut Frame, app: &mut App) -> String { } // 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 { @@ -803,6 +809,4 @@ pub fn ui(f: &mut Frame, app: &mut App) -> String { } } serde_json::to_string(&settings).unwrap() - - // TODO: Add saveDefaults } diff --git a/training_mod_tui/src/list.rs b/training_mod_tui/src/list.rs index 46a0026..fb1aa0f 100644 --- a/training_mod_tui/src/list.rs +++ b/training_mod_tui/src/list.rs @@ -15,16 +15,21 @@ impl MultiStatefulList { } pub fn idx_to_list_idx(&self, idx: usize) -> (usize, usize) { + self.idx_to_list_idx_opt(idx).unwrap_or((0, 0)) + } + + pub fn idx_to_list_idx_opt(&self, idx: usize) -> Option<(usize, usize)> { for list_section in 0..self.lists.len() { let list_section_min_idx = (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * list_section; let list_section_max_idx = std::cmp::min( (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * (list_section + 1), self.total_len); if (list_section_min_idx..list_section_max_idx).contains(&idx) { - return (list_section, idx - list_section_min_idx) + return Some((list_section, idx - list_section_min_idx)); } } - (0, 0) + + None } fn list_idx_to_idx(&self, list_idx: (usize, usize)) -> usize {