diff --git a/src/common/menu.rs b/src/common/menu.rs index e71a98e..17c76fa 100644 --- a/src/common/menu.rs +++ b/src/common/menu.rs @@ -1,13 +1,18 @@ -use crate::common::consts::get_menu_from_url; use crate::common::*; use crate::events::{Event, EVENT_QUEUE}; use crate::training::frame_counter; + +use owo_colors::OwoColorize; use ramhorns::Template; use skyline::info::get_program_id; -use skyline_web::{Background, BootDisplay, Webpage}; +use skyline::nn::hid::NpadGcState; +use skyline::nn::web::WebSessionBootMode; +use skyline_web::{Background, WebSession, Webpage}; use smash::lib::lua_const::*; use std::fs; use std::path::Path; +use training_mod_consts::{TrainingModpackMenu, MenuJsonStruct}; +use training_mod_tui::Color; static mut FRAME_COUNTER_INDEX: usize = 0; pub static mut QUICK_MENU_FRAME_COUNTER_INDEX: usize = 0; @@ -70,27 +75,49 @@ pub unsafe fn write_menu() { const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.conf"; -pub fn set_menu_from_url(last_url: &str) { - unsafe { - MENU = get_menu_from_url(MENU, last_url, false); - DEFAULTS_MENU = get_menu_from_url(MENU, last_url, true); - - if MENU.quick_menu == OnOff::Off { - if is_emulator() { - skyline::error::show_error( - 0x69, - "Cannot use web menu on emulator.\n\0", - "Only the quick menu is runnable via emulator currently.\n\0", - ); - MENU.quick_menu = OnOff::On; - } +pub unsafe fn set_menu_from_json(message: &str) { + if MENU.quick_menu == OnOff::Off { + if is_emulator() { + skyline::error::show_error( + 0x69, + "Cannot use web menu on emulator.\n\0", + "Only the quick menu is runnable via emulator currently.\n\0", + ); + MENU.quick_menu = OnOff::On; } } + if let Ok(message_json) = serde_json::from_str::(message) { + // Includes both MENU and DEFAULTS_MENU + // From Web Applet + MENU = message_json.menu; + DEFAULTS_MENU = message_json.defaults_menu; + std::fs::write( + MENU_CONF_PATH, + serde_json::to_string_pretty(&message_json).unwrap() + ) + .expect("Failed to write menu conf file"); + } else if let Ok(message_json) = serde_json::from_str::(message) { + // Only includes MENU + // From TUI + MENU = message_json; - std::fs::write(MENU_CONF_PATH, last_url).expect("Failed to write menu conf file"); - unsafe { - EVENT_QUEUE.push(Event::menu_open(last_url.to_string())); - } + let conf = MenuJsonStruct { + menu: MENU, + defaults_menu: DEFAULTS_MENU, + }; + std::fs::write( + MENU_CONF_PATH, + serde_json::to_string_pretty(&conf).unwrap() + ) + .expect("Failed to write menu conf file"); + } else { + skyline::error::show_error( + 0x70, + "Could not parse the menu response!\nPlease send a screenshot of the details page to the developers.\n\0", + message + ); + }; + EVENT_QUEUE.push(Event::menu_open(message.to_string())); } pub fn spawn_menu() { @@ -99,42 +126,15 @@ pub fn spawn_menu() { frame_counter::start_counting(FRAME_COUNTER_INDEX); frame_counter::reset_frame_count(QUICK_MENU_FRAME_COUNTER_INDEX); frame_counter::start_counting(QUICK_MENU_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"; - unsafe { - let params = MENU.to_url_params(false); - let default_params = DEFAULTS_MENU.to_url_params(true); - let page_response = Webpage::new() - .background(Background::BlurredScreenshot) - .htdocs_dir("training_modpack") - .boot_display(BootDisplay::BlurredScreenshot) - .boot_icon(true) - .start_page(&format!("{}?{}&{}", fname, params, default_params)) - .open() - .unwrap(); - - let last_url = page_response.get_last_url().unwrap(); - println!("Received URL from web menu: {}", last_url); - set_menu_from_url(last_url); - } - } else { - unsafe { + if MENU.quick_menu == OnOff::Off { + WEB_MENU_ACTIVE = true; + } else { QUICK_MENU_ACTIVE = true; } } } -use skyline::nn::hid::NpadGcState; - pub struct ButtonPresses { pub a: ButtonPress, pub b: ButtonPress, @@ -272,3 +272,196 @@ pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32 } } } + +extern "C" { + #[link_name = "render_text_to_screen"] + pub fn render_text_to_screen_cstr(str: *const skyline::libc::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)); + } +} + +pub unsafe fn quick_menu_loop() { + loop { + std::thread::sleep(std::time::Duration::from_secs(10)); + let menu = 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(); + + let mut has_slept_millis = 0; + let render_frames = 5; + let mut json_response = String::new(); + let button_presses = &mut menu::BUTTON_PRESSES; + let mut received_input = true; + loop { + button_presses.a.read_press().then(|| { + app.on_a(); + received_input = true; + }); + let b_press = &mut button_presses.b; + b_press.read_press().then(|| { + received_input = true; + if !app.outer_list { + app.on_b() + } else if frame_counter::get_frame_count(menu::QUICK_MENU_FRAME_COUNTER_INDEX) == 0 + { + // Leave menu. + menu::QUICK_MENU_ACTIVE = false; + menu::set_menu_from_json(&json_response); + } + }); + button_presses.zl.read_press().then(|| { + app.on_l(); + received_input = true; + }); + button_presses.zr.read_press().then(|| { + app.on_r(); + received_input = true; + }); + button_presses.left.read_press().then(|| { + app.on_left(); + received_input = true; + }); + button_presses.right.read_press().then(|| { + app.on_right(); + received_input = true; + }); + button_presses.up.read_press().then(|| { + app.on_up(); + received_input = true; + }); + button_presses.down.read_press().then(|| { + app.on_down(); + received_input = true; + }); + + std::thread::sleep(std::time::Duration::from_millis(16)); + has_slept_millis += 16; + if has_slept_millis < 16 * render_frames { + continue; + } + has_slept_millis = 16; + if !menu::QUICK_MENU_ACTIVE { + app = training_mod_tui::App::new(consts::get_menu()); + set_should_display_text_to_screen(false); + continue; + } + if !received_input { + continue; + } + let mut view = String::new(); + + let frame_res = terminal + .draw(|f| json_response = training_mod_tui::ui(f, &mut app)) + .unwrap(); + + use std::fmt::Write; + for (i, cell) in frame_res.buffer.content().iter().enumerate() { + match cell.fg { + Color::Black => write!(&mut view, "{}", &cell.symbol.black()), + Color::Blue => write!(&mut view, "{}", &cell.symbol.blue()), + Color::LightBlue => write!(&mut view, "{}", &cell.symbol.bright_blue()), + Color::Cyan => write!(&mut view, "{}", &cell.symbol.cyan()), + Color::LightCyan => write!(&mut view, "{}", &cell.symbol.cyan()), + Color::Red => write!(&mut view, "{}", &cell.symbol.red()), + Color::LightRed => write!(&mut view, "{}", &cell.symbol.bright_red()), + Color::LightGreen => write!(&mut view, "{}", &cell.symbol.bright_green()), + Color::Green => write!(&mut view, "{}", &cell.symbol.green()), + Color::Yellow => write!(&mut view, "{}", &cell.symbol.yellow()), + Color::LightYellow => write!(&mut view, "{}", &cell.symbol.bright_yellow()), + Color::Magenta => write!(&mut view, "{}", &cell.symbol.magenta()), + Color::LightMagenta => { + write!(&mut view, "{}", &cell.symbol.bright_magenta()) + } + _ => write!(&mut view, "{}", &cell.symbol), + } + .unwrap(); + if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 { + writeln!(&mut view).unwrap(); + } + } + writeln!(&mut view).unwrap(); + + render_text_to_screen(view.as_str()); + received_input = false; + } + } +} + +static mut WEB_MENU_ACTIVE: bool = false; + +pub unsafe fn web_session_loop() { + // Don't query the fightermanager too early otherwise it will crash... + std::thread::sleep(std::time::Duration::new(30, 0)); // sleep for 30 secs on bootup + let mut web_session: Option = None; + loop { + std::thread::sleep(std::time::Duration::from_millis(100)); + if is_ready_go() & is_training_mode() { + if web_session.is_some() { + if WEB_MENU_ACTIVE { + println!("[Training Modpack] Opening menu session..."); + let session = web_session.unwrap(); + let message_send = MenuJsonStruct { + menu: MENU, + defaults_menu: DEFAULTS_MENU, + }; + session.send_json(&message_send); + println!( + "[Training Modpack] Sending message:\n{}", + serde_json::to_string_pretty(&message_send).unwrap() + ); + session.show(); + let message_recv = session.recv(); + println!( + "[Training Modpack] Received menu from web:\n{}", + &message_recv + ); + println!("[Training Modpack] Tearing down Training Modpack menu session"); + session.exit(); + session.wait_for_exit(); + web_session = None; + set_menu_from_json(&message_recv); + WEB_MENU_ACTIVE = false; + } + } else { + // TODO + // Starting a new session causes some ingame lag. + // Investigate whether we can minimize this lag by + // waiting until the player is idle or using CPU boost mode + println!("[Training Modpack] Starting new menu session..."); + web_session = Some( + Webpage::new() + .background(Background::BlurredScreenshot) + .htdocs_dir("training_modpack") + .start_page("training_menu.html") + .open_session(WebSessionBootMode::InitiallyHidden) + .unwrap(), + ); + } + } else { + // No longer in training mode, tear down the session. + // This will avoid conflicts with other web plugins, and helps with stability. + // Having the session open too long, especially if the switch has been put to sleep, can cause freezes + if web_session.is_some() { + println!("[Training Modpack] Tearing down Training Modpack menu session"); + web_session.unwrap().exit(); + } + web_session = None; + } + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index ae1dbee..0f7752e 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -141,3 +141,9 @@ pub unsafe fn is_dead(module_accessor: &mut app::BattleObjectModuleAccessor) -> pub unsafe fn is_in_clatter(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { ControlModule::get_clatter_time(module_accessor, 0) > 0.0 } + +// Returns true if a match is currently active +pub unsafe fn is_ready_go() -> bool { + let fighter_manager = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); + FighterManager::is_ready_go(fighter_manager) +} diff --git a/src/lib.rs b/src/lib.rs index 7eb6321..601e1e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,18 +19,16 @@ mod training; #[cfg(test)] mod test; -use crate::common::consts::get_menu_from_url; use crate::common::*; use crate::events::{Event, EVENT_QUEUE}; -use skyline::libc::{c_char, mkdir}; +use skyline::libc::mkdir; use skyline::nro::{self, NroInfo}; use std::fs; -use crate::training::frame_counter; +use crate::menu::{quick_menu_loop, web_session_loop}; use owo_colors::OwoColorize; -use training_mod_consts::OnOff; -use training_mod_tui::Color; +use training_mod_consts::{OnOff, MenuJsonStruct}; fn nro_main(nro: &NroInfo<'_>) { if nro.module.isLoaded { @@ -46,26 +44,12 @@ 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 { @@ -100,19 +84,19 @@ pub fn main() { 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"); + let menu_conf = fs::read_to_string(&menu_conf_path).unwrap(); + if let Ok(menu_conf_json) = serde_json::from_str::(&menu_conf) { unsafe { - MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap(), false); - DEFAULTS_MENU = get_menu_from_url( - DEFAULTS_MENU, - std::str::from_utf8(&menu_conf).unwrap(), - true, - ); + MENU = menu_conf_json.menu; + DEFAULTS_MENU = menu_conf_json.defaults_menu; + log!("Previous menu found. Loading..."); } + } else if menu_conf.starts_with("http://localhost") { + log!("Previous menu found, with URL schema. Deleting..."); + fs::remove_file(menu_conf_path).expect("Could not delete conf file!"); } else { - log!("Previous menu found but is invalid."); + log!("Previous menu found but is invalid. Deleting..."); + fs::remove_file(menu_conf_path).expect("Could not delete conf file!"); } } else { log!("No previous menu file found."); @@ -141,118 +125,7 @@ pub fn main() { } }); - std::thread::spawn(|| { - std::thread::sleep(std::time::Duration::from_secs(10)); - let menu; - unsafe { - menu = consts::get_menu(); - } + std::thread::spawn(|| unsafe { quick_menu_loop() }); - 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 render_frames = 5; - let mut url = String::new(); - let button_presses = &mut menu::BUTTON_PRESSES; - let mut received_input = true; - loop { - button_presses.a.read_press().then(|| { - app.on_a(); - received_input = true; - }); - let b_press = &mut button_presses.b; - b_press.read_press().then(|| { - received_input = true; - if !app.outer_list { - app.on_b() - } else if frame_counter::get_frame_count(menu::QUICK_MENU_FRAME_COUNTER_INDEX) - == 0 - { - // Leave menu. - menu::QUICK_MENU_ACTIVE = false; - menu::set_menu_from_url(url.as_str()); - println!("URL: {}", url.as_str()); - } - }); - button_presses.zl.read_press().then(|| { - app.on_l(); - received_input = true; - }); - button_presses.zr.read_press().then(|| { - app.on_r(); - received_input = true; - }); - button_presses.left.read_press().then(|| { - app.on_left(); - received_input = true; - }); - button_presses.right.read_press().then(|| { - app.on_right(); - received_input = true; - }); - button_presses.up.read_press().then(|| { - app.on_up(); - received_input = true; - }); - button_presses.down.read_press().then(|| { - app.on_down(); - received_input = true; - }); - - std::thread::sleep(std::time::Duration::from_millis(16)); - has_slept_millis += 16; - if has_slept_millis < 16 * render_frames { - continue; - } - has_slept_millis = 16; - if !menu::QUICK_MENU_ACTIVE { - app = training_mod_tui::App::new(consts::get_menu()); - set_should_display_text_to_screen(false); - continue; - } - if !received_input { - continue; - } - 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().iter().enumerate() { - match cell.fg { - Color::Black => write!(&mut view, "{}", &cell.symbol.black()), - Color::Blue => write!(&mut view, "{}", &cell.symbol.blue()), - Color::LightBlue => write!(&mut view, "{}", &cell.symbol.bright_blue()), - Color::Cyan => write!(&mut view, "{}", &cell.symbol.cyan()), - Color::LightCyan => write!(&mut view, "{}", &cell.symbol.cyan()), - Color::Red => write!(&mut view, "{}", &cell.symbol.red()), - Color::LightRed => write!(&mut view, "{}", &cell.symbol.bright_red()), - Color::LightGreen => write!(&mut view, "{}", &cell.symbol.bright_green()), - Color::Green => write!(&mut view, "{}", &cell.symbol.green()), - Color::Yellow => write!(&mut view, "{}", &cell.symbol.yellow()), - Color::LightYellow => write!(&mut view, "{}", &cell.symbol.bright_yellow()), - Color::Magenta => write!(&mut view, "{}", &cell.symbol.magenta()), - Color::LightMagenta => { - write!(&mut view, "{}", &cell.symbol.bright_magenta()) - } - _ => write!(&mut view, "{}", &cell.symbol), - } - .unwrap(); - if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 { - writeln!(&mut view).unwrap(); - } - } - writeln!(&mut view).unwrap(); - - render_text_to_screen(view.as_str()); - received_input = false; - } - } - }); + std::thread::spawn(|| unsafe { web_session_loop() }); } diff --git a/src/static/js/training_modpack.js b/src/static/js/training_modpack.js index 51411d5..6b33084 100644 --- a/src/static/js/training_modpack.js +++ b/src/static/js/training_modpack.js @@ -29,6 +29,7 @@ if (isNx) { window.nx.footer.setAssign('R', '', saveDefaults, { se: '' }); window.nx.footer.setAssign('ZR', '', cycleNextTab, { se: '' }); window.nx.footer.setAssign('ZL', '', cyclePrevTab, { se: '' }); + window.nx.addEventListener("message", function(msg) { setSettingsFromJSON(msg)}); } else { document.addEventListener('keypress', (event) => { switch (event.key) { @@ -42,7 +43,7 @@ if (isNx) { break; case 'l': console.log('l'); - resetAllSubmenus(); + resetAllMenus(); break; case 'r': console.log('r'); @@ -63,15 +64,12 @@ if (isNx) { const onLoad = () => { // Activate the first tab openTab(document.querySelector('button.tab-button')); - - // Extract URL params and set appropriate settings - setSettingsFromURL(); - populateMenuFromSettings(); }; window.onload = onLoad; var settings; +var defaultSettings; var lastFocusedItem = document.querySelector('.menu-item > button'); const currentTabContent = () => { @@ -181,13 +179,17 @@ function playSound(label) { const exit = () => { playSound('SeFooterDecideBack'); - - const url = buildURLFromSettings(); + const messageObject = { + menu: settings, + defaults_menu: defaultSettings + } if (isNx) { - window.location.href = url; + window.nx.sendMessage( + JSON.stringify(messageObject) + ); } else { - console.log(url); + console.log(JSON.stringify(messageObject)); } }; @@ -205,18 +207,42 @@ function closeOrExit() { exit(); } +function setSettingsFromJSON(msg) { + // Receive a menu message and set settings + var msg_json = JSON.parse(msg.data); + settings = msg_json["menu"]; + defaultSettings = msg_json["defaults_menu"]; + populateMenuFromSettings(); +} + function setSettingsFromURL() { var { search } = window.location; + // Actual settings const settingsFromSearch = search .replace('?', '') .split('&') .reduce((accumulator, currentValue) => { var [key, value] = currentValue.split('='); - accumulator[key] = parseInt(value); + if (!key.startsWith('__')) { + accumulator[key] = parseInt(value); + } return accumulator; }, {}); - settings = settingsFromSearch; + + // Default settings + const defaultSettingsFromSearch = search + .replace('?', '') + .split('&') + .reduce((accumulator, currentValue) => { + var [key, value] = currentValue.split('='); + if (key.startsWith('__')) { + accumulator[key.replace('__','')] = parseInt(value); + } + return accumulator; + }, {}); + defaultSettings = defaultSettingsFromSearch; + populateMenuFromSettings() } function buildURLFromSettings() { @@ -288,7 +314,7 @@ function resetCurrentMenu() { const menu = document.querySelector('.modal:not(.hide)'); const menuId = menu.dataset.id; - const defaultSectionMask = settings[DEFAULTS_PREFIX + menuId]; + const defaultSectionMask = defaultSettings[menuId]; settings[menuId] = defaultSectionMask; @@ -299,8 +325,8 @@ function resetAllMenus() { // Resets all submenus to the default values if (confirm('Are you sure that you want to reset all menu settings to the default?')) { document.querySelectorAll('.menu-item').forEach(function (item) { - const defaultMenuId = DEFAULTS_PREFIX + item.id; - const defaultMask = settings[defaultMenuId]; + const defaultMenuId = item.id; + const defaultMask = defaultSettings[defaultMenuId]; settings[item.id] = defaultMask; @@ -316,9 +342,9 @@ function setHelpText(text) { function saveDefaults() { if (confirm('Are you sure that you want to change the default menu settings to the current selections?')) { document.querySelectorAll('.menu-item').forEach((item) => { - const menu = DEFAULTS_PREFIX + item.id; + const menu = item.id; - settings[menu] = getMaskFromMenuID(item.id); + defaultSettings[menu] = getMaskFromMenuID(item.id); }); } } diff --git a/training_mod_consts/Cargo.toml b/training_mod_consts/Cargo.toml index 79edaab..b9000f2 100644 --- a/training_mod_consts/Cargo.toml +++ b/training_mod_consts/Cargo.toml @@ -13,6 +13,9 @@ num-traits = "0.2" ramhorns = "0.12.0" paste = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_repr = "0.1.8" +serde_json = "1" +bitflags_serde_shim = "0.2" skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git", optional = true } [features] diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index b0421b9..d5e9f3f 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -1,17 +1,21 @@ #[macro_use] extern crate bitflags; +#[macro_use] +extern crate bitflags_serde_shim; + #[macro_use] extern crate num_derive; use core::f64::consts::PI; -use std::collections::HashMap; +use ramhorns::Content; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; #[cfg(feature = "smash")] use smash::lib::lua_const::*; use strum::IntoEnumIterator; use strum_macros::EnumIter; -use serde::{Serialize, Deserialize}; -use ramhorns::Content; +use std::collections::HashMap; pub trait ToggleTrait { fn to_toggle_strs() -> Vec<&'static str>; @@ -73,17 +77,14 @@ macro_rules! extra_bitflag_impls { all_options.iter().map(|i| i.bits() as usize).collect() } } - impl ToUrlParam for $e { - fn to_url_param(&self) -> String { - self.bits().to_string() - } - } } } pub fn get_random_int(_max: i32) -> i32 { #[cfg(feature = "smash")] - unsafe { smash::app::sv_math::rand(smash::hash40("fighter"), _max) } + unsafe { + smash::app::sv_math::rand(smash::hash40("fighter"), _max) + } #[cfg(not(feature = "smash"))] 0 @@ -101,7 +102,6 @@ pub fn random_option(arg: &[T]) -> &T { // DI / Left stick bitflags! { - #[derive(Serialize, Deserialize)] pub struct Direction : u32 { const OUT = 0x1; const UP_OUT = 0x2; @@ -163,10 +163,10 @@ impl Direction { } extra_bitflag_impls! {Direction} +impl_serde_for_bitflags!(Direction); // Ledge Option bitflags! { - #[derive(Serialize, Deserialize)] pub struct LedgeOption : u32 { const NEUTRAL = 0x1; @@ -179,7 +179,8 @@ bitflags! { impl LedgeOption { pub fn into_status(self) -> Option { - #[cfg(feature = "smash")] { + #[cfg(feature = "smash")] + { Some(match self { LedgeOption::NEUTRAL => *FIGHTER_STATUS_KIND_CLIFF_CLIMB, LedgeOption::ROLL => *FIGHTER_STATUS_KIND_CLIFF_ESCAPE, @@ -207,10 +208,10 @@ impl LedgeOption { } extra_bitflag_impls! {LedgeOption} +impl_serde_for_bitflags!(LedgeOption); // Tech options bitflags! { - #[derive(Serialize, Deserialize)] pub struct TechFlags : u32 { const NO_TECH = 0x1; const ROLL_F = 0x2; @@ -232,10 +233,10 @@ impl TechFlags { } extra_bitflag_impls! {TechFlags} +impl_serde_for_bitflags!(TechFlags); // Missed Tech Options bitflags! { - #[derive(Serialize, Deserialize)] pub struct MissTechFlags : u32 { const GETUP = 0x1; const ATTACK = 0x2; @@ -257,10 +258,13 @@ impl MissTechFlags { } extra_bitflag_impls! {MissTechFlags} +impl_serde_for_bitflags!(MissTechFlags); /// Shield States #[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] pub enum Shield { None = 0x0, Infinite = 0x1, @@ -277,10 +281,6 @@ impl Shield { Shield::Constant => "Constant", }) } - - pub fn to_url_param(&self) -> String { - (*self as i32).to_string() - } } impl ToggleTrait for Shield { @@ -295,7 +295,9 @@ impl ToggleTrait for Shield { // Save State Mirroring #[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] pub enum SaveStateMirroring { None = 0x0, Alternate = 0x1, @@ -310,15 +312,13 @@ impl SaveStateMirroring { SaveStateMirroring::Random => "Random", }) } - - fn to_url_param(&self) -> String { - (*self as i32).to_string() - } } impl ToggleTrait for SaveStateMirroring { fn to_toggle_strs() -> Vec<&'static str> { - SaveStateMirroring::iter().map(|i| i.as_str().unwrap_or("")).collect() + SaveStateMirroring::iter() + .map(|i| i.as_str().unwrap_or("")) + .collect() } fn to_toggle_vals() -> Vec { @@ -328,7 +328,6 @@ impl ToggleTrait for SaveStateMirroring { // Defensive States bitflags! { - #[derive(Serialize, Deserialize)] pub struct Defensive : u32 { const SPOT_DODGE = 0x1; const ROLL_F = 0x2; @@ -352,9 +351,10 @@ impl Defensive { } extra_bitflag_impls! {Defensive} +impl_serde_for_bitflags!(Defensive); #[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] pub enum OnOff { Off = 0, On = 1, @@ -375,10 +375,6 @@ impl OnOff { OnOff::On => "On", }) } - - pub fn to_url_param(&self) -> String { - (*self as i32).to_string() - } } impl ToggleTrait for OnOff { @@ -391,7 +387,6 @@ impl ToggleTrait for OnOff { } bitflags! { - #[derive(Serialize, Deserialize)] pub struct Action : u32 { const AIR_DODGE = 0x1; const JUMP = 0x2; @@ -424,7 +419,8 @@ bitflags! { impl Action { pub fn into_attack_air_kind(self) -> Option { - #[cfg(feature = "smash")] { + #[cfg(feature = "smash")] + { Some(match self { Action::NAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_N, Action::FAIR => *FIGHTER_COMMAND_ATTACK_AIR_KIND_F, @@ -472,9 +468,9 @@ impl Action { } extra_bitflag_impls! {Action} +impl_serde_for_bitflags!(Action); bitflags! { - #[derive(Serialize, Deserialize)] pub struct AttackAngle : u32 { const NEUTRAL = 0x1; const UP = 0x2; @@ -494,9 +490,9 @@ impl AttackAngle { } extra_bitflag_impls! {AttackAngle} +impl_serde_for_bitflags!(AttackAngle); bitflags! { - #[derive(Serialize, Deserialize)] pub struct Delay : u32 { const D0 = 0x1; const D1 = 0x2; @@ -534,7 +530,6 @@ bitflags! { // Throw Option bitflags! { - #[derive(Serialize, Deserialize)] pub struct ThrowOption : u32 { const NONE = 0x1; @@ -547,7 +542,8 @@ bitflags! { impl ThrowOption { pub fn into_cmd(self) -> Option { - #[cfg(feature = "smash")] { + #[cfg(feature = "smash")] + { Some(match self { ThrowOption::NONE => 0, ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F, @@ -575,10 +571,10 @@ impl ThrowOption { } extra_bitflag_impls! {ThrowOption} +impl_serde_for_bitflags!(ThrowOption); // Buff Option bitflags! { - #[derive(Serialize, Deserialize)] pub struct BuffOption : u32 { const ACCELERATLE = 0x1; @@ -595,7 +591,8 @@ bitflags! { impl BuffOption { pub fn into_int(self) -> Option { - #[cfg(feature = "smash")] { + #[cfg(feature = "smash")] + { Some(match self { BuffOption::ACCELERATLE => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND11_SPEED_UP, BuffOption::OOMPH => *FIGHTER_BRAVE_SPECIAL_LW_COMMAND12_ATTACK_UP, @@ -631,6 +628,7 @@ impl BuffOption { } extra_bitflag_impls! {BuffOption} +impl_serde_for_bitflags!(BuffOption); impl Delay { pub fn as_str(self) -> Option<&'static str> { @@ -676,9 +674,9 @@ impl Delay { } extra_bitflag_impls! {Delay} +impl_serde_for_bitflags!(Delay); bitflags! { - #[derive(Serialize, Deserialize)] pub struct MedDelay : u32 { const D0 = 0x1; const D5 = 0x2; @@ -758,9 +756,9 @@ impl MedDelay { } extra_bitflag_impls! {MedDelay} +impl_serde_for_bitflags!(MedDelay); bitflags! { - #[derive(Serialize, Deserialize)] pub struct LongDelay : u32 { const D0 = 0x1; const D10 = 0x2; @@ -840,9 +838,9 @@ impl LongDelay { } extra_bitflag_impls! {LongDelay} +impl_serde_for_bitflags!(LongDelay); bitflags! { - #[derive(Serialize, Deserialize)] pub struct BoolFlag : u32 { const TRUE = 0x1; const FALSE = 0x2; @@ -850,6 +848,7 @@ bitflags! { } extra_bitflag_impls! {BoolFlag} +impl_serde_for_bitflags!(BoolFlag); impl BoolFlag { pub fn into_bool(self) -> bool { @@ -865,7 +864,9 @@ impl BoolFlag { } #[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] pub enum InputFrequency { None = 0, Normal = 1, @@ -891,15 +892,13 @@ impl InputFrequency { InputFrequency::High => "High", }) } - - pub fn to_url_param(&self) -> String { - (*self as u32).to_string() - } } impl ToggleTrait for InputFrequency { fn to_toggle_strs() -> Vec<&'static str> { - InputFrequency::iter().map(|i| i.as_str().unwrap_or("")).collect() + InputFrequency::iter() + .map(|i| i.as_str().unwrap_or("")) + .collect() } fn to_toggle_vals() -> Vec { @@ -907,22 +906,11 @@ impl ToggleTrait for InputFrequency { } } -// For input delay -trait ToUrlParam { - fn to_url_param(&self) -> String; -} - -// The i32 is now in log form, need to exponentiate -// back into 2^X when we pass back to the menu -impl ToUrlParam for i32 { - fn to_url_param(&self) -> String { - 2_i32.pow(*self as u32).to_string() - } -} - /// Item Selections #[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] pub enum CharacterItem { None = 0, PlayerVariation1 = 0x1, @@ -969,15 +957,13 @@ impl CharacterItem { _ => "None", }) } - - pub fn to_url_param(&self) -> String { - (*self as i32).to_string() - } } impl ToggleTrait for CharacterItem { fn to_toggle_strs() -> Vec<&'static str> { - CharacterItem::iter().map(|i| i.as_str().unwrap_or("")).collect() + CharacterItem::iter() + .map(|i| i.as_str().unwrap_or("")) + .collect() } fn to_toggle_vals() -> Vec { @@ -985,85 +971,49 @@ impl ToggleTrait for CharacterItem { } } -// 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,)* - } - impl $e { - pub fn to_url_params(&self, defaults: bool) -> String { - let mut s = "".to_string(); - let defaults_str = match defaults { - true => &"__", // Prefix field name with "__" if it is for the default menu - false => &"", - }; - $( - s.push_str(defaults_str); - s.push_str(stringify!($field_name)); - s.push_str(&"="); - s.push_str(&self.$field_name.to_url_param()); - s.push_str(&"&"); - )* - s.pop(); - s - } - } - } +#[repr(C)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug, )] +pub struct TrainingModpackMenu { + pub hitbox_vis: OnOff, + pub stage_hazards: OnOff, + pub di_state: Direction, + pub sdi_state: Direction, + pub sdi_strength: InputFrequency, + pub clatter_strength: InputFrequency, + pub air_dodge_dir: Direction, + pub mash_state: Action, + pub follow_up: Action, + pub attack_angle: AttackAngle, + pub ledge_state: LedgeOption, + pub ledge_delay: LongDelay, + pub tech_state: TechFlags, + pub miss_tech_state: MissTechFlags, + pub shield_state: Shield, + pub defensive_state: Defensive, + pub oos_offset: Delay, + pub reaction_time: Delay, + pub shield_tilt: Direction, + pub mash_in_neutral: OnOff, + pub fast_fall: BoolFlag, + pub fast_fall_delay: Delay, + pub falling_aerials: BoolFlag, + pub aerial_delay: Delay, + pub full_hop: BoolFlag, + pub crouch: OnOff, + pub input_delay: i32, + pub save_damage: OnOff, + pub save_state_mirroring: SaveStateMirroring, + pub frame_advantage: OnOff, + pub save_state_enable: OnOff, + pub save_state_autoload: OnOff, + pub throw_state: ThrowOption, + pub throw_delay: MedDelay, + pub pummel_delay: MedDelay, + pub buff_state: BuffOption, + pub character_item: CharacterItem, + pub quick_menu: OnOff, } -url_params! { - #[repr(C)] - #[derive(Clone, Copy, Serialize, Deserialize, Debug, )] - pub struct TrainingModpackMenu { - pub hitbox_vis: OnOff, - pub stage_hazards: OnOff, - pub di_state: Direction, - pub sdi_state: Direction, - pub sdi_strength: InputFrequency, - pub clatter_strength: InputFrequency, - pub air_dodge_dir: Direction, - pub mash_state: Action, - pub follow_up: Action, - pub attack_angle: AttackAngle, - pub ledge_state: LedgeOption, - pub ledge_delay: LongDelay, - pub tech_state: TechFlags, - pub miss_tech_state: MissTechFlags, - pub shield_state: Shield, - pub defensive_state: Defensive, - pub oos_offset: Delay, - pub reaction_time: Delay, - pub shield_tilt: Direction, - pub mash_in_neutral: OnOff, - pub fast_fall: BoolFlag, - pub fast_fall_delay: Delay, - pub falling_aerials: BoolFlag, - pub aerial_delay: Delay, - pub full_hop: BoolFlag, - pub crouch: OnOff, - pub input_delay: i32, - pub save_damage: OnOff, - pub save_state_mirroring: SaveStateMirroring, - pub frame_advantage: OnOff, - pub save_state_enable: OnOff, - pub save_state_autoload: OnOff, - pub throw_state: ThrowOption, - pub throw_delay: MedDelay, - pub pummel_delay: MedDelay, - pub buff_state: BuffOption, - pub character_item: CharacterItem, - pub quick_menu: OnOff, - } -} macro_rules! set_by_str { ($obj:ident, $s:ident, $($field:ident = $rhs:expr,)*) => { @@ -1075,11 +1025,14 @@ macro_rules! set_by_str { } } -const fn num_bits() -> usize { std::mem::size_of::() * 8 } +const fn num_bits() -> usize { + std::mem::size_of::() * 8 +} fn log_2(x: u32) -> u32 { - if x == 0 { 0 } - else { + if x == 0 { + 0 + } else { num_bits::() as u32 - x.leading_zeros() - 1 } } @@ -1131,6 +1084,14 @@ impl TrainingModpackMenu { } } +#[repr(C)] +#[derive(Debug, Serialize, Deserialize)] +pub struct MenuJsonStruct { + pub menu: TrainingModpackMenu, + pub defaults_menu: TrainingModpackMenu, + // pub last_focused_submenu: &str +} + // Fighter Ids #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1146,11 +1107,11 @@ pub enum SubMenuType { } impl SubMenuType { - pub fn from_str(s : &str) -> SubMenuType { + pub fn from_str(s: &str) -> SubMenuType { match s { "toggle" => SubMenuType::TOGGLE, "slider" => SubMenuType::SLIDER, - _ => panic!("Unexpected SubMenuType!") + _ => panic!("Unexpected SubMenuType!"), } } } @@ -1224,43 +1185,34 @@ pub struct SubMenu<'a> { } impl<'a> SubMenu<'a> { - pub fn add_toggle( - &mut self, - toggle_value: usize, - toggle_title: &'a str - ) { - self.toggles.push( - Toggle { - toggle_value: toggle_value, - toggle_title: toggle_title, - checked: false - } - ); + pub fn add_toggle(&mut self, toggle_value: usize, toggle_title: &'a str) { + self.toggles.push(Toggle { + toggle_value: toggle_value, + toggle_title: toggle_title, + checked: false, + }); } - pub fn new_with_toggles( + pub fn new_with_toggles( submenu_title: &'a str, submenu_id: &'a str, help_text: &'a str, is_single_option: bool, ) -> SubMenu<'a> { - let mut instance = SubMenu { - submenu_title: submenu_title, - submenu_id: submenu_id, - help_text: help_text, - is_single_option: is_single_option, - toggles: Vec::new(), - _type: "toggle" - }; - - let values = T::to_toggle_vals(); - let titles = T::to_toggle_strs(); - for i in 0..values.len() { - instance.add_toggle( - values[i], - titles[i], - ); - } - instance + let mut instance = SubMenu { + submenu_title: submenu_title, + submenu_id: submenu_id, + help_text: help_text, + is_single_option: is_single_option, + toggles: Vec::new(), + _type: "toggle", + }; + + let values = T::to_toggle_vals(); + let titles = T::to_toggle_strs(); + for i in 0..values.len() { + instance.add_toggle(values[i], titles[i]); + } + instance } } @@ -1279,14 +1231,12 @@ impl<'a> Tab<'a> { help_text: &'a str, is_single_option: bool, ) { - self.tab_submenus.push( - SubMenu::new_with_toggles::( - submenu_title, - submenu_id, - help_text, - is_single_option, - ) - ); + self.tab_submenus.push(SubMenu::new_with_toggles::( + submenu_title, + submenu_id, + help_text, + is_single_option, + )); } } @@ -1295,39 +1245,8 @@ pub struct UiMenu<'a> { pub tabs: Vec>, } -pub fn get_menu_from_url(mut menu: TrainingModpackMenu, s: &str, defaults: bool) -> 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 mut toggle = toggle_value_split[0]; - if toggle.is_empty() | ( - // Default menu settings begin with the prefix "__" - // So if skip toggles without the prefix if defaults is true - // And skip toggles with the prefix if defaults is false - defaults ^ toggle.starts_with("__") - ) { continue } - toggle = toggle.strip_prefix("__").unwrap_or(toggle); - - let bits: u32 = toggle_value_split[1].parse().unwrap_or(0); - menu.set(toggle, bits); - } - menu -} - - - pub unsafe fn get_menu() -> UiMenu<'static> { - let mut overall_menu = UiMenu { - tabs: Vec::new(), - }; + let mut overall_menu = UiMenu { tabs: Vec::new() }; let mut mash_tab = Tab { tab_id: "mash", @@ -1420,7 +1339,6 @@ pub unsafe fn get_menu() -> UiMenu<'static> { ); overall_menu.tabs.push(mash_tab); - let mut defensive_tab = Tab { tab_id: "defensive", tab_title: "Defensive Settings", @@ -1508,13 +1426,13 @@ pub unsafe fn get_menu() -> UiMenu<'static> { "Character Item", "character_item", "Character Item: CPU/Player item to hold when loading a save state", - true + true, ); defensive_tab.add_submenu_with_toggles::( "Crouch", "crouch", "Crouch: Should the CPU crouch when on the ground", - true + true, ); overall_menu.tabs.push(defensive_tab); @@ -1569,41 +1487,42 @@ pub unsafe fn get_menu() -> UiMenu<'static> { "Stage Hazards", "stage_hazards", "Stage Hazards: Should stage hazards be present", - true + true, ); misc_tab.add_submenu_with_toggles::( "Quick Menu", "quick_menu", "Quick Menu: Should use quick or web menu", - true + true, ); overall_menu.tabs.push(misc_tab); - let non_ui_menu = MENU; - let url_params = non_ui_menu.to_url_params(false); - let toggle_values_all = url_params.split("&"); - let mut sub_menu_id_to_vals : HashMap<&str, u32> = HashMap::new(); + let non_ui_menu = serde_json::to_string(&MENU).unwrap().replace("\"", ""); + let toggle_values_all = non_ui_menu.split(',').collect::>(); + let mut sub_menu_id_to_vals: HashMap<&str, u32> = HashMap::new(); for toggle_values in toggle_values_all { - let toggle_value_split = toggle_values.split('=').collect::>(); - let mut sub_menu_id = toggle_value_split[0]; - if sub_menu_id.is_empty() { continue } - sub_menu_id = sub_menu_id.strip_prefix("__").unwrap_or(sub_menu_id); + let toggle_value_split = toggle_values.split(':').collect::>(); + let sub_menu_id = toggle_value_split[0]; + if sub_menu_id.is_empty() { + continue; + } let full_bits: u32 = toggle_value_split[1].parse().unwrap_or(0); - sub_menu_id_to_vals.insert(sub_menu_id, full_bits); + sub_menu_id_to_vals.insert(&sub_menu_id, full_bits); } - overall_menu.tabs.iter_mut() - .for_each(|tab| { - tab.tab_submenus.iter_mut().for_each(|sub_menu| { - let sub_menu_id = sub_menu.submenu_id; - sub_menu.toggles.iter_mut().for_each(|toggle| { - if sub_menu_id_to_vals.contains_key(sub_menu_id) && - (sub_menu_id_to_vals[sub_menu_id] & (toggle.toggle_value as u32) != 0) { - toggle.checked = true - } - }) + + overall_menu.tabs.iter_mut().for_each(|tab| { + tab.tab_submenus.iter_mut().for_each(|sub_menu| { + let sub_menu_id = sub_menu.submenu_id; + sub_menu.toggles.iter_mut().for_each(|toggle| { + if sub_menu_id_to_vals.contains_key(sub_menu_id) + && (sub_menu_id_to_vals[sub_menu_id] & (toggle.toggle_value as u32) != 0) + { + toggle.checked = true + } }) - }); + }) + }); overall_menu } diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs index c19f0ff..3fee42e 100644 --- a/training_mod_tui/src/lib.rs +++ b/training_mod_tui/src/lib.rs @@ -341,8 +341,6 @@ pub fn ui(f: &mut Frame, app: &mut App) -> String { f.render_widget(help_paragraph, vertical_chunks[2]); } - - let mut url = "http://localhost/".to_owned(); let mut settings = HashMap::new(); // Collect settings for toggles @@ -358,11 +356,7 @@ pub fn ui(f: &mut Frame, app: &mut App) -> String { } } } - - url.push('?'); - settings.iter() - .for_each(|(section, val)| url.push_str(format!("{}={}&", section, val).as_str())); - url + serde_json::to_string(&settings).unwrap() // TODO: Add saveDefaults // if (document.getElementById("saveDefaults").checked) { diff --git a/training_mod_tui/src/main.rs b/training_mod_tui/src/main.rs index a614566..04cd63e 100644 --- a/training_mod_tui/src/main.rs +++ b/training_mod_tui/src/main.rs @@ -38,12 +38,12 @@ fn ensure_menu_retains_multi_selections() -> Result<(), Box> { } let (mut terminal, mut app) = test_backend_setup(menu)?; - let mut url = String::new(); - let _frame_res = terminal.draw(|f| url = training_mod_tui::ui(f, &mut app))?; - + let mut json_response = String::new(); + let _frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?; + set_menu_from_json(json_response); unsafe { // At this point, we didn't change the menu at all; we should still see all missed tech flags. - assert_eq!(get_menu_from_url(MENU, url.as_str(), false).miss_tech_state, + assert_eq!(MENU.miss_tech_state, MissTechFlags::all()); } @@ -58,8 +58,8 @@ fn main() -> Result<(), Box> { #[cfg(not(feature = "has_terminal"))] { let (mut terminal, mut app) = test_backend_setup(menu)?; - let mut url = String::new(); - let frame_res = terminal.draw(|f| url = training_mod_tui::ui(f, &mut app))?; + let mut json_response = String::new(); + let frame_res = terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app))?; for (i, cell) in frame_res.buffer.content().iter().enumerate() { print!("{}", cell.symbol); @@ -69,7 +69,7 @@ fn main() -> Result<(), Box> { } println!(); - println!("URL: {}", url); + println!("json_response:\n{}", json_response); } #[cfg(feature = "has_terminal")] { @@ -111,9 +111,9 @@ fn run_app( tick_rate: Duration, ) -> io::Result { let mut last_tick = Instant::now(); - let mut url = String::new(); + let mut json_response = String::new(); loop { - terminal.draw(|f| url = training_mod_tui::ui(f, &mut app).clone())?; + terminal.draw(|f| json_response = training_mod_tui::ui(f, &mut app).clone())?; let timeout = tick_rate .checked_sub(last_tick.elapsed()) @@ -122,7 +122,7 @@ fn run_app( if crossterm::event::poll(timeout)? { if let Event::Key(key) = event::read()? { match key.code { - KeyCode::Char('q') => return Ok(url), + KeyCode::Char('q') => return Ok(json_response), KeyCode::Char('r') => app.on_r(), KeyCode::Char('l') => app.on_l(), KeyCode::Left => app.on_left(),