1
0
Fork 0
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:
jugeeya 2023-07-27 16:42:21 -07:00 committed by GitHub
parent 24ffba4e09
commit 62298ecbc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1844 additions and 857 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
#[macro_use]
pub mod structures;

View 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(),
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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