1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-20 00:46:34 +00:00

Use Native Smash Input for All Input Processing (#581)

* Initial

* Finishing up with button config

* Formatting, some clippy

* Move button config to menu

* Consts formatting

* Fix input_record_press (thanks test!)

* Properly migrate menu button inputs

* Updates to triggers, including text

* Convert to multi-select
This commit is contained in:
jugeeya 2023-08-10 07:28:13 -07:00 committed by GitHub
parent 43f7856727
commit 77f527c99a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 633 additions and 586 deletions

View file

@ -15,6 +15,7 @@ bitflags = "1.2.1"
parking_lot = { version = "0.12.0", features = ["nightly"] }
include-flate = "0.1.4"
lazy_static = "1.4.0"
modular-bitfield = "0.11.2"
owo-colors = "2.1.0"
once_cell = "1.12.0"
paste = "1.0"

View file

@ -1,54 +1,47 @@
use std::collections::HashMap;
use std::fs;
use crate::common::menu::P1_CONTROLLER_STATE;
use crate::common::*;
use crate::input::{ControllerStyle::*, *};
use crate::consts::TRAINING_MODPACK_TOML_PATH;
use lazy_static::lazy_static;
use log::info;
use serde::Deserialize;
use smash::app::lua_bind::ControlModule;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
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", 0), // *CONTROL_PAD_BUTTON_ATTACK
("SPECIAL", 1), // *CONTROL_PAD_BUTTON_SPECIAL
("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 fn button_mapping(
button_config: ButtonConfig,
style: ControllerStyle,
b: ButtonBitfield,
) -> bool {
match button_config {
ButtonConfig::A => b.a(),
ButtonConfig::B => b.b(),
ButtonConfig::X => b.x(),
ButtonConfig::Y => b.y(),
ButtonConfig::L => match style {
GCController => false,
_ => b.l(),
},
ButtonConfig::R => match style {
GCController => b.zr(),
_ => b.r(),
},
ButtonConfig::ZL => match style {
GCController => b.l() || b.real_digital_l(),
_ => b.zl() || b.left_sl() || b.right_sl(),
},
ButtonConfig::ZR => match style {
GCController => b.r() || b.real_digital_r(),
_ => b.zr() || b.left_sr() || b.right_sr(),
},
ButtonConfig::DPAD_UP => b.dpad_up(),
ButtonConfig::DPAD_DOWN => b.dpad_down(),
ButtonConfig::DPAD_LEFT => b.dpad_left(),
ButtonConfig::DPAD_RIGHT => b.dpad_right(),
ButtonConfig::PLUS => b.plus(),
ButtonConfig::MINUS => b.minus(),
ButtonConfig::LSTICK => b.stick_l(),
ButtonConfig::RSTICK => b.stick_r(),
_ => false,
}
}
static mut BUTTON_COMBO_CONFIG: BtnComboConfig = BtnComboConfig {
open_menu: BtnList {
hold: vec![],
press: vec![],
},
save_state: BtnList {
hold: vec![],
press: vec![],
},
load_state: BtnList {
hold: vec![],
press: vec![],
},
input_record: BtnList {
hold: vec![],
press: vec![],
},
input_playback: BtnList {
hold: vec![],
press: vec![],
},
};
#[derive(Debug, EnumIter, PartialEq)]
pub enum ButtonCombo {
@ -59,215 +52,50 @@ pub enum ButtonCombo {
InputPlayback,
}
#[derive(Deserialize, Default)]
struct BtnList {
hold: Vec<String>,
press: Vec<String>,
unsafe fn get_combo_keys(combo: ButtonCombo) -> ButtonConfig {
match combo {
ButtonCombo::OpenMenu => MENU.menu_open,
ButtonCombo::SaveState => MENU.save_state_save,
ButtonCombo::LoadState => MENU.save_state_load,
ButtonCombo::InputRecord => MENU.input_record,
ButtonCombo::InputPlayback => MENU.input_playback,
}
}
#[derive(Deserialize, Default)]
struct BtnComboConfig {
open_menu: BtnList,
save_state: BtnList,
load_state: BtnList,
input_record: BtnList,
input_playback: BtnList,
}
fn combo_passes(combo: ButtonCombo) -> bool {
unsafe {
let combo_keys = get_combo_keys(combo).to_vec();
let p1_controller_state = *P1_CONTROLLER_STATE.data_ptr();
#[derive(Deserialize)]
pub struct TopLevelBtnComboConfig {
button_config: BtnComboConfig,
}
let mut this_combo_passes = false;
pub fn load_from_file() {
let combo_path = TRAINING_MODPACK_TOML_PATH;
info!("Checking for previous button combo settings in {TRAINING_MODPACK_TOML_PATH}...");
let mut valid_button_config = false;
if fs::metadata(combo_path).is_ok() {
info!("Previous button combo settings found. Loading...");
let combo_conf = fs::read_to_string(combo_path)
.unwrap_or_else(|_| panic!("Could not read {}", combo_path));
let conf: Result<TopLevelBtnComboConfig, toml::de::Error> = toml::from_str(&combo_conf);
if let Ok(conf) = conf {
if validate_config(conf) {
save_all_btn_config_from_toml(&combo_conf);
valid_button_config = true;
for hold_button in &combo_keys[..] {
if button_mapping(
*hold_button,
p1_controller_state.style,
p1_controller_state.current_buttons,
) && combo_keys
.iter()
.filter(|press_button| **press_button != *hold_button)
.all(|press_button| {
button_mapping(
*press_button,
p1_controller_state.style,
p1_controller_state.just_down,
)
})
{
this_combo_passes = true;
}
}
}
if !valid_button_config {
info!("No previous button combo file found. Creating...");
fs::write(combo_path, DEFAULT_BTN_CONFIG).expect("Failed to write button config conf file");
save_all_btn_config_from_defaults();
}
}
fn save_all_btn_config_from_defaults() {
let conf = TopLevelBtnComboConfig {
button_config: BtnComboConfig {
open_menu: BtnList {
hold: vec!["SPECIAL".to_string()],
press: vec!["UPTAUNT".to_string()],
},
save_state: BtnList {
hold: vec!["SHIELD".to_string()],
press: vec!["DOWNTAUNT".to_string()],
},
load_state: BtnList {
hold: vec!["SHIELD".to_string()],
press: vec!["UPTAUNT".to_string()],
},
input_record: BtnList {
hold: vec!["ATTACK".to_string()],
press: vec!["LEFTTAUNT".to_string()],
},
input_playback: BtnList {
hold: vec!["ATTACK".to_string()],
press: vec!["RIGHTTAUNT".to_string()],
},
},
};
unsafe {
// This println is necessary. Why?.......
println!("{:?}", &conf.button_config.load_state.press);
BUTTON_COMBO_CONFIG = conf.button_config;
}
}
fn save_all_btn_config_from_toml(data: &str) {
let conf: TopLevelBtnComboConfig = toml::from_str(data).expect("Could not parse button config");
unsafe {
// This println is necessary. Why?.......
println!("{:?}", &conf.button_config.load_state.press);
BUTTON_COMBO_CONFIG = conf.button_config;
}
}
fn validate_config(conf: TopLevelBtnComboConfig) -> bool {
let conf = conf.button_config;
let configs = [conf.open_menu, conf.save_state, conf.load_state];
let bad_keys = configs
.iter()
.flat_map(|btn_list| {
btn_list
.hold
.iter()
.chain(btn_list.press.iter())
.filter(|x| !BUTTON_MAPPING.contains_key(x.to_uppercase().as_str()))
})
.collect::<Vec<&String>>();
if !bad_keys.is_empty() {
skyline::error::show_error(
0x71,
"Training Modpack custom button\nconfiguration is invalid!\0",
&format!(
"The following keys are invalid in\n{}:\n\
{:?}\n\nPossible Keys: {:#?}\0",
TRAINING_MODPACK_TOML_PATH,
&bad_keys,
BUTTON_MAPPING.keys()
),
);
false
} else {
true
}
}
unsafe fn get_combo_keys(combo: ButtonCombo) -> (&'static Vec<String>, &'static Vec<String>) {
match combo {
ButtonCombo::OpenMenu => (
&BUTTON_COMBO_CONFIG.open_menu.hold,
&BUTTON_COMBO_CONFIG.open_menu.press,
),
ButtonCombo::SaveState => (
&BUTTON_COMBO_CONFIG.save_state.hold,
&BUTTON_COMBO_CONFIG.save_state.press,
),
ButtonCombo::LoadState => (
&BUTTON_COMBO_CONFIG.load_state.hold,
&BUTTON_COMBO_CONFIG.load_state.press,
),
ButtonCombo::InputRecord => (
&BUTTON_COMBO_CONFIG.input_record.hold,
&BUTTON_COMBO_CONFIG.input_record.press,
),
ButtonCombo::InputPlayback => (
&BUTTON_COMBO_CONFIG.input_playback.hold,
&BUTTON_COMBO_CONFIG.input_playback.press,
),
}
}
fn combo_passes(
module_accessor: *mut smash::app::BattleObjectModuleAccessor,
combo: ButtonCombo,
) -> bool {
unsafe {
let (hold, press) = get_combo_keys(combo);
let this_combo_passes = hold
.iter()
.map(|hold| *BUTTON_MAPPING.get(&*hold.to_uppercase()).unwrap())
.all(|hold| ControlModule::check_button_on(module_accessor, hold))
&& press
.iter()
.map(|press| *BUTTON_MAPPING.get(&*press.to_uppercase()).unwrap())
.all(|press| ControlModule::check_button_trigger(module_accessor, press));
this_combo_passes
}
}
pub fn combo_passes_exclusive(
module_accessor: *mut smash::app::BattleObjectModuleAccessor,
combo: ButtonCombo,
) -> bool {
pub fn combo_passes_exclusive(combo: ButtonCombo) -> bool {
let other_combo_passes = ButtonCombo::iter()
.filter(|other_combo| *other_combo != combo)
.any(|other_combo| combo_passes(module_accessor, other_combo));
combo_passes(module_accessor, combo) && !other_combo_passes
.any(combo_passes);
combo_passes(combo) && !other_combo_passes
}
const DEFAULT_BTN_CONFIG: &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=["SHIELD",]
press=["DOWNTAUNT",]
[button_config.load_state]
hold=["SHIELD",]
press=["UPTAUNT",]
[button_config.input_record]
hold=["ATTACK",]
press=["LEFTTAUNT",]
[button_config.input_playback]
hold=["ATTACK",]
press=["RIGHTTAUNT",]
"#;

View file

@ -3,9 +3,9 @@ use std::fs;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Deserialize;
use skyline::nn::hid::NpadGcState;
use toml;
use crate::common::input::*;
use crate::consts::DEV_TOML_PATH;
use crate::logging::info;
@ -55,17 +55,9 @@ impl DevConfig {
}
}
pub fn handle_get_npad_state(state: *mut NpadGcState, _controller_id: *const u32) {
let a_press = 1 << 0;
let l_press = 1 << 6;
let r_press = 1 << 7;
let buttons;
unsafe {
buttons = (*state).Buttons;
}
// Occurs on L+R+A
if (buttons & a_press > 0) && (buttons & l_press > 0) && (buttons & r_press > 0) {
pub fn handle_final_input_mapping(player_idx: i32, controller_struct: &SomeControllerStruct) {
let current_buttons = controller_struct.controller.current_buttons;
if player_idx == 0 && current_buttons.l() && current_buttons.r() && current_buttons.a() {
let mut dev_config = DEV_CONFIG.lock();
*dev_config = DevConfig::load_from_toml();
}

290
src/common/input.rs Normal file
View file

@ -0,0 +1,290 @@
#![allow(dead_code)] // TODO: Yeah don't do this
use bitflags::bitflags;
use modular_bitfield::{bitfield, specifiers::*};
// 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::empty();
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
#[bitfield]
#[derive(Debug, Default, Copy, Clone)]
#[repr(C)]
pub struct ButtonBitfield {
pub dpad_up: bool,
pub dpad_right: bool,
pub dpad_down: bool,
pub dpad_left: bool,
pub x: bool,
pub a: bool,
pub b: bool,
pub y: bool,
pub l: bool,
pub r: bool,
pub zl: bool,
pub zr: bool,
pub left_sl: bool,
pub left_sr: bool,
pub right_sl: bool,
pub right_sr: bool,
pub stick_l: bool,
pub stick_r: bool,
pub plus: bool,
pub minus: bool,
pub l_up: bool,
pub l_right: bool,
pub l_down: bool,
pub l_left: bool,
pub r_up: bool,
pub r_right: bool,
pub r_down: bool,
pub r_left: bool,
pub real_digital_l: bool,
pub real_digital_r: bool,
pub unused: B2,
}
/// Controller style declaring what kind of controller is being used
#[derive(PartialEq, Eq, Default, Debug, Copy, Clone)]
#[repr(u32)]
pub enum ControllerStyle {
#[default]
Handheld = 0x1,
DualJoycon = 0x2,
LeftJoycon = 0x3,
RightJoycon = 0x4,
ProController = 0x5,
DebugPad = 0x6, // probably
GCController = 0x7,
}
#[derive(Debug, Default, Copy, Clone)]
#[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, Copy, Clone)]
#[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 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
#[derive(Debug, Default, Copy, Clone)]
#[repr(C)]
pub struct Controller {
pub vtable: 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],
pub 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 fn empty() -> MappedInputs {
MappedInputs {
buttons: Buttons::empty(),
lstick_x: 0,
lstick_y: 0,
rstick_x: 0,
rstick_y: 0,
}
}
}

View file

@ -2,25 +2,26 @@ use std::fs;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use skyline::nn::hid::{GetNpadStyleSet, NpadGcState};
use skyline::nn::hid::GetNpadStyleSet;
use training_mod_consts::MenuJsonStruct;
use training_mod_tui::AppPage;
use crate::common::button_config::button_mapping;
use crate::common::*;
use crate::consts::MENU_OPTIONS_PATH;
use crate::events::{Event, EVENT_QUEUE};
use crate::input::*;
use crate::logging::*;
// This is a special frame counter that will tick on draw()
// We'll count how long the menu has been open
pub static mut FRAME_COUNTER: u32 = 0;
const MENU_INPUT_WAIT_FRAMES: u32 = 30;
const MENU_CLOSE_WAIT_FRAMES: u32 = 60;
pub static mut QUICK_MENU_ACTIVE: bool = false;
pub unsafe fn menu_condition(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
button_config::combo_passes_exclusive(module_accessor, button_config::ButtonCombo::OpenMenu)
pub unsafe fn menu_condition() -> bool {
button_config::combo_passes_exclusive(button_config::ButtonCombo::OpenMenu)
}
pub fn load_from_file() {
@ -76,177 +77,6 @@ pub fn spawn_menu() {
}
}
pub struct ButtonPresses {
pub a: ButtonPress,
pub b: ButtonPress,
pub x: ButtonPress,
pub y: ButtonPress,
pub r: ButtonPress,
pub l: ButtonPress,
pub zr: ButtonPress,
pub zl: ButtonPress,
pub left: ButtonPress,
pub right: ButtonPress,
pub up: ButtonPress,
pub down: ButtonPress,
}
pub struct ButtonPress {
pub prev_frame_is_pressed: bool,
pub is_pressed: bool,
pub lockout_frames: usize,
}
impl ButtonPress {
pub fn read_press(&mut self) -> bool {
let is_pressed = self.is_pressed;
if self.is_pressed {
self.is_pressed = false;
if self.lockout_frames == 0 {
self.prev_frame_is_pressed = true;
self.lockout_frames = 10;
return true;
}
}
if self.lockout_frames > 0 {
self.lockout_frames -= 1;
}
self.prev_frame_is_pressed = is_pressed;
false
}
}
pub static mut BUTTON_PRESSES: ButtonPresses = ButtonPresses {
a: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
b: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
x: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
y: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
r: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
l: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
zr: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
zl: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
left: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
right: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
up: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
down: ButtonPress {
prev_frame_is_pressed: false,
is_pressed: false,
lockout_frames: 0,
},
};
pub fn handle_get_npad_state(state: *mut NpadGcState, controller_id: *const u32) {
unsafe {
let update_count = (*state).updateCount;
let flags = (*state).Flags;
if QUICK_MENU_ACTIVE {
if (*state).Buttons & (1 << 0) > 0 {
BUTTON_PRESSES.a.is_pressed = true;
}
if (*state).Buttons & (1 << 1) > 0 {
BUTTON_PRESSES.b.is_pressed = true;
}
if (*state).Buttons & (1 << 2) > 0 {
BUTTON_PRESSES.x.is_pressed = true;
}
if (*state).Buttons & (1 << 3) > 0 {
BUTTON_PRESSES.y.is_pressed = true;
}
if (*state).Buttons & (1 << 6) > 0 {
BUTTON_PRESSES.l.is_pressed = true;
}
if (*state).Buttons & (1 << 7) > 0 {
BUTTON_PRESSES.r.is_pressed = true;
}
// Special case for frame-by-frame
if FRAME_COUNTER > MENU_INPUT_WAIT_FRAMES && (*state).Buttons & (1 << 8) > 0 {
BUTTON_PRESSES.zl.is_pressed = true;
}
if (*state).Buttons & (1 << 9) > 0 {
BUTTON_PRESSES.zr.is_pressed = true;
}
if (*state).Buttons & ((1 << 12) | (1 << 16)) > 0 {
BUTTON_PRESSES.left.is_pressed = true;
}
if (*state).Buttons & ((1 << 14) | (1 << 18)) > 0 {
BUTTON_PRESSES.right.is_pressed = true;
}
if (*state).Buttons & ((1 << 15) | (1 << 19)) > 0 {
BUTTON_PRESSES.down.is_pressed = true;
}
// Special case for "UP" in menu open button combo
if FRAME_COUNTER > MENU_INPUT_WAIT_FRAMES
&& (*state).Buttons & ((1 << 13) | (1 << 17)) > 0
{
BUTTON_PRESSES.up.is_pressed = true;
}
// For digital triggers: these pressed 1/3 of the way mean we should consider a press
if controller_is_gcc(*controller_id) {
if (*state).LTrigger >= 0x2AAA {
BUTTON_PRESSES.l.is_pressed = true;
}
if (*state).RTrigger >= 0x2AAA {
BUTTON_PRESSES.r.is_pressed = true;
}
}
// If we're here, remove all other Npad presses...
// Should we exclude the home button?
(*state) = NpadGcState::default();
(*state).updateCount = update_count;
(*state).Flags = flags;
}
}
}
lazy_static! {
pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App<'static>> = Mutex::new(
training_mod_tui::App::new(unsafe { ui_menu(MENU) }, unsafe {
@ -256,22 +86,28 @@ lazy_static! {
)
})
);
pub static ref P1_CONTROLLER_STATE: Mutex<Controller> = Mutex::new(Controller::default());
}
pub unsafe fn controller_is_gcc(controller_id: u32) -> bool {
let style_set = GetNpadStyleSet(&controller_id as *const _);
(style_set.flags & (1 << 5)) > 0
}
pub unsafe fn p1_controller_is_gcc() -> bool {
let p1_controller_id = crate::training::input_delay::p1_controller_id();
controller_is_gcc(p1_controller_id)
pub fn handle_final_input_mapping(
player_idx: i32,
controller_struct: &SomeControllerStruct,
out: *mut MappedInputs,
) {
unsafe {
if player_idx == 0 {
*P1_CONTROLLER_STATE.lock() = *controller_struct.controller;
if QUICK_MENU_ACTIVE {
// If we're here, remove all other presses
*out = MappedInputs::empty();
}
}
}
}
pub unsafe fn quick_menu_loop() {
loop {
std::thread::sleep(std::time::Duration::from_secs(10));
let button_presses = &mut BUTTON_PRESSES;
let mut received_input = true;
loop {
std::thread::sleep(std::time::Duration::from_millis(16));
@ -291,15 +127,16 @@ pub unsafe fn quick_menu_loop() {
continue;
}
let is_gcc = p1_controller_is_gcc();
let p1_controller_state = *P1_CONTROLLER_STATE.data_ptr();
let style = p1_controller_state.style;
let button_presses = p1_controller_state.just_down;
let app = &mut *QUICK_MENU_APP.data_ptr();
button_presses.a.read_press().then(|| {
button_mapping(ButtonConfig::A, style, button_presses).then(|| {
app.on_a();
received_input = true;
});
let b_press = &mut button_presses.b;
b_press.read_press().then(|| {
button_mapping(ButtonConfig::B, style, button_presses).then(|| {
received_input = true;
if app.page != AppPage::SUBMENU {
app.on_b()
@ -312,59 +149,50 @@ pub unsafe fn quick_menu_loop() {
EVENT_QUEUE.push(Event::menu_open(menu_json));
}
});
button_presses.x.read_press().then(|| {
button_mapping(ButtonConfig::X, style, button_presses).then(|| {
app.save_defaults();
received_input = true;
});
button_presses.y.read_press().then(|| {
button_mapping(ButtonConfig::Y, style, button_presses).then(|| {
app.reset_all_submenus();
received_input = true;
});
button_presses.l.read_press().then(|| {
if is_gcc {
app.previous_tab();
}
button_mapping(ButtonConfig::ZL, style, button_presses).then(|| {
app.previous_tab();
received_input = true;
});
button_presses.r.read_press().then(|| {
if is_gcc {
app.next_tab();
} else {
app.reset_current_submenu();
}
button_mapping(ButtonConfig::ZR, style, button_presses).then(|| {
app.next_tab();
received_input = true;
});
button_presses.zl.read_press().then(|| {
if !is_gcc {
app.previous_tab();
}
received_input = true;
});
button_presses.zr.read_press().then(|| {
if !is_gcc {
app.next_tab();
} else {
app.reset_current_submenu();
}
received_input = true;
});
button_presses.left.read_press().then(|| {
app.on_left();
received_input = true;
});
button_presses.right.read_press().then(|| {
app.on_right();
received_input = true;
});
button_presses.up.read_press().then(|| {
app.on_up();
received_input = true;
});
button_presses.down.read_press().then(|| {
app.on_down();
button_mapping(ButtonConfig::R, style, button_presses).then(|| {
app.reset_current_submenu();
received_input = true;
});
(button_presses.dpad_left() || button_presses.l_left() || button_presses.r_left())
.then(|| {
app.on_left();
received_input = true;
});
(button_presses.dpad_right() || button_presses.l_right() || button_presses.r_right())
.then(|| {
app.on_right();
received_input = true;
});
(button_presses.dpad_up() || button_presses.l_up() || button_presses.r_up()).then(
|| {
app.on_up();
received_input = true;
},
);
(button_presses.dpad_down() || button_presses.l_down() || button_presses.r_down())
.then(|| {
app.on_down();
received_input = true;
});
if received_input {
received_input = false;
set_menu_from_json(&app.get_menu_selections());

View file

@ -10,6 +10,7 @@ pub mod button_config;
pub mod consts;
pub mod dev_config;
pub mod events;
pub mod input;
pub mod menu;
pub mod raygun_printer;
pub mod release;

View file

@ -1,4 +1,5 @@
#![feature(proc_macro_hygiene)]
#![feature(iter_intersperse)]
#![feature(const_mut_refs)]
#![feature(exclusive_range_pattern)]
#![feature(c_variadic)]
@ -74,21 +75,6 @@ pub fn main() {
unsafe {
EVENT_QUEUE.push(Event::smash_open());
notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
notification(
"Open Menu".to_string(),
"Special + Uptaunt".to_string(),
120,
);
notification(
"Save State".to_string(),
"Shield + Downtaunt".to_string(),
120,
);
notification(
"Load State".to_string(),
"Shield + Uptaunt".to_string(),
120,
);
}
hitbox_visualizer::hitbox_visualization();
@ -118,7 +104,60 @@ pub fn main() {
release::version_check();
menu::load_from_file();
button_config::load_from_file();
unsafe {
notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
notification(
"Open Menu".to_string(),
MENU.menu_open
.to_vec()
.iter()
.map(|button| button.as_str().unwrap())
.intersperse(" + ")
.collect(),
120,
);
notification(
"Save State".to_string(),
MENU.save_state_save
.to_vec()
.iter()
.map(|button| button.as_str().unwrap())
.intersperse(" + ")
.collect(),
120,
);
notification(
"Load State".to_string(),
MENU.save_state_load
.to_vec()
.iter()
.map(|button| button.as_str().unwrap())
.intersperse(" + ")
.collect(),
120,
);
notification(
"Input Record".to_string(),
MENU.input_record
.to_vec()
.iter()
.map(|button| button.as_str().unwrap())
.intersperse(" + ")
.collect(),
120,
);
notification(
"Input Playback".to_string(),
MENU.input_playback
.to_vec()
.iter()
.map(|button| button.as_str().unwrap())
.intersperse(" + ")
.collect(),
120,
);
}
std::thread::spawn(events_loop);

View file

@ -1,51 +1,30 @@
use std::collections::VecDeque;
use crate::common::input::*;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use skyline::nn::hid::{GetNpadStyleSet, NpadGcState};
use crate::common::MENU;
lazy_static! {
static ref P1_DELAYED_NPAD_STATES: Mutex<VecDeque<NpadGcState>> = Mutex::new(VecDeque::new());
static ref P1_DELAYED_INPUT_MAPPINGS: Mutex<VecDeque<MappedInputs>> =
Mutex::new(VecDeque::new());
}
pub unsafe fn p1_controller_id() -> u32 {
let min_controller_id = (0..8)
.filter(|i| GetNpadStyleSet(i as *const _).flags != 0)
.min()
.unwrap_or(0);
let handheld_id = 0x20;
if GetNpadStyleSet(&handheld_id as *const _).flags != 0 {
handheld_id
} else {
min_controller_id
}
}
pub fn handle_get_npad_state(state: *mut NpadGcState, controller_id: *const u32) {
pub fn handle_final_input_mapping(player_idx: i32, out: *mut MappedInputs) {
unsafe {
if crate::common::is_training_mode() && *controller_id == p1_controller_id() {
let mut delayed_states = P1_DELAYED_NPAD_STATES.lock();
let actual_state = *state;
if player_idx == 0 {
let mut delayed_mappings = P1_DELAYED_INPUT_MAPPINGS.lock();
let actual_mapping = *out;
if delayed_states.len() < MENU.input_delay.into_delay() as usize {
let update_count = (*state).updateCount;
let attributes = (*state).Flags;
*state = NpadGcState::default();
(*state).updateCount = update_count;
(*state).Flags = attributes;
} else if let Some(delayed_state) = delayed_states.back() {
let update_count = (*state).updateCount;
let attributes = (*state).Flags;
*state = *delayed_state;
(*state).updateCount = update_count;
(*state).Flags = attributes;
if delayed_mappings.len() < MENU.input_delay.into_delay() as usize {
*out = MappedInputs::empty();
} else if let Some(delayed_mapping) = delayed_mappings.back() {
*out = *delayed_mapping;
}
delayed_states.push_front(actual_state);
delayed_states.truncate(MENU.input_delay.into_delay() as usize);
delayed_mappings.push_front(actual_mapping);
delayed_mappings.truncate(MENU.input_delay.into_delay() as usize);
}
}
}

View file

@ -1,7 +1,7 @@
use crate::common::button_config;
use crate::common::consts::{FighterId, HitstunPlayback, OnOff};
use crate::common::input::*;
use crate::common::{get_module_accessor, is_in_hitstun, is_in_shieldstun, MENU};
use crate::training::input_recording::structures::*;
use crate::training::mash;
use crate::training::ui::notifications::{clear_notifications, color_notification};
use lazy_static::lazy_static;
@ -65,7 +65,7 @@ pub static mut CURRENT_FRAME_LENGTH: usize = 60;
lazy_static! {
static ref P1_FINAL_MAPPING: Mutex<[[MappedInputs; FINAL_RECORD_MAX]; TOTAL_SLOT_COUNT]> =
Mutex::new([[{ MappedInputs::default() }; FINAL_RECORD_MAX]; TOTAL_SLOT_COUNT]);
Mutex::new([[{ MappedInputs::empty() }; FINAL_RECORD_MAX]; TOTAL_SLOT_COUNT]);
static ref P1_FRAME_LENGTH_MAPPING: Mutex<[usize; TOTAL_SLOT_COUNT]> =
Mutex::new([60usize; TOTAL_SLOT_COUNT]);
static ref P1_STARTING_STATUSES: Mutex<[StartingStatus; TOTAL_SLOT_COUNT]> =
@ -180,16 +180,10 @@ pub unsafe fn get_command_flag_cat(module_accessor: &mut BattleObjectModuleAcces
CURRENT_RECORD_SLOT = MENU.recording_slot.into_idx();
if entry_id_int == 0 && !fighter_is_nana {
if button_config::combo_passes_exclusive(
module_accessor,
button_config::ButtonCombo::InputPlayback,
) {
if button_config::combo_passes_exclusive(button_config::ButtonCombo::InputPlayback) {
playback(MENU.playback_button_combination.get_random().into_idx());
} else if MENU.record_trigger == OnOff::On
&& button_config::combo_passes_exclusive(
module_accessor,
button_config::ButtonCombo::InputRecord,
)
&& button_config::combo_passes_exclusive(button_config::ButtonCombo::InputRecord)
{
lockout_record();
}
@ -281,7 +275,7 @@ pub unsafe fn lockout_record() {
P1_FINAL_MAPPING.lock()[CURRENT_RECORD_SLOT]
.iter_mut()
.for_each(|mapped_input| {
*mapped_input = MappedInputs::default();
*mapped_input = MappedInputs::empty();
});
CURRENT_FRAME_LENGTH = MENU.recording_frames.into_frames();
P1_FRAME_LENGTH_MAPPING.lock()[CURRENT_RECORD_SLOT] = CURRENT_FRAME_LENGTH;
@ -372,18 +366,7 @@ pub unsafe fn is_end_standby() -> bool {
lstick_movement || rstick_movement || buttons_pressed
}
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
original!()(mappings, player_idx, out, controller_struct, arg);
pub unsafe fn handle_final_input_mapping(player_idx: i32, out: *mut MappedInputs) {
if player_idx == 0 {
// if player 1
if INPUT_RECORD == Record {
@ -405,7 +388,7 @@ unsafe fn handle_final_input_mapping(
}
P1_FINAL_MAPPING.lock()[CURRENT_RECORD_SLOT][INPUT_RECORD_FRAME] = *out;
*out = MappedInputs::default(); // don't control player while recording
*out = MappedInputs::empty(); // don't control player while recording
println!("Stored Player Input! Frame: {}", INPUT_RECORD_FRAME);
}
}
@ -463,7 +446,7 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
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();
saved_mapped_inputs = MappedInputs::empty();
}
(*controller_data).buttons = saved_mapped_inputs.buttons;
@ -524,5 +507,5 @@ extern "C" {
}
pub fn init() {
skyline::install_hooks!(set_cpu_controls, handle_final_input_mapping,);
skyline::install_hooks!(set_cpu_controls);
}

View file

@ -1,5 +1,4 @@
use skyline::hooks::{getRegionAddress, InlineCtx, Region};
use skyline::nn::hid::*;
use skyline::nn::ro::LookupSymbol;
use smash::app::{self, enSEType, lua_bind::*, utility};
use smash::lib::lua_const::*;
@ -11,6 +10,7 @@ use crate::common::{
is_training_mode, menu, FIGHTER_MANAGER_ADDR, ITEM_MANAGER_ADDR, STAGE_MANAGER_ADDR,
};
use crate::hitbox_visualizer;
use crate::input::*;
use crate::logging::*;
use crate::training::character_specific::items;
@ -35,7 +35,6 @@ mod fast_fall;
mod full_hop;
pub mod input_delay;
mod input_record;
mod input_recording;
mod mash;
mod reset;
pub mod save_states;
@ -121,7 +120,7 @@ fn once_per_frame_per_fighter(
}
unsafe {
if menu::menu_condition(module_accessor) {
if menu::menu_condition() {
menu::spawn_menu();
}
@ -615,25 +614,30 @@ pub unsafe fn handle_reused_ui(
original!()(fighter_data, param_2)
}
#[allow(improper_ctypes)]
extern "C" {
fn add_nn_hid_hook(callback: fn(*mut NpadGcState, *const u32));
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
original!()(mappings, player_idx, out, controller_struct, arg);
if !is_training_mode() {
return;
}
menu::handle_final_input_mapping(player_idx, controller_struct, out);
dev_config::handle_final_input_mapping(player_idx, controller_struct);
input_delay::handle_final_input_mapping(player_idx, out);
input_record::handle_final_input_mapping(player_idx, out);
}
pub fn training_mods() {
info!("Applying training mods.");
// Input Mods
unsafe {
if let Some(_f) = (add_nn_hid_hook as *const ()).as_ref() {
add_nn_hid_hook(input_delay::handle_get_npad_state);
add_nn_hid_hook(menu::handle_get_npad_state);
add_nn_hid_hook(dev_config::handle_get_npad_state);
} else {
panic!("The NN-HID hook plugin could not be found and is required to add NRO hooks. Make sure libnn_hid_hook.nro is installed.");
}
}
unsafe {
LookupSymbol(
&mut FIGHTER_MANAGER_ADDR,
@ -705,6 +709,8 @@ pub fn training_mods() {
handle_star_ko,
// Clatter
clatter::hook_start_clatter,
// Input
handle_final_input_mapping
);
combo::init();

View file

@ -408,10 +408,8 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
&& is_dead(module_accessor);
let mut triggered_reset: bool = false;
if !is_operation_cpu(module_accessor) && !fighter_is_nana {
triggered_reset = button_config::combo_passes_exclusive(
module_accessor,
button_config::ButtonCombo::LoadState,
);
triggered_reset =
button_config::combo_passes_exclusive(button_config::ButtonCombo::LoadState);
}
if (autoload_reset || triggered_reset) && !fighter_is_nana {
if save_state.state == NoAction {
@ -626,8 +624,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
}
// Save state
if button_config::combo_passes_exclusive(module_accessor, button_config::ButtonCombo::SaveState)
{
if button_config::combo_passes_exclusive(button_config::ButtonCombo::SaveState) {
// Don't begin saving state if Nana's delayed input is captured
MIRROR_STATE = 1.0;
save_state_player(MENU.save_state_slot.as_idx() as usize).state = Save;

View file

@ -6,7 +6,7 @@ use smash::ui2d::{SmashPane, SmashTextBox};
use training_mod_tui::gauge::GaugeState;
use training_mod_tui::{App, AppPage};
use crate::{common, common::menu::QUICK_MENU_ACTIVE};
use crate::{common, common::menu::QUICK_MENU_ACTIVE, input::*};
pub static NUM_MENU_TEXT_OPTIONS: usize = 33;
pub static _NUM_MENU_TABS: usize = 3;
@ -405,7 +405,8 @@ pub unsafe fn draw(root_pane: &Pane) {
};
let tab_titles = [prev_tab, tab_selected, next_tab].map(|idx| app_tabs[idx]);
let is_gcc = common::menu::p1_controller_is_gcc();
let is_gcc =
(*common::menu::P1_CONTROLLER_STATE.data_ptr()).style == ControllerStyle::GCController;
let button_mapping = if is_gcc {
GCC_BUTTON_MAPPING.clone()
} else {

View file

@ -80,6 +80,11 @@ pub struct TrainingModpackMenu {
pub hitstun_playback: HitstunPlayback,
pub playback_mash: OnOff,
pub playback_loop: OnOff,
pub menu_open: ButtonConfig,
pub save_state_save: ButtonConfig,
pub save_state_load: ButtonConfig,
pub input_record: ButtonConfig,
pub input_playback: ButtonConfig,
}
#[repr(C)]
@ -149,7 +154,7 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
save_state_slot: SaveStateSlot::One,
randomize_slots: OnOff::Off,
save_state_mirroring: SaveStateMirroring::None,
save_state_playback: PlaybackSlot::S1,
save_state_playback: PlaybackSlot::empty(),
sdi_state: Direction::empty(),
sdi_strength: SdiFrequency::None,
shield_state: Shield::None,
@ -178,6 +183,11 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
hitstun_playback: HitstunPlayback::Hitstun,
playback_mash: OnOff::On,
playback_loop: OnOff::Off,
menu_open: ButtonConfig::B.union(ButtonConfig::DPAD_UP),
save_state_save: ButtonConfig::ZL.union(ButtonConfig::DPAD_DOWN),
save_state_load: ButtonConfig::ZL.union(ButtonConfig::DPAD_UP),
input_record: ButtonConfig::ZR.union(ButtonConfig::DPAD_DOWN),
input_playback: ButtonConfig::ZR.union(ButtonConfig::DPAD_UP),
};
pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU;
@ -806,5 +816,48 @@ pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu<'static> {
);
overall_menu.tabs.push(input_tab);
let mut button_tab = Tab {
tab_id: "button",
tab_title: "Button Config",
tab_submenus: Vec::new(),
};
button_tab.add_submenu_with_toggles::<ButtonConfig>(
"Menu Open",
"menu_open",
"Menu Open: Hold: Hold any one button and press the others to trigger",
false,
&(menu.menu_open.bits() as u32),
);
button_tab.add_submenu_with_toggles::<ButtonConfig>(
"Save State Save",
"save_state_save",
"Save State Save: Hold any one button and press the others to trigger",
false,
&(menu.save_state_save.bits() as u32),
);
button_tab.add_submenu_with_toggles::<ButtonConfig>(
"Save State Load",
"save_state_load",
"Save State Load: Hold any one button and press the others to trigger",
false,
&(menu.save_state_load.bits() as u32),
);
button_tab.add_submenu_with_toggles::<ButtonConfig>(
"Input Record",
"input_record",
"Input Record: Hold any one button and press the others to trigger",
false,
&(menu.input_record.bits() as u32),
);
button_tab.add_submenu_with_toggles::<ButtonConfig>(
"Input Playback",
"input_playback",
"Input Playback: Hold any one button and press the others to trigger",
false,
&(menu.input_playback.bits() as u32),
);
overall_menu.tabs.push(button_tab);
overall_menu
}

View file

@ -1441,3 +1441,52 @@ impl ToggleTrait for RecordingFrames {
RecordingFrames::iter().map(|i| i as u32).collect()
}
}
bitflags! {
pub struct ButtonConfig : u32 {
const A = 0b0000_0000_0000_0000_0001;
const B = 0b0000_0000_0000_0000_0010;
const X = 0b0000_0000_0000_0000_0100;
const Y = 0b0000_0000_0000_0000_1000;
const L = 0b0000_0000_0000_0001_0000;
const R = 0b0000_0000_0000_0010_0000;
const ZL = 0b0000_0000_0000_0100_0000;
const ZR = 0b0000_0000_0000_1000_0000;
const DPAD_UP = 0b0000_0000_0001_0000_0000;
const DPAD_DOWN = 0b0000_0000_0010_0000_0000;
const DPAD_LEFT = 0b0000_0000_0100_0000_0000;
const DPAD_RIGHT = 0b0000_0000_1000_0000_0000;
const PLUS = 0b0000_0001_0000_0000_0000;
const MINUS = 0b0000_0010_0000_0000_0000;
const LSTICK = 0b0000_0100_0000_0000_0000;
const RSTICK = 0b0000_1000_0000_0000_0000;
}
}
impl ButtonConfig {
// Should we use the font glyphs? Or do that special casing in the menu?
pub fn as_str(self) -> Option<&'static str> {
Some(match self {
ButtonConfig::A => "A",
ButtonConfig::B => "B",
ButtonConfig::X => "X",
ButtonConfig::Y => "Y",
ButtonConfig::L => "Pro L",
ButtonConfig::R => "Pro R; GCC Z",
ButtonConfig::ZL => "Pro ZL; GCC L",
ButtonConfig::ZR => "Pro ZR; GCC R",
ButtonConfig::DPAD_UP => "DPad Up",
ButtonConfig::DPAD_DOWN => "DPad Down",
ButtonConfig::DPAD_LEFT => "DPad Left",
ButtonConfig::DPAD_RIGHT => "DPad Right",
ButtonConfig::PLUS => "Plus",
ButtonConfig::MINUS => "Minus",
ButtonConfig::LSTICK => "Left Stick Press",
ButtonConfig::RSTICK => "Right Stick Press",
_ => return None,
})
}
}
extra_bitflag_impls! {ButtonConfig}
impl_serde_for_bitflags!(ButtonConfig);