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