Initial support for multiple controllers. (#3)

* Support for multiple controllers.

* Fix default bindings.

* Backwards compatibility with controls from previous version.
This commit is contained in:
Darío
2025-07-20 13:02:56 -03:00
committed by GitHub
parent f8ba7ac002
commit b1f0f1b3ac
9 changed files with 376 additions and 156 deletions
+3 -3
View File
@@ -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();
+30 -7
View File
@@ -70,13 +70,13 @@ namespace recomp {
};
void poll_inputs();
float get_input_analog(const InputField& field);
float get_input_analog(const std::span<const recomp::InputField> fields);
bool get_input_digital(const InputField& field);
bool get_input_digital(const std::span<const recomp::InputField> fields);
float get_input_analog(int controller_num, const InputField& field);
float get_input_analog(int controller_num, const std::span<const recomp::InputField> fields);
bool get_input_digital(int controller_num, const InputField& field);
bool get_input_digital(int controller_num, const std::span<const recomp::InputField> 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<ControllerOption> &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();
}
+106 -50
View File
@@ -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<recomp::InputField>& value) {
void assign_mapping(int controller_num, recomp::InputDevice device, recomp::GameInput input, const std::vector<recomp::InputField>& 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<recomp::InputField>& value) {
void assign_mapping_complete(int controller_num, recomp::InputDevice device, recomp::GameInput input, const std::vector<recomp::InputField>& 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<nlohmann::json>(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<recomp::GameInput>(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<recomp::GameInput>(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;
}
+25 -21
View File
@@ -7,8 +7,9 @@
// Arrays that hold the mappings for every input for keyboard and controller respectively.
using input_mapping = std::array<recomp::InputField, recomp::bindings_per_input>;
using input_mapping_array = std::array<input_mapping, static_cast<size_t>(recomp::GameInput::COUNT)>;
static input_mapping_array keyboard_input_mappings{};
static input_mapping_array controller_input_mappings{};
static std::array<input_mapping_array, 4> keyboard_input_mappings{};
static std::array<input_mapping_array, 4> controller_input_mappings{};
static std::array<recomp::ControllerGUID, 4> 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<size_t>(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<size_t>(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;
+175 -52
View File
@@ -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<SDL_GameController*> cur_controllers{};
std::mutex controllers_mutex;
std::vector<SDL_GameController*> detected_controllers{};
std::vector<recomp::ControllerOption> detected_controller_options{};
std::array<SDL_GameController*, 4> assigned_controllers{}; // Only used when Multiplayer is enabled.
std::unordered_map<SDL_JoystickID, ControllerState> controller_states;
bool single_controller = true;
std::array<float, 2> rotation_delta{};
std::array<float, 2> mouse_delta{};
@@ -38,8 +41,8 @@ static struct {
std::array<float, 2> pending_rotation_delta{};
std::array<float, 2> pending_mouse_delta{};
float cur_rumble;
bool rumble_active;
std::array<float, 4> cur_rumble{};
std::array<bool, 4> 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<size_t> 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<const recomp::InputField> fields) {
float recomp::get_input_analog(int controller_num, const std::span<const recomp::InputField> 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<const recomp::InputField> fields) {
bool recomp::get_input_digital(int controller_num, const std::span<const recomp::InputField> 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::ControllerOption> &recomp::get_controller_options() {
return InputState.detected_controller_options;
}
+1 -1
View File
@@ -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);
+3
View File
@@ -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,
{},
+23 -14
View File
@@ -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<recomp::GameInput>(scanned_input_index), scanned_binding_index, cur_device, scanned_field);
// TODO: Needs the active controller tab.
recomp::set_input_binding(0, static_cast<recomp::GameInput>(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<recomp::GameInput>(inputs.at(0).Get<size_t>());
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<recomp::GameInput>(inputs.at(0).Get<size_t>());
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<recomp::GameInput>((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;
}
+10 -8
View File
@@ -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)) {