mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2024-11-19 16:36:35 +00:00
Quick Menu + Ryujinx Compatibility (#313)
* Initial commit * Format Rust code using rustfmt * Add back fs calls * working with drawing * wow we're almost there * multi-lists working, selection within tui working * working with tabs * working with smash * amend warnings, fix menu actually saving inputs * small refactors * Fully working! * Fix warnings Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
c6c4105fc3
commit
e4e2de0a79
17 changed files with 1916 additions and 774 deletions
1
.github/workflows/rust.yml
vendored
1
.github/workflows/rust.yml
vendored
|
@ -96,6 +96,7 @@ jobs:
|
|||
cp libparam_hook.nro ${{env.SMASH_PLUGIN_DIR}}/libparam_hook.nro
|
||||
cp libnro_hook.nro ${{env.SMASH_PLUGIN_DIR}}/libnro_hook.nro
|
||||
cp libnn_hid_hook.nro ${{env.SMASH_PLUGIN_DIR}}/libnn_hid_hook.nro
|
||||
cp libtraining_modpack_menu.nro ${{env.SMASH_PLUGIN_DIR}}/libtraining_modpack_menu.nro
|
||||
cp -r static/* ${{env.SMASH_WEB_DIR}}
|
||||
rm ${{env.SMASH_WEB_DIR}}/fonts -r
|
||||
zip -r training_modpack_beta.zip atmosphere
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,7 +3,7 @@
|
|||
**/*.pyc
|
||||
Cargo.lock
|
||||
|
||||
TrainingModpackOverlay/build/
|
||||
.idea/
|
||||
|
||||
*.ovl
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ minreq = { version = "=2.2.1", features = ["https", "json-using-serde"] }
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
training_mod_consts = { path = "training_mod_consts" }
|
||||
training_mod_tui = { path = "training_mod_tui" }
|
||||
|
||||
[patch.crates-io]
|
||||
ring = { git = "https://github.com/skyline-rs/ring", branch = "0.16.20" }
|
||||
|
|
17
ryujinx_build.sh
Normal file
17
ryujinx_build.sh
Normal file
|
@ -0,0 +1,17 @@
|
|||
set -eu
|
||||
|
||||
# Obviously adjust these based on your paths
|
||||
RYUJINX_APPLICATION_PATH="/mnt/c/Users/Jdsam/Downloads/ryujinx-Release-1.0.0+ba3ae74-win_x64/Ryujinx.exe"
|
||||
SMASH_APPLICATION_PATH="C:\Users\Jdsam\Downloads\Super Smash Bros. Ultimate (World) (En,Ja,Fr,De,Es,It,Nl,Zh-Hant,Zh-Hans,Ko,Ru)\Super Smash Bros. Ultimate (World) (En,Ja,Fr,De,Es,It,Nl,Zh-Hant,Zh-Hans,Ko,Ru).xci"
|
||||
RYUJINX_SMASH_SKYLINE_PLUGINS_PATH="/mnt/c/Users/Jdsam/AppData/Roaming/Ryujinx/mods/contents/01006a800016e000/romfs/skyline/plugins"
|
||||
|
||||
# Build with release feature
|
||||
cargo skyline build --release
|
||||
|
||||
# Copy over to plugins path
|
||||
cp target/aarch64-skyline-switch/release/libtraining_modpack.nro $RYUJINX_SMASH_SKYLINE_PLUGINS_PATH
|
||||
|
||||
# Run Ryujinx
|
||||
$RYUJINX_APPLICATION_PATH "${SMASH_APPLICATION_PATH}"
|
||||
|
||||
# Here, you can run `cargo skyline set-ip {IP address...}; cargo skyline listen` for logs
|
|
@ -1,17 +1,18 @@
|
|||
use crate::common::*;
|
||||
use crate::events::{Event, EVENT_QUEUE};
|
||||
use crate::training::frame_counter;
|
||||
use ramhorns::{Content, Template};
|
||||
use crate::common::consts::get_menu_from_url;
|
||||
use ramhorns::Template;
|
||||
use skyline::info::get_program_id;
|
||||
use skyline_web::{Background, BootDisplay, Webpage};
|
||||
use smash::lib::lua_const::*;
|
||||
use std::fs;
|
||||
use std::ops::BitOr;
|
||||
use std::path::Path;
|
||||
use strum::IntoEnumIterator;
|
||||
use crate::mkdir;
|
||||
|
||||
static mut FRAME_COUNTER_INDEX: usize = 0;
|
||||
const MENU_LOCKOUT_FRAMES: u32 = 15;
|
||||
pub static mut QUICK_MENU_ACTIVE: bool = false;
|
||||
|
||||
pub fn init() {
|
||||
unsafe {
|
||||
|
@ -20,282 +21,6 @@ pub fn init() {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Content)]
|
||||
struct Slider {
|
||||
min: usize,
|
||||
max: usize,
|
||||
index: usize,
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[derive(Content)]
|
||||
struct Toggle<'a> {
|
||||
title: &'a str,
|
||||
checked: &'a str,
|
||||
index: usize,
|
||||
value: usize,
|
||||
default: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Content)]
|
||||
struct OnOffSelector<'a> {
|
||||
title: &'a str,
|
||||
checked: &'a str,
|
||||
default: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Content)]
|
||||
struct SubMenu<'a> {
|
||||
title: &'a str,
|
||||
id: &'a str,
|
||||
toggles: Vec<Toggle<'a>>,
|
||||
sliders: Vec<Slider>,
|
||||
onoffselector: Vec<OnOffSelector<'a>>,
|
||||
index: usize,
|
||||
check_against: usize,
|
||||
is_single_option: Option<bool>,
|
||||
help_text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> SubMenu<'a> {
|
||||
pub fn max_idx(&self) -> usize {
|
||||
self.toggles
|
||||
.iter()
|
||||
.max_by(|t1, t2| t1.index.cmp(&t2.index))
|
||||
.map(|t| t.index)
|
||||
.unwrap_or(self.index)
|
||||
}
|
||||
|
||||
pub fn add_toggle(&mut self, title: &'a str, checked: bool, value: usize, default: bool) {
|
||||
self.toggles.push(Toggle {
|
||||
title,
|
||||
checked: if checked { "is-appear" } else { "is-hidden" },
|
||||
index: self.max_idx() + 1,
|
||||
value,
|
||||
default: if default { "is-appear" } else { "is-hidden" },
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_slider(&mut self, min: usize, max: usize, value: usize) {
|
||||
self.sliders.push(Slider {
|
||||
min,
|
||||
max,
|
||||
index: self.max_idx() + 1,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_onoffselector(&mut self, title: &'a str, checked: bool, default: bool) {
|
||||
// TODO: Is there a more elegant way to do this?
|
||||
// The HTML only supports a single onoffselector but the SubMenu stores it as a Vec
|
||||
self.onoffselector.push(OnOffSelector {
|
||||
title,
|
||||
checked: if checked { "is-appear" } else { "is-hidden" },
|
||||
default: if default { "is-appear" } else { "is-hidden" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Content)]
|
||||
struct Menu<'a> {
|
||||
sub_menus: Vec<SubMenu<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Menu<'a> {
|
||||
pub fn max_idx(&self) -> usize {
|
||||
self.sub_menus
|
||||
.iter()
|
||||
.max_by(|x, y| x.max_idx().cmp(&y.max_idx()))
|
||||
.map(|sub_menu| sub_menu.max_idx())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn add_sub_menu(
|
||||
&mut self,
|
||||
title: &'a str,
|
||||
id: &'a str,
|
||||
check_against: usize,
|
||||
toggles: Vec<(&'a str, usize)>,
|
||||
sliders: Vec<(usize, usize, usize)>,
|
||||
defaults: usize,
|
||||
help_text: &'a str,
|
||||
) {
|
||||
let mut sub_menu = SubMenu {
|
||||
title,
|
||||
id,
|
||||
toggles: Vec::new(),
|
||||
sliders: Vec::new(),
|
||||
onoffselector: Vec::new(),
|
||||
index: self.max_idx() + 1,
|
||||
check_against,
|
||||
is_single_option: Some(true),
|
||||
help_text,
|
||||
};
|
||||
|
||||
for toggle in toggles {
|
||||
sub_menu.add_toggle(
|
||||
toggle.0,
|
||||
(check_against & toggle.1) != 0,
|
||||
toggle.1,
|
||||
(defaults & toggle.1) != 0,
|
||||
)
|
||||
}
|
||||
|
||||
for slider in sliders {
|
||||
sub_menu.add_slider(slider.0, slider.1, slider.2);
|
||||
}
|
||||
|
||||
self.sub_menus.push(sub_menu);
|
||||
}
|
||||
|
||||
pub fn add_sub_menu_sep(
|
||||
&mut self,
|
||||
title: &'a str,
|
||||
id: &'a str,
|
||||
check_against: usize,
|
||||
strs: Vec<&'a str>,
|
||||
vals: Vec<usize>,
|
||||
defaults: usize,
|
||||
help_text: &'a str,
|
||||
) {
|
||||
let mut sub_menu = SubMenu {
|
||||
title,
|
||||
id,
|
||||
toggles: Vec::new(),
|
||||
sliders: Vec::new(),
|
||||
onoffselector: Vec::new(),
|
||||
index: self.max_idx() + 1,
|
||||
check_against,
|
||||
is_single_option: None,
|
||||
help_text,
|
||||
};
|
||||
|
||||
for i in 0..strs.len() {
|
||||
sub_menu.add_toggle(
|
||||
strs[i],
|
||||
(check_against & vals[i]) != 0,
|
||||
vals[i],
|
||||
(defaults & vals[i]) != 0,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: add sliders?
|
||||
|
||||
self.sub_menus.push(sub_menu);
|
||||
}
|
||||
|
||||
pub fn add_sub_menu_onoff(
|
||||
&mut self,
|
||||
title: &'a str,
|
||||
id: &'a str,
|
||||
check_against: usize,
|
||||
checked: bool,
|
||||
default: usize,
|
||||
help_text: &'a str,
|
||||
) {
|
||||
let mut sub_menu = SubMenu {
|
||||
title,
|
||||
id,
|
||||
toggles: Vec::new(),
|
||||
sliders: Vec::new(),
|
||||
onoffselector: Vec::new(),
|
||||
index: self.max_idx() + 1,
|
||||
check_against,
|
||||
is_single_option: None,
|
||||
help_text,
|
||||
};
|
||||
|
||||
sub_menu.add_onoffselector(title, checked, (default & OnOff::On as usize) != 0);
|
||||
self.sub_menus.push(sub_menu);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! add_bitflag_submenu {
|
||||
($menu:ident, $title:literal, $id:ident, $e:ty, $help_text:literal) => {
|
||||
paste::paste!{
|
||||
let [<$id _strs>] = <$e>::to_toggle_strs();
|
||||
let [<$id _vals>] = <$e>::to_toggle_vals();
|
||||
|
||||
$menu.add_sub_menu_sep(
|
||||
$title,
|
||||
stringify!($id),
|
||||
MENU.$id.bits() as usize,
|
||||
[<$id _strs>],
|
||||
[<$id _vals>],
|
||||
DEFAULT_MENU.$id.bits() as usize,
|
||||
stringify!($help_text),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! add_single_option_submenu {
|
||||
($menu:ident, $title:literal, $id:ident, $e:ty, $help_text:literal) => {
|
||||
paste::paste!{
|
||||
let mut [<$id _toggles>] = Vec::new();
|
||||
for val in [<$e>]::iter() {
|
||||
[<$id _toggles>].push((val.as_str().unwrap_or(""), val as usize));
|
||||
}
|
||||
|
||||
$menu.add_sub_menu(
|
||||
$title,
|
||||
stringify!($id),
|
||||
MENU.$id as usize,
|
||||
[<$id _toggles>],
|
||||
[].to_vec(),
|
||||
DEFAULT_MENU.$id as usize,
|
||||
stringify!($help_text),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! add_onoff_submenu {
|
||||
($menu:ident, $title:literal, $id:ident, $help_text:literal) => {
|
||||
paste::paste! {
|
||||
$menu.add_sub_menu_onoff(
|
||||
$title,
|
||||
stringify!($id),
|
||||
MENU.$id as usize,
|
||||
(MENU.$id as usize & OnOff::On as usize) != 0,
|
||||
DEFAULT_MENU.$id as usize,
|
||||
stringify!($help_text),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_menu_from_url(mut menu: TrainingModpackMenu, s: &str) -> TrainingModpackMenu {
|
||||
let base_url_len = "http://localhost/?".len();
|
||||
let total_len = s.len();
|
||||
|
||||
let ss: String = s
|
||||
.chars()
|
||||
.skip(base_url_len)
|
||||
.take(total_len - base_url_len)
|
||||
.collect();
|
||||
|
||||
for toggle_values in ss.split('&') {
|
||||
let toggle_value_split = toggle_values.split('=').collect::<Vec<&str>>();
|
||||
let toggle = toggle_value_split[0];
|
||||
if toggle.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let toggle_vals = toggle_value_split[1];
|
||||
|
||||
let bitwise_or = <u32 as BitOr<u32>>::bitor;
|
||||
let bits = toggle_vals
|
||||
.split(',')
|
||||
.filter(|val| !val.is_empty())
|
||||
.map(|val| val.parse().unwrap())
|
||||
.fold(0, bitwise_or);
|
||||
|
||||
menu.set(toggle, bits);
|
||||
}
|
||||
menu
|
||||
}
|
||||
|
||||
pub unsafe fn menu_condition(module_accessor: &mut smash::app::BattleObjectModuleAccessor) -> bool {
|
||||
// Only check for button combination if the counter is 0 (not locked out)
|
||||
match frame_counter::get_frame_count(FRAME_COUNTER_INDEX) {
|
||||
|
@ -318,255 +43,7 @@ pub unsafe fn menu_condition(module_accessor: &mut smash::app::BattleObjectModul
|
|||
pub unsafe fn write_menu() {
|
||||
let tpl = Template::new(include_str!("../templates/menu.html")).unwrap();
|
||||
|
||||
let mut overall_menu = Menu {
|
||||
sub_menus: Vec::new(),
|
||||
};
|
||||
|
||||
// Toggle/bitflag menus
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Mash Toggles",
|
||||
mash_state,
|
||||
Action,
|
||||
"Mash Toggles: Actions to be performed as soon as possible"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Followup Toggles",
|
||||
follow_up,
|
||||
Action,
|
||||
"Followup Toggles: Actions to be performed after the Mash option"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Attack Angle",
|
||||
attack_angle,
|
||||
AttackAngle,
|
||||
"Attack Angle: For attacks that can be angled, such as some forward tilts"
|
||||
);
|
||||
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Ledge Options",
|
||||
ledge_state,
|
||||
LedgeOption,
|
||||
"Ledge Options: Actions to be taken when on the ledge"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Ledge Delay",
|
||||
ledge_delay,
|
||||
LongDelay,
|
||||
"Ledge Delay: How many frames to delay the ledge option"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Tech Options",
|
||||
tech_state,
|
||||
TechFlags,
|
||||
"Tech Options: Actions to take when slammed into a hard surface"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Miss Tech Options",
|
||||
miss_tech_state,
|
||||
MissTechFlags,
|
||||
"Miss Tech Options: Actions to take after missing a tech"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Defensive Options",
|
||||
defensive_state,
|
||||
Defensive,
|
||||
"Defensive Options: Actions to take after a ledge option, tech option, or miss tech option"
|
||||
);
|
||||
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Aerial Delay",
|
||||
aerial_delay,
|
||||
Delay,
|
||||
"Aerial Delay: How long to delay a Mash aerial attack"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"OoS Offset",
|
||||
oos_offset,
|
||||
Delay,
|
||||
"OoS Offset: How many times the CPU shield can be hit before performing a Mash option"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Reaction Time",
|
||||
reaction_time,
|
||||
Delay,
|
||||
"Reaction Time: How many frames to delay before performing an option out of shield"
|
||||
);
|
||||
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Fast Fall",
|
||||
fast_fall,
|
||||
BoolFlag,
|
||||
"Fast Fall: Should the CPU fastfall during a jump"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Fast Fall Delay",
|
||||
fast_fall_delay,
|
||||
Delay,
|
||||
"Fast Fall Delay: How many frames the CPU should delay their fastfall"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Falling Aerials",
|
||||
falling_aerials,
|
||||
BoolFlag,
|
||||
"Falling Aerials: Should aerials be performed when rising or when falling"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Full Hop",
|
||||
full_hop,
|
||||
BoolFlag,
|
||||
"Full Hop: Should the CPU perform a full hop or a short hop"
|
||||
);
|
||||
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Shield Tilt",
|
||||
shield_tilt,
|
||||
Direction,
|
||||
"Shield Tilt: Direction to tilt the shield"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"DI Direction",
|
||||
di_state,
|
||||
Direction,
|
||||
"DI Direction: Direction to angle the directional influence during hitlag"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"SDI Direction",
|
||||
sdi_state,
|
||||
Direction,
|
||||
"SDI Direction: Direction to angle the smash directional influence during hitlag"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Airdodge Direction",
|
||||
air_dodge_dir,
|
||||
Direction,
|
||||
"Airdodge Direction: Direction to angle airdodges"
|
||||
);
|
||||
|
||||
add_single_option_submenu!(
|
||||
overall_menu,
|
||||
"SDI Strength",
|
||||
sdi_strength,
|
||||
SdiStrength,
|
||||
"SDI Strength: Relative strength of the smash directional influence inputs"
|
||||
);
|
||||
add_single_option_submenu!(
|
||||
overall_menu,
|
||||
"Shield Toggles",
|
||||
shield_state,
|
||||
Shield,
|
||||
"Shield Toggles: CPU Shield Behavior"
|
||||
);
|
||||
add_single_option_submenu!(
|
||||
overall_menu,
|
||||
"Mirroring",
|
||||
save_state_mirroring,
|
||||
SaveStateMirroring,
|
||||
"Mirroring: Flips save states in the left-right direction across the stage center"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Throw Options",
|
||||
throw_state,
|
||||
ThrowOption,
|
||||
"Throw Options: Throw to be performed when a grab is landed"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Throw Delay",
|
||||
throw_delay,
|
||||
MedDelay,
|
||||
"Throw Delay: How many frames to delay the throw option"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Pummel Delay",
|
||||
pummel_delay,
|
||||
MedDelay,
|
||||
"Pummel Delay: How many frames after a grab to wait before starting to pummel"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Buff Options",
|
||||
buff_state,
|
||||
BuffOption,
|
||||
"Buff Options: Buff(s) to be applied to respective character when loading save states"
|
||||
);
|
||||
|
||||
// Slider menus
|
||||
overall_menu.add_sub_menu(
|
||||
"Input Delay",
|
||||
"input_delay",
|
||||
// unnecessary for slider?
|
||||
MENU.input_delay as usize,
|
||||
[
|
||||
("0", 0),
|
||||
("1", 1),
|
||||
("2", 2),
|
||||
("3", 3),
|
||||
("4", 4),
|
||||
("5", 5),
|
||||
("6", 6),
|
||||
("7", 7),
|
||||
("8", 8),
|
||||
("9", 9),
|
||||
("10", 10),
|
||||
]
|
||||
.to_vec(),
|
||||
[].to_vec(), //(0, 10, MENU.input_delay as usize)
|
||||
DEFAULT_MENU.input_delay as usize,
|
||||
stringify!("Input Delay: Frames to delay player inputs by"),
|
||||
);
|
||||
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Save States",
|
||||
save_state_enable,
|
||||
"Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt."
|
||||
);
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Save Damage",
|
||||
save_damage,
|
||||
"Save Damage: Should save states retain player/CPU damage"
|
||||
);
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Hitbox Visualization",
|
||||
hitbox_vis,
|
||||
"Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects"
|
||||
);
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Stage Hazards",
|
||||
stage_hazards,
|
||||
"Stage Hazards: Should stage hazards be present"
|
||||
);
|
||||
add_onoff_submenu!(overall_menu, "Frame Advantage", frame_advantage, "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable");
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Mash In Neutral",
|
||||
mash_in_neutral,
|
||||
"Mash In Neutral: Should Mash options be performed repeatedly or only when the CPU is hit"
|
||||
);
|
||||
let overall_menu = get_menu();
|
||||
|
||||
let data = tpl.render(&overall_menu);
|
||||
|
||||
|
@ -574,41 +51,45 @@ pub unsafe fn write_menu() {
|
|||
// From skyline-web
|
||||
let program_id = get_program_id();
|
||||
let htdocs_dir = "training_modpack";
|
||||
let path = Path::new("sd:/atmosphere/contents")
|
||||
let menu_dir_path = Path::new("sd:/atmosphere/contents")
|
||||
.join(&format!("{:016X}", program_id))
|
||||
.join(&format!("manual_html/html-document/{}.htdocs/", htdocs_dir))
|
||||
.join(&format!("manual_html/html-document/{}.htdocs/", htdocs_dir));
|
||||
|
||||
let menu_html_path = menu_dir_path
|
||||
.join("training_menu.html");
|
||||
fs::write(path, data).unwrap();
|
||||
|
||||
mkdir(menu_dir_path.to_str().unwrap().as_bytes().as_ptr(), 777);
|
||||
let write_resp = fs::write(menu_html_path, data);
|
||||
if write_resp.is_err() {
|
||||
println!("Error!: {}", write_resp.err().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.conf";
|
||||
|
||||
pub fn spawn_menu() {
|
||||
unsafe {
|
||||
frame_counter::reset_frame_count(FRAME_COUNTER_INDEX);
|
||||
frame_counter::start_counting(FRAME_COUNTER_INDEX);
|
||||
}
|
||||
|
||||
let fname = "training_menu.html";
|
||||
let params = unsafe { MENU.to_url_params() };
|
||||
let page_response = Webpage::new()
|
||||
.background(Background::BlurredScreenshot)
|
||||
.htdocs_dir("training_modpack")
|
||||
.boot_display(BootDisplay::BlurredScreenshot)
|
||||
.boot_icon(true)
|
||||
.start_page(&format!("{}{}", fname, params))
|
||||
.open()
|
||||
.unwrap();
|
||||
|
||||
let orig_last_url = page_response.get_last_url().unwrap();
|
||||
pub fn set_menu_from_url(orig_last_url: &str) {
|
||||
let last_url = &orig_last_url.replace("&save_defaults=1", "");
|
||||
unsafe {
|
||||
MENU = get_menu_from_url(MENU, last_url);
|
||||
|
||||
if MENU.quick_menu == OnOff::Off {
|
||||
let is_emulator = skyline::hooks::getRegionAddress(skyline::hooks::Region::Text) as u64 == 0x8004000;
|
||||
if is_emulator {
|
||||
skyline::error::show_error(
|
||||
0x69,
|
||||
"Cannot use web menu on emulator.\n",
|
||||
"Only the quick menu is runnable via emulator currently.",
|
||||
);
|
||||
}
|
||||
|
||||
MENU.quick_menu = OnOff::On;
|
||||
}
|
||||
}
|
||||
|
||||
if last_url.len() != orig_last_url.len() {
|
||||
// Save as default
|
||||
unsafe {
|
||||
DEFAULT_MENU = get_menu_from_url(DEFAULT_MENU, last_url);
|
||||
DEFAULT_MENU = MENU;
|
||||
write_menu();
|
||||
}
|
||||
let menu_defaults_conf_path = "sd:/TrainingModpack/training_modpack_menu_defaults.conf";
|
||||
|
@ -621,3 +102,152 @@ pub fn spawn_menu() {
|
|||
EVENT_QUEUE.push(Event::menu_open(last_url.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_menu() {
|
||||
unsafe {
|
||||
frame_counter::reset_frame_count(FRAME_COUNTER_INDEX);
|
||||
frame_counter::start_counting(FRAME_COUNTER_INDEX);
|
||||
}
|
||||
|
||||
let mut quick_menu = false;
|
||||
unsafe {
|
||||
if MENU.quick_menu == OnOff::On {
|
||||
quick_menu = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !quick_menu {
|
||||
let fname = "training_menu.html";
|
||||
let params = unsafe { MENU.to_url_params() };
|
||||
let page_response = Webpage::new()
|
||||
.background(Background::BlurredScreenshot)
|
||||
.htdocs_dir("training_modpack")
|
||||
.boot_display(BootDisplay::BlurredScreenshot)
|
||||
.boot_icon(true)
|
||||
.start_page(&format!("{}{}", fname, params))
|
||||
.open()
|
||||
.unwrap();
|
||||
|
||||
let orig_last_url = page_response.get_last_url().unwrap();
|
||||
|
||||
set_menu_from_url(orig_last_url);
|
||||
} else {
|
||||
unsafe {
|
||||
QUICK_MENU_ACTIVE = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use skyline::nn::hid::NpadGcState;
|
||||
|
||||
pub struct ButtonPresses {
|
||||
pub a: ButtonPress,
|
||||
pub b: ButtonPress,
|
||||
pub zr: ButtonPress,
|
||||
pub zl: ButtonPress,
|
||||
pub left: ButtonPress,
|
||||
pub right: ButtonPress,
|
||||
pub up: ButtonPress,
|
||||
pub down: ButtonPress
|
||||
}
|
||||
|
||||
pub struct ButtonPress {
|
||||
pub is_pressed: bool,
|
||||
pub lockout_frames: usize
|
||||
}
|
||||
|
||||
impl ButtonPress {
|
||||
pub fn default() -> ButtonPress {
|
||||
ButtonPress{
|
||||
is_pressed: false,
|
||||
lockout_frames: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_press(&mut self) -> bool {
|
||||
if self.is_pressed {
|
||||
self.is_pressed = false;
|
||||
if self.lockout_frames == 0 {
|
||||
self.lockout_frames = 15;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.lockout_frames > 0 {
|
||||
self.lockout_frames -= 1;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonPresses {
|
||||
pub fn default() -> ButtonPresses {
|
||||
ButtonPresses{
|
||||
a: ButtonPress::default(),
|
||||
b: ButtonPress::default(),
|
||||
zr: ButtonPress::default(),
|
||||
zl: ButtonPress::default(),
|
||||
left: ButtonPress::default(),
|
||||
right: ButtonPress::default(),
|
||||
up: ButtonPress::default(),
|
||||
down: ButtonPress::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static mut BUTTON_PRESSES : ButtonPresses = ButtonPresses{
|
||||
a: ButtonPress{is_pressed: false, lockout_frames: 0},
|
||||
b: ButtonPress{is_pressed: false, lockout_frames: 0},
|
||||
zr: ButtonPress{is_pressed: false, lockout_frames: 0},
|
||||
zl: ButtonPress{is_pressed: false, lockout_frames: 0},
|
||||
left: ButtonPress{is_pressed: false, lockout_frames: 0},
|
||||
right: ButtonPress{is_pressed: false, lockout_frames: 0},
|
||||
up: ButtonPress{is_pressed: false, lockout_frames: 0},
|
||||
down: ButtonPress{is_pressed: false, lockout_frames: 0},
|
||||
};
|
||||
|
||||
pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32) {
|
||||
unsafe {
|
||||
if menu::QUICK_MENU_ACTIVE {
|
||||
// TODO: This should make more sense, look into.
|
||||
// BUTTON_PRESSES.a.is_pressed = (*state).Buttons & (1 << 0) > 0;
|
||||
// BUTTON_PRESSES.b.is_pressed = (*state).Buttons & (1 << 1) > 0;
|
||||
// BUTTON_PRESSES.zl.is_pressed = (*state).Buttons & (1 << 8) > 0;
|
||||
// BUTTON_PRESSES.zr.is_pressed = (*state).Buttons & (1 << 9) > 0;
|
||||
// BUTTON_PRESSES.left.is_pressed = (*state).Buttons & ((1 << 12) | (1 << 16)) > 0;
|
||||
// BUTTON_PRESSES.right.is_pressed = (*state).Buttons & ((1 << 14) | (1 << 18)) > 0;
|
||||
// BUTTON_PRESSES.down.is_pressed = (*state).Buttons & ((1 << 15) | (1 << 19)) > 0;
|
||||
// BUTTON_PRESSES.up.is_pressed = (*state).Buttons & ((1 << 13) | (1 << 17)) > 0;
|
||||
if (*state).Buttons & (1 << 0) > 0 {
|
||||
BUTTON_PRESSES.a.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & (1 << 1) > 0 {
|
||||
BUTTON_PRESSES.b.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & (1 << 8) > 0 {
|
||||
BUTTON_PRESSES.zl.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & (1 << 9) > 0 {
|
||||
BUTTON_PRESSES.zr.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & ((1 << 12) | (1 << 16)) > 0 {
|
||||
BUTTON_PRESSES.left.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & ((1 << 14) | (1 << 18)) > 0 {
|
||||
BUTTON_PRESSES.right.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & ((1 << 15) | (1 << 19)) > 0 {
|
||||
BUTTON_PRESSES.down.is_pressed = true;
|
||||
}
|
||||
if (*state).Buttons & ((1 << 13) | (1 << 17)) > 0 {
|
||||
BUTTON_PRESSES.up.is_pressed = true;
|
||||
}
|
||||
|
||||
// If we're here, remove all other Npad presses...
|
||||
// Should we exclude the home button?
|
||||
(*state) = NpadGcState::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,44 +8,9 @@ use crate::common::consts::*;
|
|||
use smash::app::{self, lua_bind::*};
|
||||
use smash::lib::lua_const::*;
|
||||
|
||||
pub static BASE_MENU: consts::TrainingModpackMenu = consts::TrainingModpackMenu {
|
||||
hitbox_vis: OnOff::On,
|
||||
stage_hazards: OnOff::Off,
|
||||
di_state: Direction::empty(),
|
||||
sdi_state: Direction::empty(),
|
||||
sdi_strength: SdiStrength::Normal,
|
||||
air_dodge_dir: Direction::empty(),
|
||||
mash_state: Action::empty(),
|
||||
follow_up: Action::empty(),
|
||||
attack_angle: AttackAngle::empty(),
|
||||
ledge_state: LedgeOption::all(),
|
||||
ledge_delay: LongDelay::empty(),
|
||||
tech_state: TechFlags::all(),
|
||||
miss_tech_state: MissTechFlags::all(),
|
||||
shield_state: Shield::None,
|
||||
defensive_state: Defensive::all(),
|
||||
oos_offset: Delay::empty(),
|
||||
shield_tilt: Direction::empty(),
|
||||
reaction_time: Delay::empty(),
|
||||
mash_in_neutral: OnOff::Off,
|
||||
fast_fall: BoolFlag::empty(),
|
||||
fast_fall_delay: Delay::empty(),
|
||||
falling_aerials: BoolFlag::empty(),
|
||||
aerial_delay: Delay::empty(),
|
||||
full_hop: BoolFlag::empty(),
|
||||
input_delay: 0,
|
||||
save_damage: OnOff::On,
|
||||
save_state_mirroring: SaveStateMirroring::None,
|
||||
frame_advantage: OnOff::Off,
|
||||
save_state_enable: OnOff::On,
|
||||
throw_state: ThrowOption::NONE,
|
||||
throw_delay: MedDelay::empty(),
|
||||
pummel_delay: MedDelay::empty(),
|
||||
buff_state: BuffOption::empty(),
|
||||
};
|
||||
|
||||
pub static mut DEFAULT_MENU: TrainingModpackMenu = BASE_MENU;
|
||||
pub static mut MENU: TrainingModpackMenu = BASE_MENU;
|
||||
pub use crate::common::consts::MENU;
|
||||
pub static mut DEFAULT_MENU: TrainingModpackMenu = crate::common::consts::DEFAULT_MENU;
|
||||
pub static mut BASE_MENU: TrainingModpackMenu = unsafe { DEFAULT_MENU };
|
||||
pub static mut FIGHTER_MANAGER_ADDR: usize = 0;
|
||||
pub static mut STAGE_MANAGER_ADDR: usize = 0;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#![allow(dead_code)]
|
||||
#![allow(unused_assignments)]
|
||||
#![allow(unused_variables)]
|
||||
use crate::common::{consts::*, *};
|
||||
use crate::common::consts::*;
|
||||
use skyline::error::show_error;
|
||||
use skyline::hook;
|
||||
use skyline::hooks::A64InlineHook;
|
||||
|
|
349
src/lib.rs
349
src/lib.rs
|
@ -1,135 +1,214 @@
|
|||
#![feature(proc_macro_hygiene)]
|
||||
#![feature(with_options)]
|
||||
#![feature(const_mut_refs)]
|
||||
#![feature(exclusive_range_pattern)]
|
||||
#![feature(once_cell)]
|
||||
#![allow(
|
||||
clippy::borrow_interior_mutable_const,
|
||||
clippy::not_unsafe_ptr_arg_deref,
|
||||
clippy::missing_safety_doc,
|
||||
clippy::wrong_self_convention
|
||||
)]
|
||||
|
||||
pub mod common;
|
||||
mod hazard_manager;
|
||||
mod hitbox_visualizer;
|
||||
mod training;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use crate::common::*;
|
||||
use crate::events::{Event, EVENT_QUEUE};
|
||||
use crate::menu::get_menu_from_url;
|
||||
|
||||
use skyline::libc::mkdir;
|
||||
use skyline::nro::{self, NroInfo};
|
||||
use std::fs;
|
||||
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
fn nro_main(nro: &NroInfo<'_>) {
|
||||
if nro.module.isLoaded {
|
||||
return;
|
||||
}
|
||||
|
||||
if nro.name == "common" {
|
||||
skyline::install_hooks!(
|
||||
training::shield::handle_sub_guard_cont,
|
||||
training::directional_influence::handle_correct_damage_vector_common,
|
||||
training::sdi::process_hit_stop_delay,
|
||||
training::tech::handle_change_status,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! c_str {
|
||||
($l:tt) => {
|
||||
[$l.as_bytes(), "\u{0}".as_bytes()].concat().as_ptr();
|
||||
};
|
||||
}
|
||||
|
||||
#[skyline::main(name = "training_modpack")]
|
||||
pub fn main() {
|
||||
macro_rules! log {
|
||||
($($arg:tt)*) => {
|
||||
print!("{}", "[Training Modpack] ".green());
|
||||
println!($($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
log!("Initialized.");
|
||||
unsafe {
|
||||
EVENT_QUEUE.push(Event::smash_open());
|
||||
}
|
||||
|
||||
hitbox_visualizer::hitbox_visualization();
|
||||
hazard_manager::hazard_manager();
|
||||
training::training_mods();
|
||||
nro::add_hook(nro_main).unwrap();
|
||||
|
||||
unsafe {
|
||||
mkdir(c_str!("sd:/TrainingModpack/"), 777);
|
||||
}
|
||||
|
||||
let ovl_path = "sd:/switch/.overlays/ovlTrainingModpack.ovl";
|
||||
if fs::metadata(ovl_path).is_ok() {
|
||||
log!("Removing ovlTrainingModpack.ovl...");
|
||||
fs::remove_file(ovl_path).unwrap();
|
||||
}
|
||||
|
||||
log!("Performing version check...");
|
||||
release::version_check();
|
||||
|
||||
let menu_conf_path = "sd:/TrainingModpack/training_modpack_menu.conf";
|
||||
log!("Checking for previous menu in training_modpack_menu.conf...");
|
||||
if fs::metadata(menu_conf_path).is_ok() {
|
||||
let menu_conf = fs::read(menu_conf_path).unwrap();
|
||||
if menu_conf.starts_with(b"http://localhost") {
|
||||
log!("Previous menu found, loading from training_modpack_menu.conf");
|
||||
unsafe {
|
||||
MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap());
|
||||
}
|
||||
} else {
|
||||
log!("Previous menu found but is invalid.");
|
||||
}
|
||||
} else {
|
||||
log!("No previous menu file found.");
|
||||
}
|
||||
|
||||
let menu_defaults_conf_path = "sd:/TrainingModpack/training_modpack_menu_defaults.conf";
|
||||
log!("Checking for previous menu defaults in training_modpack_menu_defaults.conf...");
|
||||
if fs::metadata(menu_defaults_conf_path).is_ok() {
|
||||
let menu_defaults_conf = fs::read(menu_defaults_conf_path).unwrap();
|
||||
if menu_defaults_conf.starts_with(b"http://localhost") {
|
||||
log!("Menu defaults found, loading from training_modpack_menu_defaults.conf");
|
||||
unsafe {
|
||||
DEFAULT_MENU = get_menu_from_url(
|
||||
DEFAULT_MENU,
|
||||
std::str::from_utf8(&menu_defaults_conf).unwrap(),
|
||||
);
|
||||
crate::menu::write_menu();
|
||||
}
|
||||
} else {
|
||||
log!("Previous menu defaults found but are invalid.");
|
||||
}
|
||||
} else {
|
||||
log!("No previous menu defaults found.");
|
||||
}
|
||||
|
||||
std::thread::spawn(|| loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
unsafe {
|
||||
while let Some(event) = EVENT_QUEUE.pop() {
|
||||
let host = "https://my-project-1511972643240-default-rtdb.firebaseio.com";
|
||||
let path = format!(
|
||||
"/event/{}/device/{}/{}.json",
|
||||
event.event_name, event.device_id, event.event_time
|
||||
);
|
||||
|
||||
let url = format!("{}{}", host, path);
|
||||
minreq::post(url).with_json(&event).unwrap().send().ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
#![feature(proc_macro_hygiene)]
|
||||
#![feature(with_options)]
|
||||
#![feature(const_mut_refs)]
|
||||
#![feature(exclusive_range_pattern)]
|
||||
#![feature(once_cell)]
|
||||
#![allow(
|
||||
clippy::borrow_interior_mutable_const,
|
||||
clippy::not_unsafe_ptr_arg_deref,
|
||||
clippy::missing_safety_doc,
|
||||
clippy::wrong_self_convention
|
||||
)]
|
||||
|
||||
pub mod common;
|
||||
mod hazard_manager;
|
||||
mod hitbox_visualizer;
|
||||
mod training;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use crate::common::*;
|
||||
use crate::events::{Event, EVENT_QUEUE};
|
||||
use crate::common::consts::get_menu_from_url;
|
||||
|
||||
use skyline::libc::{c_char, mkdir};
|
||||
use skyline::nro::{self, NroInfo};
|
||||
use std::fs;
|
||||
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
fn nro_main(nro: &NroInfo<'_>) {
|
||||
if nro.module.isLoaded {
|
||||
return;
|
||||
}
|
||||
|
||||
if nro.name == "common" {
|
||||
skyline::install_hooks!(
|
||||
training::shield::handle_sub_guard_cont,
|
||||
training::directional_influence::handle_correct_damage_vector_common,
|
||||
training::sdi::process_hit_stop_delay,
|
||||
training::tech::handle_change_status,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
#[link_name = "render_text_to_screen"]
|
||||
pub fn render_text_to_screen_cstr(str: *const c_char);
|
||||
|
||||
#[link_name = "set_should_display_text_to_screen"]
|
||||
pub fn set_should_display_text_to_screen(toggle: bool);
|
||||
}
|
||||
|
||||
macro_rules! c_str {
|
||||
($l:tt) => {
|
||||
[$l.as_bytes(), "\u{0}".as_bytes()].concat().as_ptr();
|
||||
};
|
||||
}
|
||||
|
||||
pub fn render_text_to_screen(s: &str) {
|
||||
unsafe {
|
||||
render_text_to_screen_cstr(c_str!(s));
|
||||
}
|
||||
}
|
||||
|
||||
#[skyline::main(name = "training_modpack")]
|
||||
pub fn main() {
|
||||
macro_rules! log {
|
||||
($($arg:tt)*) => {
|
||||
print!("{}", "[Training Modpack] ".green());
|
||||
println!($($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
log!("Initialized.");
|
||||
unsafe {
|
||||
EVENT_QUEUE.push(Event::smash_open());
|
||||
}
|
||||
|
||||
hitbox_visualizer::hitbox_visualization();
|
||||
hazard_manager::hazard_manager();
|
||||
training::training_mods();
|
||||
nro::add_hook(nro_main).unwrap();
|
||||
|
||||
|
||||
unsafe {
|
||||
mkdir(c_str!("sd:/TrainingModpack/"), 777);
|
||||
}
|
||||
|
||||
let ovl_path = "sd:/switch/.overlays/ovlTrainingModpack.ovl";
|
||||
if fs::metadata(ovl_path).is_ok() {
|
||||
log!("Removing ovlTrainingModpack.ovl...");
|
||||
fs::remove_file(ovl_path).unwrap();
|
||||
}
|
||||
|
||||
log!("Performing version check...");
|
||||
release::version_check();
|
||||
|
||||
let menu_conf_path = "sd:/TrainingModpack/training_modpack_menu.conf";
|
||||
log!("Checking for previous menu in training_modpack_menu.conf...");
|
||||
if fs::metadata(menu_conf_path).is_ok() {
|
||||
let menu_conf = fs::read(menu_conf_path).unwrap();
|
||||
if menu_conf.starts_with(b"http://localhost") {
|
||||
log!("Previous menu found, loading from training_modpack_menu.conf");
|
||||
unsafe {
|
||||
MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap());
|
||||
}
|
||||
} else {
|
||||
log!("Previous menu found but is invalid.");
|
||||
}
|
||||
} else {
|
||||
log!("No previous menu file found.");
|
||||
}
|
||||
|
||||
let menu_defaults_conf_path = "sd:/TrainingModpack/training_modpack_menu_defaults.conf";
|
||||
log!("Checking for previous menu defaults in training_modpack_menu_defaults.conf...");
|
||||
if fs::metadata(menu_defaults_conf_path).is_ok() {
|
||||
let menu_defaults_conf = fs::read(menu_defaults_conf_path).unwrap();
|
||||
if menu_defaults_conf.starts_with(b"http://localhost") {
|
||||
log!("Menu defaults found, loading from training_modpack_menu_defaults.conf");
|
||||
unsafe {
|
||||
DEFAULT_MENU = get_menu_from_url(
|
||||
DEFAULT_MENU,
|
||||
std::str::from_utf8(&menu_defaults_conf).unwrap(),
|
||||
);
|
||||
crate::menu::write_menu();
|
||||
}
|
||||
} else {
|
||||
log!("Previous menu defaults found but are invalid.");
|
||||
}
|
||||
} else {
|
||||
log!("No previous menu defaults found.");
|
||||
}
|
||||
|
||||
std::thread::spawn(|| loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
unsafe {
|
||||
while let Some(event) = EVENT_QUEUE.pop() {
|
||||
let host = "https://my-project-1511972643240-default-rtdb.firebaseio.com";
|
||||
let path = format!(
|
||||
"/event/{}/device/{}/{}.json",
|
||||
event.event_name, event.device_id, event.event_time
|
||||
);
|
||||
|
||||
let url = format!("{}{}", host, path);
|
||||
minreq::post(url).with_json(&event).unwrap().send().ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn(|| {
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
let menu;
|
||||
unsafe {
|
||||
menu = crate::common::consts::get_menu();
|
||||
}
|
||||
|
||||
let mut app = training_mod_tui::App::new(menu);
|
||||
|
||||
let backend = training_mod_tui::TestBackend::new(75, 15);
|
||||
let mut terminal = training_mod_tui::Terminal::new(backend).unwrap();
|
||||
|
||||
unsafe {
|
||||
let mut has_slept_millis = 0;
|
||||
let mut url = String::new();
|
||||
let button_presses = &mut common::menu::BUTTON_PRESSES;
|
||||
loop {
|
||||
button_presses.a.read_press().then(|| app.on_a());
|
||||
button_presses.b.read_press().then(|| {
|
||||
if app.outer_list == false {
|
||||
app.on_b()
|
||||
} else {
|
||||
// Leave menu.
|
||||
menu::QUICK_MENU_ACTIVE = false;
|
||||
crate::menu::set_menu_from_url(url.as_str());
|
||||
}
|
||||
});
|
||||
button_presses.zl.read_press().then(|| app.on_l());
|
||||
button_presses.zl.read_press().then(|| app.on_r());
|
||||
button_presses.left.read_press().then(|| app.on_left());
|
||||
button_presses.right.read_press().then(|| app.on_right());
|
||||
button_presses.up.read_press().then(|| app.on_up());
|
||||
button_presses.down.read_press().then(|| app.on_down());
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(16));
|
||||
has_slept_millis += 16;
|
||||
let render_frames = 5;
|
||||
if has_slept_millis > 16 * render_frames {
|
||||
has_slept_millis = 16;
|
||||
let mut view = String::new();
|
||||
|
||||
let frame_res = terminal
|
||||
.draw(|f| url = training_mod_tui::ui(f, &mut app))
|
||||
.unwrap();
|
||||
|
||||
use std::fmt::Write;
|
||||
for (i, cell) in frame_res.buffer.content().into_iter().enumerate() {
|
||||
write!(&mut view, "{}", cell.symbol).unwrap();
|
||||
if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
|
||||
write!(&mut view, "\n").unwrap();
|
||||
}
|
||||
}
|
||||
write!(&mut view, "\n").unwrap();
|
||||
|
||||
if menu::QUICK_MENU_ACTIVE {
|
||||
render_text_to_screen(view.as_str());
|
||||
} else {
|
||||
set_should_display_text_to_screen(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::common::consts::*;
|
||||
use crate::common::*;
|
||||
use crate::is_operation_cpu;
|
||||
use crate::training::frame_counter;
|
||||
use crate::training::handle_add_limit;
|
||||
|
|
|
@ -24,7 +24,7 @@ mod attack_angle;
|
|||
mod character_specific;
|
||||
mod fast_fall;
|
||||
mod full_hop;
|
||||
mod input_delay;
|
||||
pub(crate) mod input_delay;
|
||||
mod input_record;
|
||||
mod mash;
|
||||
mod reset;
|
||||
|
@ -465,6 +465,7 @@ pub fn training_mods() {
|
|||
panic!("The NN-HID hook plugin could not be found and is required to add NRO hooks. Make sure libnn_hid_hook.nro is installed.");
|
||||
}
|
||||
add_nn_hid_hook(input_delay::handle_get_npad_state);
|
||||
add_nn_hid_hook(menu::handle_get_npad_state);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
|
|
|
@ -10,4 +10,11 @@ strum_macros = "0.21.0"
|
|||
num = "0.4.0"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git" }
|
||||
ramhorns = "0.12.0"
|
||||
paste = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["smash"]
|
||||
smash = ["skyline_smash"]
|
|
@ -5,8 +5,13 @@ extern crate bitflags;
|
|||
extern crate num_derive;
|
||||
|
||||
use core::f64::consts::PI;
|
||||
#[cfg(feature = "smash")]
|
||||
use smash::lib::lua_const::*;
|
||||
use strum_macros::EnumIter;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use ramhorns::Content;
|
||||
use strum::IntoEnumIterator;
|
||||
use std::ops::BitOr;
|
||||
|
||||
// bitflag helper function macro
|
||||
macro_rules! extra_bitflag_impls {
|
||||
|
@ -72,8 +77,12 @@ macro_rules! extra_bitflag_impls {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_random_int(max: i32) -> i32 {
|
||||
unsafe { smash::app::sv_math::rand(smash::hash40("fighter"), max) }
|
||||
pub fn get_random_int(_max: i32) -> i32 {
|
||||
#[cfg(feature = "smash")]
|
||||
unsafe { smash::app::sv_math::rand(smash::hash40("fighter"), _max) }
|
||||
|
||||
#[cfg(not(feature = "smash"))]
|
||||
0
|
||||
}
|
||||
|
||||
pub fn random_option<T>(arg: &[T]) -> &T {
|
||||
|
@ -88,8 +97,8 @@ pub fn random_option<T>(arg: &[T]) -> &T {
|
|||
|
||||
// DI / Left stick
|
||||
bitflags! {
|
||||
pub struct Direction : u32
|
||||
{
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Direction : u32 {
|
||||
const OUT = 0x1;
|
||||
const UP_OUT = 0x2;
|
||||
const UP = 0x4;
|
||||
|
@ -153,6 +162,7 @@ extra_bitflag_impls! {Direction}
|
|||
|
||||
// Ledge Option
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LedgeOption : u32
|
||||
{
|
||||
const NEUTRAL = 0x1;
|
||||
|
@ -165,14 +175,19 @@ bitflags! {
|
|||
|
||||
impl LedgeOption {
|
||||
pub fn into_status(self) -> Option<i32> {
|
||||
Some(match self {
|
||||
LedgeOption::NEUTRAL => *FIGHTER_STATUS_KIND_CLIFF_CLIMB,
|
||||
LedgeOption::ROLL => *FIGHTER_STATUS_KIND_CLIFF_ESCAPE,
|
||||
LedgeOption::JUMP => *FIGHTER_STATUS_KIND_CLIFF_JUMP1,
|
||||
LedgeOption::ATTACK => *FIGHTER_STATUS_KIND_CLIFF_ATTACK,
|
||||
LedgeOption::WAIT => *FIGHTER_STATUS_KIND_CLIFF_WAIT,
|
||||
_ => return None,
|
||||
})
|
||||
#[cfg(feature = "smash")] {
|
||||
Some(match self {
|
||||
LedgeOption::NEUTRAL => *FIGHTER_STATUS_KIND_CLIFF_CLIMB,
|
||||
LedgeOption::ROLL => *FIGHTER_STATUS_KIND_CLIFF_ESCAPE,
|
||||
LedgeOption::JUMP => *FIGHTER_STATUS_KIND_CLIFF_JUMP1,
|
||||
LedgeOption::ATTACK => *FIGHTER_STATUS_KIND_CLIFF_ATTACK,
|
||||
LedgeOption::WAIT => *FIGHTER_STATUS_KIND_CLIFF_WAIT,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smash"))]
|
||||
None
|
||||
}
|
||||
|
||||
fn as_str(self) -> Option<&'static str> {
|
||||
|
@ -191,6 +206,7 @@ extra_bitflag_impls! {LedgeOption}
|
|||
|
||||
// Tech options
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TechFlags : u32 {
|
||||
const NO_TECH = 0x1;
|
||||
const ROLL_F = 0x2;
|
||||
|
@ -215,6 +231,7 @@ extra_bitflag_impls! {TechFlags}
|
|||
|
||||
// Missed Tech Options
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MissTechFlags : u32 {
|
||||
const GETUP = 0x1;
|
||||
const ATTACK = 0x2;
|
||||
|
@ -239,7 +256,7 @@ extra_bitflag_impls! {MissTechFlags}
|
|||
|
||||
/// Shield States
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize, Deserialize)]
|
||||
pub enum Shield {
|
||||
None = 0,
|
||||
Infinite = 1,
|
||||
|
@ -264,7 +281,7 @@ impl Shield {
|
|||
|
||||
// Save State Mirroring
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize, Deserialize)]
|
||||
pub enum SaveStateMirroring {
|
||||
None = 0,
|
||||
Alternate = 1,
|
||||
|
@ -287,6 +304,7 @@ impl SaveStateMirroring {
|
|||
|
||||
// Defensive States
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Defensive : u32 {
|
||||
const SPOT_DODGE = 0x1;
|
||||
const ROLL_F = 0x2;
|
||||
|
@ -312,7 +330,7 @@ impl Defensive {
|
|||
extra_bitflag_impls! {Defensive}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum OnOff {
|
||||
Off = 0,
|
||||
On = 1,
|
||||
|
@ -340,6 +358,7 @@ impl OnOff {
|
|||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Action : u32 {
|
||||
const AIR_DODGE = 0x1;
|
||||
const JUMP = 0x2;
|
||||
|
@ -372,14 +391,19 @@ bitflags! {
|
|||
|
||||
impl Action {
|
||||
pub fn into_attack_air_kind(self) -> Option<i32> {
|
||||
Some(match self {
|
||||
Action::NAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_N,
|
||||
Action::FAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_F,
|
||||
Action::BAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_B,
|
||||
Action::DAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_LW,
|
||||
Action::UAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_HI,
|
||||
_ => return None,
|
||||
})
|
||||
#[cfg(feature = "smash")] {
|
||||
Some(match self {
|
||||
Action::NAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_N,
|
||||
Action::FAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_F,
|
||||
Action::BAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_B,
|
||||
Action::DAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_LW,
|
||||
Action::UAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_HI,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smash"))]
|
||||
None
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> Option<&'static str> {
|
||||
|
@ -417,6 +441,7 @@ impl Action {
|
|||
extra_bitflag_impls! {Action}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AttackAngle : u32 {
|
||||
const NEUTRAL = 0x1;
|
||||
const UP = 0x2;
|
||||
|
@ -438,6 +463,7 @@ impl AttackAngle {
|
|||
extra_bitflag_impls! {AttackAngle}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Delay : u32 {
|
||||
const D0 = 0x1;
|
||||
const D1 = 0x2;
|
||||
|
@ -475,6 +501,7 @@ bitflags! {
|
|||
|
||||
// Throw Option
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ThrowOption : u32
|
||||
{
|
||||
const NONE = 0x1;
|
||||
|
@ -487,14 +514,19 @@ bitflags! {
|
|||
|
||||
impl ThrowOption {
|
||||
pub fn into_cmd(self) -> Option<i32> {
|
||||
Some(match self {
|
||||
ThrowOption::NONE => 0,
|
||||
ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F,
|
||||
ThrowOption::BACKWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_B,
|
||||
ThrowOption::UP => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_HI,
|
||||
ThrowOption::DOWN => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_LW,
|
||||
_ => return None,
|
||||
})
|
||||
#[cfg(feature = "smash")] {
|
||||
Some(match self {
|
||||
ThrowOption::NONE => 0,
|
||||
ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F,
|
||||
ThrowOption::BACKWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_B,
|
||||
ThrowOption::UP => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_HI,
|
||||
ThrowOption::DOWN => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_LW,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smash"))]
|
||||
None
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> Option<&'static str> {
|
||||
|
@ -513,6 +545,7 @@ extra_bitflag_impls! {ThrowOption}
|
|||
|
||||
// Buff Option
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BuffOption : u32
|
||||
{
|
||||
const ACCELERATLE = 0x1;
|
||||
|
@ -529,18 +562,23 @@ bitflags! {
|
|||
|
||||
impl BuffOption {
|
||||
pub fn into_int(self) -> Option<i32> {
|
||||
Some(match self {
|
||||
BuffOption::ACCELERATLE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND11_SPEED_UP,
|
||||
BuffOption::OOMPH => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND12_ATTACK_UP,
|
||||
BuffOption::PSYCHE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND21_CHARGE,
|
||||
BuffOption::BOUNCE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND13_REFLECT,
|
||||
BuffOption::BREATHING => 1,
|
||||
BuffOption::ARSENE => 1,
|
||||
BuffOption::LIMIT => 1,
|
||||
BuffOption::KO => 1,
|
||||
BuffOption::WING => 1,
|
||||
_ => return None,
|
||||
})
|
||||
#[cfg(feature = "smash")] {
|
||||
Some(match self {
|
||||
BuffOption::ACCELERATLE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND11_SPEED_UP,
|
||||
BuffOption::OOMPH => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND12_ATTACK_UP,
|
||||
BuffOption::PSYCHE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND21_CHARGE,
|
||||
BuffOption::BOUNCE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND13_REFLECT,
|
||||
BuffOption::BREATHING => 1,
|
||||
BuffOption::ARSENE => 1,
|
||||
BuffOption::LIMIT => 1,
|
||||
BuffOption::KO => 1,
|
||||
BuffOption::WING => 1,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smash"))]
|
||||
None
|
||||
}
|
||||
|
||||
fn as_str(self) -> Option<&'static str> {
|
||||
|
@ -607,6 +645,7 @@ impl Delay {
|
|||
extra_bitflag_impls! {Delay}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MedDelay : u32 {
|
||||
const D0 = 0x1;
|
||||
const D5 = 0x2;
|
||||
|
@ -688,6 +727,7 @@ impl MedDelay {
|
|||
extra_bitflag_impls! {MedDelay}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LongDelay : u32 {
|
||||
const D0 = 0x1;
|
||||
const D10 = 0x2;
|
||||
|
@ -769,6 +809,7 @@ impl LongDelay {
|
|||
extra_bitflag_impls! {LongDelay}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BoolFlag : u32 {
|
||||
const TRUE = 0x1;
|
||||
const FALSE = 0x2;
|
||||
|
@ -791,7 +832,7 @@ impl BoolFlag {
|
|||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize, Deserialize)]
|
||||
pub enum SdiStrength {
|
||||
Normal = 0,
|
||||
Medium = 1,
|
||||
|
@ -834,11 +875,13 @@ impl ToUrlParam for i32 {
|
|||
// Macro to build the url parameter string
|
||||
macro_rules! url_params {
|
||||
(
|
||||
#[repr(C)]
|
||||
#[derive($($trait_name:ident, )*)]
|
||||
pub struct $e:ident {
|
||||
$(pub $field_name:ident: $field_type:ty,)*
|
||||
}
|
||||
) => {
|
||||
#[repr(C)]
|
||||
#[derive($($trait_name, )*)]
|
||||
pub struct $e {
|
||||
$(pub $field_name: $field_type,)*
|
||||
|
@ -859,9 +902,9 @@ macro_rules! url_params {
|
|||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
url_params! {
|
||||
#[derive(Clone, Copy, )]
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, )]
|
||||
pub struct TrainingModpackMenu {
|
||||
pub hitbox_vis: OnOff,
|
||||
pub stage_hazards: OnOff,
|
||||
|
@ -896,6 +939,7 @@ url_params! {
|
|||
pub throw_delay: MedDelay,
|
||||
pub pummel_delay: MedDelay,
|
||||
pub buff_state: BuffOption,
|
||||
pub quick_menu: OnOff,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -947,6 +991,7 @@ impl TrainingModpackMenu {
|
|||
throw_delay = MedDelay::from_bits(val),
|
||||
pummel_delay = MedDelay::from_bits(val),
|
||||
buff_state = BuffOption::from_bits(val),
|
||||
quick_menu = OnOff::from_val(val),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -958,3 +1003,612 @@ pub enum FighterId {
|
|||
Player = 0,
|
||||
CPU = 1,
|
||||
}
|
||||
|
||||
#[derive(Content, Clone)]
|
||||
pub struct Slider {
|
||||
pub min: usize,
|
||||
pub max: usize,
|
||||
pub index: usize,
|
||||
pub value: usize,
|
||||
}
|
||||
|
||||
#[derive(Content, Clone)]
|
||||
pub struct Toggle<'a> {
|
||||
pub title: &'a str,
|
||||
pub checked: &'a str,
|
||||
pub index: usize,
|
||||
pub value: usize,
|
||||
pub default: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Content, Clone)]
|
||||
pub struct OnOffSelector<'a> {
|
||||
pub title: &'a str,
|
||||
pub checked: &'a str,
|
||||
pub default: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SubMenuType {
|
||||
TOGGLE,
|
||||
SLIDER,
|
||||
ONOFF,
|
||||
}
|
||||
|
||||
impl SubMenuType {
|
||||
pub fn from_str(s : &str) -> SubMenuType {
|
||||
match s {
|
||||
"toggle" => SubMenuType::TOGGLE,
|
||||
"slider" => SubMenuType::SLIDER,
|
||||
"onoff" => SubMenuType::ONOFF,
|
||||
_ => panic!("Unexpected SubMenuType!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Content, Clone)]
|
||||
pub struct SubMenu<'a> {
|
||||
pub title: &'a str,
|
||||
pub id: &'a str,
|
||||
pub _type: &'a str,
|
||||
pub toggles: Vec<Toggle<'a>>,
|
||||
pub sliders: Vec<Slider>,
|
||||
pub onoffselector: Vec<OnOffSelector<'a>>,
|
||||
pub index: usize,
|
||||
pub check_against: usize,
|
||||
pub is_single_option: Option<bool>,
|
||||
pub help_text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> SubMenu<'a> {
|
||||
pub fn max_idx(&self) -> usize {
|
||||
self.toggles
|
||||
.iter()
|
||||
.max_by(|t1, t2| t1.index.cmp(&t2.index))
|
||||
.map(|t| t.index)
|
||||
.unwrap_or(self.index)
|
||||
}
|
||||
|
||||
pub fn add_toggle(&mut self, title: &'a str, checked: bool, value: usize, default: bool) {
|
||||
self.toggles.push(Toggle {
|
||||
title,
|
||||
checked: if checked { "is-appear" } else { "is-hidden" },
|
||||
index: self.max_idx() + 1,
|
||||
value,
|
||||
default: if default { "is-appear" } else { "is-hidden" },
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_slider(&mut self, min: usize, max: usize, value: usize) {
|
||||
self.sliders.push(Slider {
|
||||
min,
|
||||
max,
|
||||
index: self.max_idx() + 1,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_onoffselector(&mut self, title: &'a str, checked: bool, default: bool) {
|
||||
// TODO: Is there a more elegant way to do this?
|
||||
// The HTML only supports a single onoffselector but the SubMenu stores it as a Vec
|
||||
self.onoffselector.push(OnOffSelector {
|
||||
title,
|
||||
checked: if checked { "is-appear" } else { "is-hidden" },
|
||||
default: if default { "is-appear" } else { "is-hidden" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub static DEFAULT_MENU: TrainingModpackMenu = TrainingModpackMenu {
|
||||
hitbox_vis: OnOff::On,
|
||||
stage_hazards: OnOff::Off,
|
||||
di_state: Direction::empty(),
|
||||
sdi_state: Direction::empty(),
|
||||
sdi_strength: SdiStrength::Normal,
|
||||
air_dodge_dir: Direction::empty(),
|
||||
mash_state: Action::empty(),
|
||||
follow_up: Action::empty(),
|
||||
attack_angle: AttackAngle::empty(),
|
||||
ledge_state: LedgeOption::all(),
|
||||
ledge_delay: LongDelay::empty(),
|
||||
tech_state: TechFlags::all(),
|
||||
miss_tech_state: MissTechFlags::all(),
|
||||
shield_state: Shield::None,
|
||||
defensive_state: Defensive::all(),
|
||||
oos_offset: Delay::empty(),
|
||||
shield_tilt: Direction::empty(),
|
||||
reaction_time: Delay::empty(),
|
||||
mash_in_neutral: OnOff::Off,
|
||||
fast_fall: BoolFlag::empty(),
|
||||
fast_fall_delay: Delay::empty(),
|
||||
falling_aerials: BoolFlag::empty(),
|
||||
aerial_delay: Delay::empty(),
|
||||
full_hop: BoolFlag::empty(),
|
||||
input_delay: 0,
|
||||
save_damage: OnOff::On,
|
||||
save_state_mirroring: SaveStateMirroring::None,
|
||||
frame_advantage: OnOff::Off,
|
||||
save_state_enable: OnOff::On,
|
||||
throw_state: ThrowOption::NONE,
|
||||
throw_delay: MedDelay::empty(),
|
||||
pummel_delay: MedDelay::empty(),
|
||||
buff_state: BuffOption::empty(),
|
||||
quick_menu: OnOff::On,
|
||||
};
|
||||
|
||||
pub static mut MENU: TrainingModpackMenu = DEFAULT_MENU;
|
||||
|
||||
#[derive(Content, Clone)]
|
||||
pub struct Menu<'a> {
|
||||
pub sub_menus: Vec<SubMenu<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Menu<'a> {
|
||||
pub fn max_idx(&self) -> usize {
|
||||
self.sub_menus
|
||||
.iter()
|
||||
.max_by(|x, y| x.max_idx().cmp(&y.max_idx()))
|
||||
.map(|sub_menu| sub_menu.max_idx())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn add_sub_menu(
|
||||
&mut self,
|
||||
title: &'a str,
|
||||
id: &'a str,
|
||||
_type: &'a str,
|
||||
check_against: usize,
|
||||
toggles: Vec<(&'a str, usize)>,
|
||||
sliders: Vec<(usize, usize, usize)>,
|
||||
defaults: usize,
|
||||
help_text: &'a str,
|
||||
) {
|
||||
let mut sub_menu = SubMenu {
|
||||
title,
|
||||
id,
|
||||
_type,
|
||||
toggles: Vec::new(),
|
||||
sliders: Vec::new(),
|
||||
onoffselector: Vec::new(),
|
||||
index: self.max_idx() + 1,
|
||||
check_against,
|
||||
is_single_option: Some(true),
|
||||
help_text,
|
||||
};
|
||||
|
||||
for toggle in toggles {
|
||||
sub_menu.add_toggle(
|
||||
toggle.0,
|
||||
(check_against & toggle.1) != 0,
|
||||
toggle.1,
|
||||
(defaults & toggle.1) != 0,
|
||||
)
|
||||
}
|
||||
|
||||
for slider in sliders {
|
||||
sub_menu.add_slider(slider.0, slider.1, slider.2);
|
||||
}
|
||||
|
||||
self.sub_menus.push(sub_menu);
|
||||
}
|
||||
|
||||
pub fn add_sub_menu_sep(
|
||||
&mut self,
|
||||
title: &'a str,
|
||||
id: &'a str,
|
||||
_type: &'a str,
|
||||
check_against: usize,
|
||||
strs: Vec<&'a str>,
|
||||
vals: Vec<usize>,
|
||||
defaults: usize,
|
||||
help_text: &'a str,
|
||||
) {
|
||||
let mut sub_menu = SubMenu {
|
||||
title,
|
||||
id,
|
||||
_type,
|
||||
toggles: Vec::new(),
|
||||
sliders: Vec::new(),
|
||||
onoffselector: Vec::new(),
|
||||
index: self.max_idx() + 1,
|
||||
check_against,
|
||||
is_single_option: None,
|
||||
help_text,
|
||||
};
|
||||
|
||||
for i in 0..strs.len() {
|
||||
sub_menu.add_toggle(
|
||||
strs[i],
|
||||
(check_against & vals[i]) != 0,
|
||||
vals[i],
|
||||
(defaults & vals[i]) != 0,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: add sliders?
|
||||
|
||||
self.sub_menus.push(sub_menu);
|
||||
}
|
||||
|
||||
pub fn add_sub_menu_onoff(
|
||||
&mut self,
|
||||
title: &'a str,
|
||||
id: &'a str,
|
||||
_type: &'a str,
|
||||
check_against: usize,
|
||||
checked: bool,
|
||||
default: usize,
|
||||
help_text: &'a str,
|
||||
) {
|
||||
let mut sub_menu = SubMenu {
|
||||
title,
|
||||
id,
|
||||
_type,
|
||||
toggles: Vec::new(),
|
||||
sliders: Vec::new(),
|
||||
onoffselector: Vec::new(),
|
||||
index: self.max_idx() + 1,
|
||||
check_against,
|
||||
is_single_option: None,
|
||||
help_text,
|
||||
};
|
||||
|
||||
sub_menu.add_onoffselector(title, checked, (default & OnOff::On as usize) != 0);
|
||||
self.sub_menus.push(sub_menu);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! add_bitflag_submenu {
|
||||
($menu:ident, $title:literal, $id:ident, $e:ty, $help_text:literal) => {
|
||||
paste::paste!{
|
||||
let [<$id _strs>] = <$e>::to_toggle_strs();
|
||||
let [<$id _vals>] = <$e>::to_toggle_vals();
|
||||
|
||||
$menu.add_sub_menu_sep(
|
||||
$title,
|
||||
stringify!($id),
|
||||
"toggle",
|
||||
MENU.$id.bits() as usize,
|
||||
[<$id _strs>],
|
||||
[<$id _vals>],
|
||||
DEFAULT_MENU.$id.bits() as usize,
|
||||
stringify!($help_text),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! add_single_option_submenu {
|
||||
($menu:ident, $title:literal, $id:ident, $e:ty, $help_text:literal) => {
|
||||
paste::paste!{
|
||||
let mut [<$id _toggles>] = Vec::new();
|
||||
for val in [<$e>]::iter() {
|
||||
[<$id _toggles>].push((val.as_str().unwrap_or(""), val as usize));
|
||||
}
|
||||
|
||||
$menu.add_sub_menu(
|
||||
$title,
|
||||
stringify!($id),
|
||||
"toggle",
|
||||
MENU.$id as usize,
|
||||
[<$id _toggles>],
|
||||
[].to_vec(),
|
||||
DEFAULT_MENU.$id as usize,
|
||||
stringify!($help_text),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! add_onoff_submenu {
|
||||
($menu:ident, $title:literal, $id:ident, $help_text:literal) => {
|
||||
paste::paste! {
|
||||
$menu.add_sub_menu_onoff(
|
||||
$title,
|
||||
stringify!($id),
|
||||
"onoff",
|
||||
MENU.$id as usize,
|
||||
(MENU.$id as usize & OnOff::On as usize) != 0,
|
||||
DEFAULT_MENU.$id as usize,
|
||||
stringify!($help_text),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub unsafe fn get_menu() -> Menu<'static> {
|
||||
let mut overall_menu = Menu {
|
||||
sub_menus: Vec::new(),
|
||||
};
|
||||
|
||||
// Toggle/bitflag menus
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Mash Toggles",
|
||||
mash_state,
|
||||
Action,
|
||||
"Mash Toggles: Actions to be performed as soon as possible"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Followup Toggles",
|
||||
follow_up,
|
||||
Action,
|
||||
"Followup Toggles: Actions to be performed after the Mash option"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Attack Angle",
|
||||
attack_angle,
|
||||
AttackAngle,
|
||||
"Attack Angle: For attacks that can be angled, such as some forward tilts"
|
||||
);
|
||||
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Ledge Options",
|
||||
ledge_state,
|
||||
LedgeOption,
|
||||
"Ledge Options: Actions to be taken when on the ledge"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Ledge Delay",
|
||||
ledge_delay,
|
||||
LongDelay,
|
||||
"Ledge Delay: How many frames to delay the ledge option"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Tech Options",
|
||||
tech_state,
|
||||
TechFlags,
|
||||
"Tech Options: Actions to take when slammed into a hard surface"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Miss Tech Options",
|
||||
miss_tech_state,
|
||||
MissTechFlags,
|
||||
"Miss Tech Options: Actions to take after missing a tech"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Defensive Options",
|
||||
defensive_state,
|
||||
Defensive,
|
||||
"Defensive Options: Actions to take after a ledge option, tech option, or miss tech option"
|
||||
);
|
||||
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Aerial Delay",
|
||||
aerial_delay,
|
||||
Delay,
|
||||
"Aerial Delay: How long to delay a Mash aerial attack"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"OoS Offset",
|
||||
oos_offset,
|
||||
Delay,
|
||||
"OoS Offset: How many times the CPU shield can be hit before performing a Mash option"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Reaction Time",
|
||||
reaction_time,
|
||||
Delay,
|
||||
"Reaction Time: How many frames to delay before performing an option out of shield"
|
||||
);
|
||||
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Fast Fall",
|
||||
fast_fall,
|
||||
BoolFlag,
|
||||
"Fast Fall: Should the CPU fastfall during a jump"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Fast Fall Delay",
|
||||
fast_fall_delay,
|
||||
Delay,
|
||||
"Fast Fall Delay: How many frames the CPU should delay their fastfall"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Falling Aerials",
|
||||
falling_aerials,
|
||||
BoolFlag,
|
||||
"Falling Aerials: Should aerials be performed when rising or when falling"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Full Hop",
|
||||
full_hop,
|
||||
BoolFlag,
|
||||
"Full Hop: Should the CPU perform a full hop or a short hop"
|
||||
);
|
||||
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Shield Tilt",
|
||||
shield_tilt,
|
||||
Direction,
|
||||
"Shield Tilt: Direction to tilt the shield"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"DI Direction",
|
||||
di_state,
|
||||
Direction,
|
||||
"DI Direction: Direction to angle the directional influence during hitlag"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"SDI Direction",
|
||||
sdi_state,
|
||||
Direction,
|
||||
"SDI Direction: Direction to angle the smash directional influence during hitlag"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Airdodge Direction",
|
||||
air_dodge_dir,
|
||||
Direction,
|
||||
"Airdodge Direction: Direction to angle airdodges"
|
||||
);
|
||||
|
||||
add_single_option_submenu!(
|
||||
overall_menu,
|
||||
"SDI Strength",
|
||||
sdi_strength,
|
||||
SdiStrength,
|
||||
"SDI Strength: Relative strength of the smash directional influence inputs"
|
||||
);
|
||||
add_single_option_submenu!(
|
||||
overall_menu,
|
||||
"Shield Toggles",
|
||||
shield_state,
|
||||
Shield,
|
||||
"Shield Toggles: CPU Shield Behavior"
|
||||
);
|
||||
add_single_option_submenu!(
|
||||
overall_menu,
|
||||
"Mirroring",
|
||||
save_state_mirroring,
|
||||
SaveStateMirroring,
|
||||
"Mirroring: Flips save states in the left-right direction across the stage center"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Throw Options",
|
||||
throw_state,
|
||||
ThrowOption,
|
||||
"Throw Options: Throw to be performed when a grab is landed"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Throw Delay",
|
||||
throw_delay,
|
||||
MedDelay,
|
||||
"Throw Delay: How many frames to delay the throw option"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Pummel Delay",
|
||||
pummel_delay,
|
||||
MedDelay,
|
||||
"Pummel Delay: How many frames after a grab to wait before starting to pummel"
|
||||
);
|
||||
add_bitflag_submenu!(
|
||||
overall_menu,
|
||||
"Buff Options",
|
||||
buff_state,
|
||||
BuffOption,
|
||||
"Buff Options: Buff(s) to be applied to respective character when loading save states"
|
||||
);
|
||||
|
||||
// Slider menus
|
||||
overall_menu.add_sub_menu(
|
||||
"Input Delay",
|
||||
"input_delay",
|
||||
// unnecessary for slider?
|
||||
"toggle",
|
||||
0,
|
||||
[
|
||||
("0", 0),
|
||||
("1", 1),
|
||||
("2", 2),
|
||||
("3", 3),
|
||||
("4", 4),
|
||||
("5", 5),
|
||||
("6", 6),
|
||||
("7", 7),
|
||||
("8", 8),
|
||||
("9", 9),
|
||||
("10", 10),
|
||||
]
|
||||
.to_vec(),
|
||||
[].to_vec(), //(0, 10, MENU.input_delay as usize)
|
||||
DEFAULT_MENU.input_delay as usize,
|
||||
stringify!("Input Delay: Frames to delay player inputs by"),
|
||||
);
|
||||
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Save States",
|
||||
save_state_enable,
|
||||
"Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt."
|
||||
);
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Save Damage",
|
||||
save_damage,
|
||||
"Save Damage: Should save states retain player/CPU damage"
|
||||
);
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Hitbox Visualization",
|
||||
hitbox_vis,
|
||||
"Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects"
|
||||
);
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Stage Hazards",
|
||||
stage_hazards,
|
||||
"Stage Hazards: Should stage hazards be present"
|
||||
);
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Frame Advantage",
|
||||
frame_advantage,
|
||||
"Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable");
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Mash In Neutral",
|
||||
mash_in_neutral,
|
||||
"Mash In Neutral: Should Mash options be performed repeatedly or only when the CPU is hit"
|
||||
);
|
||||
add_onoff_submenu!(
|
||||
overall_menu,
|
||||
"Quick Menu",
|
||||
quick_menu,
|
||||
"Quick Menu: Whether to use Quick Menu or Web Menu"
|
||||
);
|
||||
|
||||
overall_menu
|
||||
}
|
||||
|
||||
pub fn get_menu_from_url(mut menu: TrainingModpackMenu, s: &str) -> TrainingModpackMenu {
|
||||
let base_url_len = "http://localhost/?".len();
|
||||
let total_len = s.len();
|
||||
|
||||
let ss: String = s
|
||||
.chars()
|
||||
.skip(base_url_len)
|
||||
.take(total_len - base_url_len)
|
||||
.collect();
|
||||
|
||||
for toggle_values in ss.split('&') {
|
||||
let toggle_value_split = toggle_values.split('=').collect::<Vec<&str>>();
|
||||
let toggle = toggle_value_split[0];
|
||||
if toggle.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let toggle_vals = toggle_value_split[1];
|
||||
|
||||
let bitwise_or = <u32 as BitOr<u32>>::bitor;
|
||||
let bits = toggle_vals
|
||||
.split(',')
|
||||
.filter(|val| !val.is_empty())
|
||||
.map(|val| val.parse().unwrap())
|
||||
.fold(0, bitwise_or);
|
||||
|
||||
menu.set(toggle, bits);
|
||||
}
|
||||
menu
|
||||
}
|
18
training_mod_tui/Cargo.toml
Normal file
18
training_mod_tui/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "training_mod_tui"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tui = { version = "0.16.0", default-features = false }
|
||||
unicode-width = "0.1.9"
|
||||
training_mod_consts = { path = "../training_mod_consts", default-features = false}
|
||||
serde_json = "1.0.79"
|
||||
bitflags = "1.2.1"
|
||||
crossterm = { version = "0.22.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
has_terminal = ["crossterm", "tui/crossterm"]
|
454
training_mod_tui/src/lib.rs
Normal file
454
training_mod_tui/src/lib.rs
Normal file
|
@ -0,0 +1,454 @@
|
|||
use training_mod_consts::{OnOffSelector, Slider, SubMenu, SubMenuType, Toggle};
|
||||
use tui::{
|
||||
backend::{Backend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Spans,
|
||||
widgets::{Tabs, Paragraph, Block, List, ListItem, ListState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub use tui::{backend::TestBackend, Terminal};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod list;
|
||||
|
||||
use crate::list::{StatefulList, MultiStatefulList};
|
||||
|
||||
/// 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
|
||||
pub struct App<'a> {
|
||||
pub tabs: StatefulList<&'a str>,
|
||||
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
|
||||
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
|
||||
pub selected_sub_menu_onoff_selectors: MultiStatefulList<OnOffSelector<'a>>,
|
||||
pub selected_sub_menu_sliders: MultiStatefulList<Slider>,
|
||||
pub outer_list: bool
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new(menu: training_mod_consts::Menu<'a>) -> App<'a> {
|
||||
let tab_specifiers = vec![
|
||||
("Mash Settings", vec![
|
||||
"Mash Toggles",
|
||||
"Followup Toggles",
|
||||
"Attack Angle",
|
||||
"Ledge Options",
|
||||
"Ledge Delay",
|
||||
"Tech Options",
|
||||
"Miss Tech Options",
|
||||
"Defensive Options",
|
||||
"Aerial Delay",
|
||||
"OoS Offset",
|
||||
"Reaction Time",
|
||||
]),
|
||||
("Defensive Settings", vec![
|
||||
"Fast Fall",
|
||||
"Fast Fall Delay",
|
||||
"Falling Aerials",
|
||||
"Full Hop",
|
||||
"Shield Tilt",
|
||||
"DI Direction",
|
||||
"SDI Direction",
|
||||
"Airdodge Direction",
|
||||
"SDI Strength",
|
||||
"Shield Toggles",
|
||||
"Mirroring",
|
||||
"Throw Options",
|
||||
"Throw Delay",
|
||||
"Pummel Delay",
|
||||
"Buff Options",
|
||||
]),
|
||||
("Other Settings", vec![
|
||||
"Input Delay",
|
||||
"Save States",
|
||||
"Save Damage",
|
||||
"Hitbox Visualization",
|
||||
"Stage Hazards",
|
||||
"Frame Advantage",
|
||||
"Mash In Neutral",
|
||||
"Quick Menu"
|
||||
])
|
||||
];
|
||||
let mut tabs: std::collections::HashMap<&str, Vec<SubMenu>> = std::collections::HashMap::new();
|
||||
tabs.insert("Mash Settings", vec![]);
|
||||
tabs.insert("Defensive Settings", vec![]);
|
||||
tabs.insert("Other Settings", vec![]);
|
||||
|
||||
for sub_menu in menu.sub_menus.iter() {
|
||||
for tab_spec in tab_specifiers.iter() {
|
||||
if tab_spec.1.contains(&sub_menu.title) {
|
||||
tabs.get_mut(tab_spec.0).unwrap().push(sub_menu.clone());
|
||||
}
|
||||
}
|
||||
};
|
||||
let num_lists = 3;
|
||||
|
||||
let mut menu_items_stateful = HashMap::new();
|
||||
tabs.keys().for_each(|k| {
|
||||
menu_items_stateful.insert(
|
||||
k.clone(),
|
||||
MultiStatefulList::with_items(tabs.get(k).unwrap().clone(), num_lists)
|
||||
);
|
||||
});
|
||||
let mut app = App {
|
||||
tabs: StatefulList::with_items(tab_specifiers.iter().map(|(tab_title, _)| *tab_title).collect()),
|
||||
menu_items: menu_items_stateful,
|
||||
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
|
||||
selected_sub_menu_onoff_selectors: MultiStatefulList::with_items(vec![], 0),
|
||||
selected_sub_menu_sliders: MultiStatefulList::with_items(vec![], 0),
|
||||
outer_list: true
|
||||
};
|
||||
app.set_sub_menu_items();
|
||||
app
|
||||
}
|
||||
|
||||
pub fn set_sub_menu_items(&mut self) {
|
||||
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
|
||||
let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap();
|
||||
|
||||
let toggles = selected_sub_menu.toggles.clone();
|
||||
let sliders = selected_sub_menu.sliders.clone();
|
||||
let onoffs = selected_sub_menu.onoffselector.clone();
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => {
|
||||
self.selected_sub_menu_toggles = MultiStatefulList::with_items(
|
||||
toggles,
|
||||
if selected_sub_menu.toggles.len() >= 3 { 3 } else { selected_sub_menu.toggles.len()} )
|
||||
},
|
||||
SubMenuType::SLIDER => {
|
||||
self.selected_sub_menu_sliders = MultiStatefulList::with_items(
|
||||
sliders,
|
||||
if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} )
|
||||
},
|
||||
SubMenuType::ONOFF => {
|
||||
self.selected_sub_menu_onoff_selectors = MultiStatefulList::with_items(
|
||||
onoffs,
|
||||
if selected_sub_menu.onoffselector.len() >= 3 { 3 } else { selected_sub_menu.onoffselector.len()} )
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn tab_selected(&self) -> &str {
|
||||
self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn sub_menu_selected(&self) -> &SubMenu {
|
||||
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
|
||||
&self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap()
|
||||
}
|
||||
|
||||
pub fn sub_menu_next(&mut self) {
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(),
|
||||
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next(),
|
||||
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub_menu_next_list(&mut self) {
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(),
|
||||
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next_list(),
|
||||
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next_list(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub_menu_previous(&mut self) {
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(),
|
||||
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous(),
|
||||
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub_menu_previous_list(&mut self) {
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(),
|
||||
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous_list(),
|
||||
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous_list(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(&str, &str)>, ListState)>) {
|
||||
(self.sub_menu_selected().title, self.sub_menu_selected().help_text,
|
||||
match SubMenuType::from_str(self.sub_menu_selected()._type) {
|
||||
SubMenuType::TOGGLE => {
|
||||
self.selected_sub_menu_toggles.lists.iter().map(|toggle_list| {
|
||||
(toggle_list.items.iter().map(
|
||||
|toggle| (toggle.checked, toggle.title)
|
||||
).collect(), toggle_list.state.clone())
|
||||
}).collect()
|
||||
},
|
||||
SubMenuType::SLIDER => {
|
||||
vec![(vec![], ListState::default())]
|
||||
},
|
||||
SubMenuType::ONOFF => {
|
||||
self.selected_sub_menu_onoff_selectors.lists.iter().map(|onoff_list| {
|
||||
(onoff_list.items.iter().map(
|
||||
|onoff| (onoff.checked, onoff.title)
|
||||
).collect(), onoff_list.state.clone())
|
||||
}).collect()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_a(&mut self) {
|
||||
if self.outer_list {
|
||||
self.outer_list = false;
|
||||
} else {
|
||||
let tab_selected = self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap();
|
||||
let (list_section, list_idx) = self.menu_items.get(tab_selected)
|
||||
.unwrap()
|
||||
.idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
|
||||
let selected_sub_menu = self.menu_items.get_mut(tab_selected)
|
||||
.unwrap()
|
||||
.lists[list_section]
|
||||
.items.get_mut(list_idx).unwrap();
|
||||
match SubMenuType::from_str(selected_sub_menu._type) {
|
||||
SubMenuType::TOGGLE => {
|
||||
let is_single_option = selected_sub_menu.is_single_option.is_some();
|
||||
let state = self.selected_sub_menu_toggles.state;
|
||||
self.selected_sub_menu_toggles.lists.iter_mut()
|
||||
.map(|list| (list.state.selected(), &mut list.items))
|
||||
.for_each(|(state, toggle_list)| toggle_list.iter_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, o)|
|
||||
if state.is_some() && i == state.unwrap() {
|
||||
if o.checked != "is-appear" {
|
||||
o.checked = "is-appear";
|
||||
} else {
|
||||
o.checked = "is-hidden";
|
||||
}
|
||||
} else if is_single_option {
|
||||
o.checked = "is-hidden";
|
||||
}
|
||||
));
|
||||
selected_sub_menu.toggles.iter_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, o)| {
|
||||
if i == state {
|
||||
if o.checked != "is-appear" {
|
||||
o.checked = "is-appear";
|
||||
} else {
|
||||
o.checked = "is-hidden";
|
||||
}
|
||||
} else if is_single_option {
|
||||
o.checked = "is-hidden";
|
||||
}
|
||||
});
|
||||
},
|
||||
SubMenuType::ONOFF => {
|
||||
let onoff = self.selected_sub_menu_onoff_selectors.selected_list_item();
|
||||
if onoff.checked != "is-appear" {
|
||||
onoff.checked = "is-appear";
|
||||
} else {
|
||||
onoff.checked = "is-hidden";
|
||||
}
|
||||
selected_sub_menu.onoffselector.iter_mut()
|
||||
.filter(|o| o.title == onoff.title)
|
||||
.for_each(|o| o.checked = onoff.checked);
|
||||
},
|
||||
SubMenuType::SLIDER => {
|
||||
// self.selected_sub_menu_sliders.selected_list_item().checked = "is-appear";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_b(&mut self) {
|
||||
self.outer_list = true;
|
||||
}
|
||||
|
||||
pub fn on_l(&mut self) {
|
||||
if self.outer_list {
|
||||
self.tabs.previous();
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_r(&mut self) {
|
||||
if self.outer_list {
|
||||
self.tabs.next();
|
||||
self.set_sub_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_up(&mut self) {
|
||||
if self.outer_list {
|
||||
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
self.sub_menu_previous();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_down(&mut self) {
|
||||
if self.outer_list {
|
||||
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
self.sub_menu_next();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_left(&mut self) {
|
||||
if self.outer_list {
|
||||
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous_list();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
self.sub_menu_previous_list();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_right(&mut self) {
|
||||
if self.outer_list {
|
||||
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next_list();
|
||||
self.set_sub_menu_items();
|
||||
} else {
|
||||
self.sub_menu_next_list();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
|
||||
let app_tabs = &app.tabs;
|
||||
let tab_selected = app_tabs.state.selected().unwrap();
|
||||
let titles = app_tabs.items.iter().cloned().enumerate().map(|(idx, tab)|{
|
||||
if idx == tab_selected {
|
||||
Spans::from(">> ".to_owned() + tab)
|
||||
} else {
|
||||
Spans::from(" ".to_owned() + tab)
|
||||
}
|
||||
}).collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().title("Ultimate Training Modpack Menu"))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.divider("|")
|
||||
.select(tab_selected);
|
||||
|
||||
let vertical_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(2),
|
||||
Constraint::Max(10),
|
||||
Constraint::Length(2)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let list_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(33), Constraint::Percentage(32), Constraint::Percentage(33)].as_ref())
|
||||
.split(vertical_chunks[1]);
|
||||
|
||||
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 in 0..app.menu_items.get(tab_selected).unwrap().lists.len() {
|
||||
let stateful_list = &app.menu_items.get(tab_selected).unwrap().lists[list_section];
|
||||
let items: Vec<ListItem> = stateful_list
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = vec![Spans::from(
|
||||
if stateful_list.state.selected().is_some() {
|
||||
i.title.to_owned()
|
||||
} else {
|
||||
" ".to_owned() + i.title
|
||||
})];
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(Block::default().title(if list_section == 0 { "Options" } else { "" }))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightBlue)
|
||||
.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 | R: Save defaults"
|
||||
);
|
||||
f.render_widget(help_paragraph, vertical_chunks[2]);
|
||||
} else {
|
||||
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 == "is-appear" { "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()
|
||||
.bg(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"
|
||||
);
|
||||
f.render_widget(help_paragraph, vertical_chunks[2]);
|
||||
}
|
||||
|
||||
|
||||
let mut url = "http://localhost/".to_owned();
|
||||
let mut settings = HashMap::new();
|
||||
|
||||
// Collect settings for toggles
|
||||
for key in app.menu_items.keys() {
|
||||
for list in &app.menu_items.get(key).unwrap().lists {
|
||||
for sub_menu in &list.items {
|
||||
let mut val = String::new();
|
||||
sub_menu.toggles.iter()
|
||||
.filter(|t| t.checked == "is-appear")
|
||||
.for_each(|t| val.push_str(format!("{},", t.value).as_str()));
|
||||
|
||||
sub_menu.onoffselector.iter()
|
||||
.for_each(|o| {
|
||||
val.push_str(
|
||||
format!("{}", if o.checked == "is-appear" { 1 } else { 0 }).as_str())
|
||||
});
|
||||
settings.insert(sub_menu.id, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
url.push_str("?");
|
||||
settings.iter()
|
||||
.for_each(|(section, val)| url.push_str(format!("{}={}&", section, val).as_str()));
|
||||
url
|
||||
|
||||
// TODO: Add saveDefaults
|
||||
// if (document.getElementById("saveDefaults").checked) {
|
||||
// url += "save_defaults=1";
|
||||
// } else {
|
||||
// url = url.slice(0, -1);
|
||||
// }
|
||||
}
|
191
training_mod_tui/src/list.rs
Normal file
191
training_mod_tui/src/list.rs
Normal file
|
@ -0,0 +1,191 @@
|
|||
use tui::widgets::ListState;
|
||||
|
||||
pub struct MultiStatefulList<T> {
|
||||
pub lists: Vec<StatefulList<T>>,
|
||||
pub state: usize,
|
||||
pub total_len: usize
|
||||
}
|
||||
|
||||
impl<T: Clone> MultiStatefulList<T> {
|
||||
pub fn selected_list_item(&mut self) -> &mut T {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
&mut self
|
||||
.lists[list_section]
|
||||
.items[list_idx]
|
||||
}
|
||||
|
||||
pub fn idx_to_list_idx(&self, idx: usize) -> (usize, usize) {
|
||||
for list_section in 0..self.lists.len() {
|
||||
let list_section_min_idx = (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * list_section;
|
||||
let list_section_max_idx = std::cmp::min(
|
||||
(self.total_len as f32 / self.lists.len() as f32).ceil() as usize * (list_section + 1),
|
||||
self.total_len);
|
||||
if (list_section_min_idx..list_section_max_idx).contains(&idx) {
|
||||
// println!("\n{}: ({}, {})", idx, list_section_min_idx, list_section_max_idx);
|
||||
return (list_section, idx - list_section_min_idx)
|
||||
}
|
||||
}
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
fn list_idx_to_idx(&self, list_idx: (usize, usize)) -> usize {
|
||||
let list_section = list_idx.0;
|
||||
let mut list_idx = list_idx.1;
|
||||
for list_section in 0..list_section {
|
||||
list_idx += self.lists[list_section].items.len();
|
||||
}
|
||||
list_idx
|
||||
}
|
||||
|
||||
pub fn with_items(items: Vec<T>, num_lists: usize) -> MultiStatefulList<T> {
|
||||
let lists = (0..num_lists).map(|list_section| {
|
||||
let list_section_min_idx = (items.len() as f32 / num_lists as f32).ceil() as usize * list_section;
|
||||
let list_section_max_idx = std::cmp::min(
|
||||
(items.len() as f32 / num_lists as f32).ceil() as usize * (list_section + 1),
|
||||
items.len());
|
||||
let mut state = ListState::default();
|
||||
if list_section == 0 {
|
||||
// Enforce state as first of list
|
||||
state.select(Some(0));
|
||||
}
|
||||
StatefulList {
|
||||
state: state,
|
||||
items: items[list_section_min_idx..list_section_max_idx].to_vec(),
|
||||
}
|
||||
}).collect();
|
||||
let total_len = items.len();
|
||||
MultiStatefulList {
|
||||
lists: lists,
|
||||
total_len: total_len,
|
||||
state: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let (list_section, _) = self.idx_to_list_idx(self.state);
|
||||
let (next_list_section, next_list_idx) = self.idx_to_list_idx(self.state+1);
|
||||
|
||||
if list_section != next_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state;
|
||||
if self.state + 1 >= self.total_len {
|
||||
state = (0, 0);
|
||||
} else {
|
||||
state = (next_list_section, next_list_idx);
|
||||
}
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let (list_section, _) = self.idx_to_list_idx(self.state);
|
||||
let (last_list_section, last_list_idx) = (self.lists.len() - 1, self.lists[self.lists.len() - 1].items.len() - 1);
|
||||
|
||||
self.lists[list_section].unselect();
|
||||
let state;
|
||||
if self.state == 0 {
|
||||
state = (last_list_section, last_list_idx);
|
||||
} else {
|
||||
let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1);
|
||||
state = (prev_list_section, prev_list_idx);
|
||||
}
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn next_list(&mut self) {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
let next_list_section = (list_section + 1) % self.lists.len();
|
||||
let next_list_idx;
|
||||
if list_idx > self.lists[next_list_section].items.len() - 1 {
|
||||
next_list_idx = self.lists[next_list_section].items.len() - 1;
|
||||
} else {
|
||||
next_list_idx = list_idx;
|
||||
}
|
||||
|
||||
if list_section != next_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state = (next_list_section, next_list_idx);
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
|
||||
pub fn previous_list(&mut self) {
|
||||
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
|
||||
let prev_list_section;
|
||||
if list_section == 0 {
|
||||
prev_list_section = self.lists.len() - 1;
|
||||
} else {
|
||||
prev_list_section = list_section - 1;
|
||||
}
|
||||
|
||||
let prev_list_idx;
|
||||
if list_idx > self.lists[prev_list_section].items.len() - 1 {
|
||||
prev_list_idx = self.lists[prev_list_section].items.len() - 1;
|
||||
} else {
|
||||
prev_list_idx = list_idx;
|
||||
}
|
||||
|
||||
if list_section != prev_list_section {
|
||||
self.lists[list_section].unselect();
|
||||
}
|
||||
let state = (prev_list_section, prev_list_idx);
|
||||
|
||||
self.lists[state.0].state.select(Some(state.1));
|
||||
self.state = self.list_idx_to_idx(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
let mut state = ListState::default();
|
||||
// Enforce state as first of list
|
||||
state.select(Some(0));
|
||||
StatefulList {
|
||||
state: state,
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
113
training_mod_tui/src/main.rs
Normal file
113
training_mod_tui/src/main.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
#[cfg(feature = "has_terminal")]
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
#[cfg(feature = "has_terminal")]
|
||||
use tui::backend::CrosstermBackend;
|
||||
#[cfg(feature = "has_terminal")]
|
||||
use std::{
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::error::Error;
|
||||
use tui::Terminal;
|
||||
|
||||
use training_mod_consts::*;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let menu;
|
||||
unsafe {
|
||||
menu = get_menu();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "has_terminal"))] {
|
||||
let mut app = training_mod_tui::App::new(menu);
|
||||
let backend = tui::backend::TestBackend::new(75, 15);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
let mut state = tui::widgets::ListState::default();
|
||||
state.select(Some(1));
|
||||
let mut url = String::new();
|
||||
let frame_res = terminal.draw(|f| url = training_mod_tui::ui(f, &mut app))?;
|
||||
|
||||
for (i, cell) in frame_res.buffer.content().into_iter().enumerate() {
|
||||
print!("{}", cell.symbol);
|
||||
if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
|
||||
print!("\n");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("URL: {}", url);
|
||||
}
|
||||
|
||||
#[cfg(feature = "has_terminal")] {
|
||||
let app = training_mod_tui::App::new(menu);
|
||||
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
} else {
|
||||
println!("URL: {}", res.as_ref().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "has_terminal")]
|
||||
fn run_app<B: tui::backend::Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: training_mod_tui::App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<String> {
|
||||
let mut last_tick = Instant::now();
|
||||
let mut url = String::new();
|
||||
loop {
|
||||
terminal.draw(|f| url = training_mod_tui::ui(f, &mut app).clone())?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(url),
|
||||
KeyCode::Char('r') => app.on_r(),
|
||||
KeyCode::Char('l') => app.on_l(),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Enter => app.on_a(),
|
||||
KeyCode::Backspace => app.on_b(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
12
training_mod_tui/training_mod_tui.iml
Normal file
12
training_mod_tui/training_mod_tui.iml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="RUST_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
Loading…
Reference in a new issue