mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2025-03-23 06:46:11 +00:00
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 <edell.matthew@gmail.com>
This commit is contained in:
parent
f014acfb5c
commit
d5c0d636a0
15 changed files with 2077 additions and 1535 deletions
5
.github/workflows/rust.yml
vendored
5
.github/workflows/rust.yml
vendored
|
@ -9,7 +9,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
checker:
|
checker:
|
||||||
name: Check, Clippy
|
name: Check, Clippy, Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -29,6 +29,9 @@ jobs:
|
||||||
run: cargo +nightly check --target=x86_64-unknown-linux-gnu
|
run: cargo +nightly check --target=x86_64-unknown-linux-gnu
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo +nightly clippy --all-targets --all-features --target=x86_64-unknown-linux-gnu
|
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:
|
plugin:
|
||||||
name: Plugin NRO
|
name: Plugin NRO
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -10,7 +10,7 @@ use skyline::nn::web::WebSessionBootMode;
|
||||||
use skyline_web::{Background, BootDisplay, WebSession, Webpage};
|
use skyline_web::{Background, BootDisplay, WebSession, Webpage};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use training_mod_consts::{MenuJsonStruct, TrainingModpackMenu};
|
use training_mod_consts::MenuJsonStruct;
|
||||||
|
|
||||||
static mut FRAME_COUNTER_INDEX: usize = 0;
|
static mut FRAME_COUNTER_INDEX: usize = 0;
|
||||||
pub static mut QUICK_MENU_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() {
|
pub unsafe fn write_web_menu_file() {
|
||||||
let tpl = Template::new(include_str!("../templates/menu.html")).unwrap();
|
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);
|
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) {
|
pub unsafe fn set_menu_from_json(message: &str) {
|
||||||
let web_response = serde_json::from_str::<MenuJsonStruct>(message);
|
let web_response = serde_json::from_str::<MenuJsonStruct>(message);
|
||||||
let tui_response = serde_json::from_str::<TrainingModpackMenu>(message);
|
|
||||||
info!("Received menu message: {message}");
|
info!("Received menu message: {message}");
|
||||||
if let Ok(message_json) = web_response {
|
if let Ok(message_json) = web_response {
|
||||||
// Includes both MENU and DEFAULTS_MENU
|
// Includes both MENU and DEFAULTS_MENU
|
||||||
|
@ -78,18 +77,7 @@ pub unsafe fn set_menu_from_json(message: &str) {
|
||||||
MENU_CONF_PATH,
|
MENU_CONF_PATH,
|
||||||
serde_json::to_string_pretty(&message_json).unwrap(),
|
serde_json::to_string_pretty(&message_json).unwrap(),
|
||||||
)
|
)
|
||||||
.expect("Failed to write menu settings file from web response");
|
.expect("Failed to write menu settings file");
|
||||||
} 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");
|
|
||||||
} else {
|
} else {
|
||||||
skyline::error::show_error(
|
skyline::error::show_error(
|
||||||
0x70,
|
0x70,
|
||||||
|
@ -105,7 +93,6 @@ pub unsafe fn set_menu_from_json(message: &str) {
|
||||||
);
|
);
|
||||||
MENU.quick_menu = OnOff::On;
|
MENU.quick_menu = OnOff::On;
|
||||||
}
|
}
|
||||||
EVENT_QUEUE.push(Event::menu_open(message.to_string()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn_menu() {
|
pub fn spawn_menu() {
|
||||||
|
@ -127,7 +114,9 @@ pub fn spawn_menu() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut app = QUICK_MENU_APP.lock();
|
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);
|
drop(app);
|
||||||
QUICK_MENU_ACTIVE = true;
|
QUICK_MENU_ACTIVE = true;
|
||||||
}
|
}
|
||||||
|
@ -137,6 +126,9 @@ pub fn spawn_menu() {
|
||||||
pub struct ButtonPresses {
|
pub struct ButtonPresses {
|
||||||
pub a: ButtonPress,
|
pub a: ButtonPress,
|
||||||
pub b: ButtonPress,
|
pub b: ButtonPress,
|
||||||
|
pub x: ButtonPress,
|
||||||
|
pub r: ButtonPress,
|
||||||
|
pub l: ButtonPress,
|
||||||
pub zr: ButtonPress,
|
pub zr: ButtonPress,
|
||||||
pub zl: ButtonPress,
|
pub zl: ButtonPress,
|
||||||
pub left: ButtonPress,
|
pub left: ButtonPress,
|
||||||
|
@ -183,6 +175,21 @@ pub static mut BUTTON_PRESSES: ButtonPresses = ButtonPresses {
|
||||||
is_pressed: false,
|
is_pressed: false,
|
||||||
lockout_frames: 0,
|
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 {
|
zr: ButtonPress {
|
||||||
prev_frame_is_pressed: false,
|
prev_frame_is_pressed: false,
|
||||||
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 {
|
if (*state).Buttons & (1 << 1) > 0 {
|
||||||
BUTTON_PRESSES.b.is_pressed = true;
|
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 {
|
if (*state).Buttons & (1 << 8) > 0 {
|
||||||
BUTTON_PRESSES.zl.is_pressed = true;
|
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 lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use training_mod_tui::AppPage;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App<'static>> =
|
pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App<'static>> =
|
||||||
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() {
|
pub unsafe fn quick_menu_loop() {
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
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 button_presses = &mut BUTTON_PRESSES;
|
||||||
let mut received_input = true;
|
let mut received_input = true;
|
||||||
loop {
|
loop {
|
||||||
|
@ -299,24 +317,37 @@ pub unsafe fn quick_menu_loop() {
|
||||||
let b_press = &mut button_presses.b;
|
let b_press = &mut button_presses.b;
|
||||||
b_press.read_press().then(|| {
|
b_press.read_press().then(|| {
|
||||||
received_input = true;
|
received_input = true;
|
||||||
if !app.outer_list {
|
if app.page != AppPage::SUBMENU {
|
||||||
app.on_b()
|
app.on_b()
|
||||||
} else if frame_counter::get_frame_count(QUICK_MENU_FRAME_COUNTER_INDEX) == 0
|
} else if frame_counter::get_frame_count(QUICK_MENU_FRAME_COUNTER_INDEX) == 0
|
||||||
&& !json_response.is_empty()
|
|
||||||
{
|
{
|
||||||
// Leave menu.
|
// Leave menu.
|
||||||
QUICK_MENU_ACTIVE = false;
|
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();
|
app.on_l();
|
||||||
received_input = true;
|
received_input = true;
|
||||||
});
|
});
|
||||||
button_presses.zr.read_press().then(|| {
|
button_presses.r.read_press().then(|| {
|
||||||
app.on_r();
|
app.on_r();
|
||||||
received_input = true;
|
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(|| {
|
button_presses.left.read_press().then(|| {
|
||||||
app.on_left();
|
app.on_left();
|
||||||
received_input = true;
|
received_input = true;
|
||||||
|
@ -335,10 +366,8 @@ pub unsafe fn quick_menu_loop() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if received_input {
|
if received_input {
|
||||||
terminal
|
|
||||||
.draw(|f| json_response = training_mod_tui::ui(f, app))
|
|
||||||
.unwrap();
|
|
||||||
received_input = false;
|
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.exit();
|
||||||
session.wait_for_exit();
|
session.wait_for_exit();
|
||||||
set_menu_from_json(&message_recv);
|
set_menu_from_json(&message_recv);
|
||||||
|
EVENT_QUEUE.push(Event::menu_open(message_recv.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn new_web_session(hidden: bool) -> WebSession {
|
unsafe fn new_web_session(hidden: bool) -> WebSession {
|
||||||
|
|
|
@ -35,6 +35,7 @@ use crate::menu::quick_menu_loop;
|
||||||
#[cfg(feature = "web_session_preload")]
|
#[cfg(feature = "web_session_preload")]
|
||||||
use crate::menu::web_session_loop;
|
use crate::menu::web_session_loop;
|
||||||
use training_mod_consts::{MenuJsonStruct, OnOff};
|
use training_mod_consts::{MenuJsonStruct, OnOff};
|
||||||
|
use crate::training::ui::notifications::notification;
|
||||||
|
|
||||||
fn nro_main(nro: &NroInfo<'_>) {
|
fn nro_main(nro: &NroInfo<'_>) {
|
||||||
if nro.module.isLoaded {
|
if nro.module.isLoaded {
|
||||||
|
@ -81,10 +82,12 @@ pub fn main() {
|
||||||
info!("Initialized.");
|
info!("Initialized.");
|
||||||
unsafe {
|
unsafe {
|
||||||
EVENT_QUEUE.push(Event::smash_open());
|
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();
|
hitbox_visualizer::hitbox_visualization();
|
||||||
hazard_manager::hazard_manager();
|
hazard_manager::hazard_manager();
|
||||||
training::training_mods();
|
training::training_mods();
|
||||||
|
|
|
@ -509,6 +509,7 @@ daikon_replace!(DAISY, daisy, 1);
|
||||||
|
|
||||||
// GenerateArticleForTarget for Peach/Diddy(/Link?) item creation
|
// GenerateArticleForTarget for Peach/Diddy(/Link?) item creation
|
||||||
static GAFT_OFFSET: usize = 0x03d40a0;
|
static GAFT_OFFSET: usize = 0x03d40a0;
|
||||||
|
|
||||||
#[skyline::hook(offset = GAFT_OFFSET)]
|
#[skyline::hook(offset = GAFT_OFFSET)]
|
||||||
pub unsafe fn handle_generate_article_for_target(
|
pub unsafe fn handle_generate_article_for_target(
|
||||||
article_module_accessor: *mut app::BattleObjectModuleAccessor,
|
article_module_accessor: *mut app::BattleObjectModuleAccessor,
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
use skyline::nn::ui2d::ResColor;
|
||||||
use crate::common::consts::FighterId;
|
use crate::common::consts::FighterId;
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
use crate::training::*;
|
use crate::training::*;
|
||||||
|
|
||||||
pub static mut FRAME_ADVANTAGE: i32 = 0;
|
pub static mut FRAME_ADVANTAGE: i32 = 0;
|
||||||
|
static mut FRAME_ADVANTAGE_STR: String = String::new();
|
||||||
static mut PLAYER_ACTIONABLE: bool = false;
|
static mut PLAYER_ACTIONABLE: bool = false;
|
||||||
static mut CPU_ACTIONABLE: bool = false;
|
static mut CPU_ACTIONABLE: bool = false;
|
||||||
static mut PLAYER_ACTIVE_FRAME: u32 = 0;
|
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) {
|
fn update_frame_advantage(new_frame_adv: i32) {
|
||||||
unsafe {
|
unsafe {
|
||||||
FRAME_ADVANTAGE = new_frame_adv;
|
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},
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub mod sdi;
|
||||||
pub mod shield;
|
pub mod shield;
|
||||||
pub mod tech;
|
pub mod tech;
|
||||||
pub mod throw;
|
pub mod throw;
|
||||||
pub mod ui_hacks;
|
pub mod ui;
|
||||||
|
|
||||||
mod air_dodge_direction;
|
mod air_dodge_direction;
|
||||||
mod attack_angle;
|
mod attack_angle;
|
||||||
|
@ -570,4 +570,5 @@ pub fn training_mods() {
|
||||||
buff::init();
|
buff::init();
|
||||||
items::init();
|
items::init();
|
||||||
tech::init();
|
tech::init();
|
||||||
|
ui::init();
|
||||||
}
|
}
|
||||||
|
|
146
src/training/ui/damage.rs
Normal file
146
src/training/ui/damage.rs
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
147
src/training/ui/display.rs
Normal file
147
src/training/ui/display.rs
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
973
src/training/ui/menu.rs
Normal file
973
src/training/ui/menu.rs
Normal file
|
@ -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::<Vec<&mut Pane>>();
|
||||||
|
|
||||||
|
panes.append(
|
||||||
|
&mut (0..NUM_MENU_TEXT_SLIDERS)
|
||||||
|
.map(|idx| {
|
||||||
|
root_pane
|
||||||
|
.find_pane_by_name_recursive(menu_text_slider_fmt!(idx))
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect::<Vec<&mut Pane>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
panes.append(
|
||||||
|
&mut (0..NUM_MENU_TEXT_SLIDERS)
|
||||||
|
.map(|idx| {
|
||||||
|
root_pane
|
||||||
|
.find_pane_by_name_recursive(menu_slider_label_fmt!(idx))
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect::<Vec<&mut Pane>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
168
src/training/ui/mod.rs
Normal file
168
src/training/ui/mod.rs
Normal file
|
@ -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<HashMap<
|
||||||
|
(String, String), Vec<(bool, PaneCreationCallback)>
|
||||||
|
>> = 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
|
||||||
|
);
|
||||||
|
}
|
69
src/training/ui/notifications.rs
Normal file
69
src/training/ui/notifications.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use skyline::nn::ui2d::ResColor;
|
||||||
|
|
||||||
|
pub static mut QUEUE: Vec<Notification<'static>> = 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);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -1329,7 +1329,7 @@ impl<'a> SubMenu<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Content, Serialize)]
|
#[derive(Content, Serialize, Clone)]
|
||||||
pub struct Tab<'a> {
|
pub struct Tab<'a> {
|
||||||
pub tab_id: &'a str,
|
pub tab_id: &'a str,
|
||||||
pub tab_title: &'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 struct UiMenu<'a> {
|
||||||
pub tabs: Vec<Tab<'a>>,
|
pub tabs: Vec<Tab<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 overall_menu = UiMenu { tabs: Vec::new() };
|
||||||
|
|
||||||
let mut mash_tab = Tab {
|
let mut mash_tab = Tab {
|
||||||
|
@ -1390,98 +1390,98 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
||||||
"mash_state",
|
"mash_state",
|
||||||
"Mash Toggles: Actions to be performed as soon as possible",
|
"Mash Toggles: Actions to be performed as soon as possible",
|
||||||
false,
|
false,
|
||||||
&(MENU.mash_state.bits as u32),
|
&(menu.mash_state.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<Action>(
|
mash_tab.add_submenu_with_toggles::<Action>(
|
||||||
"Followup Toggles",
|
"Followup Toggles",
|
||||||
"follow_up",
|
"follow_up",
|
||||||
"Followup Toggles: Actions to be performed after the Mash option",
|
"Followup Toggles: Actions to be performed after the Mash option",
|
||||||
false,
|
false,
|
||||||
&(MENU.follow_up.bits as u32),
|
&(menu.follow_up.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<MashTrigger>(
|
mash_tab.add_submenu_with_toggles::<MashTrigger>(
|
||||||
"Mash Triggers",
|
"Mash Triggers",
|
||||||
"mash_triggers",
|
"mash_triggers",
|
||||||
"Mash triggers: When the Mash Option will be performed",
|
"Mash triggers: When the Mash Option will be performed",
|
||||||
false,
|
false,
|
||||||
&(MENU.mash_triggers.bits as u32),
|
&(menu.mash_triggers.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<AttackAngle>(
|
mash_tab.add_submenu_with_toggles::<AttackAngle>(
|
||||||
"Attack Angle",
|
"Attack Angle",
|
||||||
"attack_angle",
|
"attack_angle",
|
||||||
"Attack Angle: For attacks that can be angled, such as some forward tilts",
|
"Attack Angle: For attacks that can be angled, such as some forward tilts",
|
||||||
false,
|
false,
|
||||||
&(MENU.attack_angle.bits as u32),
|
&(menu.attack_angle.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<ThrowOption>(
|
mash_tab.add_submenu_with_toggles::<ThrowOption>(
|
||||||
"Throw Options",
|
"Throw Options",
|
||||||
"throw_state",
|
"throw_state",
|
||||||
"Throw Options: Throw to be performed when a grab is landed",
|
"Throw Options: Throw to be performed when a grab is landed",
|
||||||
false,
|
false,
|
||||||
&(MENU.throw_state.bits as u32),
|
&(menu.throw_state.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<MedDelay>(
|
mash_tab.add_submenu_with_toggles::<MedDelay>(
|
||||||
"Throw Delay",
|
"Throw Delay",
|
||||||
"throw_delay",
|
"throw_delay",
|
||||||
"Throw Delay: How many frames to delay the throw option",
|
"Throw Delay: How many frames to delay the throw option",
|
||||||
false,
|
false,
|
||||||
&(MENU.throw_delay.bits as u32),
|
&(menu.throw_delay.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<MedDelay>(
|
mash_tab.add_submenu_with_toggles::<MedDelay>(
|
||||||
"Pummel Delay",
|
"Pummel Delay",
|
||||||
"pummel_delay",
|
"pummel_delay",
|
||||||
"Pummel Delay: How many frames after a grab to wait before starting to pummel",
|
"Pummel Delay: How many frames after a grab to wait before starting to pummel",
|
||||||
false,
|
false,
|
||||||
&(MENU.pummel_delay.bits as u32),
|
&(menu.pummel_delay.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
||||||
"Falling Aerials",
|
"Falling Aerials",
|
||||||
"falling_aerials",
|
"falling_aerials",
|
||||||
"Falling Aerials: Should aerials be performed when rising or when falling",
|
"Falling Aerials: Should aerials be performed when rising or when falling",
|
||||||
false,
|
false,
|
||||||
&(MENU.falling_aerials.bits as u32),
|
&(menu.falling_aerials.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
||||||
"Full Hop",
|
"Full Hop",
|
||||||
"full_hop",
|
"full_hop",
|
||||||
"Full Hop: Should the CPU perform a full hop or a short hop",
|
"Full Hop: Should the CPU perform a full hop or a short hop",
|
||||||
false,
|
false,
|
||||||
&(MENU.full_hop.bits as u32),
|
&(menu.full_hop.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||||
"Aerial Delay",
|
"Aerial Delay",
|
||||||
"aerial_delay",
|
"aerial_delay",
|
||||||
"Aerial Delay: How long to delay a Mash aerial attack",
|
"Aerial Delay: How long to delay a Mash aerial attack",
|
||||||
false,
|
false,
|
||||||
&(MENU.aerial_delay.bits as u32),
|
&(menu.aerial_delay.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
mash_tab.add_submenu_with_toggles::<BoolFlag>(
|
||||||
"Fast Fall",
|
"Fast Fall",
|
||||||
"fast_fall",
|
"fast_fall",
|
||||||
"Fast Fall: Should the CPU fastfall during a jump",
|
"Fast Fall: Should the CPU fastfall during a jump",
|
||||||
false,
|
false,
|
||||||
&(MENU.fast_fall.bits as u32),
|
&(menu.fast_fall.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||||
"Fast Fall Delay",
|
"Fast Fall Delay",
|
||||||
"fast_fall_delay",
|
"fast_fall_delay",
|
||||||
"Fast Fall Delay: How many frames the CPU should delay their fastfall",
|
"Fast Fall Delay: How many frames the CPU should delay their fastfall",
|
||||||
false,
|
false,
|
||||||
&(MENU.fast_fall_delay.bits as u32),
|
&(menu.fast_fall_delay.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||||
"OoS Offset",
|
"OoS Offset",
|
||||||
"oos_offset",
|
"oos_offset",
|
||||||
"OoS Offset: How many times the CPU shield can be hit before performing a Mash option",
|
"OoS Offset: How many times the CPU shield can be hit before performing a Mash option",
|
||||||
false,
|
false,
|
||||||
&(MENU.oos_offset.bits as u32),
|
&(menu.oos_offset.bits as u32),
|
||||||
);
|
);
|
||||||
mash_tab.add_submenu_with_toggles::<Delay>(
|
mash_tab.add_submenu_with_toggles::<Delay>(
|
||||||
"Reaction Time",
|
"Reaction Time",
|
||||||
"reaction_time",
|
"reaction_time",
|
||||||
"Reaction Time: How many frames to delay before performing a mash option",
|
"Reaction Time: How many frames to delay before performing a mash option",
|
||||||
false,
|
false,
|
||||||
&(MENU.reaction_time.bits as u32),
|
&(menu.reaction_time.bits as u32),
|
||||||
);
|
);
|
||||||
overall_menu.tabs.push(mash_tab);
|
overall_menu.tabs.push(mash_tab);
|
||||||
|
|
||||||
|
@ -1495,77 +1495,77 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
||||||
"air_dodge_dir",
|
"air_dodge_dir",
|
||||||
"Airdodge Direction: Direction to angle airdodges",
|
"Airdodge Direction: Direction to angle airdodges",
|
||||||
false,
|
false,
|
||||||
&(MENU.air_dodge_dir.bits as u32),
|
&(menu.air_dodge_dir.bits as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<Direction>(
|
defensive_tab.add_submenu_with_toggles::<Direction>(
|
||||||
"DI Direction",
|
"DI Direction",
|
||||||
"di_state",
|
"di_state",
|
||||||
"DI Direction: Direction to angle the directional influence during hitlag",
|
"DI Direction: Direction to angle the directional influence during hitlag",
|
||||||
false,
|
false,
|
||||||
&(MENU.di_state.bits as u32),
|
&(menu.di_state.bits as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<Direction>(
|
defensive_tab.add_submenu_with_toggles::<Direction>(
|
||||||
"SDI Direction",
|
"SDI Direction",
|
||||||
"sdi_state",
|
"sdi_state",
|
||||||
"SDI Direction: Direction to angle the smash directional influence during hitlag",
|
"SDI Direction: Direction to angle the smash directional influence during hitlag",
|
||||||
false,
|
false,
|
||||||
&(MENU.sdi_state.bits as u32),
|
&(menu.sdi_state.bits as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<SdiFrequency>(
|
defensive_tab.add_submenu_with_toggles::<SdiFrequency>(
|
||||||
"SDI Strength",
|
"SDI Strength",
|
||||||
"sdi_strength",
|
"sdi_strength",
|
||||||
"SDI Strength: Relative strength of the smash directional influence inputs",
|
"SDI Strength: Relative strength of the smash directional influence inputs",
|
||||||
true,
|
true,
|
||||||
&(MENU.sdi_strength as u32),
|
&(menu.sdi_strength as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<ClatterFrequency>(
|
defensive_tab.add_submenu_with_toggles::<ClatterFrequency>(
|
||||||
"Clatter Strength",
|
"Clatter Strength",
|
||||||
"clatter_strength",
|
"clatter_strength",
|
||||||
"Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.",
|
"Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.",
|
||||||
true,
|
true,
|
||||||
&(MENU.clatter_strength as u32),
|
&(menu.clatter_strength as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<LedgeOption>(
|
defensive_tab.add_submenu_with_toggles::<LedgeOption>(
|
||||||
"Ledge Options",
|
"Ledge Options",
|
||||||
"ledge_state",
|
"ledge_state",
|
||||||
"Ledge Options: Actions to be taken when on the ledge",
|
"Ledge Options: Actions to be taken when on the ledge",
|
||||||
false,
|
false,
|
||||||
&(MENU.ledge_state.bits as u32),
|
&(menu.ledge_state.bits as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<LongDelay>(
|
defensive_tab.add_submenu_with_toggles::<LongDelay>(
|
||||||
"Ledge Delay",
|
"Ledge Delay",
|
||||||
"ledge_delay",
|
"ledge_delay",
|
||||||
"Ledge Delay: How many frames to delay the ledge option",
|
"Ledge Delay: How many frames to delay the ledge option",
|
||||||
false,
|
false,
|
||||||
&(MENU.ledge_delay.bits as u32),
|
&(menu.ledge_delay.bits as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<TechFlags>(
|
defensive_tab.add_submenu_with_toggles::<TechFlags>(
|
||||||
"Tech Options",
|
"Tech Options",
|
||||||
"tech_state",
|
"tech_state",
|
||||||
"Tech Options: Actions to take when slammed into a hard surface",
|
"Tech Options: Actions to take when slammed into a hard surface",
|
||||||
false,
|
false,
|
||||||
&(MENU.tech_state.bits as u32),
|
&(menu.tech_state.bits as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<MissTechFlags>(
|
defensive_tab.add_submenu_with_toggles::<MissTechFlags>(
|
||||||
"Mistech Options",
|
"Mistech Options",
|
||||||
"miss_tech_state",
|
"miss_tech_state",
|
||||||
"Mistech Options: Actions to take after missing a tech",
|
"Mistech Options: Actions to take after missing a tech",
|
||||||
false,
|
false,
|
||||||
&(MENU.miss_tech_state.bits as u32),
|
&(menu.miss_tech_state.bits as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<Shield>(
|
defensive_tab.add_submenu_with_toggles::<Shield>(
|
||||||
"Shield Toggles",
|
"Shield Toggles",
|
||||||
"shield_state",
|
"shield_state",
|
||||||
"Shield Toggles: CPU Shield Behavior",
|
"Shield Toggles: CPU Shield Behavior",
|
||||||
true,
|
true,
|
||||||
&(MENU.shield_state as u32),
|
&(menu.shield_state as u32),
|
||||||
);
|
);
|
||||||
defensive_tab.add_submenu_with_toggles::<Direction>(
|
defensive_tab.add_submenu_with_toggles::<Direction>(
|
||||||
"Shield Tilt",
|
"Shield Tilt",
|
||||||
"shield_tilt",
|
"shield_tilt",
|
||||||
"Shield Tilt: Direction to tilt the shield",
|
"Shield Tilt: Direction to tilt the shield",
|
||||||
false, // TODO: Should this be true?
|
false, // TODO: Should this be true?
|
||||||
&(MENU.shield_tilt.bits as u32),
|
&(menu.shield_tilt.bits as u32),
|
||||||
);
|
);
|
||||||
|
|
||||||
defensive_tab.add_submenu_with_toggles::<OnOff>(
|
defensive_tab.add_submenu_with_toggles::<OnOff>(
|
||||||
|
@ -1573,7 +1573,7 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
||||||
"crouch",
|
"crouch",
|
||||||
"Crouch: Should the CPU crouch when on the ground",
|
"Crouch: Should the CPU crouch when on the ground",
|
||||||
true,
|
true,
|
||||||
&(MENU.crouch as u32),
|
&(menu.crouch as u32),
|
||||||
);
|
);
|
||||||
overall_menu.tabs.push(defensive_tab);
|
overall_menu.tabs.push(defensive_tab);
|
||||||
|
|
||||||
|
@ -1587,63 +1587,63 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
||||||
"save_state_mirroring",
|
"save_state_mirroring",
|
||||||
"Mirroring: Flips save states in the left-right direction across the stage center",
|
"Mirroring: Flips save states in the left-right direction across the stage center",
|
||||||
true,
|
true,
|
||||||
&(MENU.save_state_mirroring as u32),
|
&(menu.save_state_mirroring as u32),
|
||||||
);
|
);
|
||||||
save_state_tab.add_submenu_with_toggles::<OnOff>(
|
save_state_tab.add_submenu_with_toggles::<OnOff>(
|
||||||
"Auto Save States",
|
"Auto Save States",
|
||||||
"save_state_autoload",
|
"save_state_autoload",
|
||||||
"Auto Save States: Load save state when any fighter dies",
|
"Auto Save States: Load save state when any fighter dies",
|
||||||
true,
|
true,
|
||||||
&(MENU.save_state_autoload as u32),
|
&(menu.save_state_autoload as u32),
|
||||||
);
|
);
|
||||||
save_state_tab.add_submenu_with_toggles::<SaveDamage>(
|
save_state_tab.add_submenu_with_toggles::<SaveDamage>(
|
||||||
"Save Dmg (CPU)",
|
"Save Dmg (CPU)",
|
||||||
"save_damage_cpu",
|
"save_damage_cpu",
|
||||||
"Save Damage: Should save states retain CPU damage",
|
"Save Damage: Should save states retain CPU damage",
|
||||||
true,
|
true,
|
||||||
&(MENU.save_damage_cpu.bits as u32),
|
&(menu.save_damage_cpu.bits as u32),
|
||||||
);
|
);
|
||||||
save_state_tab.add_submenu_with_slider::<DamagePercent>(
|
save_state_tab.add_submenu_with_slider::<DamagePercent>(
|
||||||
"Dmg Range (CPU)",
|
"Dmg Range (CPU)",
|
||||||
"save_damage_limits_cpu",
|
"save_damage_limits_cpu",
|
||||||
"Limits on random damage to apply to the CPU when loading a save state",
|
"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.0 as u32),
|
||||||
&(MENU.save_damage_limits_cpu.1 as u32),
|
&(menu.save_damage_limits_cpu.1 as u32),
|
||||||
);
|
);
|
||||||
save_state_tab.add_submenu_with_toggles::<SaveDamage>(
|
save_state_tab.add_submenu_with_toggles::<SaveDamage>(
|
||||||
"Save Dmg (Player)",
|
"Save Dmg (Player)",
|
||||||
"save_damage_player",
|
"save_damage_player",
|
||||||
"Save Damage: Should save states retain player damage",
|
"Save Damage: Should save states retain player damage",
|
||||||
true,
|
true,
|
||||||
&(MENU.save_damage_player.bits as u32),
|
&(menu.save_damage_player.bits as u32),
|
||||||
);
|
);
|
||||||
save_state_tab.add_submenu_with_slider::<DamagePercent>(
|
save_state_tab.add_submenu_with_slider::<DamagePercent>(
|
||||||
"Dmg Range (Player)",
|
"Dmg Range (Player)",
|
||||||
"save_damage_limits_player",
|
"save_damage_limits_player",
|
||||||
"Limits on random damage to apply to the player when loading a save state",
|
"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.0 as u32),
|
||||||
&(MENU.save_damage_limits_player.1 as u32),
|
&(menu.save_damage_limits_player.1 as u32),
|
||||||
);
|
);
|
||||||
save_state_tab.add_submenu_with_toggles::<OnOff>(
|
save_state_tab.add_submenu_with_toggles::<OnOff>(
|
||||||
"Enable Save States",
|
"Enable Save States",
|
||||||
"save_state_enable",
|
"save_state_enable",
|
||||||
"Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt.",
|
"Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt.",
|
||||||
true,
|
true,
|
||||||
&(MENU.save_state_enable as u32),
|
&(menu.save_state_enable as u32),
|
||||||
);
|
);
|
||||||
save_state_tab.add_submenu_with_toggles::<CharacterItem>(
|
save_state_tab.add_submenu_with_toggles::<CharacterItem>(
|
||||||
"Character Item",
|
"Character Item",
|
||||||
"character_item",
|
"character_item",
|
||||||
"Character Item: CPU/Player item to hold when loading a save state",
|
"Character Item: CPU/Player item to hold when loading a save state",
|
||||||
true,
|
true,
|
||||||
&(MENU.character_item as u32),
|
&(menu.character_item as u32),
|
||||||
);
|
);
|
||||||
save_state_tab.add_submenu_with_toggles::<BuffOption>(
|
save_state_tab.add_submenu_with_toggles::<BuffOption>(
|
||||||
"Buff Options",
|
"Buff Options",
|
||||||
"buff_state",
|
"buff_state",
|
||||||
"Buff Options: Buff(s) to be applied to respective character when loading save states",
|
"Buff Options: Buff(s) to be applied to respective character when loading save states",
|
||||||
false,
|
false,
|
||||||
&(MENU.buff_state.bits as u32),
|
&(menu.buff_state.bits as u32),
|
||||||
);
|
);
|
||||||
overall_menu.tabs.push(save_state_tab);
|
overall_menu.tabs.push(save_state_tab);
|
||||||
|
|
||||||
|
@ -1657,42 +1657,42 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
|
||||||
"frame_advantage",
|
"frame_advantage",
|
||||||
"Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable",
|
"Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable",
|
||||||
true,
|
true,
|
||||||
&(MENU.frame_advantage as u32),
|
&(menu.frame_advantage as u32),
|
||||||
);
|
);
|
||||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||||
"Hitbox Visualization",
|
"Hitbox Visualization",
|
||||||
"hitbox_vis",
|
"hitbox_vis",
|
||||||
"Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects",
|
"Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects",
|
||||||
true,
|
true,
|
||||||
&(MENU.hitbox_vis as u32),
|
&(menu.hitbox_vis as u32),
|
||||||
);
|
);
|
||||||
misc_tab.add_submenu_with_toggles::<Delay>(
|
misc_tab.add_submenu_with_toggles::<Delay>(
|
||||||
"Input Delay",
|
"Input Delay",
|
||||||
"input_delay",
|
"input_delay",
|
||||||
"Input Delay: Frames to delay player inputs by",
|
"Input Delay: Frames to delay player inputs by",
|
||||||
true,
|
true,
|
||||||
&(MENU.input_delay.bits as u32),
|
&(menu.input_delay.bits as u32),
|
||||||
);
|
);
|
||||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||||
"Stage Hazards",
|
"Stage Hazards",
|
||||||
"stage_hazards",
|
"stage_hazards",
|
||||||
"Stage Hazards: Should stage hazards be present",
|
"Stage Hazards: Should stage hazards be present",
|
||||||
true,
|
true,
|
||||||
&(MENU.stage_hazards as u32),
|
&(menu.stage_hazards as u32),
|
||||||
);
|
);
|
||||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||||
"Quick Menu",
|
"Quick Menu",
|
||||||
"quick_menu",
|
"quick_menu",
|
||||||
"Quick Menu: Should use quick or web menu",
|
"Quick Menu: Should use quick or web menu",
|
||||||
true,
|
true,
|
||||||
&(MENU.quick_menu as u32),
|
&(menu.quick_menu as u32),
|
||||||
);
|
);
|
||||||
misc_tab.add_submenu_with_toggles::<OnOff>(
|
misc_tab.add_submenu_with_toggles::<OnOff>(
|
||||||
"HUD",
|
"HUD",
|
||||||
"hud",
|
"hud",
|
||||||
"HUD: Turn UI on or off",
|
"HUD: Turn UI on or off",
|
||||||
true,
|
true,
|
||||||
&(MENU.hud as u32),
|
&(menu.hud as u32),
|
||||||
);
|
);
|
||||||
overall_menu.tabs.push(misc_tab);
|
overall_menu.tabs.push(misc_tab);
|
||||||
|
|
||||||
|
|
|
@ -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::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||||
|
@ -20,6 +20,14 @@ use crate::list::{MultiStatefulList, StatefulList};
|
||||||
|
|
||||||
static NX_TUI_WIDTH: u16 = 66;
|
static NX_TUI_WIDTH: u16 = 66;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum AppPage {
|
||||||
|
SUBMENU,
|
||||||
|
TOGGLE,
|
||||||
|
SLIDER,
|
||||||
|
CONFIRMATION
|
||||||
|
}
|
||||||
|
|
||||||
/// We should hold a list of SubMenus.
|
/// We should hold a list of SubMenus.
|
||||||
/// The currently selected SubMenu should also have an associated list with necessary information.
|
/// The currently selected SubMenu should also have an associated list with necessary information.
|
||||||
/// We can convert the option types (Toggle, OnOff, Slider) to lists
|
/// 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<SubMenu<'a>>>,
|
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
|
||||||
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
|
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
|
||||||
pub selected_sub_menu_slider: DoubleEndedGauge,
|
pub selected_sub_menu_slider: DoubleEndedGauge,
|
||||||
pub outer_list: bool,
|
pub page: AppPage,
|
||||||
|
pub default_menu: (UiMenu<'a>, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> App<'a> {
|
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 num_lists = 3;
|
||||||
|
|
||||||
let mut menu_items_stateful = HashMap::new();
|
let mut menu_items_stateful = HashMap::new();
|
||||||
|
@ -48,7 +57,8 @@ impl<'a> App<'a> {
|
||||||
menu_items: menu_items_stateful,
|
menu_items: menu_items_stateful,
|
||||||
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
|
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
|
||||||
selected_sub_menu_slider: DoubleEndedGauge::new(),
|
selected_sub_menu_slider: DoubleEndedGauge::new(),
|
||||||
outer_list: true,
|
page: AppPage::SUBMENU,
|
||||||
|
default_menu: default_menu
|
||||||
};
|
};
|
||||||
app.set_sub_menu_items();
|
app.set_sub_menu_items();
|
||||||
app
|
app
|
||||||
|
@ -269,7 +279,7 @@ impl<'a> App<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Different behavior depending on the current menu location
|
/// 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
|
/// 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
|
/// Slider submenu: Swaps hover/selected state. Updates the actual SubMenu struct if going from Selected -> Hover
|
||||||
pub fn on_a(&mut self) {
|
pub fn on_a(&mut self) {
|
||||||
|
@ -288,14 +298,14 @@ impl<'a> App<'a> {
|
||||||
.items
|
.items
|
||||||
.get_mut(list_idx)
|
.get_mut(list_idx)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if self.outer_list {
|
if self.page == AppPage::SUBMENU {
|
||||||
self.outer_list = false;
|
|
||||||
match SubMenuType::from_str(selected_sub_menu._type) {
|
match SubMenuType::from_str(selected_sub_menu._type) {
|
||||||
// Need to change the slider state to MinHover so the slider shows up initially
|
// Need to change the slider state to MinHover so the slider shows up initially
|
||||||
SubMenuType::SLIDER => {
|
SubMenuType::SLIDER => {
|
||||||
|
self.page = AppPage::SLIDER;
|
||||||
self.selected_sub_menu_slider.state = GaugeState::MinHover;
|
self.selected_sub_menu_slider.state = GaugeState::MinHover;
|
||||||
}
|
}
|
||||||
_ => {}
|
SubMenuType::TOGGLE => self.page = AppPage::TOGGLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match SubMenuType::from_str(selected_sub_menu._type) {
|
match SubMenuType::from_str(selected_sub_menu._type) {
|
||||||
|
@ -371,9 +381,9 @@ impl<'a> App<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Different behavior depending on the current menu location
|
/// Different behavior depending on the current menu location
|
||||||
/// Outer list: None
|
/// Submenu selection: None
|
||||||
/// Toggle submenu: Sets self.outer_list to true
|
/// Toggle submenu: Sets page to submenu selection
|
||||||
/// Slider submenu: If in a selected state, then commit changes and change to hover. Else set self.outer_list to true
|
/// 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) {
|
pub fn on_b(&mut self) {
|
||||||
let tab_selected = self
|
let tab_selected = self
|
||||||
.tabs
|
.tabs
|
||||||
|
@ -417,26 +427,55 @@ impl<'a> App<'a> {
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
self.outer_list = true;
|
self.page = AppPage::SUBMENU;
|
||||||
self.set_sub_menu_items();
|
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::<TrainingModpackMenu>(&json).unwrap()), json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset current submenu to defaults
|
||||||
pub fn on_l(&mut self) {
|
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::<serde_json::Value>(&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::<serde_json::Value>(&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::<TrainingModpackMenu>(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.tabs.previous();
|
||||||
self.set_sub_menu_items();
|
self.set_sub_menu_items();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_r(&mut self) {
|
pub fn on_zr(&mut self) {
|
||||||
if self.outer_list {
|
if self.page == AppPage::SUBMENU {
|
||||||
self.tabs.next();
|
self.tabs.next();
|
||||||
self.set_sub_menu_items();
|
self.set_sub_menu_items();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_up(&mut self) {
|
pub fn on_up(&mut self) {
|
||||||
if self.outer_list {
|
if self.page == AppPage::SUBMENU {
|
||||||
self.menu_items
|
self.menu_items
|
||||||
.get_mut(
|
.get_mut(
|
||||||
self.tabs
|
self.tabs
|
||||||
|
@ -447,13 +486,13 @@ impl<'a> App<'a> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.previous();
|
.previous();
|
||||||
self.set_sub_menu_items();
|
self.set_sub_menu_items();
|
||||||
} else {
|
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||||
self.sub_menu_previous();
|
self.sub_menu_previous();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_down(&mut self) {
|
pub fn on_down(&mut self) {
|
||||||
if self.outer_list {
|
if self.page == AppPage::SUBMENU {
|
||||||
self.menu_items
|
self.menu_items
|
||||||
.get_mut(
|
.get_mut(
|
||||||
self.tabs
|
self.tabs
|
||||||
|
@ -464,13 +503,13 @@ impl<'a> App<'a> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.next();
|
.next();
|
||||||
self.set_sub_menu_items();
|
self.set_sub_menu_items();
|
||||||
} else {
|
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||||
self.sub_menu_next();
|
self.sub_menu_next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_left(&mut self) {
|
pub fn on_left(&mut self) {
|
||||||
if self.outer_list {
|
if self.page == AppPage::SUBMENU {
|
||||||
self.menu_items
|
self.menu_items
|
||||||
.get_mut(
|
.get_mut(
|
||||||
self.tabs
|
self.tabs
|
||||||
|
@ -481,13 +520,13 @@ impl<'a> App<'a> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.previous_list();
|
.previous_list();
|
||||||
self.set_sub_menu_items();
|
self.set_sub_menu_items();
|
||||||
} else {
|
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||||
self.sub_menu_previous_list();
|
self.sub_menu_previous_list();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_right(&mut self) {
|
pub fn on_right(&mut self) {
|
||||||
if self.outer_list {
|
if self.page == AppPage::SUBMENU {
|
||||||
self.menu_items
|
self.menu_items
|
||||||
.get_mut(
|
.get_mut(
|
||||||
self.tabs
|
self.tabs
|
||||||
|
@ -498,13 +537,221 @@ impl<'a> App<'a> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.next_list();
|
.next_list();
|
||||||
self.set_sub_menu_items();
|
self.set_sub_menu_items();
|
||||||
} else {
|
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
|
||||||
self.sub_menu_next_list();
|
self.sub_menu_next_list();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns JSON representation of current menu settings
|
||||||
|
pub fn to_json(&self) -> String {
|
||||||
|
let mut settings = Map::new();
|
||||||
|
for key in self.menu_items.keys() {
|
||||||
|
for list in &self.menu_items.get(key).unwrap().lists {
|
||||||
|
for sub_menu in &list.items {
|
||||||
|
if !sub_menu.toggles.is_empty() {
|
||||||
|
let val: u32 = sub_menu
|
||||||
|
.toggles
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.checked)
|
||||||
|
.map(|t| t.toggle_value)
|
||||||
|
.sum();
|
||||||
|
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
|
||||||
|
} else if sub_menu.slider.is_some() {
|
||||||
|
let s: &Slider = sub_menu.slider.as_ref().unwrap();
|
||||||
|
let val: Vec<u32> = vec![s.selected_min, s.selected_max];
|
||||||
|
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
|
||||||
|
} else {
|
||||||
|
panic!("Could not collect settings for {:?}", sub_menu.submenu_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::to_string(&settings).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Returns the current menu selections and the default menu selections.
|
||||||
|
pub fn get_menu_selections(&self) -> String {
|
||||||
|
serde_json::to_string(
|
||||||
|
&MenuJsonStruct {
|
||||||
|
menu: serde_json::from_str(self.to_json().as_str()).unwrap(),
|
||||||
|
defaults_menu: serde_json::from_str(self.default_menu.1.clone().as_str()).unwrap(),
|
||||||
|
}).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
fn render_submenu_page<B: Backend>(f: &mut Frame<B>, app: &mut App, list_chunks: Vec<Rect>, help_chunk: Rect) {
|
||||||
|
let tab_selected = app.tab_selected();
|
||||||
|
let mut item_help = None;
|
||||||
|
for (list_section, stateful_list) in app
|
||||||
|
.menu_items
|
||||||
|
.get(tab_selected)
|
||||||
|
.unwrap()
|
||||||
|
.lists
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let items: Vec<ListItem> = stateful_list
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
|
||||||
|
i.submenu_title.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<B: Backend>(f: &mut Frame<B>, app: &mut App, list_chunks: Vec<Rect>, help_chunk: Rect) {
|
||||||
|
let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
|
||||||
|
for list_section in 0..sub_menu_str_lists.len() {
|
||||||
|
let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
|
||||||
|
let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
|
||||||
|
let values_items: Vec<ListItem> = sub_menu_str
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
ListItem::new(vec![Spans::from(
|
||||||
|
(if s.0 { "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<B: Backend>(f: &mut Frame<B>, app: &mut App, vertical_chunk: Rect, help_chunk: Rect) {
|
||||||
|
let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider();
|
||||||
|
let abs_min = gauge_vals.abs_min;
|
||||||
|
let abs_max = gauge_vals.abs_max;
|
||||||
|
let selected_min = gauge_vals.selected_min;
|
||||||
|
let selected_max = gauge_vals.selected_max;
|
||||||
|
let lbl_ratio = 0.95; // Needed so that the upper limit label is visible
|
||||||
|
let constraints = [
|
||||||
|
Constraint::Ratio((lbl_ratio * (selected_min-abs_min) as f32) as u32, abs_max-abs_min),
|
||||||
|
Constraint::Ratio((lbl_ratio * (selected_max-selected_min) as f32) as u32, abs_max-abs_min),
|
||||||
|
Constraint::Ratio((lbl_ratio * (abs_max-selected_max) as f32) as u32, abs_max-abs_min),
|
||||||
|
Constraint::Min(3), // For upper limit label
|
||||||
|
];
|
||||||
|
let gauge_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(constraints)
|
||||||
|
.split(vertical_chunk);
|
||||||
|
|
||||||
|
let slider_lbls = [
|
||||||
|
abs_min,
|
||||||
|
selected_min,
|
||||||
|
selected_max,
|
||||||
|
abs_max,
|
||||||
|
];
|
||||||
|
for (idx, lbl) in slider_lbls.iter().enumerate() {
|
||||||
|
let mut line_set = tui::symbols::line::NORMAL;
|
||||||
|
line_set.horizontal = "-";
|
||||||
|
let mut gauge = LineGauge::default()
|
||||||
|
.ratio(1.0)
|
||||||
|
.label(format!("{}", lbl))
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.line_set(line_set)
|
||||||
|
.gauge_style(Style::default().fg(Color::White).bg(Color::Black));
|
||||||
|
if idx == 1 {
|
||||||
|
// Slider between selected_min and selected_max
|
||||||
|
match gauge_vals.state {
|
||||||
|
GaugeState::MinHover => {
|
||||||
|
gauge = gauge.style(Style::default().fg(Color::Red))
|
||||||
|
}
|
||||||
|
GaugeState::MinSelected => {
|
||||||
|
gauge = gauge.style(Style::default().fg(Color::Green))
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black));
|
||||||
|
} else if idx == 2 {
|
||||||
|
// Slider between selected_max and abs_max
|
||||||
|
match gauge_vals.state {
|
||||||
|
GaugeState::MaxHover => {
|
||||||
|
gauge = gauge.style(Style::default().fg(Color::Red))
|
||||||
|
}
|
||||||
|
GaugeState::MaxSelected => {
|
||||||
|
gauge = gauge.style(Style::default().fg(Color::Green))
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else if idx == 3 {
|
||||||
|
// Slider for abs_max
|
||||||
|
// We only want the label to show, so set the line character to " "
|
||||||
|
let mut line_set = tui::symbols::line::NORMAL;
|
||||||
|
line_set.horizontal = " ";
|
||||||
|
gauge = gauge.line_set(line_set);
|
||||||
|
|
||||||
|
// For some reason, the selected_max slider displays on top
|
||||||
|
// So we need to change the abs_max slider styling to match
|
||||||
|
// If the selected_max is close enough to the abs_max
|
||||||
|
if (selected_max as f32 / abs_max as f32) > 0.95 {
|
||||||
|
gauge = gauge.style(match gauge_vals.state {
|
||||||
|
GaugeState::MaxHover => Style::default().fg(Color::Red),
|
||||||
|
GaugeState::MaxSelected => Style::default().fg(Color::Green),
|
||||||
|
_ => Style::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.render_widget(gauge, gauge_chunks[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let help_paragraph = Paragraph::new(
|
||||||
|
help_text.replace('\"', "") + "\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<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||||
let app_tabs = &app.tabs;
|
let app_tabs = &app.tabs;
|
||||||
let tab_selected = app_tabs.state.selected().unwrap();
|
let tab_selected = app_tabs.state.selected().unwrap();
|
||||||
let mut span_selected = Spans::default();
|
let mut span_selected = Spans::default();
|
||||||
|
@ -611,202 +858,10 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
||||||
|
|
||||||
f.render_widget(tabs, vertical_chunks[0]);
|
f.render_widget(tabs, vertical_chunks[0]);
|
||||||
|
|
||||||
if app.outer_list {
|
match app.page {
|
||||||
let tab_selected = app.tab_selected();
|
AppPage::SUBMENU => render_submenu_page(f, app, list_chunks, vertical_chunks[2]),
|
||||||
let mut item_help = None;
|
AppPage::SLIDER => render_slider_page(f, app, vertical_chunks[1], vertical_chunks[2]),
|
||||||
for (list_section, stateful_list) in app
|
AppPage::TOGGLE => render_toggle_page(f, app, list_chunks, vertical_chunks[2]),
|
||||||
.menu_items
|
AppPage::CONFIRMATION => todo!()
|
||||||
.get(tab_selected)
|
|
||||||
.unwrap()
|
|
||||||
.lists
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
let items: Vec<ListItem> = stateful_list
|
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.map(|i| {
|
|
||||||
let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
|
|
||||||
i.submenu_title.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<ListItem> = 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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 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<u32> = vec![s.selected_min, s.selected_max];
|
|
||||||
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
|
|
||||||
} else {
|
|
||||||
panic!("Could not collect settings for {:?}", sub_menu.submenu_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serde_json::to_string(&settings).unwrap()
|
|
||||||
}
|
|
|
@ -17,10 +17,10 @@ use tui::Terminal;
|
||||||
|
|
||||||
use training_mod_consts::*;
|
use training_mod_consts::*;
|
||||||
|
|
||||||
fn test_backend_setup(ui_menu: UiMenu) -> Result<
|
fn test_backend_setup<'a>(ui_menu: UiMenu<'a>, menu_defaults: (UiMenu<'a>, String)) -> Result<
|
||||||
(Terminal<training_mod_tui::TestBackend>, training_mod_tui::App),
|
(Terminal<training_mod_tui::TestBackend>, training_mod_tui::App<'a>),
|
||||||
Box<dyn Error>> {
|
Box<dyn Error>> {
|
||||||
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 backend = tui::backend::TestBackend::new(75, 15);
|
||||||
let terminal = Terminal::new(backend)?;
|
let terminal = Terminal::new(backend)?;
|
||||||
let mut state = tui::widgets::ListState::default();
|
let mut state = tui::widgets::ListState::default();
|
||||||
|
@ -30,26 +30,154 @@ fn test_backend_setup(ui_menu: UiMenu) -> Result<
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
|
fn test_set_airdodge() -> Result<(), Box<dyn Error>> {
|
||||||
let menu;
|
let menu;
|
||||||
let prev_menu;
|
let mut prev_menu;
|
||||||
|
let menu_defaults;
|
||||||
unsafe {
|
unsafe {
|
||||||
prev_menu = MENU;
|
prev_menu = MENU.clone();
|
||||||
menu = get_menu();
|
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 (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
|
||||||
let mut json_response = String::new();
|
// Enter Mash Toggles
|
||||||
let _frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;
|
app.on_a();
|
||||||
|
// Set Mash Airdodge
|
||||||
|
app.on_a();
|
||||||
|
let menu_json = app.get_menu_selections();
|
||||||
|
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
|
||||||
|
let menu = menu_struct.menu;
|
||||||
|
let _ = menu_struct.defaults_menu;
|
||||||
|
prev_menu.mash_state.toggle(Action::AIR_DODGE);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&prev_menu).unwrap(),
|
||||||
|
serde_json::to_string(&menu).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
|
||||||
|
let menu;
|
||||||
|
let prev_menu;
|
||||||
|
let menu_defaults;
|
||||||
unsafe {
|
unsafe {
|
||||||
MENU = serde_json::from_str::<TrainingModpackMenu>(&json_response).unwrap();
|
prev_menu = MENU.clone();
|
||||||
// At this point, we didn't change the menu at all; we should still see all the same options.
|
menu = ui_menu(MENU);
|
||||||
assert_eq!(
|
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
|
||||||
serde_json::to_string(&prev_menu).unwrap(),
|
|
||||||
serde_json::to_string(&MENU).unwrap()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (_terminal, app) = test_backend_setup(menu, menu_defaults)?;
|
||||||
|
let menu_json = app.get_menu_selections();
|
||||||
|
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
|
||||||
|
let menu = menu_struct.menu;
|
||||||
|
let _ = menu_struct.defaults_menu;
|
||||||
|
// At this point, we didn't change the menu at all; we should still see all the same options.
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&prev_menu).unwrap(),
|
||||||
|
serde_json::to_string(&menu).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_save_and_reset_defaults() -> Result<(), Box<dyn Error>> {
|
||||||
|
let menu;
|
||||||
|
let mut prev_menu;
|
||||||
|
let menu_defaults;
|
||||||
|
unsafe {
|
||||||
|
prev_menu = MENU.clone();
|
||||||
|
menu = ui_menu(MENU);
|
||||||
|
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
|
||||||
|
|
||||||
|
// Enter Mash 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::<MenuJsonStruct>(&menu_json).unwrap();
|
||||||
|
let menu = menu_struct.menu;
|
||||||
|
let defaults_menu = menu_struct.defaults_menu;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&prev_menu).unwrap(),
|
||||||
|
serde_json::to_string(&menu).unwrap(),
|
||||||
|
"The menu should be the same as how we started"
|
||||||
|
);
|
||||||
|
prev_menu.mash_state.toggle(Action::AIR_DODGE);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&prev_menu).unwrap(),
|
||||||
|
serde_json::to_string(&defaults_menu).unwrap(),
|
||||||
|
"The defaults menu should have Mash Airdodge"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset current menu alone to defaults
|
||||||
|
app.on_l();
|
||||||
|
let menu_json = app.get_menu_selections();
|
||||||
|
let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
|
||||||
|
let menu = menu_struct.menu;
|
||||||
|
let _ = menu_struct.defaults_menu;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&prev_menu).unwrap(),
|
||||||
|
serde_json::to_string(&menu).unwrap(),
|
||||||
|
"The menu should have Mash Airdodge"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enter Mash 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::<MenuJsonStruct>(&menu_json).unwrap();
|
||||||
|
let menu = menu_struct.menu;
|
||||||
|
let _ = menu_struct.defaults_menu;
|
||||||
|
|
||||||
|
prev_menu.mash_state.toggle(Action::AIR_DODGE);
|
||||||
|
prev_menu.follow_up.toggle(Action::JUMP);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&prev_menu).unwrap(),
|
||||||
|
serde_json::to_string(&menu).unwrap(),
|
||||||
|
"The menu should have Mash Airdodge off and Followup Jump on"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,17 +185,23 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let inputs = args.get(1);
|
let inputs = args.get(1);
|
||||||
let menu;
|
let menu;
|
||||||
|
let menu_defaults;
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
menu = get_menu();
|
menu = ui_menu(MENU);
|
||||||
|
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "has_terminal"))] {
|
#[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() {
|
if inputs.is_some() {
|
||||||
inputs.unwrap().split(",").for_each(|input| {
|
inputs.unwrap().split(",").for_each(|input| {
|
||||||
match input.to_uppercase().as_str() {
|
match input.to_uppercase().as_str() {
|
||||||
|
"X" => app.on_x(),
|
||||||
"L" => app.on_l(),
|
"L" => app.on_l(),
|
||||||
"R" => app.on_r(),
|
"R" => app.on_r(),
|
||||||
|
"O" => app.on_zl(),
|
||||||
|
"P" => app.on_zr(),
|
||||||
"A" => app.on_a(),
|
"A" => app.on_a(),
|
||||||
"B" => app.on_b(),
|
"B" => app.on_b(),
|
||||||
"UP" => app.on_up(),
|
"UP" => app.on_up(),
|
||||||
|
@ -78,8 +212,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let mut json_response = String::new();
|
let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?;
|
||||||
let frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;
|
let menu_json = app.get_menu_selections();
|
||||||
|
|
||||||
for (i, cell) in frame_res.buffer.content().iter().enumerate() {
|
for (i, cell) in frame_res.buffer.content().iter().enumerate() {
|
||||||
print!("{}", cell.symbol);
|
print!("{}", cell.symbol);
|
||||||
|
@ -89,11 +223,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
println!("json_response:\n{}", json_response);
|
println!("Menu:\n{menu_json}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "has_terminal")] {
|
#[cfg(feature = "has_terminal")] {
|
||||||
let app = training_mod_tui::App::new(menu);
|
let app = training_mod_tui::App::new(menu, menu_defaults);
|
||||||
|
|
||||||
// setup terminal
|
// setup terminal
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
@ -117,10 +251,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
println!("{:?}", err)
|
println!("{:?}", err)
|
||||||
} else {
|
} else {
|
||||||
println!("JSON: {}", res.as_ref().unwrap());
|
println!("JSONs: {:#?}", res.as_ref().unwrap());
|
||||||
unsafe {
|
unsafe {
|
||||||
MENU = serde_json::from_str::<TrainingModpackMenu>(&res.as_ref().unwrap()).unwrap();
|
let menu = serde_json::from_str::<MenuJsonStruct>(&res.as_ref().unwrap()).unwrap();
|
||||||
println!("MENU: {:#?}", MENU);
|
println!("menu: {:#?}", menu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,9 +269,9 @@ fn run_app<B: tui::backend::Backend>(
|
||||||
tick_rate: Duration,
|
tick_rate: Duration,
|
||||||
) -> io::Result<String> {
|
) -> io::Result<String> {
|
||||||
let mut last_tick = Instant::now();
|
let mut last_tick = Instant::now();
|
||||||
let mut json_response = String::new();
|
|
||||||
loop {
|
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
|
let timeout = tick_rate
|
||||||
.checked_sub(last_tick.elapsed())
|
.checked_sub(last_tick.elapsed())
|
||||||
|
@ -146,7 +280,10 @@ fn run_app<B: tui::backend::Backend>(
|
||||||
if crossterm::event::poll(timeout)? {
|
if crossterm::event::poll(timeout)? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
match key.code {
|
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('r') => app.on_r(),
|
||||||
KeyCode::Char('l') => app.on_l(),
|
KeyCode::Char('l') => app.on_l(),
|
||||||
KeyCode::Left => app.on_left(),
|
KeyCode::Left => app.on_left(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue