diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6330fd0..ea1c65b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/.gitignore b/.gitignore index c9efdf0..ab16836 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ **/*.pyc Cargo.lock -TrainingModpackOverlay/build/ +.idea/ *.ovl diff --git a/Cargo.toml b/Cargo.toml index 61508f5..e97ba3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/ryujinx_build.sh b/ryujinx_build.sh new file mode 100644 index 0000000..ec6fe73 --- /dev/null +++ b/ryujinx_build.sh @@ -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 \ No newline at end of file diff --git a/src/common/menu.rs b/src/common/menu.rs index 226c3a8..2418f1d 100644 --- a/src/common/menu.rs +++ b/src/common/menu.rs @@ -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>, - sliders: Vec, - onoffselector: Vec>, - index: usize, - check_against: usize, - is_single_option: Option, - 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>, -} - -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, - 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::>(); - let toggle = toggle_value_split[0]; - if toggle.is_empty() { - continue; - } - - let toggle_vals = toggle_value_split[1]; - - let bitwise_or = >::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(); + } + } +} + diff --git a/src/common/mod.rs b/src/common/mod.rs index 8158d3f..41040f6 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -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; diff --git a/src/hazard_manager/mod.rs b/src/hazard_manager/mod.rs index f14d369..90aaf5a 100644 --- a/src/hazard_manager/mod.rs +++ b/src/hazard_manager/mod.rs @@ -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; diff --git a/src/lib.rs b/src/lib.rs index c366659..45b79d2 100644 --- a/src/lib.rs +++ b/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); + } + } + } + } + }); +} diff --git a/src/training/buff.rs b/src/training/buff.rs index 2054d93..f94d578 100644 --- a/src/training/buff.rs +++ b/src/training/buff.rs @@ -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; diff --git a/src/training/mod.rs b/src/training/mod.rs index 3c21b76..9158cb8 100644 --- a/src/training/mod.rs +++ b/src/training/mod.rs @@ -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 { diff --git a/training_mod_consts/Cargo.toml b/training_mod_consts/Cargo.toml index ece0179..79edaab 100644 --- a/training_mod_consts/Cargo.toml +++ b/training_mod_consts/Cargo.toml @@ -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" } \ No newline at end of file +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"] \ No newline at end of file diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index df124ec..03d3d7f 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -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(arg: &[T]) -> &T { @@ -88,8 +97,8 @@ pub fn random_option(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 { - 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 { - 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 { - 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 { - 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>, + pub sliders: Vec, + pub onoffselector: Vec>, + pub index: usize, + pub check_against: usize, + pub is_single_option: Option, + 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>, +} + +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, + 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::>(); + let toggle = toggle_value_split[0]; + if toggle.is_empty() { + continue; + } + + let toggle_vals = toggle_value_split[1]; + + let bitwise_or = >::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 +} \ No newline at end of file diff --git a/training_mod_tui/Cargo.toml b/training_mod_tui/Cargo.toml new file mode 100644 index 0000000..3bcfada --- /dev/null +++ b/training_mod_tui/Cargo.toml @@ -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"] \ No newline at end of file diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs new file mode 100644 index 0000000..9ca3020 --- /dev/null +++ b/training_mod_tui/src/lib.rs @@ -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>>, + pub selected_sub_menu_toggles: MultiStatefulList>, + pub selected_sub_menu_onoff_selectors: MultiStatefulList>, + pub selected_sub_menu_sliders: MultiStatefulList, + 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> = 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(f: &mut Frame, 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 = 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 = 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); + // } +} \ No newline at end of file diff --git a/training_mod_tui/src/list.rs b/training_mod_tui/src/list.rs new file mode 100644 index 0000000..a8cd63b --- /dev/null +++ b/training_mod_tui/src/list.rs @@ -0,0 +1,191 @@ +use tui::widgets::ListState; + +pub struct MultiStatefulList { + pub lists: Vec>, + pub state: usize, + pub total_len: usize +} + +impl MultiStatefulList { + 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, num_lists: usize) -> MultiStatefulList { + 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 { + pub state: ListState, + pub items: Vec, +} + +impl StatefulList { + pub fn with_items(items: Vec) -> StatefulList { + 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); + } +} \ No newline at end of file diff --git a/training_mod_tui/src/main.rs b/training_mod_tui/src/main.rs new file mode 100644 index 0000000..d8353e2 --- /dev/null +++ b/training_mod_tui/src/main.rs @@ -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> { + 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( + terminal: &mut Terminal, + mut app: training_mod_tui::App, + tick_rate: Duration, +) -> io::Result { + 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(); + } + } +} diff --git a/training_mod_tui/training_mod_tui.iml b/training_mod_tui/training_mod_tui.iml new file mode 100644 index 0000000..8f66a1c --- /dev/null +++ b/training_mod_tui/training_mod_tui.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file