mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2024-11-24 02:44:17 +00:00
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>
This commit is contained in:
parent
24ffba4e09
commit
62298ecbc1
15 changed files with 1844 additions and 857 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
2
src/training/input_recording/mod.rs
Normal file
2
src/training/input_recording/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
#[macro_use]
|
||||
pub mod structures;
|
308
src/training/input_recording/structures.rs
Normal file
308
src/training/input_recording/structures.rs
Normal file
|
@ -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<Vec<MappedInputs>>,
|
||||
pub starting_statuses: Vec<i32>,
|
||||
pub menu: TrainingModpackMenu,
|
||||
pub save_states: Vec<SavedState>,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<steve::SteveState>,
|
||||
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<steve::SteveState>,
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<f32> {
|
||||
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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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<u32> { // 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<u32> {
|
||||
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<u32> {
|
||||
#[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<u32> {
|
||||
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<u32> {
|
||||
HitstunPlayback::iter().map(|i| i as u32).collect()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue