1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-24 02:44:17 +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:
jugeeya 2023-02-02 15:01:11 -08:00 committed by GitHub
parent f014acfb5c
commit d5c0d636a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 2077 additions and 1535 deletions

View file

@ -9,7 +9,7 @@ on:
jobs:
checker:
name: Check, Clippy
name: Check, Clippy, Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -29,6 +29,9 @@ jobs:
run: cargo +nightly check --target=x86_64-unknown-linux-gnu
- name: Clippy
run: cargo +nightly clippy --all-targets --all-features --target=x86_64-unknown-linux-gnu
- name: TUI Test
working-directory: training_mod_tui
run: cargo +nightly test
plugin:
name: Plugin NRO
runs-on: ubuntu-latest

View file

@ -10,7 +10,7 @@ use skyline::nn::web::WebSessionBootMode;
use skyline_web::{Background, BootDisplay, WebSession, Webpage};
use std::fs;
use std::path::Path;
use training_mod_consts::{MenuJsonStruct, TrainingModpackMenu};
use training_mod_consts::MenuJsonStruct;
static mut FRAME_COUNTER_INDEX: usize = 0;
pub static mut QUICK_MENU_FRAME_COUNTER_INDEX: usize = 0;
@ -48,7 +48,7 @@ pub unsafe fn menu_condition(module_accessor: &mut smash::app::BattleObjectModul
pub unsafe fn write_web_menu_file() {
let tpl = Template::new(include_str!("../templates/menu.html")).unwrap();
let overall_menu = get_menu();
let overall_menu = ui_menu(MENU);
let data = tpl.render(&overall_menu);
@ -67,7 +67,6 @@ const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.json";
pub unsafe fn set_menu_from_json(message: &str) {
let web_response = serde_json::from_str::<MenuJsonStruct>(message);
let tui_response = serde_json::from_str::<TrainingModpackMenu>(message);
info!("Received menu message: {message}");
if let Ok(message_json) = web_response {
// Includes both MENU and DEFAULTS_MENU
@ -78,18 +77,7 @@ pub unsafe fn set_menu_from_json(message: &str) {
MENU_CONF_PATH,
serde_json::to_string_pretty(&message_json).unwrap(),
)
.expect("Failed to write menu settings file from web response");
} else if let Ok(message_json) = tui_response {
// Only includes MENU
// From TUI
MENU = message_json;
let conf = MenuJsonStruct {
menu: MENU,
defaults_menu: DEFAULTS_MENU,
};
std::fs::write(MENU_CONF_PATH, serde_json::to_string_pretty(&conf).unwrap())
.expect("Failed to write menu settings file from quick menu response");
.expect("Failed to write menu settings file");
} else {
skyline::error::show_error(
0x70,
@ -105,7 +93,6 @@ pub unsafe fn set_menu_from_json(message: &str) {
);
MENU.quick_menu = OnOff::On;
}
EVENT_QUEUE.push(Event::menu_open(message.to_string()));
}
pub fn spawn_menu() {
@ -127,7 +114,9 @@ pub fn spawn_menu() {
}
} else {
let mut app = QUICK_MENU_APP.lock();
*app = training_mod_tui::App::new(get_menu());
*app = training_mod_tui::App::new(
ui_menu(MENU),
(ui_menu(DEFAULTS_MENU), serde_json::to_string(&DEFAULTS_MENU).unwrap()));
drop(app);
QUICK_MENU_ACTIVE = true;
}
@ -137,6 +126,9 @@ pub fn spawn_menu() {
pub struct ButtonPresses {
pub a: ButtonPress,
pub b: ButtonPress,
pub x: ButtonPress,
pub r: ButtonPress,
pub l: ButtonPress,
pub zr: ButtonPress,
pub zl: ButtonPress,
pub left: ButtonPress,
@ -183,6 +175,21 @@ pub static mut BUTTON_PRESSES: ButtonPresses = ButtonPresses {
is_pressed: false,
lockout_frames: 0,
},
x: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
r: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
l: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
zr: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
@ -240,6 +247,15 @@ pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32
if (*state).Buttons & (1 << 1) > 0 {
BUTTON_PRESSES.b.is_pressed = true;
}
if (*state).Buttons & (1 << 2) > 0 {
BUTTON_PRESSES.x.is_pressed = true;
}
if (*state).Buttons & (1 << 6) > 0 {
BUTTON_PRESSES.l.is_pressed = true;
}
if (*state).Buttons & (1 << 7) > 0 {
BUTTON_PRESSES.r.is_pressed = true;
}
if (*state).Buttons & (1 << 8) > 0 {
BUTTON_PRESSES.zl.is_pressed = true;
}
@ -270,18 +286,20 @@ pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32
use lazy_static::lazy_static;
use parking_lot::Mutex;
use training_mod_tui::AppPage;
lazy_static! {
pub static ref QUICK_MENU_APP: Mutex<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() {
loop {
std::thread::sleep(std::time::Duration::from_secs(10));
let backend = training_mod_tui::TestBackend::new(75, 15);
let mut terminal = training_mod_tui::Terminal::new(backend).unwrap();
let mut json_response = String::new();
let button_presses = &mut BUTTON_PRESSES;
let mut received_input = true;
loop {
@ -299,24 +317,37 @@ pub unsafe fn quick_menu_loop() {
let b_press = &mut button_presses.b;
b_press.read_press().then(|| {
received_input = true;
if !app.outer_list {
if app.page != AppPage::SUBMENU {
app.on_b()
} else if frame_counter::get_frame_count(QUICK_MENU_FRAME_COUNTER_INDEX) == 0
&& !json_response.is_empty()
{
// Leave menu.
QUICK_MENU_ACTIVE = false;
set_menu_from_json(&json_response);
let menu_json = app.get_menu_selections();
set_menu_from_json(&menu_json);
EVENT_QUEUE.push(Event::menu_open(menu_json.to_string()));
}
});
button_presses.zl.read_press().then(|| {
button_presses.x.read_press().then(|| {
app.on_x();
received_input = true;
});
button_presses.l.read_press().then(|| {
app.on_l();
received_input = true;
});
button_presses.zr.read_press().then(|| {
button_presses.r.read_press().then(|| {
app.on_r();
received_input = true;
});
button_presses.zl.read_press().then(|| {
app.on_zl();
received_input = true;
});
button_presses.zr.read_press().then(|| {
app.on_zr();
received_input = true;
});
button_presses.left.read_press().then(|| {
app.on_left();
received_input = true;
@ -335,10 +366,8 @@ pub unsafe fn quick_menu_loop() {
});
if received_input {
terminal
.draw(|f| json_response = training_mod_tui::ui(f, app))
.unwrap();
received_input = false;
set_menu_from_json(&app.get_menu_selections());
}
}
}
@ -360,6 +389,7 @@ unsafe fn spawn_web_session(session: WebSession) {
session.exit();
session.wait_for_exit();
set_menu_from_json(&message_recv);
EVENT_QUEUE.push(Event::menu_open(message_recv.to_string()));
}
unsafe fn new_web_session(hidden: bool) -> WebSession {

View file

@ -35,6 +35,7 @@ use crate::menu::quick_menu_loop;
#[cfg(feature = "web_session_preload")]
use crate::menu::web_session_loop;
use training_mod_consts::{MenuJsonStruct, OnOff};
use crate::training::ui::notifications::notification;
fn nro_main(nro: &NroInfo<'_>) {
if nro.module.isLoaded {
@ -81,10 +82,12 @@ pub fn main() {
info!("Initialized.");
unsafe {
EVENT_QUEUE.push(Event::smash_open());
notification("Training Modpack", "Welcome!", 60);
notification("Open Menu", "Special + Uptaunt", 120);
notification("Save State", "Grab + Downtaunt", 120);
notification("Load State", "Grab + Uptaunt", 120);
}
training::ui_hacks::install_hooks();
hitbox_visualizer::hitbox_visualization();
hazard_manager::hazard_manager();
training::training_mods();

View file

@ -509,6 +509,7 @@ daikon_replace!(DAISY, daisy, 1);
// GenerateArticleForTarget for Peach/Diddy(/Link?) item creation
static GAFT_OFFSET: usize = 0x03d40a0;
#[skyline::hook(offset = GAFT_OFFSET)]
pub unsafe fn handle_generate_article_for_target(
article_module_accessor: *mut app::BattleObjectModuleAccessor,

View file

@ -1,8 +1,10 @@
use skyline::nn::ui2d::ResColor;
use crate::common::consts::FighterId;
use crate::common::*;
use crate::training::*;
pub static mut FRAME_ADVANTAGE: i32 = 0;
static mut FRAME_ADVANTAGE_STR: String = String::new();
static mut PLAYER_ACTIONABLE: bool = false;
static mut CPU_ACTIONABLE: bool = false;
static mut PLAYER_ACTIVE_FRAME: u32 = 0;
@ -47,6 +49,19 @@ unsafe fn is_actionable(module_accessor: *mut app::BattleObjectModuleAccessor) -
fn update_frame_advantage(new_frame_adv: i32) {
unsafe {
FRAME_ADVANTAGE = new_frame_adv;
FRAME_ADVANTAGE_STR = String::new();
FRAME_ADVANTAGE_STR.push_str(&format!("{}", FRAME_ADVANTAGE));
ui::notifications::clear_notifications("Frame Advantage");
ui::notifications::color_notification(
"Frame Advantage",
&FRAME_ADVANTAGE_STR,
60,
match FRAME_ADVANTAGE {
x if x < 0 => ResColor{r: 200, g: 8, b: 8, a: 255},
x if x == 0 => ResColor{r: 0, g: 0, b: 0, a: 255},
_ => ResColor{r: 31, g: 198, b: 0, a: 255},
}
);
}
}

View file

@ -24,7 +24,7 @@ pub mod sdi;
pub mod shield;
pub mod tech;
pub mod throw;
pub mod ui_hacks;
pub mod ui;
mod air_dodge_direction;
mod attack_angle;
@ -570,4 +570,5 @@ pub fn training_mods() {
buff::init();
items::init();
tech::init();
ui::init();
}

146
src/training/ui/damage.rs Normal file
View 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
View 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
View 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
View 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
);
}

View 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

View file

@ -1329,7 +1329,7 @@ impl<'a> SubMenu<'a> {
}
}
#[derive(Content, Serialize)]
#[derive(Content, Serialize, Clone)]
pub struct Tab<'a> {
pub tab_id: &'a str,
pub tab_title: &'a str,
@ -1372,12 +1372,12 @@ impl<'a> Tab<'a> {
}
}
#[derive(Content, Serialize)]
#[derive(Content, Serialize, Clone)]
pub struct UiMenu<'a> {
pub tabs: Vec<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 mash_tab = Tab {
@ -1390,98 +1390,98 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
"mash_state",
"Mash Toggles: Actions to be performed as soon as possible",
false,
&(MENU.mash_state.bits as u32),
&(menu.mash_state.bits as u32),
);
mash_tab.add_submenu_with_toggles::<Action>(
"Followup Toggles",
"follow_up",
"Followup Toggles: Actions to be performed after the Mash option",
false,
&(MENU.follow_up.bits as u32),
&(menu.follow_up.bits as u32),
);
mash_tab.add_submenu_with_toggles::<MashTrigger>(
"Mash Triggers",
"mash_triggers",
"Mash triggers: When the Mash Option will be performed",
false,
&(MENU.mash_triggers.bits as u32),
&(menu.mash_triggers.bits as u32),
);
mash_tab.add_submenu_with_toggles::<AttackAngle>(
"Attack Angle",
"attack_angle",
"Attack Angle: For attacks that can be angled, such as some forward tilts",
false,
&(MENU.attack_angle.bits as u32),
&(menu.attack_angle.bits as u32),
);
mash_tab.add_submenu_with_toggles::<ThrowOption>(
"Throw Options",
"throw_state",
"Throw Options: Throw to be performed when a grab is landed",
false,
&(MENU.throw_state.bits as u32),
&(menu.throw_state.bits as u32),
);
mash_tab.add_submenu_with_toggles::<MedDelay>(
"Throw Delay",
"throw_delay",
"Throw Delay: How many frames to delay the throw option",
false,
&(MENU.throw_delay.bits as u32),
&(menu.throw_delay.bits as u32),
);
mash_tab.add_submenu_with_toggles::<MedDelay>(
"Pummel Delay",
"pummel_delay",
"Pummel Delay: How many frames after a grab to wait before starting to pummel",
false,
&(MENU.pummel_delay.bits as u32),
&(menu.pummel_delay.bits as u32),
);
mash_tab.add_submenu_with_toggles::<BoolFlag>(
"Falling Aerials",
"falling_aerials",
"Falling Aerials: Should aerials be performed when rising or when falling",
false,
&(MENU.falling_aerials.bits as u32),
&(menu.falling_aerials.bits as u32),
);
mash_tab.add_submenu_with_toggles::<BoolFlag>(
"Full Hop",
"full_hop",
"Full Hop: Should the CPU perform a full hop or a short hop",
false,
&(MENU.full_hop.bits as u32),
&(menu.full_hop.bits as u32),
);
mash_tab.add_submenu_with_toggles::<Delay>(
"Aerial Delay",
"aerial_delay",
"Aerial Delay: How long to delay a Mash aerial attack",
false,
&(MENU.aerial_delay.bits as u32),
&(menu.aerial_delay.bits as u32),
);
mash_tab.add_submenu_with_toggles::<BoolFlag>(
"Fast Fall",
"fast_fall",
"Fast Fall: Should the CPU fastfall during a jump",
false,
&(MENU.fast_fall.bits as u32),
&(menu.fast_fall.bits as u32),
);
mash_tab.add_submenu_with_toggles::<Delay>(
"Fast Fall Delay",
"fast_fall_delay",
"Fast Fall Delay: How many frames the CPU should delay their fastfall",
false,
&(MENU.fast_fall_delay.bits as u32),
&(menu.fast_fall_delay.bits as u32),
);
mash_tab.add_submenu_with_toggles::<Delay>(
"OoS Offset",
"oos_offset",
"OoS Offset: How many times the CPU shield can be hit before performing a Mash option",
false,
&(MENU.oos_offset.bits as u32),
&(menu.oos_offset.bits as u32),
);
mash_tab.add_submenu_with_toggles::<Delay>(
"Reaction Time",
"reaction_time",
"Reaction Time: How many frames to delay before performing a mash option",
false,
&(MENU.reaction_time.bits as u32),
&(menu.reaction_time.bits as u32),
);
overall_menu.tabs.push(mash_tab);
@ -1495,77 +1495,77 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
"air_dodge_dir",
"Airdodge Direction: Direction to angle airdodges",
false,
&(MENU.air_dodge_dir.bits as u32),
&(menu.air_dodge_dir.bits as u32),
);
defensive_tab.add_submenu_with_toggles::<Direction>(
"DI Direction",
"di_state",
"DI Direction: Direction to angle the directional influence during hitlag",
false,
&(MENU.di_state.bits as u32),
&(menu.di_state.bits as u32),
);
defensive_tab.add_submenu_with_toggles::<Direction>(
"SDI Direction",
"sdi_state",
"SDI Direction: Direction to angle the smash directional influence during hitlag",
false,
&(MENU.sdi_state.bits as u32),
&(menu.sdi_state.bits as u32),
);
defensive_tab.add_submenu_with_toggles::<SdiFrequency>(
"SDI Strength",
"sdi_strength",
"SDI Strength: Relative strength of the smash directional influence inputs",
true,
&(MENU.sdi_strength as u32),
&(menu.sdi_strength as u32),
);
defensive_tab.add_submenu_with_toggles::<ClatterFrequency>(
"Clatter Strength",
"clatter_strength",
"Clatter Strength: Relative strength of the mashing out of grabs, buries, etc.",
true,
&(MENU.clatter_strength as u32),
&(menu.clatter_strength as u32),
);
defensive_tab.add_submenu_with_toggles::<LedgeOption>(
"Ledge Options",
"ledge_state",
"Ledge Options: Actions to be taken when on the ledge",
false,
&(MENU.ledge_state.bits as u32),
&(menu.ledge_state.bits as u32),
);
defensive_tab.add_submenu_with_toggles::<LongDelay>(
"Ledge Delay",
"ledge_delay",
"Ledge Delay: How many frames to delay the ledge option",
false,
&(MENU.ledge_delay.bits as u32),
&(menu.ledge_delay.bits as u32),
);
defensive_tab.add_submenu_with_toggles::<TechFlags>(
"Tech Options",
"tech_state",
"Tech Options: Actions to take when slammed into a hard surface",
false,
&(MENU.tech_state.bits as u32),
&(menu.tech_state.bits as u32),
);
defensive_tab.add_submenu_with_toggles::<MissTechFlags>(
"Mistech Options",
"miss_tech_state",
"Mistech Options: Actions to take after missing a tech",
false,
&(MENU.miss_tech_state.bits as u32),
&(menu.miss_tech_state.bits as u32),
);
defensive_tab.add_submenu_with_toggles::<Shield>(
"Shield Toggles",
"shield_state",
"Shield Toggles: CPU Shield Behavior",
true,
&(MENU.shield_state as u32),
&(menu.shield_state as u32),
);
defensive_tab.add_submenu_with_toggles::<Direction>(
"Shield Tilt",
"shield_tilt",
"Shield Tilt: Direction to tilt the shield",
false, // TODO: Should this be true?
&(MENU.shield_tilt.bits as u32),
&(menu.shield_tilt.bits as u32),
);
defensive_tab.add_submenu_with_toggles::<OnOff>(
@ -1573,7 +1573,7 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
"crouch",
"Crouch: Should the CPU crouch when on the ground",
true,
&(MENU.crouch as u32),
&(menu.crouch as u32),
);
overall_menu.tabs.push(defensive_tab);
@ -1587,63 +1587,63 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
"save_state_mirroring",
"Mirroring: Flips save states in the left-right direction across the stage center",
true,
&(MENU.save_state_mirroring as u32),
&(menu.save_state_mirroring as u32),
);
save_state_tab.add_submenu_with_toggles::<OnOff>(
"Auto Save States",
"save_state_autoload",
"Auto Save States: Load save state when any fighter dies",
true,
&(MENU.save_state_autoload as u32),
&(menu.save_state_autoload as u32),
);
save_state_tab.add_submenu_with_toggles::<SaveDamage>(
"Save Dmg (CPU)",
"save_damage_cpu",
"Save Damage: Should save states retain CPU damage",
true,
&(MENU.save_damage_cpu.bits as u32),
&(menu.save_damage_cpu.bits as u32),
);
save_state_tab.add_submenu_with_slider::<DamagePercent>(
"Dmg Range (CPU)",
"save_damage_limits_cpu",
"Limits on random damage to apply to the CPU when loading a save state",
&(MENU.save_damage_limits_cpu.0 as u32),
&(MENU.save_damage_limits_cpu.1 as u32),
&(menu.save_damage_limits_cpu.0 as u32),
&(menu.save_damage_limits_cpu.1 as u32),
);
save_state_tab.add_submenu_with_toggles::<SaveDamage>(
"Save Dmg (Player)",
"save_damage_player",
"Save Damage: Should save states retain player damage",
true,
&(MENU.save_damage_player.bits as u32),
&(menu.save_damage_player.bits as u32),
);
save_state_tab.add_submenu_with_slider::<DamagePercent>(
"Dmg Range (Player)",
"save_damage_limits_player",
"Limits on random damage to apply to the player when loading a save state",
&(MENU.save_damage_limits_player.0 as u32),
&(MENU.save_damage_limits_player.1 as u32),
&(menu.save_damage_limits_player.0 as u32),
&(menu.save_damage_limits_player.1 as u32),
);
save_state_tab.add_submenu_with_toggles::<OnOff>(
"Enable Save States",
"save_state_enable",
"Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt.",
true,
&(MENU.save_state_enable as u32),
&(menu.save_state_enable as u32),
);
save_state_tab.add_submenu_with_toggles::<CharacterItem>(
"Character Item",
"character_item",
"Character Item: CPU/Player item to hold when loading a save state",
true,
&(MENU.character_item as u32),
&(menu.character_item as u32),
);
save_state_tab.add_submenu_with_toggles::<BuffOption>(
"Buff Options",
"buff_state",
"Buff Options: Buff(s) to be applied to respective character when loading save states",
false,
&(MENU.buff_state.bits as u32),
&(menu.buff_state.bits as u32),
);
overall_menu.tabs.push(save_state_tab);
@ -1657,42 +1657,42 @@ pub unsafe fn get_menu() -> UiMenu<'static> {
"frame_advantage",
"Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable",
true,
&(MENU.frame_advantage as u32),
&(menu.frame_advantage as u32),
);
misc_tab.add_submenu_with_toggles::<OnOff>(
"Hitbox Visualization",
"hitbox_vis",
"Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects",
true,
&(MENU.hitbox_vis as u32),
&(menu.hitbox_vis as u32),
);
misc_tab.add_submenu_with_toggles::<Delay>(
"Input Delay",
"input_delay",
"Input Delay: Frames to delay player inputs by",
true,
&(MENU.input_delay.bits as u32),
&(menu.input_delay.bits as u32),
);
misc_tab.add_submenu_with_toggles::<OnOff>(
"Stage Hazards",
"stage_hazards",
"Stage Hazards: Should stage hazards be present",
true,
&(MENU.stage_hazards as u32),
&(menu.stage_hazards as u32),
);
misc_tab.add_submenu_with_toggles::<OnOff>(
"Quick Menu",
"quick_menu",
"Quick Menu: Should use quick or web menu",
true,
&(MENU.quick_menu as u32),
&(menu.quick_menu as u32),
);
misc_tab.add_submenu_with_toggles::<OnOff>(
"HUD",
"hud",
"HUD: Turn UI on or off",
true,
&(MENU.hud as u32),
&(menu.hud as u32),
);
overall_menu.tabs.push(misc_tab);

View file

@ -1,4 +1,4 @@
use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu};
use training_mod_consts::{MenuJsonStruct, Slider, SubMenu, SubMenuType, Toggle, UiMenu, ui_menu, TrainingModpackMenu};
use tui::{
backend::Backend,
layout::{Constraint, Corner, Direction, Layout, Rect},
@ -20,6 +20,14 @@ use crate::list::{MultiStatefulList, StatefulList};
static NX_TUI_WIDTH: u16 = 66;
#[derive(PartialEq)]
pub enum AppPage {
SUBMENU,
TOGGLE,
SLIDER,
CONFIRMATION
}
/// We should hold a list of SubMenus.
/// The currently selected SubMenu should also have an associated list with necessary information.
/// We can convert the option types (Toggle, OnOff, Slider) to lists
@ -28,11 +36,12 @@ pub struct App<'a> {
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
pub selected_sub_menu_slider: DoubleEndedGauge,
pub outer_list: bool,
pub page: AppPage,
pub default_menu: (UiMenu<'a>, String),
}
impl<'a> App<'a> {
pub fn new(menu: UiMenu<'a>) -> App<'a> {
pub fn new(menu: UiMenu<'a>, default_menu: (UiMenu<'a>, String)) -> App<'a> {
let num_lists = 3;
let mut menu_items_stateful = HashMap::new();
@ -48,7 +57,8 @@ impl<'a> App<'a> {
menu_items: menu_items_stateful,
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
selected_sub_menu_slider: DoubleEndedGauge::new(),
outer_list: true,
page: AppPage::SUBMENU,
default_menu: default_menu
};
app.set_sub_menu_items();
app
@ -269,7 +279,7 @@ impl<'a> App<'a> {
}
/// Different behavior depending on the current menu location
/// Outer list: Sets self.outer_list to false
/// Submenu list: Enters toggle or slider submenu
/// Toggle submenu: Toggles the selected submenu toggle in self.selected_sub_menu_toggles and in the actual SubMenu struct
/// Slider submenu: Swaps hover/selected state. Updates the actual SubMenu struct if going from Selected -> Hover
pub fn on_a(&mut self) {
@ -288,14 +298,14 @@ impl<'a> App<'a> {
.items
.get_mut(list_idx)
.unwrap();
if self.outer_list {
self.outer_list = false;
if self.page == AppPage::SUBMENU {
match SubMenuType::from_str(selected_sub_menu._type) {
// Need to change the slider state to MinHover so the slider shows up initially
SubMenuType::SLIDER => {
self.page = AppPage::SLIDER;
self.selected_sub_menu_slider.state = GaugeState::MinHover;
}
_ => {}
SubMenuType::TOGGLE => self.page = AppPage::TOGGLE
}
} else {
match SubMenuType::from_str(selected_sub_menu._type) {
@ -371,9 +381,9 @@ impl<'a> App<'a> {
}
/// Different behavior depending on the current menu location
/// Outer list: None
/// Toggle submenu: Sets self.outer_list to true
/// Slider submenu: If in a selected state, then commit changes and change to hover. Else set self.outer_list to true
/// Submenu selection: None
/// Toggle submenu: Sets page to submenu selection
/// Slider submenu: If in a selected state, then commit changes and change to hover. Else set page to submenu selection
pub fn on_b(&mut self) {
let tab_selected = self
.tabs
@ -417,26 +427,55 @@ impl<'a> App<'a> {
},
_ => {}
}
self.outer_list = true;
self.page = AppPage::SUBMENU;
self.set_sub_menu_items();
}
/// Save defaults command
pub fn on_x(&mut self) {
if self.page == AppPage::SUBMENU {
let json = self.to_json();
unsafe {
self.default_menu = (ui_menu(serde_json::from_str::<TrainingModpackMenu>(&json).unwrap()), json);
}
}
}
/// Reset current submenu to defaults
pub fn on_l(&mut self) {
if self.outer_list {
if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
let json = self.to_json();
let mut json_value = serde_json::from_str::<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.set_sub_menu_items();
}
}
pub fn on_r(&mut self) {
if self.outer_list {
pub fn on_zr(&mut self) {
if self.page == AppPage::SUBMENU {
self.tabs.next();
self.set_sub_menu_items();
}
}
pub fn on_up(&mut self) {
if self.outer_list {
if self.page == AppPage::SUBMENU {
self.menu_items
.get_mut(
self.tabs
@ -447,13 +486,13 @@ impl<'a> App<'a> {
.unwrap()
.previous();
self.set_sub_menu_items();
} else {
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
self.sub_menu_previous();
}
}
pub fn on_down(&mut self) {
if self.outer_list {
if self.page == AppPage::SUBMENU {
self.menu_items
.get_mut(
self.tabs
@ -464,13 +503,13 @@ impl<'a> App<'a> {
.unwrap()
.next();
self.set_sub_menu_items();
} else {
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
self.sub_menu_next();
}
}
pub fn on_left(&mut self) {
if self.outer_list {
if self.page == AppPage::SUBMENU {
self.menu_items
.get_mut(
self.tabs
@ -481,13 +520,13 @@ impl<'a> App<'a> {
.unwrap()
.previous_list();
self.set_sub_menu_items();
} else {
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
self.sub_menu_previous_list();
}
}
pub fn on_right(&mut self) {
if self.outer_list {
if self.page == AppPage::SUBMENU {
self.menu_items
.get_mut(
self.tabs
@ -498,13 +537,221 @@ impl<'a> App<'a> {
.unwrap()
.next_list();
self.set_sub_menu_items();
} else {
} else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
self.sub_menu_next_list();
}
}
/// Returns JSON representation of current menu settings
pub fn to_json(&self) -> String {
let mut settings = Map::new();
for key in self.menu_items.keys() {
for list in &self.menu_items.get(key).unwrap().lists {
for sub_menu in &list.items {
if !sub_menu.toggles.is_empty() {
let val: u32 = sub_menu
.toggles
.iter()
.filter(|t| t.checked)
.map(|t| t.toggle_value)
.sum();
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
} else if sub_menu.slider.is_some() {
let s: &Slider = sub_menu.slider.as_ref().unwrap();
let val: Vec<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 tab_selected = app_tabs.state.selected().unwrap();
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]);
if app.outer_list {
let tab_selected = app.tab_selected();
let mut item_help = None;
for (list_section, stateful_list) in app
.menu_items
.get(tab_selected)
.unwrap()
.lists
.iter()
.enumerate()
{
let items: Vec<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]);
}
match app.page {
AppPage::SUBMENU => render_submenu_page(f, app, list_chunks, vertical_chunks[2]),
AppPage::SLIDER => render_slider_page(f, app, vertical_chunks[1], vertical_chunks[2]),
AppPage::TOGGLE => render_toggle_page(f, app, list_chunks, vertical_chunks[2]),
AppPage::CONFIRMATION => todo!()
}
// Collect settings
to_json(app)
// TODO: Add saveDefaults
}
pub fn to_json(app: &App) -> String {
let mut settings = Map::new();
for key in app.menu_items.keys() {
for list in &app.menu_items.get(key).unwrap().lists {
for sub_menu in &list.items {
if !sub_menu.toggles.is_empty() {
let val: u32 = sub_menu
.toggles
.iter()
.filter(|t| t.checked)
.map(|t| t.toggle_value)
.sum();
settings.insert(sub_menu.submenu_id.to_string(), json!(val));
} else if sub_menu.slider.is_some() {
let s: &Slider = sub_menu.slider.as_ref().unwrap();
let val: Vec<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()
}
}

View file

@ -17,10 +17,10 @@ use tui::Terminal;
use training_mod_consts::*;
fn test_backend_setup(ui_menu: UiMenu) -> Result<
(Terminal<training_mod_tui::TestBackend>, training_mod_tui::App),
fn test_backend_setup<'a>(ui_menu: UiMenu<'a>, menu_defaults: (UiMenu<'a>, String)) -> Result<
(Terminal<training_mod_tui::TestBackend>, training_mod_tui::App<'a>),
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 terminal = Terminal::new(backend)?;
let mut state = tui::widgets::ListState::default();
@ -30,26 +30,154 @@ fn test_backend_setup(ui_menu: UiMenu) -> Result<
}
#[test]
fn ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
fn test_set_airdodge() -> Result<(), Box<dyn Error>> {
let menu;
let prev_menu;
let mut prev_menu;
let menu_defaults;
unsafe {
prev_menu = MENU;
menu = get_menu();
prev_menu = MENU.clone();
menu = ui_menu(MENU);
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
}
let (mut terminal, mut app) = test_backend_setup(menu)?;
let mut json_response = String::new();
let _frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;
let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
// Enter Mash Toggles
app.on_a();
// Set Mash Airdodge
app.on_a();
let menu_json = app.get_menu_selections();
let menu_struct = serde_json::from_str::<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 {
MENU = serde_json::from_str::<TrainingModpackMenu>(&json_response).unwrap();
// At this point, we didn't change the menu at all; we should still see all the same options.
assert_eq!(
serde_json::to_string(&prev_menu).unwrap(),
serde_json::to_string(&MENU).unwrap()
);
prev_menu = MENU.clone();
menu = ui_menu(MENU);
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
}
let (_terminal, app) = test_backend_setup(menu, menu_defaults)?;
let menu_json = app.get_menu_selections();
let menu_struct = serde_json::from_str::<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(())
}
@ -57,17 +185,23 @@ fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
let inputs = args.get(1);
let menu;
let menu_defaults;
unsafe {
menu = get_menu();
menu = ui_menu(MENU);
menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
}
#[cfg(not(feature = "has_terminal"))] {
let (mut terminal, mut app) = test_backend_setup(menu)?;
let (mut terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
if inputs.is_some() {
inputs.unwrap().split(",").for_each(|input| {
match input.to_uppercase().as_str() {
"X" => app.on_x(),
"L" => app.on_l(),
"R" => app.on_r(),
"O" => app.on_zl(),
"P" => app.on_zr(),
"A" => app.on_a(),
"B" => app.on_b(),
"UP" => app.on_up(),
@ -78,8 +212,8 @@ fn main() -> Result<(), Box<dyn Error>> {
}
})
}
let mut json_response = String::new();
let frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?;
let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?;
let menu_json = app.get_menu_selections();
for (i, cell) in frame_res.buffer.content().iter().enumerate() {
print!("{}", cell.symbol);
@ -89,11 +223,11 @@ fn main() -> Result<(), Box<dyn Error>> {
}
println!();
println!("json_response:\n{}", json_response);
println!("Menu:\n{menu_json}");
}
#[cfg(feature = "has_terminal")] {
let app = training_mod_tui::App::new(menu);
let app = training_mod_tui::App::new(menu, menu_defaults);
// setup terminal
enable_raw_mode()?;
@ -117,10 +251,10 @@ fn main() -> Result<(), Box<dyn Error>> {
if let Err(err) = res {
println!("{:?}", err)
} else {
println!("JSON: {}", res.as_ref().unwrap());
println!("JSONs: {:#?}", res.as_ref().unwrap());
unsafe {
MENU = serde_json::from_str::<TrainingModpackMenu>(&res.as_ref().unwrap()).unwrap();
println!("MENU: {:#?}", MENU);
let menu = serde_json::from_str::<MenuJsonStruct>(&res.as_ref().unwrap()).unwrap();
println!("menu: {:#?}", menu);
}
}
}
@ -135,9 +269,9 @@ fn run_app<B: tui::backend::Backend>(
tick_rate: Duration,
) -> io::Result<String> {
let mut last_tick = Instant::now();
let mut json_response = String::new();
loop {
terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app).clone())?;
terminal.draw(|f| training_mod_tui::ui(f, &mut app).clone())?;
let menu_json = app.get_menu_selections();
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
@ -146,7 +280,10 @@ fn run_app<B: tui::backend::Backend>(
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(json_response),
KeyCode::Char('q') => return Ok(menu_json),
KeyCode::Char('x') => app.on_x(),
KeyCode::Char('p') => app.on_zr(),
KeyCode::Char('o') => app.on_zl(),
KeyCode::Char('r') => app.on_r(),
KeyCode::Char('l') => app.on_l(),
KeyCode::Left => app.on_left(),