From b1f0f1b3ac980b9fc5d40a468069e4187a76b36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo?= Date: Sun, 20 Jul 2025 13:02:56 -0300 Subject: [PATCH] Initial support for multiple controllers. (#3) * Support for multiple controllers. * Fix default bindings. * Backwards compatibility with controls from previous version. --- include/banjo_config.h | 6 +- include/recomp_input.h | 37 +++++-- src/game/config.cpp | 156 ++++++++++++++++++--------- src/game/controls.cpp | 46 ++++---- src/game/input.cpp | 227 +++++++++++++++++++++++++++++++--------- src/game/recomp_api.cpp | 2 +- src/main/main.cpp | 3 + src/ui/ui_config.cpp | 37 ++++--- src/ui/ui_state.cpp | 18 ++-- 9 files changed, 376 insertions(+), 156 deletions(-) diff --git a/include/banjo_config.h b/include/banjo_config.h index b9d7ac5..aace8e4 100644 --- a/include/banjo_config.h +++ b/include/banjo_config.h @@ -15,9 +15,9 @@ namespace banjo { void save_config(); void reset_input_bindings(); - void reset_cont_input_bindings(); - void reset_kb_input_bindings(); - void reset_single_input_binding(recomp::InputDevice device, recomp::GameInput input); + void reset_cont_input_bindings(int controller_num); + void reset_kb_input_bindings(int controller_num); + void reset_single_input_binding(int controller_num, recomp::InputDevice device, recomp::GameInput input); std::filesystem::path get_app_folder_path(); diff --git a/include/recomp_input.h b/include/recomp_input.h index e534b5c..fe671a7 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -70,13 +70,13 @@ namespace recomp { }; void poll_inputs(); - float get_input_analog(const InputField& field); - float get_input_analog(const std::span fields); - bool get_input_digital(const InputField& field); - bool get_input_digital(const std::span fields); + float get_input_analog(int controller_num, const InputField& field); + float get_input_analog(int controller_num, const std::span fields); + bool get_input_digital(int controller_num, const InputField& field); + bool get_input_digital(int controller_num, const std::span fields); void get_gyro_deltas(float* x, float* y); void get_mouse_deltas(float* x, float* y); - void get_right_analog(float* x, float* y); + void get_right_analog(int controller_num, float* x, float* y); enum class InputDevice { Controller, @@ -157,8 +157,28 @@ namespace recomp { const std::string& get_input_name(GameInput input); const std::string& get_input_enum_name(GameInput input); GameInput get_input_from_enum_name(const std::string_view name); - InputField& get_input_binding(GameInput input, size_t binding_index, InputDevice device); - void set_input_binding(GameInput input, size_t binding_index, InputDevice device, InputField value); + InputField& get_input_binding(int controller_num, GameInput input, size_t binding_index, InputDevice device); + void set_input_binding(int controller_num, GameInput input, size_t binding_index, InputDevice device, InputField value); + + struct ControllerGUID { + std::string serial; + int vendor{}; + int product{}; + int version{}; + int crc16{}; + int player_index{}; + }; + + void set_input_controller_guid(int controller_num, const ControllerGUID &guid); + ControllerGUID get_input_controller_guid(int controller_num); + + struct ControllerOption { + std::string name; + ControllerGUID guid; + }; + + void refresh_controller_options(); + const std::vector &get_controller_options(); bool get_n64_input(int controller_num, uint16_t* buttons_out, float* x_out, float* y_out); void set_rumble(int controller_num, bool); @@ -195,6 +215,9 @@ namespace recomp { BackgroundInputMode get_background_input_mode(); void set_background_input_mode(BackgroundInputMode mode); + bool get_single_controller_mode(); + void set_single_controller_mode(bool single_controller); + bool game_input_disabled(); bool all_input_disabled(); } diff --git a/src/game/config.cpp b/src/game/config.cpp index e361b74..2b8a06d 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -129,6 +129,19 @@ namespace recomp { j.at("input_type").get_to(field.input_type); j.at("input_id").get_to(field.input_id); } + + void to_json(json& j, const ControllerGUID& guid) { + j = json{ {"serial", guid.serial}, {"vendor", guid.vendor}, {"product", guid.product}, {"version", guid.version}, {"crc16", guid.crc16}, {"player_index", guid.player_index} }; + } + + void from_json(const json& j, ControllerGUID& guid) { + j.at("serial").get_to(guid.serial); + j.at("vendor").get_to(guid.vendor); + j.at("product").get_to(guid.product); + j.at("version").get_to(guid.version); + j.at("crc16").get_to(guid.crc16); + j.at("player_index").get_to(guid.player_index); + } } std::filesystem::path banjo::get_app_folder_path() { @@ -266,64 +279,68 @@ bool load_general_config(const std::filesystem::path& path) { return true; } -void assign_mapping(recomp::InputDevice device, recomp::GameInput input, const std::vector& value) { +void assign_mapping(int controller_num, recomp::InputDevice device, recomp::GameInput input, const std::vector& value) { for (size_t binding_index = 0; binding_index < std::min(value.size(), recomp::bindings_per_input); binding_index++) { - recomp::set_input_binding(input, binding_index, device, value[binding_index]); + recomp::set_input_binding(controller_num, input, binding_index, device, value[binding_index]); } }; // same as assign_mapping, except will clear unassigned bindings if not in value -void assign_mapping_complete(recomp::InputDevice device, recomp::GameInput input, const std::vector& value) { +void assign_mapping_complete(int controller_num, recomp::InputDevice device, recomp::GameInput input, const std::vector& value) { for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) { if (binding_index >= value.size()) { - recomp::set_input_binding(input, binding_index, device, recomp::InputField{}); + recomp::set_input_binding(controller_num, input, binding_index, device, recomp::InputField{}); } else { - recomp::set_input_binding(input, binding_index, device, value[binding_index]); + recomp::set_input_binding(controller_num, input, binding_index, device, value[binding_index]); } } }; -void assign_all_mappings(recomp::InputDevice device, const recomp::DefaultN64Mappings& values) { - assign_mapping_complete(device, recomp::GameInput::A, values.a); - assign_mapping_complete(device, recomp::GameInput::B, values.b); - assign_mapping_complete(device, recomp::GameInput::Z, values.z); - assign_mapping_complete(device, recomp::GameInput::START, values.start); - assign_mapping_complete(device, recomp::GameInput::DPAD_UP, values.dpad_up); - assign_mapping_complete(device, recomp::GameInput::DPAD_DOWN, values.dpad_down); - assign_mapping_complete(device, recomp::GameInput::DPAD_LEFT, values.dpad_left); - assign_mapping_complete(device, recomp::GameInput::DPAD_RIGHT, values.dpad_right); - assign_mapping_complete(device, recomp::GameInput::L, values.l); - assign_mapping_complete(device, recomp::GameInput::R, values.r); - assign_mapping_complete(device, recomp::GameInput::C_UP, values.c_up); - assign_mapping_complete(device, recomp::GameInput::C_DOWN, values.c_down); - assign_mapping_complete(device, recomp::GameInput::C_LEFT, values.c_left); - assign_mapping_complete(device, recomp::GameInput::C_RIGHT, values.c_right); +void assign_all_mappings(int controller_num, recomp::InputDevice device, const recomp::DefaultN64Mappings& values) { + assign_mapping_complete(controller_num, device, recomp::GameInput::A, values.a); + assign_mapping_complete(controller_num, device, recomp::GameInput::B, values.b); + assign_mapping_complete(controller_num, device, recomp::GameInput::Z, values.z); + assign_mapping_complete(controller_num, device, recomp::GameInput::START, values.start); + assign_mapping_complete(controller_num, device, recomp::GameInput::DPAD_UP, values.dpad_up); + assign_mapping_complete(controller_num, device, recomp::GameInput::DPAD_DOWN, values.dpad_down); + assign_mapping_complete(controller_num, device, recomp::GameInput::DPAD_LEFT, values.dpad_left); + assign_mapping_complete(controller_num, device, recomp::GameInput::DPAD_RIGHT, values.dpad_right); + assign_mapping_complete(controller_num, device, recomp::GameInput::L, values.l); + assign_mapping_complete(controller_num, device, recomp::GameInput::R, values.r); + assign_mapping_complete(controller_num, device, recomp::GameInput::C_UP, values.c_up); + assign_mapping_complete(controller_num, device, recomp::GameInput::C_DOWN, values.c_down); + assign_mapping_complete(controller_num, device, recomp::GameInput::C_LEFT, values.c_left); + assign_mapping_complete(controller_num, device, recomp::GameInput::C_RIGHT, values.c_right); - assign_mapping_complete(device, recomp::GameInput::X_AXIS_NEG, values.analog_left); - assign_mapping_complete(device, recomp::GameInput::X_AXIS_POS, values.analog_right); - assign_mapping_complete(device, recomp::GameInput::Y_AXIS_NEG, values.analog_down); - assign_mapping_complete(device, recomp::GameInput::Y_AXIS_POS, values.analog_up); + assign_mapping_complete(controller_num, device, recomp::GameInput::X_AXIS_NEG, values.analog_left); + assign_mapping_complete(controller_num, device, recomp::GameInput::X_AXIS_POS, values.analog_right); + assign_mapping_complete(controller_num, device, recomp::GameInput::Y_AXIS_NEG, values.analog_down); + assign_mapping_complete(controller_num, device, recomp::GameInput::Y_AXIS_POS, values.analog_up); - assign_mapping_complete(device, recomp::GameInput::TOGGLE_MENU, values.toggle_menu); - assign_mapping_complete(device, recomp::GameInput::ACCEPT_MENU, values.accept_menu); - assign_mapping_complete(device, recomp::GameInput::APPLY_MENU, values.apply_menu); + assign_mapping_complete(controller_num, device, recomp::GameInput::TOGGLE_MENU, values.toggle_menu); + assign_mapping_complete(controller_num, device, recomp::GameInput::ACCEPT_MENU, values.accept_menu); + assign_mapping_complete(controller_num, device, recomp::GameInput::APPLY_MENU, values.apply_menu); }; void banjo::reset_input_bindings() { - assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); - assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); + assign_all_mappings(0, recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); + + for (int i = 0; i < 4; i++) { + assign_all_mappings(i, recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); + } } -void banjo::reset_cont_input_bindings() { - assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); +void banjo::reset_cont_input_bindings(int controller_num) { + assign_all_mappings(controller_num, recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); } -void banjo::reset_kb_input_bindings() { - assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); +void banjo::reset_kb_input_bindings(int controller_num) { + assign_all_mappings(controller_num, recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); } -void banjo::reset_single_input_binding(recomp::InputDevice device, recomp::GameInput input) { +void banjo::reset_single_input_binding(int controller_num, recomp::InputDevice device, recomp::GameInput input) { assign_mapping_complete( + controller_num, device, input, recomp::get_default_mapping_for_input( @@ -368,32 +385,39 @@ bool load_graphics_config(const std::filesystem::path& path) { return true; } -void add_input_bindings(nlohmann::json& out, recomp::GameInput input, recomp::InputDevice device) { +void add_input_bindings(nlohmann::json& out, int controller_num, recomp::GameInput input, recomp::InputDevice device) { const std::string& input_name = recomp::get_input_enum_name(input); nlohmann::json& out_array = out[input_name]; out_array = nlohmann::json::array(); for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) { - out_array[binding_index] = recomp::get_input_binding(input, binding_index, device); + out_array[binding_index] = recomp::get_input_binding(controller_num, input, binding_index, device); } }; +constexpr int controls_version = 2; + bool save_controls_config(const std::filesystem::path& path) { nlohmann::json config_json{}; + config_json["version"] = controls_version; + config_json["players"] = std::vector(4); + for (size_t i = 0; i < config_json["players"].size(); i++) { + nlohmann::json &player = config_json["players"][i]; + player["keyboard"] = {}; + player["controller"] = {}; - config_json["keyboard"] = {}; - config_json["controller"] = {}; + for (size_t j = 0; j < recomp::get_num_inputs(); j++) { + recomp::GameInput cur_input = static_cast(j); + add_input_bindings(player["keyboard"], i, cur_input, recomp::InputDevice::Keyboard); + add_input_bindings(player["controller"], i, cur_input, recomp::InputDevice::Controller); + } - for (size_t i = 0; i < recomp::get_num_inputs(); i++) { - recomp::GameInput cur_input = static_cast(i); - - add_input_bindings(config_json["keyboard"], cur_input, recomp::InputDevice::Keyboard); - add_input_bindings(config_json["controller"], cur_input, recomp::InputDevice::Controller); + player["controller"]["guid"] = recomp::get_input_controller_guid(i); } return save_json_with_backups(path, config_json); } -bool load_input_device_from_json(const nlohmann::json& config_json, recomp::InputDevice device, const std::string& key) { +bool load_input_device_from_json(const nlohmann::json& config_json, int controller_num, recomp::InputDevice device, const std::string& key) { // Check if the json object for the given key exists. auto find_it = config_json.find(key); if (find_it == config_json.end()) { @@ -410,6 +434,7 @@ bool load_input_device_from_json(const nlohmann::json& config_json, recomp::Inpu auto find_input_it = mappings_json.find(input_name); if (find_input_it == mappings_json.end() || !find_input_it->is_array()) { assign_mapping( + controller_num, device, cur_input, recomp::get_default_mapping_for_input( @@ -427,26 +452,57 @@ bool load_input_device_from_json(const nlohmann::json& config_json, recomp::Inpu for (size_t binding_index = 0; binding_index < std::min(recomp::bindings_per_input, input_json.size()); binding_index++) { recomp::InputField cur_field{}; recomp::from_json(input_json[binding_index], cur_field); - recomp::set_input_binding(cur_input, binding_index, device, cur_field); + recomp::set_input_binding(controller_num, cur_input, binding_index, device, cur_field); } } return true; } +void load_bindings_config(const nlohmann::json &config_json, int controller_num) { + if (!load_input_device_from_json(config_json, controller_num, recomp::InputDevice::Keyboard, "keyboard")) { + assign_all_mappings(controller_num, recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); + } + + if (!load_input_device_from_json(config_json, controller_num, recomp::InputDevice::Controller, "controller")) { + assign_all_mappings(controller_num, recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); + } +} + bool load_controls_config(const std::filesystem::path& path) { nlohmann::json config_json{}; if (!read_json_with_backups(path, config_json)) { return false; } - if (!load_input_device_from_json(config_json, recomp::InputDevice::Keyboard, "keyboard")) { - assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); + auto version_it = config_json.find("version"); + if (version_it != config_json.end()) { + auto players_it = config_json.find("players"); + for (size_t i = 0; i < 4; i++) { + const bool player_exists = players_it != config_json.end() && players_it->is_array() && (players_it->size() > i); + const nlohmann::json &player = player_exists ? (*players_it)[i] : nlohmann::json(); + load_bindings_config(player, (int)(i)); + + auto controller_it = player.find("controller"); + if (controller_it != player.end()) { + const nlohmann::json &controller = *controller_it; + auto guid_it = controller.find("guid"); + if (guid_it != controller.end()) { + recomp::set_input_controller_guid((int)(i), *guid_it); + } + } + } + } + else { + // Version 1 of the format only had bindings for Player 1 on the root element. + load_bindings_config(config_json, 0); + + for (int i = 1; i < 4; i++) { + // Assign defaults for every other controller. + assign_all_mappings(i, recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); + } } - if (!load_input_device_from_json(config_json, recomp::InputDevice::Controller, "controller")) { - assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); - } return true; } diff --git a/src/game/controls.cpp b/src/game/controls.cpp index 0bb3857..2c51266 100644 --- a/src/game/controls.cpp +++ b/src/game/controls.cpp @@ -7,8 +7,9 @@ // Arrays that hold the mappings for every input for keyboard and controller respectively. using input_mapping = std::array; using input_mapping_array = std::array(recomp::GameInput::COUNT)>; -static input_mapping_array keyboard_input_mappings{}; -static input_mapping_array controller_input_mappings{}; +static std::array keyboard_input_mappings{}; +static std::array controller_input_mappings{}; +static std::array controller_guids{}; // Make the button value array, which maps a button index to its bit field. #define DEFINE_INPUT(name, value, readable) uint16_t(value##u), @@ -53,8 +54,8 @@ recomp::GameInput recomp::get_input_from_enum_name(const std::string_view enum_n } // Due to an RmlUi limitation this can't be const. Ideally it would return a const reference or even just a straight up copy. -recomp::InputField& recomp::get_input_binding(GameInput input, size_t binding_index, recomp::InputDevice device) { - input_mapping_array& device_mappings = (device == recomp::InputDevice::Controller) ? controller_input_mappings : keyboard_input_mappings; +recomp::InputField& recomp::get_input_binding(int controller_num, GameInput input, size_t binding_index, recomp::InputDevice device) { + input_mapping_array& device_mappings = (device == recomp::InputDevice::Controller) ? controller_input_mappings[controller_num] : keyboard_input_mappings[controller_num]; input_mapping& cur_input_mapping = device_mappings.at(static_cast(input)); if (binding_index < cur_input_mapping.size()) { @@ -66,8 +67,8 @@ recomp::InputField& recomp::get_input_binding(GameInput input, size_t binding_in } } -void recomp::set_input_binding(recomp::GameInput input, size_t binding_index, recomp::InputDevice device, recomp::InputField value) { - input_mapping_array& device_mappings = (device == recomp::InputDevice::Controller) ? controller_input_mappings : keyboard_input_mappings; +void recomp::set_input_binding(int controller_num, recomp::GameInput input, size_t binding_index, recomp::InputDevice device, recomp::InputField value) { + input_mapping_array &device_mappings = (device == recomp::InputDevice::Controller) ? controller_input_mappings[controller_num] : keyboard_input_mappings[controller_num]; input_mapping& cur_input_mapping = device_mappings.at(static_cast(input)); if (binding_index < cur_input_mapping.size()) { @@ -75,37 +76,40 @@ void recomp::set_input_binding(recomp::GameInput input, size_t binding_index, re } } +void recomp::set_input_controller_guid(int controller_num, const ControllerGUID &guid) { + controller_guids[controller_num] = guid; +} + +recomp::ControllerGUID recomp::get_input_controller_guid(int controller_num) { + return controller_guids[controller_num]; +} + bool recomp::get_n64_input(int controller_num, uint16_t* buttons_out, float* x_out, float* y_out) { uint16_t cur_buttons = 0; float cur_x = 0.0f; float cur_y = 0.0f; - - if (controller_num != 0) { - return false; - } - if (!recomp::game_input_disabled()) { for (size_t i = 0; i < n64_button_values.size(); i++) { size_t input_index = (size_t)GameInput::N64_BUTTON_START + i; - cur_buttons |= recomp::get_input_digital(keyboard_input_mappings[input_index]) ? n64_button_values[i] : 0; - cur_buttons |= recomp::get_input_digital(controller_input_mappings[input_index]) ? n64_button_values[i] : 0; + cur_buttons |= recomp::get_input_digital(controller_num, keyboard_input_mappings[controller_num][input_index]) ? n64_button_values[i] : 0; + cur_buttons |= recomp::get_input_digital(controller_num, controller_input_mappings[controller_num][input_index]) ? n64_button_values[i] : 0; } float joystick_deadzone = recomp::get_joystick_deadzone() / 100.0f; - float joystick_x = recomp::get_input_analog(controller_input_mappings[(size_t)GameInput::X_AXIS_POS]) - - recomp::get_input_analog(controller_input_mappings[(size_t)GameInput::X_AXIS_NEG]); + float joystick_x = recomp::get_input_analog(controller_num, controller_input_mappings[controller_num][(size_t)GameInput::X_AXIS_POS]) + - recomp::get_input_analog(controller_num, controller_input_mappings[controller_num][(size_t)GameInput::X_AXIS_NEG]); - float joystick_y = recomp::get_input_analog(controller_input_mappings[(size_t)GameInput::Y_AXIS_POS]) - - recomp::get_input_analog(controller_input_mappings[(size_t)GameInput::Y_AXIS_NEG]); + float joystick_y = recomp::get_input_analog(controller_num, controller_input_mappings[controller_num][(size_t)GameInput::Y_AXIS_POS]) + - recomp::get_input_analog(controller_num, controller_input_mappings[controller_num][(size_t)GameInput::Y_AXIS_NEG]); recomp::apply_joystick_deadzone(joystick_x, joystick_y, &joystick_x, &joystick_y); - cur_x = recomp::get_input_analog(keyboard_input_mappings[(size_t)GameInput::X_AXIS_POS]) - - recomp::get_input_analog(keyboard_input_mappings[(size_t)GameInput::X_AXIS_NEG]) + joystick_x; + cur_x = recomp::get_input_analog(controller_num, keyboard_input_mappings[controller_num][(size_t)GameInput::X_AXIS_POS]) + - recomp::get_input_analog(controller_num, keyboard_input_mappings[controller_num][(size_t)GameInput::X_AXIS_NEG]) + joystick_x; - cur_y = recomp::get_input_analog(keyboard_input_mappings[(size_t)GameInput::Y_AXIS_POS]) - - recomp::get_input_analog(keyboard_input_mappings[(size_t)GameInput::Y_AXIS_NEG]) + joystick_y; + cur_y = recomp::get_input_analog(controller_num, keyboard_input_mappings[controller_num][(size_t)GameInput::Y_AXIS_POS]) + - recomp::get_input_analog(controller_num, keyboard_input_mappings[controller_num][(size_t)GameInput::Y_AXIS_NEG]) + joystick_y; } *buttons_out = cur_buttons; diff --git a/src/game/input.cpp b/src/game/input.cpp index 6e4bccc..1ce3e0c 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -28,9 +28,12 @@ static struct { SDL_Keymod keymod = SDL_Keymod::KMOD_NONE; int numkeys = 0; std::atomic_int32_t mouse_wheel_pos = 0; - std::mutex cur_controllers_mutex; - std::vector cur_controllers{}; + std::mutex controllers_mutex; + std::vector detected_controllers{}; + std::vector detected_controller_options{}; + std::array assigned_controllers{}; // Only used when Multiplayer is enabled. std::unordered_map controller_states; + bool single_controller = true; std::array rotation_delta{}; std::array mouse_delta{}; @@ -38,8 +41,8 @@ static struct { std::array pending_rotation_delta{}; std::array pending_mouse_delta{}; - float cur_rumble; - bool rumble_active; + std::array cur_rumble{}; + std::array rumble_active{}; } InputState; static struct { @@ -175,8 +178,9 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) { break; case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: if (scanning_device != recomp::InputDevice::COUNT) { - auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller); - auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller); + // TODO: Needs the active controller tab index. + auto menuToggleBinding0 = recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller); + auto menuToggleBinding1 = recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller); // note - magic number: 0 is InputType::None if ((menuToggleBinding0.input_type != 0 && event->cbutton.button == menuToggleBinding0.input_id) || (menuToggleBinding1.input_type != 0 && event->cbutton.button == menuToggleBinding1.input_id)) { @@ -476,14 +480,71 @@ void recomp::poll_inputs() { InputState.keymod = SDL_GetModState(); { - std::lock_guard lock{ InputState.cur_controllers_mutex }; - InputState.cur_controllers.clear(); + std::lock_guard lock{ InputState.controllers_mutex }; + InputState.detected_controllers.clear(); + + static std::vector free_controllers; + free_controllers.clear(); for (const auto& [id, state] : InputState.controller_states) { (void)id; // Avoid unused variable warning. SDL_GameController* controller = state.controller; if (controller != nullptr) { - InputState.cur_controllers.push_back(controller); + free_controllers.emplace_back(InputState.detected_controllers.size()); + InputState.detected_controllers.push_back(controller); + } + } + + // Assign controllers based on configuration. + InputState.assigned_controllers.fill(nullptr); + + // FIXME: Use active player count instead of iterating on all possible players. + Uint16 vendor, product, version, crc16; + for (size_t i = 0; i < 4; i++) { + recomp::ControllerGUID controller_guid = recomp::get_input_controller_guid(i); + int min_index_difference = INT_MAX; + size_t j = 0; + while (j < free_controllers.size()) { + SDL_GameController *controller = InputState.detected_controllers[free_controllers[j]]; + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller); + if (joystick != nullptr) { + SDL_JoystickGUID joystick_guid = SDL_JoystickGetGUID(joystick); + const char *joystick_serial = SDL_JoystickGetSerial(joystick); + SDL_GetJoystickGUIDInfo(joystick_guid, &vendor, &product, &version, &crc16); + + bool empty_serials = controller_guid.serial.empty() && joystick_serial == nullptr; + bool matching_serials = joystick_serial != nullptr && strcmp(joystick_serial, controller_guid.serial.c_str()) == 0; + if (vendor == controller_guid.vendor && product == controller_guid.product && version == controller_guid.version && crc16 == controller_guid.crc16 && (empty_serials || matching_serials)) { + // The controller seems to be a match, but we use the controller with the least difference in player index to sort out potential duplicates. + int joystick_player_index = SDL_JoystickGetPlayerIndex(joystick); + int index_difference = abs(controller_guid.player_index - joystick_player_index); + if (min_index_difference > index_difference) { + InputState.assigned_controllers[i] = controller; + min_index_difference = index_difference; + free_controllers.erase(free_controllers.begin() + j); + continue; + } + } + } + + j++; + } + } + + // Do a second pass to assign controllers that are currently unused to the remaining players that failed to be assigned a controller. + for (int i = 0; i < 4; i++) { + if (InputState.assigned_controllers[i] != nullptr) { + continue; + } + + if (!free_controllers.empty()) { + // Assign a free controller only. + InputState.assigned_controllers[i] = InputState.detected_controllers[free_controllers.front()]; + free_controllers.erase(free_controllers.begin()); + } + else { + // Prefer not to assign a controller if none of them are free. + break; } } } @@ -501,9 +562,7 @@ void recomp::poll_inputs() { } void recomp::set_rumble(int controller_num, bool on) { - if (controller_num == 0) { - InputState.rumble_active = on; - } + InputState.rumble_active[controller_num] = on; } ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) { @@ -528,36 +587,52 @@ static float smoothstep(float from, float to, float amount) { // Update rumble to attempt to mimic the way n64 rumble ramps up and falls off void recomp::update_rumble() { - // Note: values are not accurate! just approximations based on feel - if (InputState.rumble_active) { - InputState.cur_rumble += 0.17f; - if (InputState.cur_rumble > 1) InputState.cur_rumble = 1; - } - else { - InputState.cur_rumble *= 0.92f; - InputState.cur_rumble -= 0.01f; - if (InputState.cur_rumble < 0) InputState.cur_rumble = 0; - } - float smooth_rumble = smoothstep(0, 1, InputState.cur_rumble); + for (size_t i = 0; i < InputState.cur_rumble.size(); i++) { + // Note: values are not accurate! just approximations based on feel + if (InputState.rumble_active[i]) { + InputState.cur_rumble[i] += 0.17f; + if (InputState.cur_rumble[i] > 1) InputState.cur_rumble[i] = 1; + } + else { + InputState.cur_rumble[i] *= 0.92f; + InputState.cur_rumble[i] -= 0.01f; + if (InputState.cur_rumble[i] < 0) InputState.cur_rumble[i] = 0; + } + float smooth_rumble = smoothstep(0, 1, InputState.cur_rumble[i]); - uint16_t rumble_strength = smooth_rumble * (recomp::get_rumble_strength() * 0xFFFF / 100); - uint32_t duration = 1000000; // Dummy duration value that lasts long enough to matter as the game will reset rumble on its own. - { - std::lock_guard lock{ InputState.cur_controllers_mutex }; - for (const auto& controller : InputState.cur_controllers) { - SDL_GameControllerRumble(controller, 0, rumble_strength, duration); + uint16_t rumble_strength = smooth_rumble * (recomp::get_rumble_strength() * 0xFFFF / 100); + uint32_t duration = 1000000; // Dummy duration value that lasts long enough to matter as the game will reset rumble on its own. + { + std::lock_guard lock{ InputState.controllers_mutex }; + if (InputState.single_controller) { + for (const auto &controller : InputState.detected_controllers) { + SDL_GameControllerRumble(controller, 0, rumble_strength, duration); + } + } + else { + if (InputState.assigned_controllers[i] != nullptr) { + SDL_GameControllerRumble(InputState.assigned_controllers[i], 0, rumble_strength, duration); + } + } } } } -bool controller_button_state(int32_t input_id) { +bool controller_button_state(int controller_num, int32_t input_id) { if (input_id >= 0 && input_id < SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MAX) { SDL_GameControllerButton button = (SDL_GameControllerButton)input_id; bool ret = false; { - std::lock_guard lock{ InputState.cur_controllers_mutex }; - for (const auto& controller : InputState.cur_controllers) { - ret |= SDL_GameControllerGetButton(controller, button); + std::lock_guard lock{ InputState.controllers_mutex }; + if (InputState.single_controller) { + for (const auto &controller : InputState.detected_controllers) { + ret |= SDL_GameControllerGetButton(controller, button); + } + } + else { + if (InputState.assigned_controllers[controller_num] != nullptr) { + ret |= SDL_GameControllerGetButton(InputState.assigned_controllers[controller_num], button); + } } } @@ -568,16 +643,15 @@ bool controller_button_state(int32_t input_id) { static std::atomic_bool right_analog_suppressed = false; -float controller_axis_state(int32_t input_id, bool allow_suppression) { +float controller_axis_state(int controller_num, int32_t input_id, bool allow_suppression) { if (abs(input_id) - 1 < SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_MAX) { SDL_GameControllerAxis axis = (SDL_GameControllerAxis)(abs(input_id) - 1); bool negative_range = input_id < 0; float ret = 0.0f; { - std::lock_guard lock{ InputState.cur_controllers_mutex }; - for (const auto& controller : InputState.cur_controllers) { - float cur_val = SDL_GameControllerGetAxis(controller, axis) * (1/32768.0f); + auto gather_axis_state = [&](SDL_GameController* controller) { + float cur_val = SDL_GameControllerGetAxis(controller, axis) * (1 / 32768.0f); if (negative_range) { cur_val = -cur_val; } @@ -588,6 +662,18 @@ float controller_axis_state(int32_t input_id, bool allow_suppression) { cur_val = 0; } ret += std::clamp(cur_val, 0.0f, 1.0f); + }; + + std::lock_guard lock{ InputState.controllers_mutex }; + if (InputState.single_controller) { + for (SDL_GameController *controller : InputState.detected_controllers) { + gather_axis_state(controller); + } + } + else { + if (InputState.assigned_controllers[controller_num] != nullptr) { + gather_axis_state(InputState.assigned_controllers[controller_num]); + } } } @@ -596,7 +682,7 @@ float controller_axis_state(int32_t input_id, bool allow_suppression) { return false; } -float recomp::get_input_analog(const recomp::InputField& field) { +float recomp::get_input_analog(int controller_num, const recomp::InputField& field) { switch ((InputType)field.input_type) { case InputType::Keyboard: if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) { @@ -607,9 +693,9 @@ float recomp::get_input_analog(const recomp::InputField& field) { } return 0.0f; case InputType::ControllerDigital: - return controller_button_state(field.input_id) ? 1.0f : 0.0f; + return controller_button_state(controller_num, field.input_id) ? 1.0f : 0.0f; case InputType::ControllerAnalog: - return controller_axis_state(field.input_id, true); + return controller_axis_state(controller_num, field.input_id, true); case InputType::Mouse: // TODO mouse support return 0.0f; @@ -618,15 +704,15 @@ float recomp::get_input_analog(const recomp::InputField& field) { } } -float recomp::get_input_analog(const std::span fields) { +float recomp::get_input_analog(int controller_num, const std::span fields) { float ret = 0.0f; for (const auto& field : fields) { - ret += get_input_analog(field); + ret += get_input_analog(controller_num, field); } return std::clamp(ret, 0.0f, 1.0f); } -bool recomp::get_input_digital(const recomp::InputField& field) { +bool recomp::get_input_digital(int controller_num, const recomp::InputField& field) { switch ((InputType)field.input_type) { case InputType::Keyboard: if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) { @@ -637,10 +723,10 @@ bool recomp::get_input_digital(const recomp::InputField& field) { } return false; case InputType::ControllerDigital: - return controller_button_state(field.input_id); + return controller_button_state(controller_num, field.input_id); case InputType::ControllerAnalog: // TODO adjustable threshold - return controller_axis_state(field.input_id, true) >= axis_threshold; + return controller_axis_state(controller_num, field.input_id, true) >= axis_threshold; case InputType::Mouse: // TODO mouse support return false; @@ -649,10 +735,10 @@ bool recomp::get_input_digital(const recomp::InputField& field) { } } -bool recomp::get_input_digital(const std::span fields) { +bool recomp::get_input_digital(int controller_num, const std::span fields) { bool ret = 0; for (const auto& field : fields) { - ret |= get_input_digital(field); + ret |= get_input_digital(controller_num, field); } return ret; } @@ -706,13 +792,13 @@ void recomp::apply_joystick_deadzone(float x_in, float y_in, float* x_out, float *y_out = y_in; } -void recomp::get_right_analog(float* x, float* y) { +void recomp::get_right_analog(int controller_num, float* x, float* y) { float x_val = - controller_axis_state((SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX + 1), false) - - controller_axis_state(-(SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX + 1), false); + controller_axis_state(controller_num, (SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX + 1), false) - + controller_axis_state(controller_num, -(SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX + 1), false); float y_val = - controller_axis_state((SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY + 1), false) - - controller_axis_state(-(SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY + 1), false); + controller_axis_state(controller_num, (SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY + 1), false) - + controller_axis_state(controller_num, -(SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY + 1), false); recomp::apply_joystick_deadzone(x_val, y_val, x, y); } @@ -730,6 +816,14 @@ bool recomp::all_input_disabled() { return scanning_device != recomp::InputDevice::COUNT; } +bool recomp::get_single_controller_mode() { + return InputState.single_controller; +} + +void recomp::set_single_controller_mode(bool single_controller) { + InputState.single_controller = single_controller; +} + std::string controller_button_to_string(SDL_GameControllerButton button) { switch (button) { case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_A: @@ -895,3 +989,32 @@ std::string recomp::InputField::to_string() const { return std::to_string(input_type) + "," + std::to_string(input_id); } } + +void recomp::refresh_controller_options() { + // TODO: Is it okay to use this to make sure the detected controllers are up to date here? + recomp::poll_inputs(); + + std::lock_guard lock{ InputState.controllers_mutex }; + InputState.detected_controller_options.clear(); + for (SDL_GameController* controller : InputState.detected_controllers) { + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller); + if (joystick == nullptr) { + continue; + } + + Uint16 vendor, product, version, crc16; + const char *joystick_name = SDL_JoystickName(joystick); + const char *joystick_serial = SDL_JoystickGetSerial(joystick); + std::string joystick_name_string = joystick_name != nullptr ? std::string(joystick_name) : "Unknown controller"; + SDL_JoystickGUID joystick_guid = SDL_JoystickGetGUID(joystick); + int joystick_player_index = SDL_JoystickGetPlayerIndex(joystick); + SDL_GetJoystickGUIDInfo(joystick_guid, &vendor, &product, &version, &crc16); + + ControllerGUID guid = { joystick_serial != nullptr ? std::string(joystick_serial) : std::string(), vendor, product, version, crc16, joystick_player_index}; + InputState.detected_controller_options.emplace_back(ControllerOption{ joystick_name_string, guid }); + } +} + +const std::vector &recomp::get_controller_options() { + return InputState.detected_controller_options; +} \ No newline at end of file diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp index 45b151b..1bc5952 100644 --- a/src/game/recomp_api.cpp +++ b/src/game/recomp_api.cpp @@ -157,7 +157,7 @@ extern "C" void recomp_get_camera_inputs(uint8_t* rdram, recomp_context* ctx) { float x, y; - recomp::get_right_analog(&x, &y); + recomp::get_right_analog(0, &x, &y); float magnitude = sqrtf(x * x + y * y); diff --git a/src/main/main.cpp b/src/main/main.cpp index 10d2902..deb157a 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -663,6 +663,9 @@ int main(int argc, char** argv) { // Register the .rtz texture pack file format with the previous content type as its only allowed content type. recomp::mods::register_mod_container_type("rtz", std::vector{ texture_pack_content_type_id }, false); + // TODO: Where is it best to place this? + recomp::set_single_controller_mode(false); + recomp::start( project_version, {}, diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp index 0dbba71..f57f934 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -110,7 +110,8 @@ int recomp::get_scanned_input_index() { } void recomp::finish_scanning_input(recomp::InputField scanned_field) { - recomp::set_input_binding(static_cast(scanned_input_index), scanned_binding_index, cur_device, scanned_field); + // TODO: Needs the active controller tab. + recomp::set_input_binding(0, static_cast(scanned_input_index), scanned_binding_index, cur_device, scanned_field); scanned_input_index = -1; scanned_binding_index = -1; controls_model_handle.DirtyVariable("inputs"); @@ -593,13 +594,14 @@ public: constructor.BindFunc("gfx_help__apply", [](Rml::Variant& out) { if (cont_active) { + // TODO: Needs the active controller tab. out = \ - (recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller).to_string() != "" ? - " " + recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller).to_string() : + (recomp::get_input_binding(0, recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller).to_string() != "" ? + " " + recomp::get_input_binding(0, recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller).to_string() : "" ) + \ - (recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller).to_string() != "" ? - " " + recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller).to_string() : + (recomp::get_input_binding(0, recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller).to_string() != "" ? + " " + recomp::get_input_binding(0, recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller).to_string() : "" ); } else { @@ -644,9 +646,11 @@ public: constructor.BindEventCallback("reset_input_bindings_to_defaults", [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { if (cur_device == recomp::InputDevice::Controller) { - banjo::reset_cont_input_bindings(); + // TODO: Needs the active controller tab. + banjo::reset_cont_input_bindings(0); } else { - banjo::reset_kb_input_bindings(); + // TODO: Needs the active controller tab. + banjo::reset_kb_input_bindings(0); } model_handle.DirtyAllVariables(); nav_help_model_handle.DirtyVariable("nav_help__accept"); @@ -658,7 +662,8 @@ public: [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { recomp::GameInput input = static_cast(inputs.at(0).Get()); for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) { - recomp::set_input_binding(input, binding_index, cur_device, recomp::InputField{}); + // TODO: Needs the active controller tab. + recomp::set_input_binding(0, input, binding_index, cur_device, recomp::InputField{}); } model_handle.DirtyVariable("inputs"); graphics_model_handle.DirtyVariable("gfx_help__apply"); @@ -666,8 +671,9 @@ public: constructor.BindEventCallback("reset_single_input_binding_to_default", [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { + // TODO: Needs the active controller tab. recomp::GameInput input = static_cast(inputs.at(0).Get()); - banjo::reset_single_input_binding(cur_device, input); + banjo::reset_single_input_binding(0, cur_device, input); model_handle.DirtyVariable("inputs"); nav_help_model_handle.DirtyVariable("nav_help__accept"); nav_help_model_handle.DirtyVariable("nav_help__exit"); @@ -704,7 +710,8 @@ public: virtual int Size(void* ptr) override { return recomp::bindings_per_input; } virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override { recomp::GameInput input = static_cast((uintptr_t)ptr); - return Rml::DataVariable{&input_field_definition_instance, &recomp::get_input_binding(input, address.index, cur_device)}; + // TODO: Needs the active controller tab. + return Rml::DataVariable{&input_field_definition_instance, &recomp::get_input_binding(0, input, address.index, cur_device)}; } }; // Static instance of the InputField array variable definition to have a fixed pointer to return to RmlUi. @@ -794,20 +801,22 @@ public: }); constructor.BindFunc("nav_help__accept", [](Rml::Variant& out) { + // TODO: Needs the active controller tab. if (cont_active) { out = \ - recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller).to_string() + \ - recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller).to_string(); + recomp::get_input_binding(0, recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller).to_string() + \ + recomp::get_input_binding(0, recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller).to_string(); } else { out = PF_KEYBOARD_ENTER; } }); constructor.BindFunc("nav_help__exit", [](Rml::Variant& out) { + // TODO: Needs the active controller tab. if (cont_active) { out = \ - recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller).to_string() + \ - recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller).to_string(); + recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller).to_string() + \ + recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller).to_string(); } else { out = PF_KEYBOARD_ESCAPE; } diff --git a/src/ui/ui_state.cpp b/src/ui/ui_state.cpp index 7f492ad..d8dacc0 100644 --- a/src/ui/ui_state.cpp +++ b/src/ui/ui_state.cpp @@ -464,9 +464,10 @@ bool recompui::try_deque_event(SDL_Event& out) { } int cont_button_to_key(SDL_ControllerButtonEvent& button) { + // TODO: Needs the active controller tab. // Configurable accept button in menu - auto menuAcceptBinding0 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller); - auto menuAcceptBinding1 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller); + auto menuAcceptBinding0 = recomp::get_input_binding(0, recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller); + auto menuAcceptBinding1 = recomp::get_input_binding(0, recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller); // note - magic number: 0 is InputType::None if ((menuAcceptBinding0.input_type != 0 && button.button == menuAcceptBinding0.input_id) || (menuAcceptBinding1.input_type != 0 && button.button == menuAcceptBinding1.input_id)) { @@ -474,8 +475,8 @@ int cont_button_to_key(SDL_ControllerButtonEvent& button) { } // Configurable apply button in menu - auto menuApplyBinding0 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller); - auto menuApplyBinding1 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller); + auto menuApplyBinding0 = recomp::get_input_binding(0, recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller); + auto menuApplyBinding1 = recomp::get_input_binding(0, recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller); // note - magic number: 0 is InputType::None if ((menuApplyBinding0.input_type != 0 && button.button == menuApplyBinding0.input_id) || (menuApplyBinding1.input_type != 0 && button.button == menuApplyBinding1.input_id)) { @@ -483,8 +484,8 @@ int cont_button_to_key(SDL_ControllerButtonEvent& button) { } // Allows closing the menu - auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller); - auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller); + auto menuToggleBinding0 = recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller); + auto menuToggleBinding1 = recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller); // note - magic number: 0 is InputType::None if ((menuToggleBinding0.input_type != 0 && button.button == menuToggleBinding0.input_id) || (menuToggleBinding1.input_type != 0 && button.button == menuToggleBinding1.input_id)) { @@ -713,8 +714,9 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s } break; case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: - auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller); - auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller); + // TODO: Needs the active controller tab. + auto menuToggleBinding0 = recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller); + auto menuToggleBinding1 = recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller); // note - magic number: 0 is InputType::None if ((menuToggleBinding0.input_type != 0 && cur_event.cbutton.button == menuToggleBinding0.input_id) || (menuToggleBinding1.input_type != 0 && cur_event.cbutton.button == menuToggleBinding1.input_id)) {