2023-08-16 06:15:11 -07:00
use std ::cmp ::Ordering ;
2023-08-04 12:23:22 -05:00
use lazy_static ::lazy_static ;
use parking_lot ::Mutex ;
use skyline ::nn ::ui2d ::ResColor ;
use smash ::app ::{ lua_bind ::* , utility , BattleObjectModuleAccessor } ;
use smash ::lib ::lua_const ::* ;
2023-08-16 06:15:11 -07:00
use InputRecordState ::* ;
use PossessionState ::* ;
use crate ::common ::consts ::{ FighterId , HitstunPlayback , OnOff , RecordTrigger } ;
use crate ::common ::input ::* ;
2024-01-10 17:58:27 -05:00
use crate ::common ::offsets ::OFFSET_SET_CPU_CONTROLS ;
2023-08-16 06:15:11 -07:00
use crate ::common ::{ button_config , is_training_mode } ;
2023-09-01 13:53:04 -07:00
use crate ::common ::{
get_module_accessor , is_in_hitstun , is_in_shieldstun , try_get_module_accessor , MENU ,
} ;
2023-08-16 06:15:11 -07:00
use crate ::training ::mash ;
use crate ::training ::ui ::notifications ::{ clear_notifications , color_notification } ;
2023-12-02 12:02:43 -05:00
use crate ::{ error , warn } ;
2023-08-04 12:23:22 -05:00
#[ derive(PartialEq, Debug) ]
pub enum InputRecordState {
None ,
Pause ,
Record ,
Playback ,
}
#[ derive(PartialEq, Debug) ]
pub enum PossessionState {
Player ,
Cpu ,
Lockout ,
Standby ,
}
#[ derive(PartialEq, 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 ,
}
2023-09-01 13:53:04 -07:00
pub const STICK_NEUTRAL : f32 = 0.2 ;
pub const STICK_CLAMP_MULTIPLIER : f32 = 1.0 / 120.0 ; // 120.0 = CLAMP_MAX
2023-08-06 17:07:01 -07:00
const FINAL_RECORD_MAX : usize = 600 ; // Maximum length for input recording sequences (capacity)
2023-08-04 12:23:22 -05:00
const TOTAL_SLOT_COUNT : usize = 5 ; // Total number of input recording slots
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?
2023-08-06 17:07:01 -07:00
pub static mut CURRENT_FRAME_LENGTH : usize = 60 ;
2023-08-04 12:23:22 -05:00
lazy_static! {
static ref P1_FINAL_MAPPING : Mutex < [ [ MappedInputs ; FINAL_RECORD_MAX ] ; TOTAL_SLOT_COUNT ] > =
2023-08-10 07:28:13 -07:00
Mutex ::new ( [ [ { MappedInputs ::empty ( ) } ; FINAL_RECORD_MAX ] ; TOTAL_SLOT_COUNT ] ) ;
2023-08-06 17:07:01 -07:00
static ref P1_FRAME_LENGTH_MAPPING : Mutex < [ usize ; TOTAL_SLOT_COUNT ] > =
Mutex ::new ( [ 60 usize ; TOTAL_SLOT_COUNT ] ) ;
2023-08-04 12:23:22 -05:00
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
2023-12-02 12:02:43 -05:00
if MENU . hitstun_playback = = HitstunPlayback ::INSTANT {
2023-08-04 12:23:22 -05:00
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
2023-12-02 12:02:43 -05:00
if MENU . hitstun_playback = = HitstunPlayback ::HITSTOP
2023-08-04 12:23:22 -05:00
& & ! 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
2023-12-02 12:02:43 -05:00
if MENU . hitstun_playback = = HitstunPlayback ::HITSTUN & & can_transition ( cpu_module_accessor )
2023-08-04 12:23:22 -05:00
{
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 {
2023-08-07 11:59:47 -07:00
playback ( Some ( mash ::queued_playback_slot ( ) ) ) ;
2023-08-04 12:23:22 -05:00
}
}
// 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 ,
}
}
2023-09-21 13:25:53 -05:00
#[ allow(clippy::unnecessary_unwrap) ]
2023-09-15 13:37:53 -05:00
pub unsafe fn handle_recording ( ) {
2023-09-21 13:05:37 -05:00
let player_module_accessor = try_get_module_accessor ( FighterId ::Player ) ;
let cpu_module_accessor = try_get_module_accessor ( FighterId ::CPU ) ;
if player_module_accessor . is_some ( ) & & cpu_module_accessor . is_some ( ) {
handle_recording_for_fighter ( & mut * player_module_accessor . unwrap ( ) ) ;
handle_recording_for_fighter ( & mut * cpu_module_accessor . unwrap ( ) ) ;
}
2023-09-15 13:37:53 -05:00
}
unsafe fn handle_recording_for_fighter ( module_accessor : & mut BattleObjectModuleAccessor ) {
2023-08-04 12:23:22 -05:00
// Allow this because sometimes we want to make sure our NNSDK doesn't have
// an erroneous definition
#[ allow(clippy::unnecessary_cast) ]
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 ;
2023-12-02 12:02:43 -05:00
CURRENT_RECORD_SLOT = MENU . recording_slot . into_idx ( ) . unwrap_or ( 0 ) ;
2023-08-04 12:23:22 -05:00
if entry_id_int = = 0 & & ! fighter_is_nana {
2023-09-01 10:55:21 -07:00
if button_config ::combo_passes ( button_config ::ButtonCombo ::InputPlayback ) {
2023-08-24 11:51:26 -04:00
playback ( MENU . playback_button_slots . get_random ( ) . into_idx ( ) ) ;
2023-12-02 12:02:43 -05:00
} else if MENU . record_trigger . contains ( & RecordTrigger ::COMMAND )
2023-09-01 10:55:21 -07:00
& & button_config ::combo_passes ( button_config ::ButtonCombo ::InputRecord )
2023-08-07 20:11:26 -07:00
{
2023-08-04 12:23:22 -05:00
lockout_record ( ) ;
}
2023-08-11 15:33:11 -05:00
if INPUT_RECORD = = None {
clear_notifications ( " Input Recording " ) ;
}
2023-08-12 17:37:27 -05:00
// Handle recording end
2023-08-04 12:23:22 -05:00
if ( INPUT_RECORD = = Record | | INPUT_RECORD = = Playback )
2023-08-06 17:07:01 -07:00
& & INPUT_RECORD_FRAME > = CURRENT_FRAME_LENGTH - 1
2023-08-04 12:23:22 -05:00
{
POSSESSION = Player ;
if mash ::is_playback_queued ( ) {
mash ::reset ( ) ;
}
2023-08-12 17:37:27 -05:00
// If we need to crop the recording for neutral input
// INPUT_RECORD_FRAME must be > 0 to prevent bounding errors
2023-12-02 12:02:43 -05:00
if INPUT_RECORD = = Record & & MENU . recording_crop = = OnOff ::ON & & INPUT_RECORD_FRAME > 0
2023-08-12 17:37:27 -05:00
{
while INPUT_RECORD_FRAME > 0 & & is_input_neutral ( INPUT_RECORD_FRAME - 1 ) {
// Discard frames at the end of the recording until the last frame with input
INPUT_RECORD_FRAME - = 1 ;
}
CURRENT_FRAME_LENGTH = INPUT_RECORD_FRAME ;
P1_FRAME_LENGTH_MAPPING . lock ( ) [ CURRENT_RECORD_SLOT ] = CURRENT_FRAME_LENGTH ;
}
INPUT_RECORD_FRAME = 0 ;
2023-12-02 12:02:43 -05:00
if MENU . playback_loop = = OnOff ::ON & & INPUT_RECORD = = Playback {
2023-08-07 11:59:47 -07:00
playback ( Some ( CURRENT_PLAYBACK_SLOT ) ) ;
2023-08-12 17:37:27 -05:00
} else {
INPUT_RECORD = None ;
2023-08-07 11:59:47 -07:00
}
2023-08-04 12:23:22 -05:00
}
}
// Handle Possession Coloring
if entry_id_int = = 1 & & POSSESSION = = Lockout {
clear_notifications ( " Input Recording " ) ;
color_notification (
" Input Recording " . to_string ( ) ,
" Lockout " . to_owned ( ) ,
60 ,
ResColor {
2023-08-11 15:33:11 -05:00
r : 8 ,
2023-08-04 12:23:22 -05:00
g : 8 ,
2023-08-11 15:33:11 -05:00
b : 200 ,
2023-08-04 12:23:22 -05:00
a : 255 ,
} ,
) ;
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 {
clear_notifications ( " Input Recording " ) ;
color_notification (
" Input Recording " . to_string ( ) ,
" Standby " . to_owned ( ) ,
60 ,
ResColor {
r : 200 ,
g : 8 ,
b : 200 ,
a : 255 ,
} ,
) ;
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 {
clear_notifications ( " Input Recording " ) ;
color_notification (
" Input Recording " . to_string ( ) ,
" Recording " . to_owned ( ) ,
60 ,
ResColor {
r : 200 ,
g : 8 ,
b : 8 ,
a : 255 ,
} ,
) ;
set_color_rgb_2 (
module_accessor ,
1.0 ,
0.0 ,
0.0 ,
* MODEL_COLOR_TYPE_COLOR_BLEND ,
) ;
2023-08-11 15:33:11 -05:00
} else if entry_id_int = = 1 & & POSSESSION = = Player & & INPUT_RECORD = = Playback {
// Displays if the inputs from the current frame were a result of playback
if INPUT_RECORD_FRAME = = 0 | | INPUT_RECORD_FRAME = = 1 {
// can be either, seems like a thread issue
clear_notifications ( " Input Recording " ) ;
color_notification (
" Input Recording " . to_string ( ) ,
" Playback " . to_owned ( ) ,
CURRENT_FRAME_LENGTH as u32 ,
ResColor {
r : 0 ,
g : 0 ,
b : 0 ,
a : 255 ,
} ,
) ;
}
2023-08-04 12:23:22 -05:00
}
}
pub unsafe fn lockout_record ( ) {
INPUT_RECORD = Pause ;
INPUT_RECORD_FRAME = 0 ;
POSSESSION = Lockout ;
P1_FINAL_MAPPING . lock ( ) [ CURRENT_RECORD_SLOT ]
. iter_mut ( )
. for_each ( | mapped_input | {
2023-08-10 07:28:13 -07:00
* mapped_input = MappedInputs ::empty ( ) ;
2023-08-04 12:23:22 -05:00
} ) ;
2023-08-24 11:51:26 -04:00
CURRENT_FRAME_LENGTH = MENU . recording_duration . into_frames ( ) ;
2023-08-06 17:07:01 -07:00
P1_FRAME_LENGTH_MAPPING . lock ( ) [ CURRENT_RECORD_SLOT ] = CURRENT_FRAME_LENGTH ;
2023-08-04 12:23:22 -05:00
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 ;
}
2023-08-07 11:59:47 -07:00
// Returns whether we did playback
pub unsafe fn playback ( slot : Option < usize > ) -> bool {
2023-08-04 12:23:22 -05:00
if INPUT_RECORD = = Pause {
2023-12-02 12:02:43 -05:00
warn! ( " Tried to playback during lockout! " ) ;
2023-08-07 11:59:47 -07:00
return false ;
2023-08-04 12:23:22 -05:00
}
2023-08-07 11:59:47 -07:00
if slot . is_none ( ) {
2023-12-02 12:02:43 -05:00
warn! ( " Tried to playback without a slot selected! " ) ;
2023-08-07 11:59:47 -07:00
return false ;
2023-08-04 12:23:22 -05:00
}
2023-08-07 11:59:47 -07:00
let slot = slot . unwrap ( ) ;
2023-08-04 12:23:22 -05:00
2023-08-04 15:45:07 -07:00
CURRENT_PLAYBACK_SLOT = slot ;
2023-08-06 17:07:01 -07:00
CURRENT_FRAME_LENGTH = P1_FRAME_LENGTH_MAPPING . lock ( ) [ CURRENT_PLAYBACK_SLOT ] ;
2023-08-04 12:23:22 -05:00
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 ) ;
2023-08-07 11:59:47 -07:00
true
}
2023-08-04 12:23:22 -05:00
2023-08-07 11:59:47 -07:00
pub unsafe fn playback_ledge ( slot : Option < usize > ) {
let did_playback = playback ( slot ) ;
if did_playback {
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 ) ;
2023-08-11 10:33:46 -05:00
let status_kind = StatusModule ::status_kind ( cpu_module_accessor ) ;
2023-08-07 11:59:47 -07:00
if status_kind = = * FIGHTER_STATUS_KIND_CLIFF_CATCH {
BUFFER_FRAME - = 1 ;
}
2023-08-04 12:23:22 -05:00
}
}
pub unsafe fn stop_playback ( ) {
INPUT_RECORD = None ;
INPUT_RECORD_FRAME = 0 ;
Functional PT Savestates and additional debug (#616)
* Add reusable print_fighter_info()
* rustfmt
* Clippy
* starting charge
* Working Pikmin Save States, motion not working, order can be messed up by loading during an aerial, if you save too quickly you lose pikmin in the state
* often failing boid read
* notes
* Prevent Pikmin from being cleaned up by the regular cleanup process
* migration progress
* skyline-smash branch, more pikmin work
* Using buff to spawn pikmin, character specific file for cleanliness
* failed reordering attempt
* more failed ordering
* almost kind of works but out of order
* failed all, need to try individual hold and status change
* hard reorder crash
* battleobject not boma
* comment clarification
* messing around with printing
* printing, about to try to find where autonomy is being set
* solution found, going to hook backshield to find where autonomy is being set to true
* found where to hook, will start hooking
* switched to charge, issues with inline hook
* need to only have autonomy ignored on save state load
* distance and status controls (doesn't work perfectly)
* Christmas Miracle
* Working w/ Cleanup
* training mode check, improved owner check
* Debug for hooking, Working Implementation (No Transform, No Charge)
* Working for Aegis, PT having issues (LRA into state load works, but not regular state load)
* Working ptrainer hats
* debugging ptrainer
* great debug
* Pokemon Swaps to Saved State
* invisible ball, need to silence trainer/ball and hook joint eff
* cpu isn't being saved properly, lra + state load crash fixed
* CPU working, unsure if flag is necessary though
* Visual fixes working, can't load states again if player if trainer, cpu getting stuck?
* working, trainer stuck if no move on state load, switch initialization issue
* PT is saved
* debug cleanup
* rustfmt, still needs clippy and main merge
* clippy fmt
* one more fmt
* prevent flying plate from existing if you load state during death
* debug cleanup
* rustfmt
* cleanup
2023-09-01 19:02:02 -05:00
POSSESSION = Player ;
2023-08-04 12:23:22 -05:00
}
2023-08-12 17:37:27 -05:00
pub unsafe fn is_input_neutral ( input_frame : usize ) -> bool {
2023-08-04 12:23:22 -05:00
// Returns whether we should be done with standby this frame (if any significant controller input has been made)
2023-08-12 17:37:27 -05:00
let frame_input = P1_FINAL_MAPPING . lock ( ) [ CURRENT_RECORD_SLOT ] [ input_frame ] ;
2023-08-04 12:23:22 -05:00
let clamped_lstick_x =
2023-08-12 17:37:27 -05:00
( ( frame_input . lstick_x as f32 ) * STICK_CLAMP_MULTIPLIER ) . clamp ( - 1.0 , 1.0 ) ;
2023-08-04 12:23:22 -05:00
let clamped_lstick_y =
2023-08-12 17:37:27 -05:00
( ( frame_input . lstick_y as f32 ) * STICK_CLAMP_MULTIPLIER ) . clamp ( - 1.0 , 1.0 ) ;
2023-08-04 12:23:22 -05:00
let clamped_rstick_x =
2023-08-12 17:37:27 -05:00
( ( frame_input . rstick_x as f32 ) * STICK_CLAMP_MULTIPLIER ) . clamp ( - 1.0 , 1.0 ) ;
2023-08-04 12:23:22 -05:00
let clamped_rstick_y =
2023-08-12 17:37:27 -05:00
( ( frame_input . rstick_y as f32 ) * STICK_CLAMP_MULTIPLIER ) . clamp ( - 1.0 , 1.0 ) ;
2023-08-04 12:23:22 -05:00
// No buttons pressed or just flick jump-- if they really did a stick jump, we'd have lstick movement as well
let buttons_pressed =
2023-08-12 17:37:27 -05:00
! ( frame_input . buttons . is_empty ( ) | | frame_input . buttons = = Buttons ::FLICK_JUMP ) ;
2023-08-04 12:23:22 -05:00
let lstick_movement =
clamped_lstick_x . abs ( ) > = STICK_NEUTRAL | | clamped_lstick_y . abs ( ) > = STICK_NEUTRAL ;
let rstick_movement =
clamped_rstick_x . abs ( ) > = STICK_NEUTRAL | | clamped_rstick_y . abs ( ) > = STICK_NEUTRAL ;
2023-08-12 17:37:27 -05:00
! ( lstick_movement | | rstick_movement | | buttons_pressed )
2023-08-04 12:23:22 -05:00
}
2023-08-10 07:28:13 -07:00
pub unsafe fn handle_final_input_mapping ( player_idx : i32 , out : * mut MappedInputs ) {
2023-08-04 12:23:22 -05:00
if player_idx = = 0 {
// if player 1
if INPUT_RECORD = = Record {
// check for standby before starting action:
2023-08-12 17:37:27 -05:00
if POSSESSION = = Standby & & ! is_input_neutral ( 0 ) {
2023-08-04 12:23:22 -05:00
// 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 ( ) [ CURRENT_RECORD_SLOT ] [ INPUT_RECORD_FRAME ] = * out ;
2023-08-10 07:28:13 -07:00
* out = MappedInputs ::empty ( ) ; // don't control player while recording
2023-08-04 12:23:22 -05:00
}
2023-08-11 10:33:46 -05:00
// Don't allow for player input during Lockout
if POSSESSION = = Lockout {
* out = MappedInputs ::empty ( ) ;
}
2023-08-04 12:23:22 -05:00
}
}
2024-01-10 17:58:27 -05:00
#[ skyline::hook(offset = *OFFSET_SET_CPU_CONTROLS) ] // After cpu controls are assigned from ai calls
2023-08-04 12:23:22 -05:00
unsafe fn set_cpu_controls ( p_data : * mut * mut u8 ) {
call_original! ( p_data ) ;
2023-08-15 23:57:38 -07:00
if ! is_training_mode ( ) {
return ;
}
2023-08-16 06:29:14 -07:00
2023-08-04 12:23:22 -05:00
let controller_data = * p_data . add ( 1 ) as * mut ControlModuleInternal ;
2023-08-11 15:33:11 -05:00
let _controller_no = ( * controller_data ) . controller_index ;
2023-08-04 12:23:22 -05:00
// 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 ( ) ;
}
2023-09-01 13:53:04 -07:00
let cpu_module_accessor = try_get_module_accessor ( FighterId ::CPU ) ;
// Sometimes we can try to grab their module accessor before they are valid?
if cpu_module_accessor . is_none ( ) {
return ;
}
let cpu_module_accessor = cpu_module_accessor . unwrap ( ) ;
2023-08-04 12:23:22 -05:00
if INPUT_RECORD = = Pause {
match LOCKOUT_FRAME . cmp ( & 0 ) {
Ordering ::Greater = > LOCKOUT_FRAME - = 1 ,
Ordering ::Equal = > {
INPUT_RECORD = Record ;
POSSESSION = Standby ;
}
2023-12-02 12:02:43 -05:00
Ordering ::Less = > error! ( " LOCKOUT_FRAME OUT OF BOUNDS " ) ,
2023-08-04 12:23:22 -05:00
}
}
if INPUT_RECORD = = Record | | INPUT_RECORD = = Playback {
2023-08-11 15:33:11 -05:00
// if we aren't facing the way we were when we initially recorded, we reverse horizontal inputs
let mut x_input_multiplier = RECORDED_LR * CURRENT_LR ;
// Don't flip Shulk's dial inputs
2023-08-04 12:23:22 -05:00
let fighter_kind = utility ::get_kind ( & mut * cpu_module_accessor ) ;
if fighter_kind = = * FIGHTER_KIND_SHULK {
let circle_menu_flag = WorkModule ::is_flag (
& mut * cpu_module_accessor ,
* FIGHTER_SHULK_INSTANCE_WORK_ID_FLAG_SPECIAL_N_CIRCLE_MENU ,
) ;
if circle_menu_flag {
// While in dial, don't flip horizontal inputs
x_input_multiplier = 1.0 ;
}
// If we have issues with the frame after the dial comes out, change condition to
// circle_menu_flag && FIGHTER_SHULK_INSTANCE_WORK_ID_INT_SPECIAL_N_DECIDE_INTERVAL_FRAME > 1
}
2023-08-11 10:33:46 -05:00
// Prevent us from falling off of the ledge in standby
if StatusModule ::status_kind ( cpu_module_accessor ) = = * FIGHTER_STATUS_KIND_CLIFF_WAIT
& & is_standby ( )
& & WorkModule ::get_int (
cpu_module_accessor ,
* FIGHTER_STATUS_CLIFF_WORK_INT_CATCH_REST_TIME ,
) < 50
{
WorkModule ::set_int (
cpu_module_accessor ,
200 ,
* FIGHTER_STATUS_CLIFF_WORK_INT_CATCH_REST_TIME ,
) ;
}
2023-08-04 12:23:22 -05:00
let mut saved_mapped_inputs = P1_FINAL_MAPPING . lock ( ) [ if INPUT_RECORD = = Record {
CURRENT_RECORD_SLOT
} else {
CURRENT_PLAYBACK_SLOT
} ] [ 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
2023-08-10 07:28:13 -07:00
saved_mapped_inputs = MappedInputs ::empty ( ) ;
2023-08-04 12:23:22 -05:00
}
( * 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
let mut clamped_lstick_x = x_input_multiplier
* ( ( saved_mapped_inputs . lstick_x as f32 ) * STICK_CLAMP_MULTIPLIER ) . clamp ( - 1.0 , 1.0 ) ;
let mut clamped_lstick_y =
( ( saved_mapped_inputs . lstick_y as f32 ) * STICK_CLAMP_MULTIPLIER ) . clamp ( - 1.0 , 1.0 ) ;
clamped_lstick_x = if clamped_lstick_x . abs ( ) > = STICK_NEUTRAL {
clamped_lstick_x
} else {
0.0
} ;
clamped_lstick_y = if clamped_lstick_y . abs ( ) > = STICK_NEUTRAL {
clamped_lstick_y
} else {
0.0
} ;
( * controller_data ) . clamped_lstick_x = clamped_lstick_x ;
( * controller_data ) . clamped_lstick_y = clamped_lstick_y ;
// 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 ;
2023-08-06 17:07:01 -07:00
} else if INPUT_RECORD_FRAME < CURRENT_FRAME_LENGTH - 1 & & POSSESSION ! = Standby {
2023-08-04 12:23:22 -05:00
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 ( ) {
2023-08-10 07:28:13 -07:00
skyline ::install_hooks! ( set_cpu_controls ) ;
2023-08-04 12:23:22 -05:00
}