From 62298ecbc125efdf38d7352fea62f4949cdae0a2 Mon Sep 17 00:00:00 2001 From: jugeeya Date: Thu, 27 Jul 2023 16:42:21 -0700 Subject: [PATCH] Input Recording (#539) * GCC+tinkering * First working pass with 8-byte structure usage. Control scheme is effectively copied for CPU. * CPU override, sticks not working properly * Recording working again during record, playback issues. Also, aerials are all nair, and jump/Dash Attack issues. * Structure creation cleanup * Continued testing, still aerial, jump, and dash attack issues * Fix Dockerfile * Rework with ControlModuleInternal and ControlModuleStored - same exact issues * Fix playback inconsistency * Dead code of trying to find attack air address * It works! Fix mash.rs overrides of input playback * Action passing groundwork; Clatter off by default for now * All Overrides implemented, notes on Snake * Input Submenu, dummy slots and save state/mash playback interaction * Nana command fix, mash fix * Initial Savestate/Ledge record (slight issues) * Add TODO * Lasso handled, ledge jump issue fixed, shield decay with input recording fixed, standby setup begun * first frame playback clear, fix neutral getup * Standy by groundwork, broken by our frame late record impl * Prepare for structure change * On time small structure recording, needs ledge adjustments * Coloring fix for poessession, lockout implemented and full ledge functionality * Cleaning and menu fix * Fix ledge option loop and shield holding on mash * Fix shielding issues * turn off playback * Enable input recording * Resolving comments 1,2,4,5 * Resolve comments 6, 7, and 8 * External Mash Function * No Trigger for Overrides, Clatter and Tumble Added * LR Support, Full Hop Fix * Starting Status WIP, Structure WIP, LedgeOption PLAYBACK renaming * WIP Playback Mash - OoS issues - WIP Slots * Merge branch 'main' into for-restructure prep * more merge prep * Return None fix * More cleanup * Block -> Shieldstun * Don't crash on missing menu icons * Add input recording tab to prevent crash * Fix general override behavior * Fix teching overrides * Additional merge changes * Nana fixes (also on master so this will be awkward) * Additional Merge Fixes * Remove extra tab, prevent panic on missing input recording items * Remove some TODOs --------- Co-authored-by: GradualSyrup <68757075+GradualSyrup@users.noreply.github.com> --- src/common/events.rs | 2 +- src/common/mod.rs | 2 +- src/training/fast_fall.rs | 5 +- src/training/full_hop.rs | 5 + src/training/input_record.rs | 494 ++++++- src/training/input_recording/mod.rs | 2 + src/training/input_recording/structures.rs | 308 ++++ src/training/ledge.rs | 51 +- src/training/mash.rs | 25 +- src/training/mod.rs | 16 +- src/training/save_states.rs | 46 +- src/training/shield.rs | 31 +- src/training/ui/menu.rs | 18 +- training_mod_consts/src/lib.rs | 1524 ++++++++++---------- training_mod_consts/src/options.rs | 172 +++ 15 files changed, 1844 insertions(+), 857 deletions(-) create mode 100644 src/training/input_recording/mod.rs create mode 100644 src/training/input_recording/structures.rs diff --git a/src/common/events.rs b/src/common/events.rs index f0aca32..cd3ac9e 100644 --- a/src/common/events.rs +++ b/src/common/events.rs @@ -173,7 +173,7 @@ impl Event { } } -fn smash_version() -> String { +pub fn smash_version() -> String { let mut smash_version = oe::DisplayVersion { name: [0; 16] }; unsafe { diff --git a/src/common/mod.rs b/src/common/mod.rs index 769d924..e7441fc 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -96,7 +96,7 @@ pub fn is_idle(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_in_hitstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - // TODO: Should this be *FIGHTER_STATUS_KIND_DAMAGE..*FIGHTER_STATUS_KIND_DAMAGE_AIR ? + // TODO: Need to add EWGF'd out of shield to this? (*FIGHTER_STATUS_KIND_DAMAGE..*FIGHTER_STATUS_KIND_DAMAGE_FALL).contains(&status_kind) } diff --git a/src/training/fast_fall.rs b/src/training/fast_fall.rs index db35466..c8f3a4a 100644 --- a/src/training/fast_fall.rs +++ b/src/training/fast_fall.rs @@ -3,7 +3,7 @@ use smash::lib::lua_const::*; use smash::phx::{Hash40, Vector3f}; use crate::common::*; -use crate::training::frame_counter; +use crate::training::{frame_counter, input_record}; static mut FRAME_COUNTER: usize = 0; @@ -83,6 +83,9 @@ fn is_correct_status(module_accessor: &mut app::BattleObjectModuleAccessor) -> b unsafe { status = StatusModule::status_kind(module_accessor); + if input_record::is_playback() { + return false; + } } // Allow fast fall when falling diff --git a/src/training/full_hop.rs b/src/training/full_hop.rs index 9006746..f408e95 100644 --- a/src/training/full_hop.rs +++ b/src/training/full_hop.rs @@ -1,5 +1,6 @@ use smash::app::{self, lua_bind::*}; use smash::lib::lua_const::*; +use crate::training::input_record; use crate::common::*; @@ -67,5 +68,9 @@ unsafe fn should_return_none_in_check_button( return true; } + if input_record::is_playback() { + return true; + } + false } diff --git a/src/training/input_record.rs b/src/training/input_record.rs index 16a8369..68bdfd1 100644 --- a/src/training/input_record.rs +++ b/src/training/input_record.rs @@ -1,80 +1,414 @@ -use lazy_static::lazy_static; -use parking_lot::Mutex; -use skyline::nn::hid::NpadHandheldState; -use smash::app::{lua_bind::*, BattleObjectModuleAccessor}; -use smash::lib::lua_const::*; - -use InputRecordState::*; - -use crate::training::input_delay::p1_controller_id; - -lazy_static! { - static ref P1_NPAD_STATES: Mutex<[NpadHandheldState; 90]> = - Mutex::new([{ NpadHandheldState::default() }; 90]); -} - -pub static mut INPUT_RECORD: InputRecordState = None; -pub static mut INPUT_RECORD_FRAME: usize = 0; - -#[derive(PartialEq)] -pub enum InputRecordState { - None, - Record, - Playback, -} - -pub unsafe fn get_command_flag_cat(module_accessor: &mut BattleObjectModuleAccessor) { - let entry_id_int = WorkModule::get_int(module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID); - - if entry_id_int == 0 { - // Attack + Dpad Right: Playback - if ControlModule::check_button_on(module_accessor, *CONTROL_PAD_BUTTON_ATTACK) - && ControlModule::check_button_trigger(module_accessor, *CONTROL_PAD_BUTTON_APPEAL_S_R) - { - playback(); - } - // Attack + Dpad Left: Record - else if ControlModule::check_button_on(module_accessor, *CONTROL_PAD_BUTTON_ATTACK) - && ControlModule::check_button_trigger(module_accessor, *CONTROL_PAD_BUTTON_APPEAL_S_L) - { - record(); - } - - if INPUT_RECORD == Record || INPUT_RECORD == Playback { - if INPUT_RECORD_FRAME >= P1_NPAD_STATES.lock().len() - 1 { - if INPUT_RECORD == Record { - INPUT_RECORD = Playback; - } - INPUT_RECORD_FRAME = 0; - } else { - INPUT_RECORD_FRAME += 1; - } - } - } -} - -pub unsafe fn record() { - INPUT_RECORD = Record; - P1_NPAD_STATES.lock().iter_mut().for_each(|state| { - *state = NpadHandheldState::default(); - }); - INPUT_RECORD_FRAME = 0; -} - -pub unsafe fn playback() { - INPUT_RECORD = Playback; - INPUT_RECORD_FRAME = 0; -} - -#[allow(dead_code)] -pub unsafe fn handle_get_npad_state(state: *mut NpadHandheldState, controller_id: *const u32) { - if *controller_id == p1_controller_id() { - if INPUT_RECORD == Record { - P1_NPAD_STATES.lock()[INPUT_RECORD_FRAME] = *state; - } - } else if INPUT_RECORD == Record || INPUT_RECORD == Playback { - let update_count = (*state).updateCount; - *state = P1_NPAD_STATES.lock()[INPUT_RECORD_FRAME]; - (*state).updateCount = update_count; - } -} +use smash::app::{BattleObjectModuleAccessor, lua_bind::*, utility}; +use smash::lib::lua_const::*; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use crate::training::input_recording::structures::*; +use crate::common::consts::{RecordTrigger, HitstunPlayback, FighterId}; +use crate::common::{MENU, get_module_accessor, is_in_hitstun, is_in_shieldstun}; +use crate::training::mash; + +#[derive(PartialEq)] +#[derive(Debug)] +pub enum InputRecordState { + None, + Pause, + Record, + Playback, +} + +#[derive(PartialEq)] +#[derive(Debug)] +pub enum PossessionState { + Player, + Cpu, + Lockout, + Standby, +} + +#[derive(PartialEq)] +#[derive(Copy, Clone)] +pub enum StartingStatus { + Aerial, // FIGHTER_STATUS_KIND_ATTACK_AIR TODO: This shouldn't happen without starting input recording in the air - when would we want this? + // Probably should lock input recordings to either the ground or the air + Airdodge, // FIGHTER_STATUS_KIND_ESCAPE_AIR, FIGHTER_STATUS_KIND_ESCAPE_AIR_SLIDE + // Other statuses cannot be used to hitstun cancel via damage_fly_attack_frame/damage_fly_escape_frame + // Some statuses can leave shield earlier though, so we should check for this + SpecialHi, // Up B: FIGHTER_STATUS_KIND_SPECIAL_HI, + Jump, // FIGHTER_STATUS_KIND_JUMP_SQUAT + DoubleJump, //FIGHTER_STATUS_KIND_JUMP_AERIAL + Spotdodge, // FIGHTER_STATUS_KIND_ESCAPE + Roll, // FIGHTER_STATUS_KIND_ESCAPE_F, FIGHTER_STATUS_KIND_ESCAPE_B + Grab, // FIGHTER_STATUS_KIND_CATCH + Other, +} + +use InputRecordState::*; +use PossessionState::*; + +const FINAL_RECORD_MAX: usize = 150; // Maximum length for input recording sequences (capacity) +const TOTAL_SLOT_COUNT: usize = 5; // Total number of input recording slots +pub static mut FINAL_RECORD_FRAME: usize = FINAL_RECORD_MAX; // The final frame to play back of the currently recorded sequence (size) +pub static mut INPUT_RECORD: InputRecordState = InputRecordState::None; +pub static mut INPUT_RECORD_FRAME: usize = 0; +pub static mut POSSESSION: PossessionState = PossessionState::Player; +pub static mut LOCKOUT_FRAME: usize = 0; +pub static mut BUFFER_FRAME: usize = 0; +pub static mut RECORDED_LR: f32 = 1.0; // The direction the CPU was facing before the current recording was recorded +pub static mut CURRENT_LR: f32 = 1.0; // The direction the CPU was facing at the beginning of this playback +pub static mut STARTING_STATUS: i32 = 0; // The first status entered in the recording outside of waits + // used to calculate if the input playback should begin before hitstun would normally end (hitstun cancel, monado art?) +pub static mut CURRENT_RECORD_SLOT: usize = 0; // Which slot is being used for recording right now? Want to make sure this is synced with menu choices, maybe just use menu instead +pub static mut CURRENT_PLAYBACK_SLOT: usize = 0; // Which slot is being used for playback right now? + +lazy_static! { + static ref P1_FINAL_MAPPING: Mutex<[MappedInputs; FINAL_RECORD_MAX]> = + Mutex::new([{ + MappedInputs::default() + }; FINAL_RECORD_MAX]); + static ref P1_STARTING_STATUSES: Mutex<[StartingStatus; TOTAL_SLOT_COUNT]> = + Mutex::new([{StartingStatus::Other}; TOTAL_SLOT_COUNT]); +} + +unsafe fn can_transition(module_accessor: *mut BattleObjectModuleAccessor) -> bool { + let transition_term = into_transition_term(into_starting_status(STARTING_STATUS)); + WorkModule::is_enable_transition_term(module_accessor, transition_term) +} + +unsafe fn should_mash_playback() { + // Don't want to interrupt recording + if is_recording() { + return; + } + if !mash::is_playback_queued() { + return; + } + // playback is queued, so we might want to begin this frame + // if we're currently playing back, we don't want to interrupt (this may change for layered multislot playback, but for now this is fine) + if is_playback() { + return; + } + let mut should_playback = false; + let cpu_module_accessor = get_module_accessor(FighterId::CPU); + // depending on our current status, we want to wait for different timings to begin playback + + // TODO: This isn't the best way to write this I'm sure, want to rewrite + + if is_in_hitstun(&mut *cpu_module_accessor) { + // if we're in hitstun and want to enter the frame we start hitstop for SDI, start if we're in any damage status instantly + if MENU.hitstun_playback == HitstunPlayback::Instant { + should_playback = true; + } + // if we want to wait until we exit hitstop and begin flying away for shield art etc, start if we're not in hitstop + if MENU.hitstun_playback == HitstunPlayback::Hitstop && !StopModule::is_stop(cpu_module_accessor) { + should_playback = true; + } + // if we're in hitstun and want to wait till FAF to act, then we want to match our starting status to the correct transition term to see if we can hitstun cancel + if MENU.hitstun_playback == HitstunPlayback::Hitstun { + if can_transition(cpu_module_accessor) { + should_playback = true; + } + } + } else if is_in_shieldstun(&mut *cpu_module_accessor) { + // TODO: Add instant shieldstun toggle for shield art out of electric hitstun? Idk that's so specific + if can_transition(cpu_module_accessor) { + should_playback = true; + } + } else if can_transition(cpu_module_accessor) { + should_playback = true; + } + + + // how do we deal with buffering motion inputs out of shield? You can't complete them in one frame, but they can definitely be buffered during shield drop + // probably need a separate standby setting for grounded, aerial, shield, where shield starts once you let go of shield, and aerial keeps you in the air? + + if should_playback { + playback(); + } +} + +// TODO: set up a better match later and make this into one func + +fn into_starting_status(status: i32) -> StartingStatus { + if status == *FIGHTER_STATUS_KIND_ATTACK_AIR { + return StartingStatus::Aerial; + } else if (*FIGHTER_STATUS_KIND_ESCAPE_AIR..*FIGHTER_STATUS_KIND_ESCAPE_AIR_SLIDE).contains(&status) { + return StartingStatus::Airdodge; + } else if status == *FIGHTER_STATUS_KIND_SPECIAL_HI { + return StartingStatus::SpecialHi; + } else if status == *FIGHTER_STATUS_KIND_JUMP_SQUAT { + return StartingStatus::Jump; + } else if status == *FIGHTER_STATUS_KIND_JUMP_AERIAL { + return StartingStatus::DoubleJump; + } else if status == *FIGHTER_STATUS_KIND_ESCAPE { + return StartingStatus::Spotdodge; + } else if (*FIGHTER_STATUS_KIND_ESCAPE_F..*FIGHTER_STATUS_KIND_ESCAPE_B).contains(&status) { + return StartingStatus::Roll; + } else if status == *FIGHTER_STATUS_KIND_CATCH { + return StartingStatus::Grab; + } + StartingStatus::Other +} + +fn into_transition_term(starting_status: StartingStatus) -> i32 { + match starting_status { + StartingStatus::Aerial => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_ATTACK_AIR, + StartingStatus::Airdodge => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_ESCAPE_AIR, + StartingStatus::Other => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_SPECIAL_S, // placeholder - most likely don't want to use this in final build, and have a different set of checks + StartingStatus::SpecialHi => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_SPECIAL_HI, + StartingStatus::Jump => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_JUMP_SQUAT, + StartingStatus::DoubleJump => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_JUMP_AERIAL, + StartingStatus::Spotdodge => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_ESCAPE, + StartingStatus::Roll => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_ESCAPE_F, + StartingStatus::Grab => *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_CATCH, + } +} + +pub unsafe fn get_command_flag_cat(module_accessor: &mut BattleObjectModuleAccessor) { + let entry_id_int = + WorkModule::get_int(module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID) as i32; + let fighter_kind = utility::get_kind(module_accessor); + let fighter_is_nana = fighter_kind == *FIGHTER_KIND_NANA; + + if entry_id_int == 0 && !fighter_is_nana { + // Attack + Dpad Right: Playback + if ControlModule::check_button_on(module_accessor, *CONTROL_PAD_BUTTON_ATTACK) + && ControlModule::check_button_trigger(module_accessor, *CONTROL_PAD_BUTTON_APPEAL_S_R) { + //crate::common::raygun_printer::print_string(&mut *module_accessor, "PLAYBACK"); + playback(); + println!("Playback Command Received!"); //debug + } + // Attack + Dpad Left: Record + else if ControlModule::check_button_on(module_accessor, *CONTROL_PAD_BUTTON_ATTACK) + && ControlModule::check_button_trigger(module_accessor, *CONTROL_PAD_BUTTON_APPEAL_S_L) + && MENU.record_trigger == RecordTrigger::Command + { + //crate::common::raygun_printer::print_string(&mut *module_accessor, "RECORDING"); + lockout_record(); + println!("Record Command Received!"); //debug + } + + + // may need to move this to another func + if INPUT_RECORD == Record || INPUT_RECORD == Playback { + if INPUT_RECORD_FRAME >= FINAL_RECORD_FRAME - 1 { + INPUT_RECORD = None; + POSSESSION = Player; + INPUT_RECORD_FRAME = 0; + if mash::is_playback_queued() { + mash::reset(); + } + } + } + } + + // Handle Possession Coloring + //let model_color_type = *MODEL_COLOR_TYPE_COLOR_BLEND; + if entry_id_int == 1 && POSSESSION == Lockout { + set_color_rgb_2(module_accessor,0.0,0.0,1.0,*MODEL_COLOR_TYPE_COLOR_BLEND); + } else if entry_id_int == 1 && POSSESSION == Standby { + set_color_rgb_2(module_accessor,1.0,0.0,1.0,*MODEL_COLOR_TYPE_COLOR_BLEND); + } else if entry_id_int == 1 && POSSESSION == Cpu { + set_color_rgb_2(module_accessor,1.0,0.0,0.0,*MODEL_COLOR_TYPE_COLOR_BLEND); + } +} + +pub unsafe fn lockout_record() { + INPUT_RECORD = Pause; + INPUT_RECORD_FRAME = 0; + POSSESSION = Lockout; + P1_FINAL_MAPPING.lock().iter_mut().for_each(|mapped_input| { + *mapped_input = MappedInputs::default(); + }); + LOCKOUT_FRAME = 30; // This needs to be this high or issues occur dropping shield - but does this cause problems when trying to record ledge? + BUFFER_FRAME = 0; + // Store the direction the CPU is facing when we initially record, so we can turn their inputs around if needed + let cpu_module_accessor = get_module_accessor(FighterId::CPU); + RECORDED_LR = PostureModule::lr(cpu_module_accessor); + CURRENT_LR = RECORDED_LR; +} + +pub unsafe fn _record() { + INPUT_RECORD = Record; + POSSESSION = Cpu; + // Reset mappings to nothing, and then start recording. Likely want to reset in case we cut off recording early. + P1_FINAL_MAPPING.lock().iter_mut().for_each(|mapped_input| { + *mapped_input = MappedInputs::default(); + }); + INPUT_RECORD_FRAME = 0; + LOCKOUT_FRAME = 0; + BUFFER_FRAME = 0; +} + +pub unsafe fn playback() { + if INPUT_RECORD == Pause { + println!("Tried to playback during lockout!"); + return; + } + INPUT_RECORD = Playback; + POSSESSION = Player; + INPUT_RECORD_FRAME = 0; + BUFFER_FRAME = 0; + let cpu_module_accessor = get_module_accessor(FighterId::CPU); + CURRENT_LR = PostureModule::lr(cpu_module_accessor); +} + +pub unsafe fn playback_ledge() { + if INPUT_RECORD == Pause { + println!("Tried to playback during lockout!"); + return; + } + INPUT_RECORD = Playback; + POSSESSION = Player; + INPUT_RECORD_FRAME = 0; + BUFFER_FRAME = 5; // So we can make sure the option is buffered and won't get ledge trumped if delay is 0 + // drop down from ledge can't be buffered on the same frame as jump/attack/roll/ngu so we have to do this + // Need to buffer 1 less frame for non-lassos + let cpu_module_accessor = get_module_accessor(FighterId::CPU); + let status_kind = StatusModule::status_kind(cpu_module_accessor) as i32; + if status_kind == *FIGHTER_STATUS_KIND_CLIFF_CATCH { + BUFFER_FRAME -= 1; + } + CURRENT_LR = PostureModule::lr(cpu_module_accessor); +} + +pub unsafe fn stop_playback() { + INPUT_RECORD = None; + INPUT_RECORD_FRAME = 0; +} + +pub unsafe fn is_end_standby() -> bool { + // Returns whether we should be done with standby this frame (if the fighter is no longer in a waiting status) + let cpu_module_accessor = get_module_accessor(FighterId::CPU); + let status_kind = StatusModule::status_kind(cpu_module_accessor) as i32; + ![ + *FIGHTER_STATUS_KIND_WAIT, + *FIGHTER_STATUS_KIND_CLIFF_WAIT, + ] + .contains(&status_kind) +} + +static FIM_OFFSET: usize = 0x17504a0; +// TODO: Should we define all of our offsets in one file? Should at least be a good start for changing to be based on ASM instructions +#[skyline::hook(offset = FIM_OFFSET)] +unsafe fn handle_final_input_mapping( + mappings: *mut ControllerMapping, + player_idx: i32, // Is this the player index, or plugged in controller index? Need to check, assuming player for now - is this 0 indexed or 1? + out: *mut MappedInputs, + controller_struct: &mut SomeControllerStruct, + arg: bool +) { + // go through the original mapping function first + let _ret = original!()(mappings, player_idx, out, controller_struct, arg); + if player_idx == 0 { // if player 1 + if INPUT_RECORD == Record { + // check for standby before starting action: + if POSSESSION == Standby && is_end_standby() { + // last input made us start an action, so start recording and end standby. + INPUT_RECORD_FRAME += 1; + POSSESSION = Cpu; + } + + if INPUT_RECORD_FRAME == 1 { + // We're on the second frame of recording, grabbing the status should give us the status that resulted from the first frame of input + // We'll want to save this status so that we use the correct TRANSITION TERM for hitstun cancelling out of damage fly + let cpu_module_accessor = get_module_accessor(FighterId::CPU); + P1_STARTING_STATUSES.lock()[CURRENT_PLAYBACK_SLOT] = into_starting_status(StatusModule::status_kind(cpu_module_accessor)); + STARTING_STATUS = StatusModule::status_kind(cpu_module_accessor); // TODO: Handle this based on slot later instead + } + + P1_FINAL_MAPPING.lock()[INPUT_RECORD_FRAME] = *out; + *out = MappedInputs::default(); // don't control player while recording + println!("Stored Player Input! Frame: {}",INPUT_RECORD_FRAME); + } + } +} + +#[skyline::hook(offset = 0x2da180)] // After cpu controls are assigned from ai calls +unsafe fn set_cpu_controls(p_data: *mut *mut u8) { + call_original!(p_data); + let controller_data = *p_data.add(1) as *mut ControlModuleInternal; + let controller_no = (*controller_data).controller_index; + + // Check if we need to begin playback this frame due to a mash toggle + // TODO: Setup STARTING_STATUS based on current playback slot here + + // This check prevents out of shield if mash exiting is on + if INPUT_RECORD == None { + should_mash_playback(); + } + + if INPUT_RECORD == Pause { + if LOCKOUT_FRAME > 0 { + LOCKOUT_FRAME -= 1; + } else if LOCKOUT_FRAME == 0 { + INPUT_RECORD = Record; + POSSESSION = Standby; + } else { + println!("LOCKOUT_FRAME OUT OF BOUNDS"); + } + } + + if INPUT_RECORD == Record || INPUT_RECORD == Playback { + let x_input_multiplier = RECORDED_LR * CURRENT_LR; // if we aren't facing the way we were when we initially recorded, we reverse horizontal inputs + println!("Overriding Cpu Player: {}, Frame: {}, BUFFER_FRAME: {}, STARTING_STATUS: {}, INPUT_RECORD: {:#?}, POSSESSION: {:#?}", controller_no, INPUT_RECORD_FRAME, BUFFER_FRAME, STARTING_STATUS, INPUT_RECORD, POSSESSION); + let mut saved_mapped_inputs = P1_FINAL_MAPPING.lock()[INPUT_RECORD_FRAME]; + + if BUFFER_FRAME <= 3 && BUFFER_FRAME > 0 { + // Our option is already buffered, now we need to 0 out inputs to make sure our future controls act like flicks/presses instead of holding the button + saved_mapped_inputs = MappedInputs::default(); + } + + (*controller_data).buttons = saved_mapped_inputs.buttons; + (*controller_data).stick_x = x_input_multiplier * ((saved_mapped_inputs.lstick_x as f32) / (i8::MAX as f32)); + (*controller_data).stick_y = (saved_mapped_inputs.lstick_y as f32) / (i8::MAX as f32); + // Clamp stick inputs for separate part of structure + const NEUTRAL: f32 = 0.2; + const CLAMP_MAX: f32 = 120.0; + let clamp_mul = 1.0 / CLAMP_MAX; + let mut clamped_lstick_x = x_input_multiplier * ((saved_mapped_inputs.lstick_x as f32) * clamp_mul).clamp(-1.0, 1.0); + let mut clamped_lstick_y = ((saved_mapped_inputs.lstick_y as f32) * clamp_mul).clamp(-1.0, 1.0); + clamped_lstick_x = if clamped_lstick_x.abs() >= NEUTRAL { clamped_lstick_x } else { 0.0 }; + clamped_lstick_y = if clamped_lstick_y.abs() >= NEUTRAL { clamped_lstick_y } else { 0.0 }; + (*controller_data).clamped_lstick_x = clamped_lstick_x; + (*controller_data).clamped_lstick_y = clamped_lstick_y; + //println!("CPU Buttons: {:#018b}", (*controller_data).buttons); + + // Keep counting frames, unless we're in standby waiting for an input, or are buffering an option + // When buffering an option, we keep inputting the first frame of input during the buffer window + if BUFFER_FRAME > 0 { + BUFFER_FRAME -= 1; + } else if INPUT_RECORD_FRAME < FINAL_RECORD_FRAME - 1 && POSSESSION != Standby { + INPUT_RECORD_FRAME += 1; + } + } +} + +pub unsafe fn is_playback() -> bool { + INPUT_RECORD == Record || INPUT_RECORD == Playback +} + +pub unsafe fn is_recording() -> bool { + INPUT_RECORD == Record +} + +pub unsafe fn is_standby() -> bool { + POSSESSION == Standby || POSSESSION == Lockout +} + +extern "C" { // TODO: we should be using this from skyline + #[link_name = "\u{1}_ZN3app8lua_bind31ModelModule__set_color_rgb_implEPNS_26BattleObjectModuleAccessorEfffNS_16MODEL_COLOR_TYPEE"] + pub fn set_color_rgb_2( + arg1: *mut BattleObjectModuleAccessor, + arg2: f32, + arg3: f32, + arg4: f32, + arg5: i32, + ); +} + +pub fn init() { + skyline::install_hooks!( + set_cpu_controls, + handle_final_input_mapping, + ); +} diff --git a/src/training/input_recording/mod.rs b/src/training/input_recording/mod.rs new file mode 100644 index 0000000..616d62b --- /dev/null +++ b/src/training/input_recording/mod.rs @@ -0,0 +1,2 @@ +#[macro_use] +pub mod structures; \ No newline at end of file diff --git a/src/training/input_recording/structures.rs b/src/training/input_recording/structures.rs new file mode 100644 index 0000000..11d3a77 --- /dev/null +++ b/src/training/input_recording/structures.rs @@ -0,0 +1,308 @@ +#![allow(dead_code)] // TODO: Yeah don't do this +use bitflags::bitflags; +use crate::common::release::CURRENT_VERSION; +use crate::common::events::smash_version; +use crate::training::save_states::SavedState; +use training_mod_consts::TrainingModpackMenu; + +use crate::default_save_state; +use crate::training::character_specific::steve; +use crate::training::charge::ChargeState; +use crate::training::save_states::SaveState::NoAction; + + +// Need to define necesary structures here. Probably should move to consts or something. Realistically, should be in skyline smash prob tho. + +// Final final controls used for controlmodule +// can I actually derive these? +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct ControlModuleInternal { + pub vtable: *mut u8, + pub controller_index: i32, + pub buttons: Buttons, + pub stick_x: f32, + pub stick_y: f32, + pub padding: [f32; 2], + pub unk: [u32; 8], + pub clamped_lstick_x: f32, + pub clamped_lstick_y: f32, + pub padding2: [f32; 2], + pub clamped_rstick_x: f32, + pub clamped_rstick_y: f32, +} + +impl ControlModuleInternal { + pub fn _clear(&mut self) { // Try to nullify controls so we can't control player 1 during recording + self.stick_x = 0.0; + self.stick_y = 0.0; + self.buttons = Buttons::NONE; + self.clamped_lstick_x = 0.0; + self.clamped_lstick_y = 0.0; + self.clamped_rstick_x = 0.0; + self.clamped_rstick_y = 0.0; + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct ControlModuleStored { // Custom type for saving only necessary controls/not saving vtable + pub buttons: Buttons, + pub stick_x: f32, + pub stick_y: f32, + pub padding: [f32; 2], + pub unk: [u32; 8], + pub clamped_lstick_x: f32, + pub clamped_lstick_y: f32, + pub padding2: [f32; 2], + pub clamped_rstick_x: f32, + pub clamped_rstick_y: f32, +} + +// Re-ordered bitfield the game uses for buttons - TODO: Is this a problem? What's the original order? +pub type ButtonBitfield = i32; // may need to actually implement? Not for now though + +/// Controller style declaring what kind of controller is being used +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +#[repr(u32)] +pub enum ControllerStyle { + Handheld = 0x1, + DualJoycon = 0x2, + LeftJoycon = 0x3, + RightJoycon = 0x4, + ProController = 0x5, + DebugPad = 0x6, // probably + GCController = 0x7 +} + +#[repr(C)] +pub struct AutorepeatInfo { + field: [u8; 0x18] +} + +// Can map any of these over any button - what does this mean? +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum InputKind { + Attack = 0x0, + Special = 0x1, + Jump = 0x2, + Guard = 0x3, + Grab = 0x4, + SmashAttack = 0x5, + AppealHi = 0xA, + AppealS = 0xB, + AppealLw = 0xC, + Unset = 0xD, +} + +// 0x50 Byte struct containing the information for controller mappings +#[derive(Debug)] +#[repr(C)] +pub struct ControllerMapping { + pub gc_l: InputKind, + pub gc_r: InputKind, + pub gc_z: InputKind, + pub gc_dup: InputKind, + pub gc_dlr: InputKind, + pub gc_ddown: InputKind, + pub gc_a: InputKind, + pub gc_b: InputKind, + pub gc_cstick: InputKind, + pub gc_y: InputKind, + pub gc_x: InputKind, + pub gc_rumble: bool, + pub gc_absmash: bool, + pub gc_tapjump: bool, + pub gc_sensitivity: u8, + // 0xF + pub pro_l: InputKind, + pub pro_r: InputKind, + pub pro_zl: InputKind, + pub pro_zr: InputKind, + pub pro_dup: InputKind, + pub pro_dlr: InputKind, + pub pro_ddown: InputKind, + pub pro_a: InputKind, + pub pro_b: InputKind, + pub pro_cstick: InputKind, + pub pro_x: InputKind, + pub pro_y: InputKind, + pub pro_rumble: bool, + pub pro_absmash: bool, + pub pro_tapjump: bool, + pub pro_sensitivity: u8, + // 0x1F + pub joy_shoulder: InputKind, + pub joy_zshoulder: InputKind, + pub joy_sl: InputKind, + pub joy_sr: InputKind, + pub joy_up: InputKind, + pub joy_right: InputKind, + pub joy_left: InputKind, + pub joy_down: InputKind, + pub joy_rumble: bool, + pub joy_absmash: bool, + pub joy_tapjump: bool, + pub joy_sensitivity: u8, + // 0x2B + pub _2b: u8, + pub _2c: u8, + pub _2d: u8, + pub _2e: u8, + pub _2f: u8, + pub _30: u8, + pub _31: u8, + pub _32: u8, + pub is_absmash: bool, + pub _34: [u8; 0x1C] +} + + +//type Buttons = u32; // may need to actually implement (like label and such)? Not for now though +bitflags! { + pub struct Buttons: u32 { + const NONE = 0x0; // does adding this cause problems? + const ATTACK = 0x1; + const SPECIAL = 0x2; + const JUMP = 0x4; + const GUARD = 0x8; + const CATCH = 0x10; + const SMASH = 0x20; + const JUMP_MINI = 0x40; + const CSTICK_ON = 0x80; + const STOCK_SHARE = 0x100; + const ATTACK_RAW = 0x200; + const APPEAL_HI = 0x400; + const SPECIAL_RAW = 0x800; + const APPEAL_LW = 0x1000; + const APPEAL_SL = 0x2000; + const APPEAL_SR = 0x4000; + const FLICK_JUMP = 0x8000; + const GUARD_HOLD = 0x10000; + const SPECIAL_RAW2 = 0x20000; + } +} + +// Controller class used internally by the game +#[repr(C)] +pub struct Controller { + pub vtable: *const u64, + pub current_buttons: ButtonBitfield, + pub previous_buttons: ButtonBitfield, + pub left_stick_x: f32, + pub left_stick_y: f32, + pub left_trigger: f32, + pub _left_padding: u32, + pub right_stick_x: f32, + pub right_stick_y: f32, + pub right_trigger: f32, + pub _right_padding: u32, + pub gyro: [f32; 4], + pub button_timespan: AutorepeatInfo, + pub lstick_timespan: AutorepeatInfo, + pub rstick_timespan: AutorepeatInfo, + pub just_down: ButtonBitfield, + pub just_release: ButtonBitfield, + pub autorepeat_keys: u32, + pub autorepeat_threshold: u32, + pub autorepeat_initial_press_threshold: u32, + pub style: ControllerStyle, + pub controller_id: u32, + pub primary_controller_color1: u32, + pub primary_controller_color2: u32, + pub secondary_controller_color1: u32, + pub secondary_controller_color2: u32, + pub led_pattern: u8, + pub button_autorepeat_initial_press: bool, + pub lstick_autorepeat_initial_press: bool, + pub rstick_autorepeat_initial_press: bool, + pub is_valid_controller: bool, + pub _x_b9: [u8; 2], + pub is_connected: bool, + pub is_left_connected: bool, + pub is_right_connected: bool, + pub is_wired: bool, + pub is_left_wired: bool, + pub is_right_wired: bool, + pub _x_c1: [u8; 3], + pub npad_number: u32, + pub _x_c8: [u8; 8] +} + +// SomeControllerStruct used in hooked function - need to ask blujay what this is again +#[repr(C)] +pub struct SomeControllerStruct { + padding: [u8; 0x10], + controller: &'static mut Controller +} + +// Define struct used for final controller inputs +#[derive(Copy, Clone)] +#[repr(C)] +pub struct MappedInputs { + pub buttons: Buttons, + pub lstick_x: i8, + pub lstick_y: i8, + pub rstick_x: i8, + pub rstick_y: i8 +} + +impl MappedInputs { // pub needed? + pub fn default() -> MappedInputs { + MappedInputs { + buttons: Buttons::NONE, + lstick_x: 0, + lstick_y: 0, + rstick_x: 0, + rstick_y: 0 + } + } +} + +// Final Structure containing all input recording slots, menu options, and save states. +// 5 Input Recording Slots should be fine for now for most mix up scenarios +// When loading a "scenario", we want to load all menu options (with maybe overrides in the config?), load savestate(s), and load input recording slots. +// If we have submenus for input recording slots, we need to get that info as well, and we want to apply saved damage from save states to the menu. +// Damage range seems to be saved in menu for range of damage, so that's taken care of with menu. + +#[derive(Clone)] +#[repr(C)] +pub struct Scenario { + pub record_slots: Vec>, + pub starting_statuses: Vec, + pub menu: TrainingModpackMenu, + pub save_states: Vec, + pub player_char: i32, // fighter_kind + pub cpu_char: i32, // fighter_kind + pub stage: i32, // index of stage, but -1 = random + pub title: String, + pub description: String, + pub mod_version: String, + pub smash_version: String, + // depending on version, we need to modify newly added menu options, so that regardless of their defaults they reflect the previous version to minimize breakage of old scenarios + // we may also add more scenario parts to the struct in the future etc. + // pub screenshot: image???? + // datetime? + // author? + // mirroring? + +} + +impl Scenario { + pub fn default() -> Scenario { + Scenario { + record_slots: vec![vec![MappedInputs::default(); 600]; 5], + starting_statuses: vec![0], + menu: crate::common::consts::DEFAULTS_MENU, + save_states: vec![default_save_state!(); 5], + player_char: 0, + cpu_char: 0, + stage: -1, // index of stage, but -1 = random/any stage + title: "Scenario Title".to_string(), + description: "Description...".to_string(), + mod_version: CURRENT_VERSION.to_string(), + smash_version: smash_version(), + } + } +} \ No newline at end of file diff --git a/src/training/ledge.rs b/src/training/ledge.rs index 448dbb8..0aca3e2 100644 --- a/src/training/ledge.rs +++ b/src/training/ledge.rs @@ -5,6 +5,7 @@ use crate::common::consts::*; use crate::common::*; use crate::training::frame_counter; use crate::training::mash; +use crate::training::input_record; const NOT_SET: u32 = 9001; static mut LEDGE_DELAY: u32 = NOT_SET; @@ -74,13 +75,29 @@ pub unsafe fn force_option(module_accessor: &mut app::BattleObjectModuleAccessor let flag_cliff = WorkModule::is_flag(module_accessor, *FIGHTER_INSTANCE_WORK_ID_FLAG_CATCH_CLIFF); let current_frame = MotionModule::frame(module_accessor) as i32; - let should_buffer = (LEDGE_DELAY == 0) && (current_frame == 19) && (!flag_cliff); + let status_kind = StatusModule::status_kind(module_accessor) as i32; + let should_buffer_playback = (LEDGE_DELAY == 0) && (current_frame == 13); // 18 - 5 of buffer + let should_buffer; + let prev_status_kind = StatusModule::prev_status_kind(module_accessor, 0); + + if status_kind == *FIGHTER_STATUS_KIND_CLIFF_WAIT && prev_status_kind == *FIGHTER_STATUS_KIND_CLIFF_CATCH { // For regular ledge grabs, we were just in catch and want to buffer on this frame + should_buffer = (LEDGE_DELAY == 0) && (current_frame == 19) && (!flag_cliff); + } else if status_kind == *FIGHTER_STATUS_KIND_CLIFF_WAIT { // otherwise we're in "wait" from grabbing with lasso, so we want to buffer on frame + should_buffer = (LEDGE_DELAY == 0) && (current_frame == 18) && (flag_cliff); + } else { + should_buffer = false; + } if !WorkModule::is_enable_transition_term( module_accessor, *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_CLIFF_ATTACK, ) { // Not able to take any action yet + // We buffer playback on frame 18 because we don't change status this frame from inputting on next frame; do we need to do one earlier for lasso? + if should_buffer_playback && LEDGE_CASE == LedgeOption::PLAYBACK && MENU.record_trigger != RecordTrigger::Ledge && MENU.ledge_delay != LongDelay::empty() { + input_record::playback_ledge(); + return; + } // This check isn't reliable for buffered options in time, so don't return if we need to buffer an option this frame if !should_buffer { return; @@ -99,7 +116,13 @@ pub unsafe fn force_option(module_accessor: &mut app::BattleObjectModuleAccessor let status = LEDGE_CASE.into_status().unwrap_or(0); - StatusModule::change_status_request_from_script(module_accessor, status, true); + if LEDGE_CASE == LedgeOption::PLAYBACK { + if MENU.record_trigger != RecordTrigger::Ledge { + input_record::playback(); + } + } else { + StatusModule::change_status_request_from_script(module_accessor, status, true); + } if MENU.mash_triggers.contains(MashTrigger::LEDGE) { if LEDGE_CASE == LedgeOption::NEUTRAL && MENU.ledge_neutral_override != Action::empty() { @@ -124,6 +147,7 @@ pub unsafe fn is_enable_transition_term( if !is_operation_cpu(&mut *_module_accessor) { return None; } + // Only handle ledge scenarios from menu if StatusModule::status_kind(_module_accessor) != *FIGHTER_STATUS_KIND_CLIFF_WAIT || MENU.ledge_state == LedgeOption::empty() @@ -131,9 +155,8 @@ pub unsafe fn is_enable_transition_term( return None; } - // Disallow the default cliff-climb if we are waiting - if (LEDGE_CASE == LedgeOption::WAIT - || frame_counter::get_frame_count(LEDGE_DELAY_COUNTER) < LEDGE_DELAY) + // Disallow the default cliff-climb if we are waiting or we wait as part of a recording + if (LEDGE_CASE == LedgeOption::WAIT || frame_counter::get_frame_count(LEDGE_DELAY_COUNTER) < LEDGE_DELAY) && term == *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_CLIFF_CLIMB { return Some(false); @@ -145,8 +168,24 @@ pub fn get_command_flag_cat(module_accessor: &mut app::BattleObjectModuleAccesso if !is_operation_cpu(module_accessor) { return; } - + + // Set up check for beginning of ledge grab unsafe { + let current_frame = MotionModule::frame(module_accessor) as i32; + // Frame 18 is right before actionability for cliff catch + let just_grabbed_ledge = (StatusModule::status_kind(module_accessor) as i32 == *FIGHTER_STATUS_KIND_CLIFF_CATCH) && current_frame == 18; + // Needs to be a frame earlier for lasso grabs + let just_lassoed_ledge = (StatusModule::status_kind(module_accessor) as i32 == *FIGHTER_STATUS_KIND_CLIFF_WAIT) && current_frame == 17; + // Begin recording on ledge if this is the recording trigger + if (just_grabbed_ledge || just_lassoed_ledge) && MENU.record_trigger == RecordTrigger::Ledge && !input_record::is_standby() { + input_record::lockout_record(); + return; + } + // Behave normally if we're playing back recorded inputs or controlling the cpu + if input_record::is_playback() { + return; + } + if MENU.ledge_state == LedgeOption::empty() { return; } diff --git a/src/training/mash.rs b/src/training/mash.rs index fc73463..a4fef5e 100644 --- a/src/training/mash.rs +++ b/src/training/mash.rs @@ -7,6 +7,7 @@ use crate::training::character_specific; use crate::training::fast_fall; use crate::training::frame_counter; use crate::training::full_hop; +use crate::training::input_record; use crate::training::shield; use crate::training::{attack_angle, save_states}; @@ -22,6 +23,10 @@ static mut FALLING_AERIAL: bool = false; static mut AERIAL_DELAY_COUNTER: usize = 0; static mut AERIAL_DELAY: u32 = 0; +pub fn is_playback_queued() -> bool { + get_current_buffer() == Action::PLAYBACK +} + pub fn buffer_action(action: Action) { unsafe { if !QUEUE.is_empty() { @@ -32,6 +37,20 @@ pub fn buffer_action(action: Action) { if action == Action::empty() { return; } + + // We want to allow for triggering a mash to end playback for neutral playbacks, but not for SDI/disadv playbacks + unsafe { + // exit playback if we want to perform mash actions out of it + // TODO: Figure out some way to deal with trying to playback into another playback + if MENU.playback_mash == OnOff::On && !input_record::is_recording() && !input_record::is_standby() && !is_playback_queued() && action != Action::PLAYBACK { + println!("Stopping mash playback for menu option!"); + input_record::stop_playback(); + } + // if we don't want to leave playback on mash actions, then don't perform the mash + if input_record::is_playback() { + return; + } + } attack_angle::roll_direction(); @@ -71,7 +90,7 @@ pub fn get_current_buffer() -> Action { } } -fn reset() { +pub fn reset() { unsafe { QUEUE.pop(); } @@ -325,6 +344,10 @@ unsafe fn perform_action(module_accessor: &mut app::BattleObjectModuleAccessor) get_flag(module_accessor, *FIGHTER_STATUS_KIND_DASH, 0) } + Action::PLAYBACK => { + // Because these status changes take place after we would receive input from the controller, we need to queue input playback 1 frame before we can act + return 0; // We don't ever want to explicitly provide any command flags here; if we're trying to do input recording, the playback handles it all + } _ => get_attack_flag(module_accessor, action), } } diff --git a/src/training/mod.rs b/src/training/mod.rs index c835662..423372e 100644 --- a/src/training/mod.rs +++ b/src/training/mod.rs @@ -30,6 +30,7 @@ pub mod ui; mod air_dodge_direction; mod attack_angle; mod character_specific; +mod input_recording; mod fast_fall; mod full_hop; pub mod input_delay; @@ -77,6 +78,9 @@ pub unsafe fn handle_get_attack_air_kind( return ori; } + if input_record::is_playback() { + return ori; + } mash::get_attack_air_kind(module_accessor).unwrap_or(ori) } @@ -192,6 +196,15 @@ pub unsafe fn get_stick_dir(module_accessor: &mut app::BattleObjectModuleAccesso return ori; } + let situation_kind = StatusModule::situation_kind(module_accessor); + if situation_kind == *SITUATION_KIND_CLIFF { + return ori; + } + + if input_record::is_playback() { + return ori; + } + attack_angle::mod_get_stick_dir(module_accessor).unwrap_or(ori) } @@ -270,7 +283,7 @@ pub unsafe fn handle_change_motion( #[skyline::hook(replace = WorkModule::is_enable_transition_term)] pub unsafe fn handle_is_enable_transition_term( - module_accessor: *mut app::BattleObjectModuleAccessor, + module_accessor: &mut app::BattleObjectModuleAccessor, transition_term: i32, ) -> bool { let ori = original!()(module_accessor, transition_term); @@ -601,5 +614,6 @@ pub fn training_mods() { buff::init(); items::init(); tech::init(); + input_record::init(); ui::init(); } diff --git a/src/training/save_states.rs b/src/training/save_states.rs index 8a51970..bfd77ba 100644 --- a/src/training/save_states.rs +++ b/src/training/save_states.rs @@ -18,10 +18,13 @@ use crate::common::consts::get_random_int; use crate::common::consts::FighterId; use crate::common::consts::OnOff; use crate::common::consts::SaveStateMirroring; +use crate::common::consts::RecordTrigger; +//TODO: Cleanup above use crate::common::consts::SAVE_STATES_TOML_PATH; use crate::common::is_dead; use crate::common::MENU; use crate::is_operation_cpu; +use crate::training::input_record; use crate::training::buff; use crate::training::character_specific::steve; use crate::training::charge::{self, ChargeState}; @@ -54,7 +57,7 @@ extern "C" { } #[derive(Serialize, Deserialize, PartialEq, Copy, Clone, Debug)] -enum SaveState { +pub enum SaveState { Save, NoAction, KillPlayer, @@ -65,18 +68,19 @@ enum SaveState { } #[derive(Serialize, Deserialize, Copy, Clone, Debug)] -struct SavedState { - x: f32, - y: f32, - percent: f32, - lr: f32, - situation_kind: i32, - state: SaveState, - fighter_kind: i32, - charge: ChargeState, - steve_state: Option, +pub struct SavedState { + pub x: f32, + pub y: f32, + pub percent: f32, + pub lr: f32, + pub situation_kind: i32, + pub state: SaveState, + pub fighter_kind: i32, + pub charge: ChargeState, + pub steve_state: Option, } +#[macro_export] macro_rules! default_save_state { () => { SavedState { @@ -393,7 +397,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) && save_state.state == NoAction && is_dead(module_accessor); let mut triggered_reset: bool = false; - if !is_operation_cpu(module_accessor) { + if !is_operation_cpu(module_accessor) && !fighter_is_nana { triggered_reset = button_config::combo_passes_exclusive( module_accessor, button_config::ButtonCombo::LoadState, @@ -412,16 +416,15 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) save_state_cpu(slot).state = KillPlayer; } MIRROR_STATE = should_mirror(); + // end input recording playback + input_record::stop_playback(); return; } // Kill the fighter and move them to camera bounds - if save_state.state == KillPlayer { + if save_state.state == KillPlayer && !fighter_is_nana { on_ptrainer_death(module_accessor); - if !is_dead(module_accessor) && - // Don't kill Nana again, since she already gets killed by the game from Popo's death - !fighter_is_nana - { + if !is_dead(module_accessor) { on_death(fighter_kind, module_accessor); StatusModule::change_status_request(module_accessor, *FIGHTER_STATUS_KIND_DEAD, false); } @@ -595,6 +598,15 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) save_state.state = NanaPosMove; } + // if we're recording on state load, record + if MENU.record_trigger == RecordTrigger::SaveState { + input_record::lockout_record(); + } + // otherwise, begin input recording playback if selected + else if MENU.save_state_playback == OnOff::On { + input_record::playback(); + } + return; } diff --git a/src/training/shield.rs b/src/training/shield.rs index 607825e..35cd347 100644 --- a/src/training/shield.rs +++ b/src/training/shield.rs @@ -8,8 +8,7 @@ use smash::lua2cpp::L2CFighterCommon; use crate::common::consts::*; use crate::common::*; -use crate::training::mash; -use crate::training::{frame_counter, save_states}; +use crate::training::{mash, frame_counter, save_states, input_record}; // How many hits to hold shield until picking an Out Of Shield option static mut MULTI_HIT_OFFSET: u32 = 0; @@ -106,7 +105,7 @@ pub unsafe fn get_param_float( param_type: u64, param_hash: u64, ) -> Option { - if !is_operation_cpu(module_accessor) { + if !is_operation_cpu(module_accessor) || input_record::is_playback() { // shield normally during playback return None; } @@ -177,6 +176,12 @@ pub unsafe fn param_installer() { } pub fn should_hold_shield(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + // Don't let shield override input recording playback + unsafe { + if input_record::is_playback() || input_record::is_standby() { + return false; + } + } // Mash shield if mash::request_shield(module_accessor) { return true; @@ -238,13 +243,6 @@ unsafe fn mod_handle_sub_guard_cont(fighter: &mut L2CFighterCommon) { return; } - if MENU.mash_triggers.contains(MashTrigger::SHIELDSTUN) { - if MENU.shieldstun_override == Action::empty() { - mash::external_buffer_menu_mash(MENU.mash_state.get_random()) - } else { - mash::external_buffer_menu_mash(MENU.shieldstun_override.get_random()) - } - } let action = mash::get_current_buffer(); if handle_escape_option(fighter, module_accessor) { @@ -358,6 +356,19 @@ fn needs_oos_handling_drop_shield() -> bool { return true; } + // Make sure we only flicker shield when Airdodge and Shield mash options are selected + if action == Action::AIR_DODGE { + let shield_state; + unsafe { + shield_state = &MENU.shield_state; + } + // If we're supposed to be holding shield, let airdodge make us drop shield + if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) { + suspend_shield(Action::AIR_DODGE); + } + return true; + } + if action == Action::SHIELD { let shield_state; unsafe { diff --git a/src/training/ui/menu.rs b/src/training/ui/menu.rs index ec1a43e..b49ed77 100644 --- a/src/training/ui/menu.rs +++ b/src/training/ui/menu.rs @@ -112,10 +112,11 @@ unsafe fn render_submenu_page(app: &App, root_pane: &mut Pane) { // Hide all icon images, and strategically mark the icon that // corresponds with a particular button to be visible. submenu_ids.iter().for_each(|id| { - menu_button - .find_pane_by_name_recursive(id) - .unwrap_or_else(|| panic!("Unable to find icon {} in layout.arc", id)) - .set_visible(id == &submenu.submenu_id); + let pane = menu_button.find_pane_by_name_recursive(id); + match pane { + Some(p) => p.set_visible(id == &submenu.submenu_id), + None => (), + } }); menu_button @@ -192,10 +193,11 @@ unsafe fn render_toggle_page(app: &App, root_pane: &mut Pane) { let submenu_ids = app.submenu_ids(); submenu_ids.iter().for_each(|id| { - menu_button - .find_pane_by_name_recursive(id) - .unwrap_or_else(|| panic!("Unable to find icon {} in layout.arc", id)) - .set_visible(false); + let pane = menu_button.find_pane_by_name_recursive(id); + match pane { + Some(p) => p.set_visible(false), + None => (), + } }); title_text.set_text_string(name); diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index 90935e8..7087fc9 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -1,731 +1,793 @@ -#[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 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, -} - -#[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_str(s: &str) -> SubMenuType { - match s { - "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, - 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(), -}; - -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<'a> { - pub toggle_value: u32, - pub toggle_title: &'a str, - pub checked: bool, -} - -#[derive(Clone, Serialize)] -pub struct SubMenu<'a> { - pub submenu_title: &'a str, - pub submenu_id: &'a str, - pub help_text: &'a str, - pub is_single_option: bool, - pub toggles: Vec>, - pub slider: Option, - pub _type: &'a str, -} - -impl<'a> SubMenu<'a> { - pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: &'a str, checked: bool) { - self.toggles.push(Toggle { - toggle_value, - toggle_title, - checked, - }); - } - pub fn new_with_toggles( - submenu_title: &'a str, - submenu_id: &'a str, - help_text: &'a str, - is_single_option: bool, - initial_value: &u32, - ) -> SubMenu<'a> { - let mut instance = SubMenu { - submenu_title: submenu_title, - submenu_id: submenu_id, - help_text: help_text, - is_single_option: is_single_option, - toggles: Vec::new(), - slider: None, - _type: "toggle", - }; - - let values = T::to_toggle_vals(); - let titles = T::to_toggle_strs(); - 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], 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: &'a str, - submenu_id: &'a str, - help_text: &'a str, - initial_lower_value: &u32, - initial_upper_value: &u32, - ) -> SubMenu<'a> { - 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", - } - } -} - -#[derive(Serialize, Clone)] -pub struct Tab<'a> { - pub tab_id: &'a str, - pub tab_title: &'a str, - pub tab_submenus: Vec>, -} - -impl<'a> Tab<'a> { - pub fn add_submenu_with_toggles( - &mut self, - submenu_title: &'a str, - submenu_id: &'a str, - help_text: &'a str, - is_single_option: bool, - initial_value: &u32, - ) { - self.tab_submenus.push(SubMenu::new_with_toggles::( - submenu_title, - submenu_id, - help_text, - is_single_option, - initial_value, - )); - } - - pub fn add_submenu_with_slider( - &mut self, - submenu_title: &'a str, - submenu_id: &'a str, - help_text: &'a str, - initial_lower_value: &u32, - initial_upper_value: &u32, - ) { - self.tab_submenus.push(SubMenu::new_with_slider::( - submenu_title, - submenu_id, - help_text, - initial_lower_value, - initial_upper_value, - )) - } -} - -#[derive(Serialize, Clone)] -pub struct UiMenu<'a> { - pub tabs: Vec>, -} - -pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu<'static> { - let mut overall_menu = UiMenu { tabs: Vec::new() }; - - let mut mash_tab = Tab { - tab_id: "mash", - tab_title: "Mash Settings", - tab_submenus: Vec::new(), - }; - mash_tab.add_submenu_with_toggles::( - "Mash Toggles", - "mash_state", - "Mash Toggles: Actions to be performed as soon as possible", - false, - &(menu.mash_state.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Followup Toggles", - "follow_up", - "Followup Toggles: Actions to be performed after a Mash option", - false, - &(menu.follow_up.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Mash Triggers", - "mash_triggers", - "Mash triggers: Configure what causes the CPU to perform a Mash option", - false, - &(menu.mash_triggers.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Attack Angle", - "attack_angle", - "Attack Angle: For attacks that can be angled, such as some forward tilts", - false, - &(menu.attack_angle.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Throw Options", - "throw_state", - "Throw Options: Throw to be performed when a grab is landed", - false, - &(menu.throw_state.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Throw Delay", - "throw_delay", - "Throw Delay: How many frames to delay the throw option", - false, - &(menu.throw_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Pummel Delay", - "pummel_delay", - "Pummel Delay: How many frames after a grab to wait before starting to pummel", - false, - &(menu.pummel_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Falling Aerials", - "falling_aerials", - "Falling Aerials: Should aerials be performed when rising or when falling", - false, - &(menu.falling_aerials.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Full Hop", - "full_hop", - "Full Hop: Should the CPU perform a full hop or a short hop", - false, - &(menu.full_hop.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Aerial Delay", - "aerial_delay", - "Aerial Delay: How long to delay a Mash aerial attack", - false, - &(menu.aerial_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Fast Fall", - "fast_fall", - "Fast Fall: Should the CPU fastfall during a jump", - false, - &(menu.fast_fall.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Fast Fall Delay", - "fast_fall_delay", - "Fast Fall Delay: How many frames the CPU should delay their fastfall", - false, - &(menu.fast_fall_delay.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "OoS Offset", - "oos_offset", - "OoS Offset: How many times the CPU shield can be hit before performing a Mash option", - false, - &(menu.oos_offset.bits()), - ); - mash_tab.add_submenu_with_toggles::( - "Reaction Time", - "reaction_time", - "Reaction Time: How many frames to delay before performing a mash option", - false, - &(menu.reaction_time.bits()), - ); - overall_menu.tabs.push(mash_tab); - - let mut override_tab = Tab { - tab_id: "override", - tab_title: "Override Settings", - tab_submenus: Vec::new(), - }; - override_tab.add_submenu_with_toggles::( - "Ledge Neutral Getup", - "ledge_neutral_override", - "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge", - false, - &(menu.ledge_neutral_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Roll", - "ledge_roll_override", - "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge", - false, - &(menu.ledge_roll_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Jump", - "ledge_jump_override", - "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge", - false, - &(menu.ledge_jump_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Attack", - "ledge_attack_override", - "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge", - false, - &(menu.ledge_attack_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Tech Action", - "tech_action_override", - "Tech Action Override: Mash Actions to be performed after any tech action", - false, - &(menu.tech_action_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Clatter", - "clatter_override", - "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab, bury, etc)", - false, - &(menu.clatter_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Tumble", - "tumble_override", - "Tumble Override: Mash Actions to be performed after exiting a tumble state", - false, - &(menu.tumble_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Hitstun", - "hitstun_override", - "Hitstun Override: Mash Actions to be performed after exiting a hitstun state", - false, - &(menu.hitstun_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Parry", - "parry_override", - "Parry Override: Mash Actions to be performed after a parry", - false, - &(menu.parry_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Shieldstun", - "shieldstun_override", - "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state", - false, - &(menu.shieldstun_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Footstool", - "footstool_override", - "Footstool Override: Mash Actions to be performed after exiting a footstool state", - false, - &(menu.footstool_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Landing", - "landing_override", - "Landing Override: Mash Actions to be performed after landing on the ground", - false, - &(menu.landing_override.bits()), - ); - override_tab.add_submenu_with_toggles::( - "Ledge Trump", - "trump_override", - "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state", - false, - &(menu.trump_override.bits()), - ); - overall_menu.tabs.push(override_tab); - - let mut defensive_tab = Tab { - tab_id: "defensive", - tab_title: "Defensive Settings", - tab_submenus: Vec::new(), - }; - defensive_tab.add_submenu_with_toggles::( - "Airdodge Direction", - "air_dodge_dir", - "Airdodge Direction: Direction to angle airdodges", - false, - &(menu.air_dodge_dir.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "DI Direction", - "di_state", - "DI Direction: Direction to angle the directional influence during hitlag", - false, - &(menu.di_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "SDI Direction", - "sdi_state", - "SDI Direction: Direction to angle the smash directional influence during hitlag", - false, - &(menu.sdi_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "SDI Strength", - "sdi_strength", - "SDI Strength: Relative strength of the smash directional influence inputs", - true, - &(menu.sdi_strength as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Clatter Strength", - "clatter_strength", - "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc.", - true, - &(menu.clatter_strength as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Ledge Options", - "ledge_state", - "Ledge Options: Actions to be taken when on the ledge", - false, - &(menu.ledge_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Ledge Delay", - "ledge_delay", - "Ledge Delay: How many frames to delay the ledge option", - false, - &(menu.ledge_delay.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Tech Options", - "tech_state", - "Tech Options: Actions to take when slammed into a hard surface", - false, - &(menu.tech_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Mistech Options", - "miss_tech_state", - "Mistech Options: Actions to take after missing a tech", - false, - &(menu.miss_tech_state.bits()), - ); - defensive_tab.add_submenu_with_toggles::( - "Shield Toggles", - "shield_state", - "Shield Toggles: CPU Shield Behavior", - true, - &(menu.shield_state as u32), - ); - defensive_tab.add_submenu_with_toggles::( - "Shield Tilt", - "shield_tilt", - "Shield Tilt: Direction to tilt the shield", - false, // TODO: Should this be true? - &(menu.shield_tilt.bits()), - ); - - defensive_tab.add_submenu_with_toggles::( - "Crouch", - "crouch", - "Crouch: Have the CPU crouch when on the ground", - true, - &(menu.crouch as u32), - ); - overall_menu.tabs.push(defensive_tab); - - let mut save_state_tab = Tab { - tab_id: "save_state", - tab_title: "Save States", - tab_submenus: Vec::new(), - }; - save_state_tab.add_submenu_with_toggles::( - "Mirroring", - "save_state_mirroring", - "Mirroring: Flips save states in the left-right direction across the stage center", - true, - &(menu.save_state_mirroring as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Auto Save States", - "save_state_autoload", - "Auto Save States: Load save state when any fighter dies", - true, - &(menu.save_state_autoload as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Save Dmg (CPU)", - "save_damage_cpu", - "Save Damage: Should save states retain CPU damage", - true, - &(menu.save_damage_cpu.bits()), - ); - save_state_tab.add_submenu_with_slider::( - "Dmg Range (CPU)", - "save_damage_limits_cpu", - "Limits on random damage to apply to the CPU when loading a save state", - &(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)", - "save_damage_player", - "Save Damage: Should save states retain player damage", - true, - &(menu.save_damage_player.bits() as u32), - ); - save_state_tab.add_submenu_with_slider::( - "Dmg Range (Player)", - "save_damage_limits_player", - "Limits on random damage to apply to the player when loading a save state", - &(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", - "save_state_enable", - "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.", - true, - &(menu.save_state_enable as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Save State Slot", - "save_state_slot", - "Save State Slot: Save and load states from different slots.", - true, - &(menu.save_state_slot as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Randomize Slots", - "randomize_slots", - "Randomize Slots: Randomize slot when loading save state.", - true, - &(menu.randomize_slots as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Character Item", - "character_item", - "Character Item: The item to give to the player's fighter when loading a save state", - true, - &(menu.character_item as u32), - ); - save_state_tab.add_submenu_with_toggles::( - "Buff Options", - "buff_state", - "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state", - false, - &(menu.buff_state.bits()), - ); - overall_menu.tabs.push(save_state_tab); - - let mut misc_tab = Tab { - tab_id: "misc", - tab_title: "Misc Settings", - tab_submenus: Vec::new(), - }; - misc_tab.add_submenu_with_toggles::( - "Frame Advantage", - "frame_advantage", - "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable", - true, - &(menu.frame_advantage as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Hitbox Visualization", - "hitbox_vis", - "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)", - true, - &(menu.hitbox_vis as u32), - ); - misc_tab.add_submenu_with_toggles::( - "Input Delay", - "input_delay", - "Input Delay: Frames to delay player inputs by", - true, - &(menu.input_delay.bits()), - ); - misc_tab.add_submenu_with_toggles::( - "Stage Hazards", - "stage_hazards", - "Stage Hazards: Turn stage hazards on/off", - true, - &(menu.stage_hazards as u32), - ); - misc_tab.add_submenu_with_toggles::( - "HUD", - "hud", - "HUD: Show/hide elements of the UI", - true, - &(menu.hud as u32), - ); - overall_menu.tabs.push(misc_tab); - - overall_menu -} +#[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 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 save_state_playback: OnOff, + pub recording_slot: RecordSlot, + pub playback_slot: PlaybackSlot, + pub playback_mash: OnOff, + pub record_trigger: RecordTrigger, + pub hitstun_playback: HitstunPlayback, +} + +#[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_str(s: &str) -> SubMenuType { + match s { + "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, + 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(), + save_state_playback: OnOff::Off, + recording_slot: RecordSlot::S1, + playback_slot: PlaybackSlot::S1, + playback_mash: OnOff::On, + record_trigger: RecordTrigger::None, //Command? + hitstun_playback: HitstunPlayback::Hitstun, + // TODO: alphabetize? +}; + +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<'a> { + pub toggle_value: u32, + pub toggle_title: &'a str, + pub checked: bool, +} + +#[derive(Clone, Serialize)] +pub struct SubMenu<'a> { + pub submenu_title: &'a str, + pub submenu_id: &'a str, + pub help_text: &'a str, + pub is_single_option: bool, + pub toggles: Vec>, + pub slider: Option, + pub _type: &'a str, +} + +impl<'a> SubMenu<'a> { + pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: &'a str, checked: bool) { + self.toggles.push(Toggle { + toggle_value, + toggle_title, + checked, + }); + } + pub fn new_with_toggles( + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + is_single_option: bool, + initial_value: &u32, + ) -> SubMenu<'a> { + let mut instance = SubMenu { + submenu_title: submenu_title, + submenu_id: submenu_id, + help_text: help_text, + is_single_option: is_single_option, + toggles: Vec::new(), + slider: None, + _type: "toggle", + }; + + let values = T::to_toggle_vals(); + let titles = T::to_toggle_strs(); + 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], 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: &'a str, + submenu_id: &'a str, + help_text: &'a str, + initial_lower_value: &u32, + initial_upper_value: &u32, + ) -> SubMenu<'a> { + 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", + } + } +} + +#[derive(Serialize, Clone)] +pub struct Tab<'a> { + pub tab_id: &'a str, + pub tab_title: &'a str, + pub tab_submenus: Vec>, +} + +impl<'a> Tab<'a> { + pub fn add_submenu_with_toggles( + &mut self, + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + is_single_option: bool, + initial_value: &u32, + ) { + self.tab_submenus.push(SubMenu::new_with_toggles::( + submenu_title, + submenu_id, + help_text, + is_single_option, + initial_value, + )); + } + + pub fn add_submenu_with_slider( + &mut self, + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + initial_lower_value: &u32, + initial_upper_value: &u32, + ) { + self.tab_submenus.push(SubMenu::new_with_slider::( + submenu_title, + submenu_id, + help_text, + initial_lower_value, + initial_upper_value, + )) + } +} + +#[derive(Serialize, Clone)] +pub struct UiMenu<'a> { + pub tabs: Vec>, +} + +pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu<'static> { + let mut overall_menu = UiMenu { tabs: Vec::new() }; + + let mut mash_tab = Tab { + tab_id: "mash", + tab_title: "Mash Settings", + tab_submenus: Vec::new(), + }; + mash_tab.add_submenu_with_toggles::( + "Mash Toggles", + "mash_state", + "Mash Toggles: Actions to be performed as soon as possible", + false, + &(menu.mash_state.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Followup Toggles", + "follow_up", + "Followup Toggles: Actions to be performed after a Mash option", + false, + &(menu.follow_up.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Mash Triggers", + "mash_triggers", + "Mash triggers: Configure what causes the CPU to perform a Mash option", + false, + &(menu.mash_triggers.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Attack Angle", + "attack_angle", + "Attack Angle: For attacks that can be angled, such as some forward tilts", + false, + &(menu.attack_angle.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Throw Options", + "throw_state", + "Throw Options: Throw to be performed when a grab is landed", + false, + &(menu.throw_state.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Throw Delay", + "throw_delay", + "Throw Delay: How many frames to delay the throw option", + false, + &(menu.throw_delay.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Pummel Delay", + "pummel_delay", + "Pummel Delay: How many frames after a grab to wait before starting to pummel", + false, + &(menu.pummel_delay.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Falling Aerials", + "falling_aerials", + "Falling Aerials: Should aerials be performed when rising or when falling", + false, + &(menu.falling_aerials.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Full Hop", + "full_hop", + "Full Hop: Should the CPU perform a full hop or a short hop", + false, + &(menu.full_hop.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Aerial Delay", + "aerial_delay", + "Aerial Delay: How long to delay a Mash aerial attack", + false, + &(menu.aerial_delay.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Fast Fall", + "fast_fall", + "Fast Fall: Should the CPU fastfall during a jump", + false, + &(menu.fast_fall.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Fast Fall Delay", + "fast_fall_delay", + "Fast Fall Delay: How many frames the CPU should delay their fastfall", + false, + &(menu.fast_fall_delay.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "OoS Offset", + "oos_offset", + "OoS Offset: How many times the CPU shield can be hit before performing a Mash option", + false, + &(menu.oos_offset.bits()), + ); + mash_tab.add_submenu_with_toggles::( + "Reaction Time", + "reaction_time", + "Reaction Time: How many frames to delay before performing a mash option", + false, + &(menu.reaction_time.bits()), + ); + overall_menu.tabs.push(mash_tab); + + let mut override_tab = Tab { + tab_id: "override", + tab_title: "Override Settings", + tab_submenus: Vec::new(), + }; + override_tab.add_submenu_with_toggles::( + "Ledge Neutral Getup", + "ledge_neutral_override", + "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge", + false, + &(menu.ledge_neutral_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Ledge Roll", + "ledge_roll_override", + "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge", + false, + &(menu.ledge_roll_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Ledge Jump", + "ledge_jump_override", + "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge", + false, + &(menu.ledge_jump_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Ledge Attack", + "ledge_attack_override", + "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge", + false, + &(menu.ledge_attack_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Tech Action", + "tech_action_override", + "Tech Action Override: Mash Actions to be performed after any tech action", + false, + &(menu.tech_action_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Clatter", + "clatter_override", + "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab, bury, etc)", + false, + &(menu.clatter_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Tumble", + "tumble_override", + "Tumble Override: Mash Actions to be performed after exiting a tumble state", + false, + &(menu.tumble_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Hitstun", + "hitstun_override", + "Hitstun Override: Mash Actions to be performed after exiting a hitstun state", + false, + &(menu.hitstun_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Parry", + "parry_override", + "Parry Override: Mash Actions to be performed after a parry", + false, + &(menu.parry_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Shieldstun", + "shieldstun_override", + "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state", + false, + &(menu.shieldstun_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Footstool", + "footstool_override", + "Footstool Override: Mash Actions to be performed after exiting a footstool state", + false, + &(menu.footstool_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Landing", + "landing_override", + "Landing Override: Mash Actions to be performed after landing on the ground", + false, + &(menu.landing_override.bits()), + ); + override_tab.add_submenu_with_toggles::( + "Ledge Trump", + "trump_override", + "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state", + false, + &(menu.trump_override.bits()), + ); + overall_menu.tabs.push(override_tab); + + let mut defensive_tab = Tab { + tab_id: "defensive", + tab_title: "Defensive Settings", + tab_submenus: Vec::new(), + }; + defensive_tab.add_submenu_with_toggles::( + "Airdodge Direction", + "air_dodge_dir", + "Airdodge Direction: Direction to angle airdodges", + false, + &(menu.air_dodge_dir.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "DI Direction", + "di_state", + "DI Direction: Direction to angle the directional influence during hitlag", + false, + &(menu.di_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "SDI Direction", + "sdi_state", + "SDI Direction: Direction to angle the smash directional influence during hitlag", + false, + &(menu.sdi_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "SDI Strength", + "sdi_strength", + "SDI Strength: Relative strength of the smash directional influence inputs", + true, + &(menu.sdi_strength as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Clatter Strength", + "clatter_strength", + "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc.", + true, + &(menu.clatter_strength as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Ledge Options", + "ledge_state", + "Ledge Options: Actions to be taken when on the ledge", + false, + &(menu.ledge_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "Ledge Delay", + "ledge_delay", + "Ledge Delay: How many frames to delay the ledge option", + false, + &(menu.ledge_delay.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "Tech Options", + "tech_state", + "Tech Options: Actions to take when slammed into a hard surface", + false, + &(menu.tech_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "Mistech Options", + "miss_tech_state", + "Mistech Options: Actions to take after missing a tech", + false, + &(menu.miss_tech_state.bits()), + ); + defensive_tab.add_submenu_with_toggles::( + "Shield Toggles", + "shield_state", + "Shield Toggles: CPU Shield Behavior", + true, + &(menu.shield_state as u32), + ); + defensive_tab.add_submenu_with_toggles::( + "Shield Tilt", + "shield_tilt", + "Shield Tilt: Direction to tilt the shield", + false, // TODO: Should this be true? + &(menu.shield_tilt.bits()), + ); + + defensive_tab.add_submenu_with_toggles::( + "Crouch", + "crouch", + "Crouch: Have the CPU crouch when on the ground", + true, + &(menu.crouch as u32), + ); + overall_menu.tabs.push(defensive_tab); + + let mut save_state_tab = Tab { + tab_id: "save_state", + tab_title: "Save States", + tab_submenus: Vec::new(), + }; + save_state_tab.add_submenu_with_toggles::( + "Mirroring", + "save_state_mirroring", + "Mirroring: Flips save states in the left-right direction across the stage center", + true, + &(menu.save_state_mirroring as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Auto Save States", + "save_state_autoload", + "Auto Save States: Load save state when any fighter dies", + true, + &(menu.save_state_autoload as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Save Dmg (CPU)", + "save_damage_cpu", + "Save Damage: Should save states retain CPU damage", + true, + &(menu.save_damage_cpu.bits()), + ); + save_state_tab.add_submenu_with_slider::( + "Dmg Range (CPU)", + "save_damage_limits_cpu", + "Limits on random damage to apply to the CPU when loading a save state", + &(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)", + "save_damage_player", + "Save Damage: Should save states retain player damage", + true, + &(menu.save_damage_player.bits() as u32), + ); + save_state_tab.add_submenu_with_slider::( + "Dmg Range (Player)", + "save_damage_limits_player", + "Limits on random damage to apply to the player when loading a save state", + &(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", + "save_state_enable", + "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.", + true, + &(menu.save_state_enable as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Save State Slot", + "save_state_slot", + "Save State Slot: Save and load states from different slots.", + true, + &(menu.save_state_slot as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Randomize Slots", + "randomize_slots", + "Randomize Slots: Randomize slot when loading save state.", + true, + &(menu.randomize_slots as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Character Item", + "character_item", + "Character Item: The item to give to the player's fighter when loading a save state", + true, + &(menu.character_item as u32), + ); + save_state_tab.add_submenu_with_toggles::( + "Buff Options", + "buff_state", + "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state", + false, + &(menu.buff_state.bits()), + ); + overall_menu.tabs.push(save_state_tab); + + let mut misc_tab = Tab { + tab_id: "misc", + tab_title: "Misc Settings", + tab_submenus: Vec::new(), + }; + misc_tab.add_submenu_with_toggles::( + "Frame Advantage", + "frame_advantage", + "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable", + true, + &(menu.frame_advantage as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Hitbox Visualization", + "hitbox_vis", + "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)", + true, + &(menu.hitbox_vis as u32), + ); + misc_tab.add_submenu_with_toggles::( + "Input Delay", + "input_delay", + "Input Delay: Frames to delay player inputs by", + true, + &(menu.input_delay.bits()), + ); + misc_tab.add_submenu_with_toggles::( + "Stage Hazards", + "stage_hazards", + "Stage Hazards: Turn stage hazards on/off", + true, + &(menu.stage_hazards as u32), + ); + misc_tab.add_submenu_with_toggles::( + "HUD", + "hud", + "HUD: Show/hide elements of the UI", + true, + &(menu.hud as u32), + ); + overall_menu.tabs.push(misc_tab); + + let mut input_tab = Tab { + tab_id: "input", + tab_title: "Input Recording", + tab_submenus: Vec::new(), + }; + input_tab.add_submenu_with_toggles::( + "Save State Playback", + "save_state_playback", + "Save State Playback: Begin recorded input playback upon loading a save state", + true, + &(menu.save_state_playback as u32), + ); + input_tab.add_submenu_with_toggles::( + "Recording Slot", + "recording_slot", + "Recording Slot: Choose which slot to record into", + true, + &(menu.recording_slot as u32), + ); + input_tab.add_submenu_with_toggles::( // TODO: This menu should really be a submenu inside Action menus, probably want to be able to customize for each action + "Playback Slots", + "playback_slot", + "Playback Slots: Choose which slots to choose between for playback when this action is triggered", + false, + &(menu.playback_slot.bits() as u32), + ); + input_tab.add_submenu_with_toggles::( + "Mash Ends Playback", + "playback_mash", + "Mash Ends Playback: End input recording playback when a mash trigger occurs", + true, + &(menu.playback_mash as u32), + ); + input_tab.add_submenu_with_toggles::( + "Recording Trigger", + "record_trigger", + "Recording Trigger: What condition is required to begin recording input", + true, + &(menu.record_trigger as u32), + ); + input_tab.add_submenu_with_toggles::( + "Hitstun Playback Trigger", + "hitstun_playback", + "Hitstun Playback Trigger: When to begin playing back inputs on hitstun mash trigger", + true, + &(menu.hitstun_playback as u32), + ); + overall_menu.tabs.push(input_tab); + + overall_menu +} diff --git a/training_mod_consts/src/options.rs b/training_mod_consts/src/options.rs index 7cbee20..0c7c376 100644 --- a/training_mod_consts/src/options.rs +++ b/training_mod_consts/src/options.rs @@ -191,6 +191,7 @@ bitflags! { const JUMP = 0x4; const ATTACK = 0x8; const WAIT = 0x10; + const PLAYBACK = 0x20; } } @@ -204,6 +205,7 @@ impl LedgeOption { LedgeOption::JUMP => *FIGHTER_STATUS_KIND_CLIFF_JUMP1, LedgeOption::ATTACK => *FIGHTER_STATUS_KIND_CLIFF_ATTACK, LedgeOption::WAIT => *FIGHTER_STATUS_KIND_CLIFF_WAIT, + LedgeOption::PLAYBACK => *FIGHTER_STATUS_KIND_NONE, _ => return None, }) } @@ -219,6 +221,7 @@ impl LedgeOption { LedgeOption::JUMP => "Jump", LedgeOption::ATTACK => "Getup Attack", LedgeOption::WAIT => "Wait", + LedgeOption::PLAYBACK => "Input Playback", _ => return None, }) } @@ -411,6 +414,7 @@ bitflags! { // TODO: Make work const DASH = 0x0080_0000; const DASH_ATTACK = 0x0100_0000; + const PLAYBACK = 0x0200_0000; } } @@ -459,6 +463,7 @@ impl Action { Action::GRAB => "Grab", Action::DASH => "Dash", Action::DASH_ATTACK => "Dash Attack", + Action::PLAYBACK => "Input Playback", _ => return None, }) } @@ -1146,3 +1151,170 @@ impl ToggleTrait for SaveStateSlot { SaveStateSlot::iter().map(|i| i as u32).collect() } } + + +// Input Recording Slot +#[repr(u32)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] +pub enum RecordSlot { + S1 = 0x1, + S2 = 0x2, + S3 = 0x4, + S4 = 0x8, + S5 = 0x10, +} + +impl RecordSlot { + pub fn into_int(self) -> Option { // TODO: Do I need an into_int here? + #[cfg(feature = "smash")] + { + Some(match self { + RecordSlot::S1 => 1, + RecordSlot::S2 => 2, + RecordSlot::S3 => 3, + RecordSlot::S4 => 4, + RecordSlot::S5 => 5, + }) + } + + #[cfg(not(feature = "smash"))] + None + } + + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + RecordSlot::S1 => "Slot One", + RecordSlot::S2 => "Slot Two", + RecordSlot::S3 => "Slot Three", + RecordSlot::S4 => "Slot Four", + RecordSlot::S5 => "Slot Five", + }) + } +} + +impl ToggleTrait for RecordSlot { + fn to_toggle_strs() -> Vec<&'static str> { + RecordSlot::iter() + .map(|i| i.as_str().unwrap_or("")) + .collect() + } + + fn to_toggle_vals() -> Vec { + RecordSlot::iter().map(|i| i as u32).collect() + } +} + +// Input Playback Slot +bitflags! { + pub struct PlaybackSlot : u32 + { + const S1 = 0x1; + const S2 = 0x2; + const S3 = 0x4; + const S4 = 0x8; + const S5 = 0x10; + } +} + +impl PlaybackSlot { + pub fn into_int(self) -> Option { + #[cfg(feature = "smash")] + { + Some(match self { + PlaybackSlot::S1 => 1, + PlaybackSlot::S2 => 2, + PlaybackSlot::S3 => 3, + PlaybackSlot::S4 => 4, + PlaybackSlot::S5 => 5, + _ => return None, + }) + } + + #[cfg(not(feature = "smash"))] + None + } + + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + PlaybackSlot::S1 => "Slot One", + PlaybackSlot::S2 => "Slot Two", + PlaybackSlot::S3 => "Slot Three", + PlaybackSlot::S4 => "Slot Four", + PlaybackSlot::S5 => "Slot Five", + _ => return None, + }) + } +} + +extra_bitflag_impls! {PlaybackSlot} +impl_serde_for_bitflags!(PlaybackSlot); + +// Input Recording Trigger Type +#[repr(u32)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] +pub enum RecordTrigger { + None = 0, + Command = 0x1, + SaveState = 0x2, + Ledge = 0x4, +} + +impl RecordTrigger { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + RecordTrigger::None => "None", + RecordTrigger::Command => "Button Combination", + RecordTrigger::SaveState => "Save State Load", + RecordTrigger::Ledge => "Ledge Grab", + }) + } +} + +impl ToggleTrait for RecordTrigger { + fn to_toggle_strs() -> Vec<&'static str> { + RecordTrigger::iter() + .map(|i| i.as_str().unwrap_or("")) + .collect() + } + + fn to_toggle_vals() -> Vec { + RecordTrigger::iter().map(|i| i as u32).collect() + } +} + +// If doing input recording out of hitstun, when does playback begin after? +#[repr(u32)] +#[derive( + Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr, +)] +pub enum HitstunPlayback { // Should these start at 0? All of my new menu structs need some review, I'm just doing whatever atm + Hitstun = 0x1, + Hitstop = 0x2, + Instant = 0x4, +} + +impl HitstunPlayback { + pub fn as_str(self) -> Option<&'static str> { + Some(match self { + HitstunPlayback::Hitstun => "As Hitstun Ends", + HitstunPlayback::Hitstop => "As Hitstop Ends", + HitstunPlayback::Instant => "As Hitstop Begins", + }) + } +} + +impl ToggleTrait for HitstunPlayback { + fn to_toggle_strs() -> Vec<&'static str> { + HitstunPlayback::iter() + .map(|i| i.as_str().unwrap_or("")) + .collect() + } + + fn to_toggle_vals() -> Vec { + HitstunPlayback::iter().map(|i| i as u32).collect() + } +} \ No newline at end of file