From cc78dc7e4a36e560abfefba118742e3f3a7b6c3c Mon Sep 17 00:00:00 2001 From: asimon-1 <40246417+asimon-1@users.noreply.github.com> Date: Fri, 18 Aug 2023 00:21:59 -0400 Subject: [PATCH] Automatically Keep the Modpack Up-to-Date (#595) * Grab releases from Github and install * Refactor * Fix crash * Work on fixing install() crash * Fix the crash by increasing stack size * Block main() execution on the auto-updater * Improve error handling * Delete hash.txt * Fix get_update_policy() * Use current time as default last_update_version; Compare publish date to last_update_version to determine if update should be applied. * Use skyline_web dialogs for user_wants_to_install. Use default selections on emulator instead. * Fix some logic * Convert CURRENT_VERSION to a Mutex; implement functions to update the config file * Adjust logging * Remove unneeded file * Allow unwrap after is_some() check * Fix format * Auto-updater (squashed) * Move update policy from config file to menu * Skip version check on emulator * Rustfmt, clippy --------- Co-authored-by: jugeeya --- .cargo/config.toml | 4 + Cargo.toml | 3 + src/common/dialog.rs | 62 + src/common/events.rs | 2 +- src/common/mod.rs | 1 + src/common/release.rs | 274 ++++- src/lib.rs | 17 +- training_mod_consts/Cargo.toml | 5 +- training_mod_consts/src/config.rs | 146 +++ training_mod_consts/src/files.rs | 1 + training_mod_consts/src/lib.rs | 1784 ++++++++++++++-------------- training_mod_consts/src/options.rs | 40 + 12 files changed, 1386 insertions(+), 953 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 src/common/dialog.rs create mode 100644 training_mod_consts/src/config.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..d2581c8 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[env] +AUTHOR = "jugeeya" +REPO_NAME = "UltimateTrainingModpack" +USER_AGENT = "UltimateTrainingModpack" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 717c59b..a78dc6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,9 @@ training_mod_tui = { path = "training_mod_tui" } native-tls = { version = "0.2.11", features = ["vendored"] } log = "0.4.17" byte-unit = "4.0.18" +zip = { version = "0.6", default-features = false, features = ["deflate"]} +anyhow = "1.0.72" + [patch.crates-io] native-tls = { git = "https://github.com/skyline-rs/rust-native-tls", branch = "switch-timeout-panic" } diff --git a/src/common/dialog.rs b/src/common/dialog.rs new file mode 100644 index 0000000..b672ecc --- /dev/null +++ b/src/common/dialog.rs @@ -0,0 +1,62 @@ +use crate::common::is_emulator; +use skyline_web::dialog::{Dialog, DialogOption}; +use skyline_web::dialog_ok::DialogOk; + +/// Returns true for yes, false for no +/// Returns false if you cancel with B +/// Returns `emulator_default` if you're on emulator +pub fn yes_no(prompt: String, emulator_default: bool) -> bool { + if is_emulator() { + return emulator_default; + } + Dialog::yes_no(prompt) +} + +/// Returns true for yes, false for no +/// Returns false if you cancel with B +/// Returns `emulator_default` if you're on emulator +pub fn no_yes(prompt: String, emulator_default: bool) -> bool { + if is_emulator() { + return emulator_default; + } + Dialog::no_yes(prompt) +} + +/// Returns true for ok, false for cancel +/// Returns false if you cancel with B +/// Returns `emulator_default` if you're on emulator +pub fn ok_cancel(prompt: String, emulator_default: bool) -> bool { + if is_emulator() { + return emulator_default; + } + Dialog::ok_cancel(prompt) +} + +/// Returns `left` for the left option, +/// Returns `right` for the right option +/// Returns `default` if you cancel with B +/// Returns `emulator_default` if you're on emulator +pub fn left_right( + prompt: String, + left: String, + right: String, + default: String, + emulator_default: String, +) -> String { + if is_emulator() { + return emulator_default; + } + match Dialog::new(prompt, left.clone(), right.clone()).show() { + DialogOption::Left => left, + DialogOption::Right => right, + DialogOption::Default => default, + } +} + +/// Always returns true after you accept the prompt +pub fn dialog_ok(prompt: String) -> bool { + if is_emulator() { + return true; + } + DialogOk::ok(prompt) +} diff --git a/src/common/events.rs b/src/common/events.rs index f2e6d0f..588009e 100644 --- a/src/common/events.rs +++ b/src/common/events.rs @@ -151,7 +151,7 @@ impl Event { device_id: DEVICE_ID.get().unwrap().to_string(), event_time, session_id: SESSION_ID.get().unwrap().to_string(), - mod_version: CURRENT_VERSION.to_string(), + mod_version: CURRENT_VERSION.lock().to_string(), smash_version: smash_version(), ..Default::default() } diff --git a/src/common/mod.rs b/src/common/mod.rs index e916512..d8be0d4 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -9,6 +9,7 @@ use crate::common::consts::*; pub mod button_config; pub mod consts; pub mod dev_config; +pub mod dialog; pub mod events; pub mod input; pub mod menu; diff --git a/src/common/release.rs b/src/common/release.rs index 2bcf0bb..c707405 100644 --- a/src/common/release.rs +++ b/src/common/release.rs @@ -1,74 +1,224 @@ -use std::fs; - -use skyline_web::dialog_ok::DialogOk; - -use crate::consts::{ - LEGACY_MENU_OPTIONS_PATH, MENU_DEFAULT_OPTIONS_PATH, MENU_OPTIONS_PATH, VERSION_TXT_PATH, -}; +#![allow(clippy::unnecessary_unwrap)] +use crate::consts::*; +use crate::dialog; use crate::logging::*; +use crate::MENU; +use anyhow::{anyhow, Result}; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use serde_json::Value; +use std::io::{Error, ErrorKind}; +use zip::ZipArchive; -pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -enum VersionCheck { - Current, - NoFile, - Update, +lazy_static! { + pub static ref CURRENT_VERSION: Mutex = + Mutex::new(get_current_version().expect("Could not determine current version!")); } -fn is_current_version(fpath: &str) -> VersionCheck { - // Create a blank version file if it doesn't exists - if fs::metadata(fpath).is_err() { - fs::File::create(fpath).expect("Could not create version file!"); - return VersionCheck::NoFile; - } - - if fs::read_to_string(fpath).unwrap_or_else(|_| "".to_string()) == CURRENT_VERSION { - VersionCheck::Current - } else { - VersionCheck::Update - } +#[derive(Debug)] +pub struct Release { + pub url: String, + pub tag: String, + pub published_at: String, } -fn record_current_version(fpath: &str) { - // Write the current version to the version file - fs::write(fpath, CURRENT_VERSION).expect("Could not record current version!") -} - -pub fn version_check() { - match is_current_version(VERSION_TXT_PATH) { - VersionCheck::Current => { - // Version is current, no need to take any action +impl Release { + /// Downloads and installs the release + pub fn install(self: &Release) -> Result<()> { + info!("Installing asset from URL: {}", &self.url); + let response = minreq::get(&self.url) + .with_header("User-Agent", "UltimateTrainingModpack") + .with_header("Accept", "application/octet-stream") + .send_lazy()?; + info!( + "Ok response from Github. Status Code: {}", + &response.status_code + ); + let mut vec = Vec::new(); + for result in response { + let (byte, length) = result?; + vec.reserve(length); + vec.push(byte); } - VersionCheck::Update => { - // Display dialog box on launch if changing versions - DialogOk::ok( - format!( - "Thank you for installing version {CURRENT_VERSION} of the Training Modpack.\n\n\ - Due to a breaking change in this version, your menu selections and defaults must be reset once.\n\n\ - Please refer to the Github page and the Discord server for a full list of recent features, bugfixes, and other changes." - ) - ); - // Remove old menu selections, silently ignoring errors (i.e. if the file doesn't exist) - [ - MENU_OPTIONS_PATH, - MENU_DEFAULT_OPTIONS_PATH, - LEGACY_MENU_OPTIONS_PATH, - ] - .iter() - .for_each(|path| { - fs::remove_file(path).unwrap_or_else(|_| error!("Couldn't remove {path}")) + info!("Finished receiving .zip file from GitHub."); + info!("Unpacking .zip file..."); + let mut zip = ZipArchive::new(std::io::Cursor::new(vec))?; + zip.extract(UNPACK_PATH)?; + info!("Finished unpacking update"); + + info!("Updating config file with last update time..."); + TrainingModpackConfig::change_last_update_version(&self.published_at)?; + dialog::dialog_ok( + "The Training Modpack has been updated.\n\n\ + Your game will now restart." + .to_string(), + ); + info!("Finished. Restarting..."); + unsafe { + skyline::nn::oe::RequestToRelaunchApplication(); + } + // Don't need a return type here because this area is unreachable + } + + pub fn to_string(self: &Release) -> String { + format!("{} - {}", self.tag, self.published_at) + } + + pub fn is_older_than_installed(self: &Release) -> bool { + // String comparison is good enough because for RFC3339 format, + // alphabetical order == chronological order + // + // https://datatracker.ietf.org/doc/html/rfc3339#section-5.1 + let current_version = CURRENT_VERSION.lock(); + self.published_at.as_str() < current_version.as_str() + } +} + +fn get_update_policy() -> UpdatePolicy { + unsafe { MENU.update_policy } +} + +fn get_release(beta: bool) -> Result { + // Get the list of releases from Github + let url = format!( + "https://api.github.com/repos/{}/{}/releases", + env!("AUTHOR"), + env!("REPO_NAME") + ); + let response = minreq::get(url) + .with_header("User-Agent", env!("USER_AGENT")) + .with_header("Accept", "application/json") + .send()?; + + let json: Vec = serde_json::from_str(response.as_str()?)?; + + // Parse the list to determine the latest stable and beta release + let mut stable_release: Option = None; + let mut beta_release: Option = None; + for release in json.into_iter() { + // The list is ordered by date w/ most recent releases first + // so we only need to get the first of each type + let is_prerelease = release["prerelease"] + .as_bool() + .ok_or_else(|| anyhow!("prerelease is not a bool"))?; + if is_prerelease && beta_release.is_none() { + // Assumes that the first asset exists and is the right one + let url = release["assets"][0]["url"] + .as_str() + .ok_or_else(|| anyhow!("Could not parse beta asset url"))?; + let tag = release["tag_name"] + .as_str() + .ok_or_else(|| anyhow!("Could not parse beta asset tag_name"))?; + let published_at = release["published_at"] + .as_str() + .ok_or_else(|| anyhow!("Could not parse beta asset published_at"))?; + beta_release = Some(Release { + url: url.to_string(), + tag: tag.to_string(), + published_at: published_at.to_string(), + }); + } else if !is_prerelease && stable_release.is_none() { + // Assumes that the first asset exists and is the right one + let url = release["assets"][0]["url"] + .as_str() + .ok_or_else(|| anyhow!("Could not parse stable asset url"))?; + let tag = release["tag_name"] + .as_str() + .ok_or_else(|| anyhow!("Could not parse stable asset tag_name"))?; + let published_at = release["published_at"] + .as_str() + .ok_or_else(|| anyhow!("Could not parse stable asset published_at"))?; + stable_release = Some(Release { + url: url.to_string(), + tag: tag.to_string(), + published_at: published_at.to_string(), }); - record_current_version(VERSION_TXT_PATH); } - VersionCheck::NoFile => { - // Display dialog box on fresh installation - DialogOk::ok( - format!( - "Thank you for installing version {CURRENT_VERSION} of the Training Modpack.\n\n\ - Please refer to the Github page and the Discord server for a full list of features and instructions on how to utilize the improved Training Mode." - ) - ); - record_current_version(VERSION_TXT_PATH); + if beta_release.is_some() && stable_release.is_some() { + // Don't iterate needlessly, we already found both releases + break; + } + } + if beta && beta_release.is_some() { + Ok(beta_release.unwrap()) + } else if !beta && stable_release.is_some() { + Ok(stable_release.unwrap()) + } else { + Err(anyhow!( + "The specified release was not found in the GitHub JSON response!" + )) + } +} + +fn user_wants_to_install() -> bool { + dialog::yes_no( + "There is a new update available for the Training Modpack. \n\n\ + Do you want to install it?" + .to_string(), + true, + ) +} + +fn get_current_version() -> Result { + let config = TrainingModpackConfig::load(); + match config { + Ok(c) => { + info!("Config file found and parsed. Loading..."); + Ok(c.update.last_update_version) + } + Err(e) + if e.is::() + && e.downcast_ref::().unwrap().kind() == ErrorKind::NotFound => + { + warn!("No config file found, creating default..."); + TrainingModpackConfig::create_default()?; + get_current_version() + } + Err(e) => { + // Some other error, re-raise it + Err(e) + } + } +} + +pub fn perform_version_check() { + let update_policy = get_update_policy(); + info!("Update Policy is {}", update_policy); + let mut release_to_apply = match update_policy { + UpdatePolicy::Stable => get_release(false), + UpdatePolicy::Beta => get_release(true), + UpdatePolicy::Disabled => { + // User does not want to update at all + Err(anyhow!("Updates are disabled per UpdatePolicy")) + } + }; + if release_to_apply.is_ok() { + let published_at = release_to_apply.as_ref().unwrap().published_at.clone(); + let current_version = CURRENT_VERSION.lock(); + info!("Current version: {}", current_version); + info!("Github version: {}", published_at); + drop(current_version); // Explicitly unlock, since we also acquire a lock in is_older_than_installed() + if release_to_apply.as_ref().unwrap().is_older_than_installed() { + release_to_apply = Err(anyhow!( + "Update is older than the current installed version.", + )) + } + } + + // Perform Update + match release_to_apply { + Ok(release) => { + if user_wants_to_install() { + info!("Installing update: {}", &release.to_string()); + if let Err(e) = release.install() { + error!("Failed to install the update. Reason: {:?}", e); + } + } else { + info!("User declined the update."); + } + } + Err(e) => { + warn!("Did not install update. Reason: {:?}", e); } } } diff --git a/src/lib.rs b/src/lib.rs index f6f77fb..218034d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,7 @@ pub fn main() { init_logger().unwrap(); info!("Initialized."); + unsafe { EVENT_QUEUE.push(Event::smash_open()); notification("Training Modpack".to_string(), "Welcome!".to_string(), 60); @@ -99,11 +100,21 @@ pub fn main() { }); } - info!("Performing version check..."); - release::version_check(); - menu::load_from_file(); + if !is_emulator() { + info!("Performing version check..."); + let _updater = std::thread::Builder::new() + .stack_size(0x20000) + .spawn(move || { + release::perform_version_check(); + }) + .unwrap(); + let _result = _updater.join(); + } else { + info!("Skipping version check because we are using an emulator"); + } + unsafe { notification("Training Modpack".to_string(), "Welcome!".to_string(), 60); notification("Open Menu".to_string(), MENU.menu_open.to_string(), 120); diff --git a/training_mod_consts/Cargo.toml b/training_mod_consts/Cargo.toml index 821b71e..123ff14 100644 --- a/training_mod_consts/Cargo.toml +++ b/training_mod_consts/Cargo.toml @@ -15,8 +15,11 @@ serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1.8" serde_json = "1" bitflags_serde_shim = "0.2" +skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" } skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git", optional = true } +toml = "0.5.9" +anyhow = "1.0.72" [features] default = ["smash"] -smash = ["skyline_smash"] \ No newline at end of file +smash = ["skyline_smash"] diff --git a/training_mod_consts/src/config.rs b/training_mod_consts/src/config.rs new file mode 100644 index 0000000..251ec0f --- /dev/null +++ b/training_mod_consts/src/config.rs @@ -0,0 +1,146 @@ +use crate::files::*; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +use skyline::nn::time; + +use std::fs; +use std::io::{Error, ErrorKind}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Top level struct which represents the entirety of the modpack config +/// (Does not include in-game menu settings) +/// Each field here is a section of training_modpack.toml +#[derive(Serialize, Deserialize)] +pub struct TrainingModpackConfig { + pub update: UpdaterConfig, +} + +impl TrainingModpackConfig { + pub fn new() -> TrainingModpackConfig { + TrainingModpackConfig { + update: UpdaterConfig::default(), + } + } + + /// Attempts to load the config from file + pub fn load() -> Result { + if fs::metadata(TRAINING_MODPACK_TOML_PATH).is_ok() { + let toml_config_str = fs::read_to_string(TRAINING_MODPACK_TOML_PATH)?; + let parsed = toml::from_str::(&toml_config_str)?; + Ok(parsed) + } else { + Err(Error::from(ErrorKind::NotFound).into()) + } + } + + pub fn load_or_create() -> Result { + match TrainingModpackConfig::load() { + Ok(c) => Ok(c), + Err(e) + if e.is::() + && e.downcast_ref::().unwrap().kind() == ErrorKind::NotFound => + { + TrainingModpackConfig::create_default()?; + TrainingModpackConfig::load() + } + Err(e) => { + // Some other error, re-raise it + Err(e) + } + } + } + + /// Creates a default config and saves to file + /// Returns Err if the file already exists + pub fn create_default() -> Result<()> { + if fs::metadata(TRAINING_MODPACK_TOML_PATH).is_ok() { + Err(Error::from(ErrorKind::AlreadyExists).into()) + } else { + let default_config: TrainingModpackConfig = TrainingModpackConfig::new(); + let contents = toml::to_string(&default_config)?; + fs::write(TRAINING_MODPACK_TOML_PATH, contents)?; + Ok(()) + } + } + + pub fn change_last_update_version(last_update_version: &str) -> Result<()> { + let mut config = TrainingModpackConfig::load()?; + config.update.last_update_version = last_update_version.to_string(); + let contents = toml::to_string(&config)?; + fs::write(TRAINING_MODPACK_TOML_PATH, contents)?; + Ok(()) + } +} + +/// Since we can't rely on most time based libraries, this is a seconds -> date/time string based on the `chrono` crates implementation +/// God bless blujay and Raytwo +/// https://github.com/Raytwo/ARCropolis/blob/9dc1d59d1e8a3dcac433b10a90bb5b3fabad6c00/src/logging.rs#L15-L49 +fn format_time_string(seconds: u64) -> String { + let leapyear = |year| -> bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) }; + + static YEAR_TABLE: [[u64; 12]; 2] = [ + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], + [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], + ]; + + let mut year = 1970; + + let seconds_in_day = seconds % 86400; + let mut day_number = seconds / 86400; + + let sec = seconds_in_day % 60; + let min = (seconds_in_day % 3600) / 60; + let hours = seconds_in_day / 3600; + loop { + let year_length = if leapyear(year) { 366 } else { 365 }; + + if day_number >= year_length { + day_number -= year_length; + year += 1; + } else { + break; + } + } + let mut month = 0; + while day_number >= YEAR_TABLE[if leapyear(year) { 1 } else { 0 }][month] { + day_number -= YEAR_TABLE[if leapyear(year) { 1 } else { 0 }][month]; + month += 1; + } + + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + year, + month + 1, + day_number + 1, + hours, + min, + sec + ) +} + +fn now_utc() -> String { + unsafe { + time::Initialize(); + } + let current_epoch_seconds = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + format_time_string(current_epoch_seconds) +} + +/// Config section for the automatic updater +#[derive(Serialize, Deserialize, Clone)] +pub struct UpdaterConfig { + pub last_update_version: String, +} + +impl UpdaterConfig { + pub fn default() -> UpdaterConfig { + UpdaterConfig { + last_update_version: now_utc(), + } + } +} diff --git a/training_mod_consts/src/files.rs b/training_mod_consts/src/files.rs index 7bcbc99..0d5acaa 100644 --- a/training_mod_consts/src/files.rs +++ b/training_mod_consts/src/files.rs @@ -12,3 +12,4 @@ pub const LEGACY_MENU_OPTIONS_PATH: &str = "sd:/ultimate/TrainingModpack/training_modpack_menu.json"; pub const MENU_DEFAULT_OPTIONS_PATH: &str = "sd:/ultimate/TrainingModpack/training_modpack_menu_defaults.conf"; +pub const UNPACK_PATH: &str = "sd:/"; diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index 2fa9718..e368922 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -1,886 +1,898 @@ -#![feature(iter_intersperse)] -#[macro_use] -extern crate bitflags; - -#[macro_use] -extern crate bitflags_serde_shim; - -#[macro_use] -extern crate num_derive; - -use serde::{Deserialize, Serialize}; - -pub mod options; -pub use options::*; -pub mod files; -pub use files::*; - -#[repr(C)] -#[derive(Clone, Copy, Serialize, Deserialize, Debug)] -pub struct TrainingModpackMenu { - pub aerial_delay: Delay, - pub air_dodge_dir: Direction, - pub attack_angle: AttackAngle, - pub buff_state: BuffOption, - pub character_item: CharacterItem, - pub clatter_strength: ClatterFrequency, - pub crouch: OnOff, - pub di_state: Direction, - pub falling_aerials: BoolFlag, - pub fast_fall_delay: Delay, - pub fast_fall: BoolFlag, - pub follow_up: Action, - pub frame_advantage: OnOff, - pub full_hop: BoolFlag, - pub hitbox_vis: OnOff, - pub hud: OnOff, - pub input_delay: Delay, - pub ledge_delay: LongDelay, - pub ledge_state: LedgeOption, - pub mash_state: Action, - pub mash_triggers: MashTrigger, - pub miss_tech_state: MissTechFlags, - pub oos_offset: Delay, - pub pummel_delay: MedDelay, - pub reaction_time: Delay, - pub save_damage_cpu: SaveDamage, - pub save_damage_limits_cpu: DamagePercent, - pub save_damage_player: SaveDamage, - pub save_damage_limits_player: DamagePercent, - pub save_state_autoload: OnOff, - pub save_state_enable: OnOff, - pub save_state_slot: SaveStateSlot, - pub randomize_slots: OnOff, - pub save_state_mirroring: SaveStateMirroring, - pub save_state_playback: PlaybackSlot, - pub sdi_state: Direction, - pub sdi_strength: SdiFrequency, - pub shield_state: Shield, - pub shield_tilt: Direction, - pub stage_hazards: OnOff, - pub tech_state: TechFlags, - pub throw_delay: MedDelay, - pub throw_state: ThrowOption, - pub ledge_neutral_override: Action, - pub ledge_roll_override: Action, - pub ledge_jump_override: Action, - pub ledge_attack_override: Action, - pub tech_action_override: Action, - pub clatter_override: Action, - pub tumble_override: Action, - pub hitstun_override: Action, - pub parry_override: Action, - pub shieldstun_override: Action, - pub footstool_override: Action, - pub landing_override: Action, - pub trump_override: Action, - pub recording_slot: RecordSlot, - pub record_trigger: RecordTrigger, - pub recording_frames: RecordingFrames, - pub playback_button_combination: PlaybackSlot, - pub hitstun_playback: HitstunPlayback, - pub playback_mash: OnOff, - pub playback_loop: OnOff, - pub menu_open: ButtonConfig, - pub save_state_save: ButtonConfig, - pub save_state_load: ButtonConfig, - pub input_record: ButtonConfig, - pub input_playback: ButtonConfig, - pub recording_crop: OnOff, -} - -#[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)] -pub enum FighterId { - Player = 0, - CPU = 1, -} - -#[derive(Clone)] -pub enum SubMenuType { - TOGGLE, - SLIDER, -} - -impl SubMenuType { - pub fn from_string(s: &String) -> SubMenuType { - match s.as_str() { - "toggle" => SubMenuType::TOGGLE, - "slider" => SubMenuType::SLIDER, - _ => panic!("Unexpected SubMenuType!"), - } - } -} - -pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu { - aerial_delay: Delay::empty(), - air_dodge_dir: Direction::empty(), - attack_angle: AttackAngle::empty(), - buff_state: BuffOption::empty(), - character_item: CharacterItem::None, - clatter_strength: ClatterFrequency::None, - crouch: OnOff::Off, - di_state: Direction::empty(), - falling_aerials: BoolFlag::FALSE, - fast_fall_delay: Delay::empty(), - fast_fall: BoolFlag::FALSE, - follow_up: Action::empty(), - frame_advantage: OnOff::Off, - full_hop: BoolFlag::TRUE, - hitbox_vis: OnOff::On, - hud: OnOff::On, - input_delay: Delay::D0, - ledge_delay: LongDelay::empty(), - ledge_state: LedgeOption::default(), - mash_state: Action::empty(), - mash_triggers: MashTrigger::default(), - miss_tech_state: MissTechFlags::all(), - oos_offset: Delay::empty(), - pummel_delay: MedDelay::empty(), - reaction_time: Delay::empty(), - save_damage_cpu: SaveDamage::DEFAULT, - save_damage_limits_cpu: DamagePercent::default(), - save_damage_player: SaveDamage::DEFAULT, - save_damage_limits_player: DamagePercent::default(), - save_state_autoload: OnOff::Off, - save_state_enable: OnOff::On, - save_state_slot: SaveStateSlot::One, - randomize_slots: OnOff::Off, - save_state_mirroring: SaveStateMirroring::None, - save_state_playback: PlaybackSlot::empty(), - sdi_state: Direction::empty(), - sdi_strength: SdiFrequency::None, - shield_state: Shield::None, - shield_tilt: Direction::empty(), - stage_hazards: OnOff::Off, - tech_state: TechFlags::all(), - throw_delay: MedDelay::empty(), - throw_state: ThrowOption::NONE, - ledge_neutral_override: Action::empty(), - ledge_roll_override: Action::empty(), - ledge_jump_override: Action::empty(), - ledge_attack_override: Action::empty(), - tech_action_override: Action::empty(), - clatter_override: Action::empty(), - tumble_override: Action::empty(), - hitstun_override: Action::empty(), - parry_override: Action::empty(), - shieldstun_override: Action::empty(), - footstool_override: Action::empty(), - landing_override: Action::empty(), - trump_override: Action::empty(), - recording_slot: RecordSlot::S1, - recording_frames: RecordingFrames::F150, - record_trigger: RecordTrigger::COMMAND, - playback_button_combination: PlaybackSlot::S1, - hitstun_playback: HitstunPlayback::Hitstun, - playback_mash: OnOff::On, - playback_loop: OnOff::Off, - menu_open: ButtonConfig::B.union(ButtonConfig::DPAD_UP), - save_state_save: ButtonConfig::ZL.union(ButtonConfig::DPAD_DOWN), - save_state_load: ButtonConfig::ZL.union(ButtonConfig::DPAD_UP), - input_record: ButtonConfig::ZR.union(ButtonConfig::DPAD_DOWN), - input_playback: ButtonConfig::ZR.union(ButtonConfig::DPAD_UP), - recording_crop: OnOff::On, -}; - -pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU; - -#[derive(Clone, Serialize)] -pub struct Slider { - pub selected_min: u32, - pub selected_max: u32, - pub abs_min: u32, - pub abs_max: u32, -} - -#[derive(Clone, Serialize)] -pub struct Toggle { - pub toggle_value: u32, - pub toggle_title: String, - pub checked: bool, -} - -#[derive(Clone, Serialize)] -pub struct SubMenu { - pub submenu_title: String, - pub submenu_id: String, - pub help_text: String, - pub is_single_option: bool, - pub toggles: Vec, - pub slider: Option, - pub _type: String, -} - -impl SubMenu { - pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: String, checked: bool) { - self.toggles.push(Toggle { - toggle_value, - toggle_title, - checked, - }); - } - pub fn new_with_toggles( - submenu_title: String, - submenu_id: String, - help_text: String, - is_single_option: bool, - initial_value: &u32, - ) -> SubMenu { - 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(), - slider: None, - _type: "toggle".to_string(), - }; - - let values = T::to_toggle_vals(); - let titles = T::to_toggle_strings(); - for i in 0..values.len() { - let checked: bool = - (values[i] & initial_value) > 0 || (!values[i] == 0 && initial_value == &0); - instance.add_toggle(values[i], titles[i].clone(), checked); - } - // Select the first option if there's nothing selected atm but it's a single option submenu - if is_single_option && instance.toggles.iter().all(|t| !t.checked) { - instance.toggles[0].checked = true; - } - instance - } - pub fn new_with_slider( - submenu_title: String, - submenu_id: String, - help_text: String, - initial_lower_value: &u32, - initial_upper_value: &u32, - ) -> SubMenu { - let min_max = S::get_limits(); - SubMenu { - submenu_title: submenu_title, - submenu_id: submenu_id, - help_text: help_text, - is_single_option: false, - toggles: Vec::new(), - slider: Some(Slider { - selected_min: *initial_lower_value, - selected_max: *initial_upper_value, - abs_min: min_max.0, - abs_max: min_max.1, - }), - _type: "slider".to_string(), - } - } -} - -#[derive(Serialize, Clone)] -pub struct Tab { - pub tab_id: String, - pub tab_title: String, - pub tab_submenus: Vec, -} - -impl Tab { - pub fn add_submenu_with_toggles( - &mut self, - submenu_title: String, - submenu_id: String, - help_text: String, - is_single_option: bool, - initial_value: &u32, - ) { - self.tab_submenus.push(SubMenu::new_with_toggles::( - submenu_title.to_string(), - submenu_id.to_string(), - help_text.to_string(), - is_single_option, - initial_value, - )); - } - - pub fn add_submenu_with_slider( - &mut self, - submenu_title: String, - submenu_id: String, - help_text: String, - initial_lower_value: &u32, - initial_upper_value: &u32, - ) { - self.tab_submenus.push(SubMenu::new_with_slider::( - submenu_title.to_string(), - submenu_id.to_string(), - help_text.to_string(), - initial_lower_value, - initial_upper_value, - )) - } -} - -#[derive(Serialize, Clone)] -pub struct UiMenu { - pub tabs: Vec, -} - -pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu { - let mut overall_menu = UiMenu { tabs: Vec::new() }; - - let mut mash_tab = Tab { - tab_id: "mash".to_string(), - tab_title: "Mash Settings".to_string(), - tab_submenus: Vec::new(), - }; - mash_tab.add_submenu_with_toggles::( - "Mash Toggles".to_string(), - "mash_state".to_string(), - "Mash Toggles: Actions to be performed as soon as possible".to_string(), - false, - &(menu.mash_state.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Followup Toggles".to_string(), - "follow_up".to_string(), - "Followup Toggles: Actions to be performed after a Mash option".to_string(), - false, - &(menu.follow_up.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Mash Triggers".to_string(), - "mash_triggers".to_string(), - "Mash triggers: Configure what causes the CPU to perform a Mash option".to_string(), - false, - &(menu.mash_triggers.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Attack Angle".to_string(), - "attack_angle".to_string(), - "Attack Angle: For attacks that can be angled, such as some forward tilts".to_string(), - false, - &(menu.attack_angle.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Throw Options".to_string(), - "throw_state".to_string(), - "Throw Options: Throw to be performed when a grab is landed".to_string(), - false, - &(menu.throw_state.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Throw Delay".to_string(), - "throw_delay".to_string(), - "Throw Delay: How many frames to delay the throw option".to_string(), - false, - &(menu.throw_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Pummel Delay".to_string(), - "pummel_delay".to_string(), - "Pummel Delay: How many frames after a grab to wait before starting to pummel".to_string(), - false, - &(menu.pummel_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Falling Aerials".to_string(), - "falling_aerials".to_string(), - "Falling Aerials: Should aerials be performed when rising or when falling".to_string(), - false, - &(menu.falling_aerials.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Full Hop".to_string(), - "full_hop".to_string(), - "Full Hop: Should the CPU perform a full hop or a short hop".to_string(), - false, - &(menu.full_hop.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Aerial Delay".to_string(), - "aerial_delay".to_string(), - "Aerial Delay: How long to delay a Mash aerial attack".to_string(), - false, - &(menu.aerial_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Fast Fall".to_string(), - "fast_fall".to_string(), - "Fast Fall: Should the CPU fastfall during a jump".to_string(), - false, - &(menu.fast_fall.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Fast Fall Delay".to_string(), - "fast_fall_delay".to_string(), - "Fast Fall Delay: How many frames the CPU should delay their fastfall".to_string(), - false, - &(menu.fast_fall_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "OoS Offset".to_string(), - "oos_offset".to_string(), - "OoS Offset: How many times the CPU shield can be hit before performing a Mash option" - .to_string(), - false, - &(menu.oos_offset.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Reaction Time".to_string(), - "reaction_time".to_string(), - "Reaction Time: How many frames to delay before performing a mash option".to_string(), - false, - &(menu.reaction_time.bits()), - ); - overall_menu.tabs.push(mash_tab); - - let mut override_tab = Tab { - tab_id: "override".to_string(), - tab_title: "Override Settings".to_string(), - tab_submenus: Vec::new(), - }; - override_tab.add_submenu_with_toggles::( - "Ledge Neutral Getup".to_string(), - "ledge_neutral_override".to_string(), - "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge" - .to_string(), - false, - &(menu.ledge_neutral_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Roll".to_string(), - "ledge_roll_override".to_string(), - "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge" - .to_string(), - false, - &(menu.ledge_roll_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Jump".to_string(), - "ledge_jump_override".to_string(), - "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge" - .to_string(), - false, - &(menu.ledge_jump_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Attack".to_string(), - "ledge_attack_override".to_string(), - "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge" - .to_string(), - false, - &(menu.ledge_attack_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Tech Action".to_string(), - "tech_action_override".to_string(), - "Tech Action Override: Mash Actions to be performed after any tech action".to_string(), - false, - &(menu.tech_action_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Clatter".to_string(), - "clatter_override".to_string(), - "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab.to_string(), bury, etc)".to_string(), - false, - &(menu.clatter_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Tumble".to_string(), - "tumble_override".to_string(), - "Tumble Override: Mash Actions to be performed after exiting a tumble state".to_string(), - false, - &(menu.tumble_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Hitstun".to_string(), - "hitstun_override".to_string(), - "Hitstun Override: Mash Actions to be performed after exiting a hitstun state".to_string(), - false, - &(menu.hitstun_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Parry".to_string(), - "parry_override".to_string(), - "Parry Override: Mash Actions to be performed after a parry".to_string(), - false, - &(menu.parry_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Shieldstun".to_string(), - "shieldstun_override".to_string(), - "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state" - .to_string(), - false, - &(menu.shieldstun_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Footstool".to_string(), - "footstool_override".to_string(), - "Footstool Override: Mash Actions to be performed after exiting a footstool state" - .to_string(), - false, - &(menu.footstool_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Landing".to_string(), - "landing_override".to_string(), - "Landing Override: Mash Actions to be performed after landing on the ground".to_string(), - false, - &(menu.landing_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Trump".to_string(), - "trump_override".to_string(), - "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state" - .to_string(), - false, - &(menu.trump_override.bits()), - ); - overall_menu.tabs.push(override_tab); - - let mut defensive_tab = Tab { - tab_id: "defensive".to_string(), - tab_title: "Defensive Settings".to_string(), - tab_submenus: Vec::new(), - }; - defensive_tab.add_submenu_with_toggles::( - "Airdodge Direction".to_string(), - "air_dodge_dir".to_string(), - "Airdodge Direction: Direction to angle airdodges".to_string(), - false, - &(menu.air_dodge_dir.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "DI Direction".to_string(), - "di_state".to_string(), - "DI Direction: Direction to angle the directional influence during hitlag".to_string(), - false, - &(menu.di_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "SDI Direction".to_string(), - "sdi_state".to_string(), - "SDI Direction: Direction to angle the smash directional influence during hitlag" - .to_string(), - false, - &(menu.sdi_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "SDI Strength".to_string(), - "sdi_strength".to_string(), - "SDI Strength: Relative strength of the smash directional influence inputs".to_string(), - true, - &(menu.sdi_strength as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Clatter Strength".to_string(), - "clatter_strength".to_string(), - "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc." - .to_string(), - true, - &(menu.clatter_strength as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Ledge Options".to_string(), - "ledge_state".to_string(), - "Ledge Options: Actions to be taken when on the ledge".to_string(), - false, - &(menu.ledge_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Ledge Delay".to_string(), - "ledge_delay".to_string(), - "Ledge Delay: How many frames to delay the ledge option".to_string(), - false, - &(menu.ledge_delay.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Tech Options".to_string(), - "tech_state".to_string(), - "Tech Options: Actions to take when slammed into a hard surface".to_string(), - false, - &(menu.tech_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Mistech Options".to_string(), - "miss_tech_state".to_string(), - "Mistech Options: Actions to take after missing a tech".to_string(), - false, - &(menu.miss_tech_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Shield Toggles".to_string(), - "shield_state".to_string(), - "Shield Toggles: CPU Shield Behavior".to_string(), - true, - &(menu.shield_state as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Shield Tilt".to_string(), - "shield_tilt".to_string(), - "Shield Tilt: Direction to tilt the shield".to_string(), - false, // TODO: Should this be true? - &(menu.shield_tilt.bits()), - ); - - defensive_tab.add_submenu_with_toggles::( - "Crouch".to_string(), - "crouch".to_string(), - "Crouch: Have the CPU crouch when on the ground".to_string(), - true, - &(menu.crouch as u32), - ); - overall_menu.tabs.push(defensive_tab); - - let mut save_state_tab = Tab { - tab_id: "save_state".to_string(), - tab_title: "Save States".to_string(), - tab_submenus: Vec::new(), - }; - save_state_tab.add_submenu_with_toggles::( - "Mirroring".to_string(), - "save_state_mirroring".to_string(), - "Mirroring: Flips save states in the left-right direction across the stage center" - .to_string(), - true, - &(menu.save_state_mirroring as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Auto Save States".to_string(), - "save_state_autoload".to_string(), - "Auto Save States: Load save state when any fighter dies".to_string(), - true, - &(menu.save_state_autoload as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Save Dmg (CPU)".to_string(), - "save_damage_cpu".to_string(), - "Save Damage: Should save states retain CPU damage".to_string(), - true, - &(menu.save_damage_cpu.bits()), - ); - save_state_tab.add_submenu_with_slider::( - "Dmg Range (CPU)".to_string(), - "save_damage_limits_cpu".to_string(), - "Limits on random damage to apply to the CPU when loading a save state".to_string(), - &(menu.save_damage_limits_cpu.0 as u32), - &(menu.save_damage_limits_cpu.1 as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Save Dmg (Player)".to_string(), - "save_damage_player".to_string(), - "Save Damage: Should save states retain player damage".to_string(), - true, - &(menu.save_damage_player.bits() as u32), - ); - save_state_tab.add_submenu_with_slider::( - "Dmg Range (Player)".to_string(), - "save_damage_limits_player".to_string(), - "Limits on random damage to apply to the player when loading a save state".to_string(), - &(menu.save_damage_limits_player.0 as u32), - &(menu.save_damage_limits_player.1 as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Enable Save States".to_string(), - "save_state_enable".to_string(), - "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.".to_string(), - true, - &(menu.save_state_enable as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Save State Slot".to_string(), - "save_state_slot".to_string(), - "Save State Slot: Save and load states from different slots.".to_string(), - true, - &(menu.save_state_slot as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Randomize Slots".to_string(), - "randomize_slots".to_string(), - "Randomize Slots: Randomize slot when loading save state.".to_string(), - true, - &(menu.randomize_slots as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Character Item".to_string(), - "character_item".to_string(), - "Character Item: The item to give to the player's fighter when loading a save state" - .to_string(), - true, - &(menu.character_item as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Buff Options".to_string(), - "buff_state".to_string(), - "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state" - .to_string(), - false, - &(menu.buff_state.bits()), - ); - save_state_tab.add_submenu_with_toggles::( - "Save State Playback".to_string(), - "save_state_playback".to_string(), - "Save State Playback: Choose which slots to playback input recording upon loading a save state".to_string(), - false, - &(menu.save_state_playback.bits() as u32), - ); - overall_menu.tabs.push(save_state_tab); - - let mut misc_tab = Tab { - tab_id: "misc".to_string(), - tab_title: "Misc Settings".to_string(), - tab_submenus: Vec::new(), - }; - misc_tab.add_submenu_with_toggles::( - "Frame Advantage".to_string(), - "frame_advantage".to_string(), - "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable".to_string(), - true, - &(menu.frame_advantage as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Hitbox Visualization".to_string(), - "hitbox_vis".to_string(), - "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)".to_string(), - true, - &(menu.hitbox_vis as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Input Delay".to_string(), - "input_delay".to_string(), - "Input Delay: Frames to delay player inputs by".to_string(), - true, - &(menu.input_delay.bits()), - ); - misc_tab.add_submenu_with_toggles::( - "Stage Hazards".to_string(), - "stage_hazards".to_string(), - "Stage Hazards: Turn stage hazards on/off".to_string(), - true, - &(menu.stage_hazards as u32), - ); - misc_tab.add_submenu_with_toggles::( - "HUD".to_string(), - "hud".to_string(), - "HUD: Show/hide elements of the UI".to_string(), - true, - &(menu.hud as u32), - ); - overall_menu.tabs.push(misc_tab); - - let mut input_tab = Tab { - tab_id: "input".to_string(), - tab_title: "Input Recording".to_string(), - tab_submenus: Vec::new(), - }; - input_tab.add_submenu_with_toggles::( - "Recording Slot".to_string(), - "recording_slot".to_string(), - "Recording Slot: Choose which slot to record into".to_string(), - true, - &(menu.recording_slot as u32), - ); - input_tab.add_submenu_with_toggles::( - "Recording Trigger".to_string(), - "record_trigger".to_string(), - format!("Recording Trigger: Whether to begin recording via button combination ({}) or upon loading a Save State", menu.input_record.combination_string()), - false, - &(menu.record_trigger.bits() as u32), - ); - input_tab.add_submenu_with_toggles::( - "Recording Frames".to_string(), - "recording_frames".to_string(), - "Recording Frames: Number of frames to record for in the current slot".to_string(), - true, - &(menu.recording_frames as u32), - ); - input_tab.add_submenu_with_toggles::( - "Playback Button Combination".to_string(), - "playback_button_combination".to_string(), - format!("Playback Button Combination: Choose which slots to playback input recording upon pressing button combination ({})", menu.input_playback.combination_string()), - false, - &(menu.playback_button_combination.bits() as u32), - ); - input_tab.add_submenu_with_toggles::( - "Playback Hitstun Timing".to_string(), - "hitstun_playback".to_string(), - "Playback Hitstun Timing: When to begin playing back inputs when a hitstun mash trigger occurs".to_string(), - true, - &(menu.hitstun_playback as u32), - ); - input_tab.add_submenu_with_toggles::( - "Playback Mash Interrupt".to_string(), - "playback_mash".to_string(), - "Playback Mash Interrupt: End input playback when a mash trigger occurs".to_string(), - true, - &(menu.playback_mash as u32), - ); - input_tab.add_submenu_with_toggles::( - "Playback Loop".to_string(), - "playback_loop".to_string(), - "Playback Loop: Repeat triggered input playbacks indefinitely".to_string(), - true, - &(menu.playback_loop as u32), - ); - input_tab.add_submenu_with_toggles::( - "Recording Crop".to_string(), - "recording_crop".to_string(), - "Recording Crop: Remove neutral input frames at the end of your recording".to_string(), - true, - &(menu.recording_crop as u32), - ); - overall_menu.tabs.push(input_tab); - - let mut button_tab = Tab { - tab_id: "button".to_string(), - tab_title: "Button Config".to_string(), - tab_submenus: Vec::new(), - }; - button_tab.add_submenu_with_toggles::( - "Menu Open".to_string(), - "menu_open".to_string(), - "Menu Open: Hold: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.menu_open.bits() as u32), - ); - button_tab.add_submenu_with_toggles::( - "Save State Save".to_string(), - "save_state_save".to_string(), - "Save State Save: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.save_state_save.bits() as u32), - ); - - button_tab.add_submenu_with_toggles::( - "Save State Load".to_string(), - "save_state_load".to_string(), - "Save State Load: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.save_state_load.bits() as u32), - ); - button_tab.add_submenu_with_toggles::( - "Input Record".to_string(), - "input_record".to_string(), - "Input Record: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.input_record.bits() as u32), - ); - button_tab.add_submenu_with_toggles::( - "Input Playback".to_string(), - "input_playback".to_string(), - "Input Playback: Hold any one button and press the others to trigger".to_string(), - false, - &(menu.input_playback.bits() as u32), - ); - overall_menu.tabs.push(button_tab); - - overall_menu -} +#![feature(iter_intersperse)] +#[macro_use] +extern crate bitflags; + +#[macro_use] +extern crate bitflags_serde_shim; + +#[macro_use] +extern crate num_derive; + +use serde::{Deserialize, Serialize}; + +pub mod options; +pub use options::*; +pub mod files; +pub use files::*; +pub mod config; +pub use config::*; + +#[repr(C)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +pub struct TrainingModpackMenu { + pub aerial_delay: Delay, + pub air_dodge_dir: Direction, + pub attack_angle: AttackAngle, + pub buff_state: BuffOption, + pub character_item: CharacterItem, + pub clatter_strength: ClatterFrequency, + pub crouch: OnOff, + pub di_state: Direction, + pub falling_aerials: BoolFlag, + pub fast_fall_delay: Delay, + pub fast_fall: BoolFlag, + pub follow_up: Action, + pub frame_advantage: OnOff, + pub full_hop: BoolFlag, + pub hitbox_vis: OnOff, + pub hud: OnOff, + pub input_delay: Delay, + pub ledge_delay: LongDelay, + pub ledge_state: LedgeOption, + pub mash_state: Action, + pub mash_triggers: MashTrigger, + pub miss_tech_state: MissTechFlags, + pub oos_offset: Delay, + pub pummel_delay: MedDelay, + pub reaction_time: Delay, + pub save_damage_cpu: SaveDamage, + pub save_damage_limits_cpu: DamagePercent, + pub save_damage_player: SaveDamage, + pub save_damage_limits_player: DamagePercent, + pub save_state_autoload: OnOff, + pub save_state_enable: OnOff, + pub save_state_slot: SaveStateSlot, + pub randomize_slots: OnOff, + pub save_state_mirroring: SaveStateMirroring, + pub save_state_playback: PlaybackSlot, + pub sdi_state: Direction, + pub sdi_strength: SdiFrequency, + pub shield_state: Shield, + pub shield_tilt: Direction, + pub stage_hazards: OnOff, + pub tech_state: TechFlags, + pub throw_delay: MedDelay, + pub throw_state: ThrowOption, + pub ledge_neutral_override: Action, + pub ledge_roll_override: Action, + pub ledge_jump_override: Action, + pub ledge_attack_override: Action, + pub tech_action_override: Action, + pub clatter_override: Action, + pub tumble_override: Action, + pub hitstun_override: Action, + pub parry_override: Action, + pub shieldstun_override: Action, + pub footstool_override: Action, + pub landing_override: Action, + pub trump_override: Action, + pub recording_slot: RecordSlot, + pub record_trigger: RecordTrigger, + pub recording_frames: RecordingFrames, + pub playback_button_combination: PlaybackSlot, + pub hitstun_playback: HitstunPlayback, + pub playback_mash: OnOff, + pub playback_loop: OnOff, + pub menu_open: ButtonConfig, + pub save_state_save: ButtonConfig, + pub save_state_load: ButtonConfig, + pub input_record: ButtonConfig, + pub input_playback: ButtonConfig, + pub recording_crop: OnOff, + pub update_policy: UpdatePolicy, +} + +#[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)] +pub enum FighterId { + Player = 0, + CPU = 1, +} + +#[derive(Clone)] +pub enum SubMenuType { + TOGGLE, + SLIDER, +} + +impl SubMenuType { + pub fn from_string(s: &String) -> SubMenuType { + match s.as_str() { + "toggle" => SubMenuType::TOGGLE, + "slider" => SubMenuType::SLIDER, + _ => panic!("Unexpected SubMenuType!"), + } + } +} + +pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu { + aerial_delay: Delay::empty(), + air_dodge_dir: Direction::empty(), + attack_angle: AttackAngle::empty(), + buff_state: BuffOption::empty(), + character_item: CharacterItem::None, + clatter_strength: ClatterFrequency::None, + crouch: OnOff::Off, + di_state: Direction::empty(), + falling_aerials: BoolFlag::FALSE, + fast_fall_delay: Delay::empty(), + fast_fall: BoolFlag::FALSE, + follow_up: Action::empty(), + frame_advantage: OnOff::Off, + full_hop: BoolFlag::TRUE, + hitbox_vis: OnOff::On, + hud: OnOff::On, + input_delay: Delay::D0, + ledge_delay: LongDelay::empty(), + ledge_state: LedgeOption::default(), + mash_state: Action::empty(), + mash_triggers: MashTrigger::default(), + miss_tech_state: MissTechFlags::all(), + oos_offset: Delay::empty(), + pummel_delay: MedDelay::empty(), + reaction_time: Delay::empty(), + save_damage_cpu: SaveDamage::DEFAULT, + save_damage_limits_cpu: DamagePercent::default(), + save_damage_player: SaveDamage::DEFAULT, + save_damage_limits_player: DamagePercent::default(), + save_state_autoload: OnOff::Off, + save_state_enable: OnOff::On, + save_state_slot: SaveStateSlot::One, + randomize_slots: OnOff::Off, + save_state_mirroring: SaveStateMirroring::None, + save_state_playback: PlaybackSlot::empty(), + sdi_state: Direction::empty(), + sdi_strength: SdiFrequency::None, + shield_state: Shield::None, + shield_tilt: Direction::empty(), + stage_hazards: OnOff::Off, + tech_state: TechFlags::all(), + throw_delay: MedDelay::empty(), + throw_state: ThrowOption::NONE, + ledge_neutral_override: Action::empty(), + ledge_roll_override: Action::empty(), + ledge_jump_override: Action::empty(), + ledge_attack_override: Action::empty(), + tech_action_override: Action::empty(), + clatter_override: Action::empty(), + tumble_override: Action::empty(), + hitstun_override: Action::empty(), + parry_override: Action::empty(), + shieldstun_override: Action::empty(), + footstool_override: Action::empty(), + landing_override: Action::empty(), + trump_override: Action::empty(), + recording_slot: RecordSlot::S1, + recording_frames: RecordingFrames::F150, + record_trigger: RecordTrigger::COMMAND, + playback_button_combination: PlaybackSlot::S1, + hitstun_playback: HitstunPlayback::Hitstun, + playback_mash: OnOff::On, + playback_loop: OnOff::Off, + menu_open: ButtonConfig::B.union(ButtonConfig::DPAD_UP), + save_state_save: ButtonConfig::ZL.union(ButtonConfig::DPAD_DOWN), + save_state_load: ButtonConfig::ZL.union(ButtonConfig::DPAD_UP), + input_record: ButtonConfig::ZR.union(ButtonConfig::DPAD_DOWN), + input_playback: ButtonConfig::ZR.union(ButtonConfig::DPAD_UP), + recording_crop: OnOff::On, + update_policy: UpdatePolicy::default(), +}; + +pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU; + +#[derive(Clone, Serialize)] +pub struct Slider { + pub selected_min: u32, + pub selected_max: u32, + pub abs_min: u32, + pub abs_max: u32, +} + +#[derive(Clone, Serialize)] +pub struct Toggle { + pub toggle_value: u32, + pub toggle_title: String, + pub checked: bool, +} + +#[derive(Clone, Serialize)] +pub struct SubMenu { + pub submenu_title: String, + pub submenu_id: String, + pub help_text: String, + pub is_single_option: bool, + pub toggles: Vec, + pub slider: Option, + pub _type: String, +} + +impl SubMenu { + pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: String, checked: bool) { + self.toggles.push(Toggle { + toggle_value, + toggle_title, + checked, + }); + } + pub fn new_with_toggles( + submenu_title: String, + submenu_id: String, + help_text: String, + is_single_option: bool, + initial_value: &u32, + ) -> SubMenu { + 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(), + slider: None, + _type: "toggle".to_string(), + }; + + let values = T::to_toggle_vals(); + let titles = T::to_toggle_strings(); + for i in 0..values.len() { + let checked: bool = + (values[i] & initial_value) > 0 || (!values[i] == 0 && initial_value == &0); + instance.add_toggle(values[i], titles[i].clone(), checked); + } + // Select the first option if there's nothing selected atm but it's a single option submenu + if is_single_option && instance.toggles.iter().all(|t| !t.checked) { + instance.toggles[0].checked = true; + } + instance + } + pub fn new_with_slider( + submenu_title: String, + submenu_id: String, + help_text: String, + initial_lower_value: &u32, + initial_upper_value: &u32, + ) -> SubMenu { + let min_max = S::get_limits(); + SubMenu { + submenu_title: submenu_title, + submenu_id: submenu_id, + help_text: help_text, + is_single_option: false, + toggles: Vec::new(), + slider: Some(Slider { + selected_min: *initial_lower_value, + selected_max: *initial_upper_value, + abs_min: min_max.0, + abs_max: min_max.1, + }), + _type: "slider".to_string(), + } + } +} + +#[derive(Serialize, Clone)] +pub struct Tab { + pub tab_id: String, + pub tab_title: String, + pub tab_submenus: Vec, +} + +impl Tab { + pub fn add_submenu_with_toggles( + &mut self, + submenu_title: String, + submenu_id: String, + help_text: String, + is_single_option: bool, + initial_value: &u32, + ) { + self.tab_submenus.push(SubMenu::new_with_toggles::( + submenu_title.to_string(), + submenu_id.to_string(), + help_text.to_string(), + is_single_option, + initial_value, + )); + } + + pub fn add_submenu_with_slider( + &mut self, + submenu_title: String, + submenu_id: String, + help_text: String, + initial_lower_value: &u32, + initial_upper_value: &u32, + ) { + self.tab_submenus.push(SubMenu::new_with_slider::( + submenu_title.to_string(), + submenu_id.to_string(), + help_text.to_string(), + initial_lower_value, + initial_upper_value, + )) + } +} + +#[derive(Serialize, Clone)] +pub struct UiMenu { + pub tabs: Vec, +} + +pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu { + let mut overall_menu = UiMenu { tabs: Vec::new() }; + + let mut mash_tab = Tab { + tab_id: "mash".to_string(), + tab_title: "Mash Settings".to_string(), + tab_submenus: Vec::new(), + }; + mash_tab.add_submenu_with_toggles::( + "Mash Toggles".to_string(), + "mash_state".to_string(), + "Mash Toggles: Actions to be performed as soon as possible".to_string(), + false, + &(menu.mash_state.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Followup Toggles".to_string(), + "follow_up".to_string(), + "Followup Toggles: Actions to be performed after a Mash option".to_string(), + false, + &(menu.follow_up.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Mash Triggers".to_string(), + "mash_triggers".to_string(), + "Mash triggers: Configure what causes the CPU to perform a Mash option".to_string(), + false, + &(menu.mash_triggers.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Attack Angle".to_string(), + "attack_angle".to_string(), + "Attack Angle: For attacks that can be angled, such as some forward tilts".to_string(), + false, + &(menu.attack_angle.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Throw Options".to_string(), + "throw_state".to_string(), + "Throw Options: Throw to be performed when a grab is landed".to_string(), + false, + &(menu.throw_state.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Throw Delay".to_string(), + "throw_delay".to_string(), + "Throw Delay: How many frames to delay the throw option".to_string(), + false, + &(menu.throw_delay.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Pummel Delay".to_string(), + "pummel_delay".to_string(), + "Pummel Delay: How many frames after a grab to wait before starting to pummel".to_string(), + false, + &(menu.pummel_delay.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Falling Aerials".to_string(), + "falling_aerials".to_string(), + "Falling Aerials: Should aerials be performed when rising or when falling".to_string(), + false, + &(menu.falling_aerials.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Full Hop".to_string(), + "full_hop".to_string(), + "Full Hop: Should the CPU perform a full hop or a short hop".to_string(), + false, + &(menu.full_hop.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Aerial Delay".to_string(), + "aerial_delay".to_string(), + "Aerial Delay: How long to delay a Mash aerial attack".to_string(), + false, + &(menu.aerial_delay.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Fast Fall".to_string(), + "fast_fall".to_string(), + "Fast Fall: Should the CPU fastfall during a jump".to_string(), + false, + &(menu.fast_fall.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Fast Fall Delay".to_string(), + "fast_fall_delay".to_string(), + "Fast Fall Delay: How many frames the CPU should delay their fastfall".to_string(), + false, + &(menu.fast_fall_delay.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "OoS Offset".to_string(), + "oos_offset".to_string(), + "OoS Offset: How many times the CPU shield can be hit before performing a Mash option" + .to_string(), + false, + &(menu.oos_offset.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Reaction Time".to_string(), + "reaction_time".to_string(), + "Reaction Time: How many frames to delay before performing a mash option".to_string(), + false, + &(menu.reaction_time.bits()), + ); + overall_menu.tabs.push(mash_tab); + + let mut override_tab = Tab { + tab_id: "override".to_string(), + tab_title: "Override Settings".to_string(), + tab_submenus: Vec::new(), + }; + override_tab.add_submenu_with_toggles::( + "Ledge Neutral Getup".to_string(), + "ledge_neutral_override".to_string(), + "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge" + .to_string(), + false, + &(menu.ledge_neutral_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Ledge Roll".to_string(), + "ledge_roll_override".to_string(), + "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge" + .to_string(), + false, + &(menu.ledge_roll_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Ledge Jump".to_string(), + "ledge_jump_override".to_string(), + "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge" + .to_string(), + false, + &(menu.ledge_jump_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Ledge Attack".to_string(), + "ledge_attack_override".to_string(), + "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge" + .to_string(), + false, + &(menu.ledge_attack_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Tech Action".to_string(), + "tech_action_override".to_string(), + "Tech Action Override: Mash Actions to be performed after any tech action".to_string(), + false, + &(menu.tech_action_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Clatter".to_string(), + "clatter_override".to_string(), + "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab.to_string(), bury, etc)".to_string(), + false, + &(menu.clatter_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Tumble".to_string(), + "tumble_override".to_string(), + "Tumble Override: Mash Actions to be performed after exiting a tumble state".to_string(), + false, + &(menu.tumble_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Hitstun".to_string(), + "hitstun_override".to_string(), + "Hitstun Override: Mash Actions to be performed after exiting a hitstun state".to_string(), + false, + &(menu.hitstun_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Parry".to_string(), + "parry_override".to_string(), + "Parry Override: Mash Actions to be performed after a parry".to_string(), + false, + &(menu.parry_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Shieldstun".to_string(), + "shieldstun_override".to_string(), + "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state" + .to_string(), + false, + &(menu.shieldstun_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Footstool".to_string(), + "footstool_override".to_string(), + "Footstool Override: Mash Actions to be performed after exiting a footstool state" + .to_string(), + false, + &(menu.footstool_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Landing".to_string(), + "landing_override".to_string(), + "Landing Override: Mash Actions to be performed after landing on the ground".to_string(), + false, + &(menu.landing_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Ledge Trump".to_string(), + "trump_override".to_string(), + "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state" + .to_string(), + false, + &(menu.trump_override.bits()), + ); + overall_menu.tabs.push(override_tab); + + let mut defensive_tab = Tab { + tab_id: "defensive".to_string(), + tab_title: "Defensive Settings".to_string(), + tab_submenus: Vec::new(), + }; + defensive_tab.add_submenu_with_toggles::( + "Airdodge Direction".to_string(), + "air_dodge_dir".to_string(), + "Airdodge Direction: Direction to angle airdodges".to_string(), + false, + &(menu.air_dodge_dir.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "DI Direction".to_string(), + "di_state".to_string(), + "DI Direction: Direction to angle the directional influence during hitlag".to_string(), + false, + &(menu.di_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "SDI Direction".to_string(), + "sdi_state".to_string(), + "SDI Direction: Direction to angle the smash directional influence during hitlag" + .to_string(), + false, + &(menu.sdi_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "SDI Strength".to_string(), + "sdi_strength".to_string(), + "SDI Strength: Relative strength of the smash directional influence inputs".to_string(), + true, + &(menu.sdi_strength as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Clatter Strength".to_string(), + "clatter_strength".to_string(), + "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc." + .to_string(), + true, + &(menu.clatter_strength as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Ledge Options".to_string(), + "ledge_state".to_string(), + "Ledge Options: Actions to be taken when on the ledge".to_string(), + false, + &(menu.ledge_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "Ledge Delay".to_string(), + "ledge_delay".to_string(), + "Ledge Delay: How many frames to delay the ledge option".to_string(), + false, + &(menu.ledge_delay.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "Tech Options".to_string(), + "tech_state".to_string(), + "Tech Options: Actions to take when slammed into a hard surface".to_string(), + false, + &(menu.tech_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "Mistech Options".to_string(), + "miss_tech_state".to_string(), + "Mistech Options: Actions to take after missing a tech".to_string(), + false, + &(menu.miss_tech_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "Shield Toggles".to_string(), + "shield_state".to_string(), + "Shield Toggles: CPU Shield Behavior".to_string(), + true, + &(menu.shield_state as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Shield Tilt".to_string(), + "shield_tilt".to_string(), + "Shield Tilt: Direction to tilt the shield".to_string(), + false, // TODO: Should this be true? + &(menu.shield_tilt.bits()), + ); + + defensive_tab.add_submenu_with_toggles::( + "Crouch".to_string(), + "crouch".to_string(), + "Crouch: Have the CPU crouch when on the ground".to_string(), + true, + &(menu.crouch as u32), + ); + overall_menu.tabs.push(defensive_tab); + + let mut save_state_tab = Tab { + tab_id: "save_state".to_string(), + tab_title: "Save States".to_string(), + tab_submenus: Vec::new(), + }; + save_state_tab.add_submenu_with_toggles::( + "Mirroring".to_string(), + "save_state_mirroring".to_string(), + "Mirroring: Flips save states in the left-right direction across the stage center" + .to_string(), + true, + &(menu.save_state_mirroring as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Auto Save States".to_string(), + "save_state_autoload".to_string(), + "Auto Save States: Load save state when any fighter dies".to_string(), + true, + &(menu.save_state_autoload as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Save Dmg (CPU)".to_string(), + "save_damage_cpu".to_string(), + "Save Damage: Should save states retain CPU damage".to_string(), + true, + &(menu.save_damage_cpu.bits()), + ); + save_state_tab.add_submenu_with_slider::( + "Dmg Range (CPU)".to_string(), + "save_damage_limits_cpu".to_string(), + "Limits on random damage to apply to the CPU when loading a save state".to_string(), + &(menu.save_damage_limits_cpu.0 as u32), + &(menu.save_damage_limits_cpu.1 as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Save Dmg (Player)".to_string(), + "save_damage_player".to_string(), + "Save Damage: Should save states retain player damage".to_string(), + true, + &(menu.save_damage_player.bits() as u32), + ); + save_state_tab.add_submenu_with_slider::( + "Dmg Range (Player)".to_string(), + "save_damage_limits_player".to_string(), + "Limits on random damage to apply to the player when loading a save state".to_string(), + &(menu.save_damage_limits_player.0 as u32), + &(menu.save_damage_limits_player.1 as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Enable Save States".to_string(), + "save_state_enable".to_string(), + "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.".to_string(), + true, + &(menu.save_state_enable as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Save State Slot".to_string(), + "save_state_slot".to_string(), + "Save State Slot: Save and load states from different slots.".to_string(), + true, + &(menu.save_state_slot as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Randomize Slots".to_string(), + "randomize_slots".to_string(), + "Randomize Slots: Randomize slot when loading save state.".to_string(), + true, + &(menu.randomize_slots as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Character Item".to_string(), + "character_item".to_string(), + "Character Item: The item to give to the player's fighter when loading a save state" + .to_string(), + true, + &(menu.character_item as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Buff Options".to_string(), + "buff_state".to_string(), + "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state" + .to_string(), + false, + &(menu.buff_state.bits()), + ); + save_state_tab.add_submenu_with_toggles::( + "Save State Playback".to_string(), + "save_state_playback".to_string(), + "Save State Playback: Choose which slots to playback input recording upon loading a save state".to_string(), + false, + &(menu.save_state_playback.bits() as u32), + ); + overall_menu.tabs.push(save_state_tab); + + let mut misc_tab = Tab { + tab_id: "misc".to_string(), + tab_title: "Misc Settings".to_string(), + tab_submenus: Vec::new(), + }; + misc_tab.add_submenu_with_toggles::( + "Frame Advantage".to_string(), + "frame_advantage".to_string(), + "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable".to_string(), + true, + &(menu.frame_advantage as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Hitbox Visualization".to_string(), + "hitbox_vis".to_string(), + "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)".to_string(), + true, + &(menu.hitbox_vis as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Input Delay".to_string(), + "input_delay".to_string(), + "Input Delay: Frames to delay player inputs by".to_string(), + true, + &(menu.input_delay.bits()), + ); + misc_tab.add_submenu_with_toggles::( + "Stage Hazards".to_string(), + "stage_hazards".to_string(), + "Stage Hazards: Turn stage hazards on/off".to_string(), + true, + &(menu.stage_hazards as u32), + ); + misc_tab.add_submenu_with_toggles::( + "HUD".to_string(), + "hud".to_string(), + "HUD: Show/hide elements of the UI".to_string(), + true, + &(menu.hud as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Auto-Update".to_string(), + "update_policy".to_string(), + "Auto-Update: What type of Training Modpack updates to automatically apply. (CONSOLE ONLY)" + .to_string(), + true, + &(menu.update_policy as u32), + ); + overall_menu.tabs.push(misc_tab); + + let mut input_tab = Tab { + tab_id: "input".to_string(), + tab_title: "Input Recording".to_string(), + tab_submenus: Vec::new(), + }; + input_tab.add_submenu_with_toggles::( + "Recording Slot".to_string(), + "recording_slot".to_string(), + "Recording Slot: Choose which slot to record into".to_string(), + true, + &(menu.recording_slot as u32), + ); + input_tab.add_submenu_with_toggles::( + "Recording Trigger".to_string(), + "record_trigger".to_string(), + format!("Recording Trigger: Whether to begin recording via button combination ({}) or upon loading a Save State", menu.input_record.combination_string()), + false, + &(menu.record_trigger.bits() as u32), + ); + input_tab.add_submenu_with_toggles::( + "Recording Frames".to_string(), + "recording_frames".to_string(), + "Recording Frames: Number of frames to record for in the current slot".to_string(), + true, + &(menu.recording_frames as u32), + ); + input_tab.add_submenu_with_toggles::( + "Playback Button Combination".to_string(), + "playback_button_combination".to_string(), + format!("Playback Button Combination: Choose which slots to playback input recording upon pressing button combination ({})", menu.input_playback.combination_string()), + false, + &(menu.playback_button_combination.bits() as u32), + ); + input_tab.add_submenu_with_toggles::( + "Playback Hitstun Timing".to_string(), + "hitstun_playback".to_string(), + "Playback Hitstun Timing: When to begin playing back inputs when a hitstun mash trigger occurs".to_string(), + true, + &(menu.hitstun_playback as u32), + ); + input_tab.add_submenu_with_toggles::( + "Playback Mash Interrupt".to_string(), + "playback_mash".to_string(), + "Playback Mash Interrupt: End input playback when a mash trigger occurs".to_string(), + true, + &(menu.playback_mash as u32), + ); + input_tab.add_submenu_with_toggles::( + "Playback Loop".to_string(), + "playback_loop".to_string(), + "Playback Loop: Repeat triggered input playbacks indefinitely".to_string(), + true, + &(menu.playback_loop as u32), + ); + input_tab.add_submenu_with_toggles::( + "Recording Crop".to_string(), + "recording_crop".to_string(), + "Recording Crop: Remove neutral input frames at the end of your recording".to_string(), + true, + &(menu.recording_crop as u32), + ); + overall_menu.tabs.push(input_tab); + + let mut button_tab = Tab { + tab_id: "button".to_string(), + tab_title: "Button Config".to_string(), + tab_submenus: Vec::new(), + }; + button_tab.add_submenu_with_toggles::( + "Menu Open".to_string(), + "menu_open".to_string(), + "Menu Open: Hold: Hold any one button and press the others to trigger".to_string(), + false, + &(menu.menu_open.bits() as u32), + ); + button_tab.add_submenu_with_toggles::( + "Save State Save".to_string(), + "save_state_save".to_string(), + "Save State Save: Hold any one button and press the others to trigger".to_string(), + false, + &(menu.save_state_save.bits() as u32), + ); + + button_tab.add_submenu_with_toggles::( + "Save State Load".to_string(), + "save_state_load".to_string(), + "Save State Load: Hold any one button and press the others to trigger".to_string(), + false, + &(menu.save_state_load.bits() as u32), + ); + button_tab.add_submenu_with_toggles::( + "Input Record".to_string(), + "input_record".to_string(), + "Input Record: Hold any one button and press the others to trigger".to_string(), + false, + &(menu.input_record.bits() as u32), + ); + button_tab.add_submenu_with_toggles::( + "Input Playback".to_string(), + "input_playback".to_string(), + "Input Playback: Hold any one button and press the others to trigger".to_string(), + false, + &(menu.input_playback.bits() as u32), + ); + overall_menu.tabs.push(button_tab); + + overall_menu +} diff --git a/training_mod_consts/src/options.rs b/training_mod_consts/src/options.rs index 741fa94..e2d6e0c 100644 --- a/training_mod_consts/src/options.rs +++ b/training_mod_consts/src/options.rs @@ -1663,3 +1663,43 @@ impl fmt::Display for ButtonConfig { extra_bitflag_impls! {ButtonConfig} impl_serde_for_bitflags!(ButtonConfig); + +#[repr(u32)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] +pub enum UpdatePolicy { + Stable, + Beta, + Disabled, +} + +impl UpdatePolicy { + pub const fn default() -> UpdatePolicy { + UpdatePolicy::Stable + } +} + +impl fmt::Display for UpdatePolicy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match *self { + UpdatePolicy::Stable => "Stable", + UpdatePolicy::Beta => "Beta", + UpdatePolicy::Disabled => "Disabled", + } + ) + } +} + +impl ToggleTrait for UpdatePolicy { + fn to_toggle_vals() -> Vec { + UpdatePolicy::iter().map(|i| i as u32).collect() + } + + fn to_toggle_strings() -> Vec { + UpdatePolicy::iter().map(|i| i.to_string()).collect() + } +}