1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-24 02:44:17 +00:00

Use ui2d-based Quick Menu; show dmg percentage on save state % reload with random damage (#450)

* A bunch of things

* Current progress

* Fix for ResAnimationContent

* Figure out Parts*

* Cleanup, just because

* New pane working!!!

* New null pane for hierarchy

* Success with parent pane

* Generate multiple panes

* Multiple panes, almost working text color

* MaterialColor test, but fails

* Forgot bitfield-struct

* Vtable for material. Fixes SetWhiteColor!

* Refactor color changing, change naming scheme

* Just Frame Advantage

* Merge

* Delete T_test.txt

* Delete set_txt_num_01.txt

* Delete libtraining_modpack.nro

* Format Rust code using rustfmt

* Ignore shell scripts in repo languages

* General refactor, add basis for quick menu

* Small refactor, fix ordering of submenu options

* Toggles, sliders

* Tons of progress...

* Correct dmg updater, remove old quick menu backend

* Fix damage percentage display

* Small QoL

* Format Rust code using rustfmt

* More edits. Use Quit Training button as Modpack Menu header

* Finish merge

* Format Rust code using rustfmt

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
jugeeya 2022-12-29 20:09:22 -08:00 committed by GitHub
parent 9a7b5fdf85
commit 52e3528292
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 921 additions and 186 deletions

View file

@ -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

View file

@ -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.

View file

@ -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<training_mod_tui::App<'static>> =
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;
}
}

View file

@ -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);

View file

@ -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)]

View file

@ -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<const TEX_COORD_COUNT: usize> {
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<const PROPERTY_COUNT: usize> {
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<const TEX_COORD_COUNT: usize> {
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<const TEX_COORD_COUNT: usize, const FRAME_COUNT: usize> {
pub window: ResWindow,
content: ResWindowContentWithTexCoords<TEX_COORD_COUNT>,
frame_offset_table: [u32; FRAME_COUNT],
frames: [ResWindowFrame; FRAME_COUNT],
}

View file

@ -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);
}
});

View file

@ -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<B: Backend>(f: &mut Frame<B>, 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<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
}
}
serde_json::to_string(&settings).unwrap()
// TODO: Add saveDefaults
}

View file

@ -15,16 +15,21 @@ impl<T: Clone> MultiStatefulList<T> {
}
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 {