1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-20 00:46:34 +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:
jugeeya 2022-03-20 11:09:25 -07:00 committed by GitHub
parent c6c4105fc3
commit e4e2de0a79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1916 additions and 774 deletions

View file

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

@ -3,7 +3,7 @@
**/*.pyc
Cargo.lock
TrainingModpackOverlay/build/
.idea/
*.ovl

View file

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

View file

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

View file

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

View file

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

View file

@ -20,9 +20,9 @@ mod test;
use crate::common::*;
use crate::events::{Event, EVENT_QUEUE};
use crate::menu::get_menu_from_url;
use crate::common::consts::get_menu_from_url;
use skyline::libc::mkdir;
use skyline::libc::{c_char, mkdir};
use skyline::nro::{self, NroInfo};
use std::fs;
@ -43,12 +43,26 @@ fn nro_main(nro: &NroInfo<'_>) {
}
}
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 {
@ -68,6 +82,7 @@ pub fn main() {
training::training_mods();
nro::add_hook(nro_main).unwrap();
unsafe {
mkdir(c_str!("sd:/TrainingModpack/"), 777);
}
@ -118,7 +133,7 @@ pub fn main() {
}
std::thread::spawn(|| loop {
std::thread::sleep(std::time::Duration::from_secs(5));
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";
@ -132,4 +147,68 @@ pub fn main() {
}
}
});
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);
}
}
}
}
});
}

View file

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

View file

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

View file

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

View file

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

View 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
View 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);
// }
}

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

View 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();
}
}
}

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