diff --git a/Cargo.toml b/Cargo.toml
index 7c6d61c..2c46617 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ strum_macros = "0.21.0"
 minreq = { version = "=2.2.1", features = ["https", "json-using-serde"] }
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
+toml = "0.5.9"
 training_mod_consts = { path = "training_mod_consts" }
 training_mod_tui = { path = "training_mod_tui" }
 
diff --git a/src/common/button_config.rs b/src/common/button_config.rs
new file mode 100644
index 0000000..a5f8bcc
--- /dev/null
+++ b/src/common/button_config.rs
@@ -0,0 +1,141 @@
+use lazy_static::lazy_static;
+use parking_lot::Mutex;
+use serde::Deserialize;
+use std::collections::HashMap;
+use toml;
+
+lazy_static! {
+    // Using the LuaConst names wasn't working for some reason...
+    static ref BUTTON_MAPPING: HashMap<&'static str, i32> = HashMap::from([
+        ("ATTACK", 0xE),  // *CONTROL_PAD_BUTTON_ATTACK_RAW
+        ("SPECIAL", 0xF), // *CONTROL_PAD_BUTTON_SPECIAL_RAW
+        ("SHIELD", 3), // *CONTROL_PAD_BUTTON_GUARD
+        ("GRAB", 9), // *CONTROL_PAD_BUTTON_CATCH
+        ("JUMP", 2), // *CONTROL_PAD_BUTTON_JUMP
+        ("UPTAUNT", 5), // *CONTROL_PAD_BUTTON_APPEAL_HI
+        ("DOWNTAUNT", 6), // *CONTROL_PAD_BUTTON_APPEAL_LW
+        ("LEFTTAUNT", 7), // *CONTROL_PAD_BUTTON_APPEAL_S_L
+        ("RIGHTTAUNT", 8), // *CONTROL_PAD_BUTTON_APPEAL_S_R
+        ("SHARESTOCK", 0xD), // *CONTROL_PAD_BUTTON_STOCK_SHARE
+        ("JUMPMINI", 0xA), // *CONTROL_PAD_BUTTON_JUMP_MINI
+    ]);
+    pub static ref OPEN_MENU_BTN_HOLD: Mutex<Vec<i32>> = Mutex::new(vec![BUTTON_MAPPING["SPECIAL"]]);
+    pub static ref OPEN_MENU_BTN_PRESS: Mutex<Vec<i32>> = Mutex::new(vec![BUTTON_MAPPING["UPTAUNT"]]);
+    pub static ref SAVE_STATE_BTN_HOLD: Mutex<Vec<i32>> = Mutex::new(vec![BUTTON_MAPPING["GRAB"]]);
+    pub static ref SAVE_STATE_BTN_PRESS: Mutex<Vec<i32>> = Mutex::new(vec![BUTTON_MAPPING["DOWNTAUNT"]]);
+    pub static ref LOAD_STATE_BTN_HOLD: Mutex<Vec<i32>> = Mutex::new(vec![BUTTON_MAPPING["GRAB"]]);
+    pub static ref LOAD_STATE_BTN_PRESS: Mutex<Vec<i32>> = Mutex::new(vec![BUTTON_MAPPING["UPTAUNT"]]);
+}
+
+#[derive(Deserialize)]
+struct BtnList {
+    hold: Vec<String>,
+    press: Vec<String>,
+}
+
+#[derive(Deserialize)]
+struct BtnComboConfig {
+    open_menu: BtnList,
+    save_state: BtnList,
+    load_state: BtnList,
+}
+
+#[derive(Deserialize)]
+struct TopLevelBtnComboConfig {
+    button_config: BtnComboConfig,
+}
+
+fn save_btn_config(btnlist: BtnList, mutex_hold: &Mutex<Vec<i32>>, mutex_press: &Mutex<Vec<i32>>) {
+    let bad_keys: Vec<&String> = btnlist.hold.iter()
+        .chain(btnlist.press.iter())
+        .filter(|x| !BUTTON_MAPPING.contains_key(x.as_str()))
+        .collect();
+    if !bad_keys.is_empty() {
+        skyline::error::show_error(
+            0x71,
+            "Training Modpack custom button\nconfiguration is invalid!",
+            &format!("The following keys are invalid in\nsd:/TrainingModpack/training_modpack.toml:\n{:?}", &bad_keys)
+        );
+    }
+
+    // HOLD
+    let mut global_hold = mutex_hold.lock();
+    let vecopt_hold: Vec<Option<&i32>> = btnlist
+        .hold
+        .iter()
+        .map(|x| BUTTON_MAPPING.get(x.as_str()))
+        .collect();
+    if vecopt_hold.iter().all(|x| x.is_some()) {
+        // All entries valid keys of BUTTON_MAPPING
+        global_hold.clear();
+        global_hold.extend_from_slice(
+            &vecopt_hold
+                .into_iter()
+                .map(|x| *x.unwrap())
+                .collect::<Vec<i32>>(),
+        );
+    }
+
+    // PRESS
+    let mut global_press = mutex_press.lock();
+    let vecopt_press: Vec<Option<&i32>> = btnlist
+        .press
+        .into_iter()
+        .map(|x| BUTTON_MAPPING.get(x.as_str()))
+        .collect();
+    if vecopt_press.iter().all(|x| x.is_some()) {
+        // All entries valid keys of BUTTON_MAPPING
+        global_press.clear();
+        global_press.extend_from_slice(
+            &vecopt_press
+                .into_iter()
+                .map(|x| *x.unwrap())
+                .collect::<Vec<i32>>(),
+        );
+    }
+}
+
+pub fn save_all_btn_config_from_toml(data: &str) {
+    let conf: TopLevelBtnComboConfig = toml::from_str(data).unwrap();
+    let open_menu_conf: BtnList = conf.button_config.open_menu;
+    let save_state_conf: BtnList = conf.button_config.save_state;
+    let load_state_conf: BtnList = conf.button_config.load_state;
+
+    save_btn_config(open_menu_conf, &OPEN_MENU_BTN_HOLD, &OPEN_MENU_BTN_PRESS);
+    save_btn_config(save_state_conf, &SAVE_STATE_BTN_HOLD, &SAVE_STATE_BTN_PRESS);
+    save_btn_config(load_state_conf, &LOAD_STATE_BTN_HOLD, &LOAD_STATE_BTN_PRESS);
+}
+
+pub const DEFAULT_BTN_CONFIG: &'static str = r#"[button_config]
+# Available Options:
+#
+# ATTACK
+# SPECIAL
+# SHIELD
+# GRAB
+# JUMP
+# UPTAUNT
+# DOWNTAUNT
+# LEFTTAUNT
+# RIGHTTAUNT
+# SHARESTOCK
+# JUMPMINI
+#
+# It is recommended to only put one button in the "press" section for each button
+# combination, but you can add several buttons to "hold" like this:
+# hold=["ATTACK", "SPECIAL",]
+#
+# SHARESTOCK is typically A+B
+# JUMPMINI is the combination of two jump buttons
+[button_config.open_menu]
+hold=["SPECIAL",]
+press=["UPTAUNT",]
+
+[button_config.save_state]
+hold=["GRAB",]
+press=["DOWNTAUNT",]
+
+[button_config.load_state]
+hold=["GRAB",]
+press=["UPTAUNT",]
+"#;
diff --git a/src/common/menu.rs b/src/common/menu.rs
index 6b866d5..5d18fdd 100644
--- a/src/common/menu.rs
+++ b/src/common/menu.rs
@@ -36,11 +36,15 @@ pub unsafe fn menu_condition(module_accessor: &mut smash::app::BattleObjectModul
     // Only check for button combination if the counter is 0 (not locked out)
     match frame_counter::get_frame_count(FRAME_COUNTER_INDEX) {
         0 => {
-            ControlModule::check_button_on(module_accessor, *CONTROL_PAD_BUTTON_SPECIAL)
-                && ControlModule::check_button_on_trriger(
-                    module_accessor,
-                    *CONTROL_PAD_BUTTON_APPEAL_HI,
-                )
+            let open_menu_btn_hold = button_config::OPEN_MENU_BTN_HOLD.lock();
+            let open_menu_btn_press = button_config::OPEN_MENU_BTN_PRESS.lock();
+            let return_value: bool = open_menu_btn_hold
+                .iter()
+                .all(|btn| ControlModule::check_button_on(module_accessor, *btn))
+                && open_menu_btn_press
+                    .iter()
+                    .all(|btn| ControlModule::check_button_trigger(module_accessor, *btn));
+            return_value
         }
         1..MENU_LOCKOUT_FRAMES => false,
         _ => {
@@ -73,7 +77,7 @@ pub unsafe fn write_menu() {
     }
 }
 
-const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.conf";
+const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.json";
 
 pub unsafe fn set_menu_from_json(message: &str) {
     if let Ok(message_json) = serde_json::from_str::<MenuJsonStruct>(message) {
@@ -85,7 +89,7 @@ pub unsafe fn set_menu_from_json(message: &str) {
             MENU_CONF_PATH,
             serde_json::to_string_pretty(&message_json).unwrap(),
         )
-        .expect("Failed to write menu conf file");
+        .expect("Failed to write menu settings file");
     } else if let Ok(message_json) = serde_json::from_str::<TrainingModpackMenu>(message) {
         // Only includes MENU
         // From TUI
@@ -96,7 +100,7 @@ pub unsafe fn set_menu_from_json(message: &str) {
             defaults_menu: DEFAULTS_MENU,
         };
         std::fs::write(MENU_CONF_PATH, serde_json::to_string_pretty(&conf).unwrap())
-            .expect("Failed to write menu conf file");
+            .expect("Failed to write menu settings file");
     } else {
         skyline::error::show_error(
             0x70,
diff --git a/src/common/mod.rs b/src/common/mod.rs
index bd9cc19..bd98281 100644
--- a/src/common/mod.rs
+++ b/src/common/mod.rs
@@ -1,3 +1,4 @@
+pub mod button_config;
 pub mod consts;
 pub mod events;
 pub mod menu;
diff --git a/src/common/release.rs b/src/common/release.rs
index ef7bed0..b24de22 100644
--- a/src/common/release.rs
+++ b/src/common/release.rs
@@ -46,6 +46,7 @@ pub fn version_check() {
             );
             // Remove old menu selections, silently ignoring errors (i.e. if the file doesn't exist)
             fs::remove_file("sd:/TrainingModpack/training_modpack_menu.conf").unwrap_or({});
+            fs::remove_file("sd:/TrainingModpack/training_modpack_menu.json").unwrap_or({});
             fs::remove_file("sd:/TrainingModpack/training_modpack_menu_defaults.conf")
                 .unwrap_or({});
             record_current_version(VERSION_FILE_PATH);
diff --git a/src/lib.rs b/src/lib.rs
index 2be5209..81f1433 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -81,8 +81,8 @@ pub fn main() {
     log!("Performing version check...");
     release::version_check();
 
-    let menu_conf_path = "sd:/TrainingModpack/training_modpack_menu.conf";
-    log!("Checking for previous menu in training_modpack_menu.conf...");
+    let menu_conf_path = "sd:/TrainingModpack/training_modpack_menu.json";
+    log!("Checking for previous menu in training_modpack_menu.json...");
     if fs::metadata(menu_conf_path).is_ok() {
         let menu_conf = fs::read_to_string(&menu_conf_path).unwrap();
         if let Ok(menu_conf_json) = serde_json::from_str::<MenuJsonStruct>(&menu_conf) {
@@ -93,15 +93,29 @@ pub fn main() {
             }
         } else if menu_conf.starts_with("http://localhost") {
             log!("Previous menu found, with URL schema. Deleting...");
-            fs::remove_file(menu_conf_path).expect("Could not delete conf file!");
+            fs::remove_file(menu_conf_path).expect("Could not delete menu conf file!");
         } else {
             log!("Previous menu found but is invalid. Deleting...");
-            fs::remove_file(menu_conf_path).expect("Could not delete conf file!");
+            fs::remove_file(menu_conf_path).expect("Could not delete menu conf file!");
         }
     } else {
         log!("No previous menu file found.");
     }
 
+    let combo_path = "sd:/TrainingModpack/training_modpack.toml";
+    log!("Checking for previous button combo settings in training_modpack.toml...");
+    if fs::metadata(combo_path).is_ok() {
+        log!("Previous button combo settings found. Loading...");
+        let combo_conf = fs::read_to_string(&combo_path).unwrap();
+        button_config::save_all_btn_config_from_toml(&combo_conf);
+    } else {
+        log!("No previous button combo file found. Creating...");
+        std::fs::write(combo_path, button_config::DEFAULT_BTN_CONFIG)
+            .expect("Failed to write button config conf file");
+        // No need to run save_all_btn_config_from_toml()
+        // since the statics are preloaded with the defaults
+    }
+
     if is_emulator() {
         unsafe {
             DEFAULTS_MENU.quick_menu = OnOff::On;
diff --git a/src/training/save_states.rs b/src/training/save_states.rs
index 66212d8..93977b9 100644
--- a/src/training/save_states.rs
+++ b/src/training/save_states.rs
@@ -1,3 +1,5 @@
+use crate::is_operation_cpu;
+use crate::common::button_config;
 use crate::common::consts::get_random_int;
 use crate::common::consts::FighterId;
 use crate::common::consts::OnOff;
@@ -233,9 +235,17 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
     let autoload_reset = MENU.save_state_autoload == OnOff::On
         && save_state.state == NoAction
         && is_dead(module_accessor);
-    let triggered_reset =
-        ControlModule::check_button_on(module_accessor, *CONTROL_PAD_BUTTON_CATCH)
-            && ControlModule::check_button_trigger(module_accessor, *CONTROL_PAD_BUTTON_APPEAL_HI);
+    let mut triggered_reset: bool = false;
+    if !is_operation_cpu(module_accessor) {
+        let load_state_btn_hold = button_config::LOAD_STATE_BTN_HOLD.lock();
+        let load_state_btn_press = button_config::LOAD_STATE_BTN_PRESS.lock();
+        triggered_reset = load_state_btn_hold
+            .iter()
+            .all(|btn| ControlModule::check_button_on(module_accessor, *btn))
+            && load_state_btn_press
+                .iter()
+                .all(|btn| ControlModule::check_button_trigger(module_accessor, *btn));
+    }
     if (autoload_reset || triggered_reset) && !fighter_is_nana {
         if save_state.state == NoAction {
             SAVE_STATE_PLAYER.state = KillPlayer;
@@ -420,11 +430,17 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
     }
 
     // Grab + Dpad down: Save state
-    if ControlModule::check_button_on(module_accessor, *CONTROL_PAD_BUTTON_CATCH)
-        && ControlModule::check_button_trigger(module_accessor, *CONTROL_PAD_BUTTON_APPEAL_LW)
-        && !fighter_is_nana
-    // Don't begin saving state if Nana's delayed input is captured
-    {
+    let save_state_btn_hold = button_config::SAVE_STATE_BTN_HOLD.lock();
+    let save_state_btn_press = button_config::SAVE_STATE_BTN_PRESS.lock();
+    let save_state_condition: bool = save_state_btn_hold
+        .iter()
+        .all(|btn| ControlModule::check_button_on(module_accessor, *btn))
+        && save_state_btn_press
+            .iter()
+            .all(|btn| ControlModule::check_button_trigger(module_accessor, *btn))
+        && !fighter_is_nana;
+    if save_state_condition {
+        // Don't begin saving state if Nana's delayed input is captured
         MIRROR_STATE = 1.0;
         SAVE_STATE_PLAYER.state = Save;
         SAVE_STATE_CPU.state = Save;