1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-24 10:54:16 +00:00

Refactor to use Web Sessions (#384)

* Initial commit

* don't rewrite file each time

* Format Rust code using rustfmt

* Receive menu settings from web via message

* Adjust logic

* Stub changes for json messages

* Rust: receive menu as json

* Small JS changes for desktop

* Attempt to fix exit issues

* Fix filename issue

* JS: Export menu as json

* Use JSON for Rust -> JS message

* Update .conf file schema to json

* Tear down session when leaving training mode

* Remove URL behavior, rename WebAppletResponse -> MenuJsonStruct

* Update TUI to use JSON

Co-authored-by: jugeeya <jugeeya@live.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
asimon-1 2022-09-07 22:34:39 -04:00 committed by GitHub
parent 55ee4733e2
commit 36c6e8859a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 484 additions and 470 deletions

View file

@ -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,11 +75,7 @@ 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);
pub unsafe fn set_menu_from_json(message: &str) {
if MENU.quick_menu == OnOff::Off {
if is_emulator() {
skyline::error::show_error(
@ -85,12 +86,38 @@ pub fn set_menu_from_url(last_url: &str) {
MENU.quick_menu = OnOff::On;
}
}
}
if let Ok(message_json) = serde_json::from_str::<MenuJsonStruct>(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::<TrainingModpackMenu>(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);
}
if MENU.quick_menu == OnOff::Off {
WEB_MENU_ACTIVE = true;
} else {
unsafe {
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<WebSession> = 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;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<T>(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<i32> {
#[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<usize> {
@ -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<i32> {
#[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<i32> {
#[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<i32> {
#[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<usize> {
@ -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<usize> {
@ -985,42 +971,6 @@ 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
}
}
}
}
url_params! {
#[repr(C)]
#[derive(Clone, Copy, Serialize, Deserialize, Debug, )]
pub struct TrainingModpackMenu {
@ -1063,7 +1013,7 @@ url_params! {
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<T>() -> usize { std::mem::size_of::<T>() * 8 }
const fn num_bits<T>() -> usize {
std::mem::size_of::<T>() * 8
}
fn log_2(x: u32) -> u32 {
if x == 0 { 0 }
else {
if x == 0 {
0
} else {
num_bits::<u32>() 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)]
@ -1150,7 +1111,7 @@ impl SubMenuType {
match s {
"toggle" => SubMenuType::TOGGLE,
"slider" => SubMenuType::SLIDER,
_ => panic!("Unexpected SubMenuType!")
_ => panic!("Unexpected SubMenuType!"),
}
}
}
@ -1224,18 +1185,12 @@ 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 {
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
}
);
checked: false,
});
}
pub fn new_with_toggles<T: ToggleTrait>(
submenu_title: &'a str,
@ -1249,16 +1204,13 @@ impl<'a> SubMenu<'a> {
help_text: help_text,
is_single_option: is_single_option,
toggles: Vec::new(),
_type: "toggle"
_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.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::<T>(
self.tab_submenus.push(SubMenu::new_with_toggles::<T>(
submenu_title,
submenu_id,
help_text,
is_single_option,
)
);
));
}
}
@ -1295,39 +1245,8 @@ pub struct UiMenu<'a> {
pub tabs: Vec<Tab<'a>>,
}
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::<Vec<&str>>();
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::<OnOff>(
"Crouch",
"crouch",
"Crouch: Should the CPU crouch when on the ground",
true
true,
);
overall_menu.tabs.push(defensive_tab);
@ -1569,36 +1487,37 @@ 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::<OnOff>(
"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 non_ui_menu = serde_json::to_string(&MENU).unwrap().replace("\"", "");
let toggle_values_all = non_ui_menu.split(',').collect::<Vec<&str>>();
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::<Vec<&str>>();
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::<Vec<&str>>();
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| {
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) {
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
}
})

View file

@ -341,8 +341,6 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, 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<B: Backend>(f: &mut Frame<B>, 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) {

View file

@ -38,12 +38,12 @@ fn ensure_menu_retains_multi_selections() -> Result<(), Box<dyn Error>> {
}
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<dyn Error>> {
#[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<dyn Error>> {
}
println!();
println!("URL: {}", url);
println!("json_response:\n{}", json_response);
}
#[cfg(feature = "has_terminal")] {
@ -111,9 +111,9 @@ fn run_app<B: tui::backend::Backend>(
tick_rate: Duration,
) -> io::Result<String> {
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<B: tui::backend::Backend>(
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(),