From 752e92996250b0b1f87e499035c9851758980505 Mon Sep 17 00:00:00 2001 From: Nick Benthem Date: Mon, 23 Mar 2026 22:29:22 -0400 Subject: [PATCH 1/5] Add ControllerPortMode enum and per-port input mapping arrays Introduce the core infrastructure for multi-controller support: - ControllerPortMode enum (Off/Keyboard/Controller) with JSON serialization - Per-port mapping arrays (4 ports x inputs x bindings) - Port-aware get/set_input_binding overloads with backward-compat defaults - Port mode state management (get/set_port_mode, get_port_count) Existing behavior is preserved: get_n64_input still only reads port 0. --- include/recomp_input.h | 22 ++++++++++++++++++ src/game/controls.cpp | 51 +++++++++++++++++++++++++++--------------- src/game/input.cpp | 22 ++++++++++++++++++ 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/include/recomp_input.h b/include/recomp_input.h index baaffca..b7dd49e 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -84,6 +84,25 @@ namespace recomp { COUNT }; + enum class ControllerPortMode { + Off, + Keyboard, + Controller, + OptionCount + }; + + NLOHMANN_JSON_SERIALIZE_ENUM(recomp::ControllerPortMode, { + {recomp::ControllerPortMode::Off, "Off"}, + {recomp::ControllerPortMode::Keyboard, "Keyboard"}, + {recomp::ControllerPortMode::Controller, "Controller"} + }); + + constexpr int max_ports = 4; + + ControllerPortMode get_port_mode(int port); + void set_port_mode(int port, ControllerPortMode mode); + int get_port_count(); + void start_scanning_input(InputDevice device); void stop_scanning_input(); void finish_scanning_input(InputField scanned_field); @@ -150,6 +169,7 @@ namespace recomp { extern const DefaultN64Mappings default_n64_keyboard_mappings; extern const DefaultN64Mappings default_n64_controller_mappings; + const DefaultN64Mappings& get_default_keyboard_mappings_for_port(int port); constexpr size_t bindings_per_input = 2; @@ -158,7 +178,9 @@ namespace recomp { 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); + InputField& get_input_binding(GameInput input, size_t binding_index, InputDevice device, int port); void set_input_binding(GameInput input, size_t binding_index, InputDevice device, InputField value); + void set_input_binding(GameInput input, size_t binding_index, InputDevice device, InputField value, int port); bool get_n64_input(int controller_num, uint16_t* buttons_out, float* x_out, float* y_out); void set_rumble(int controller_num, bool); diff --git a/src/game/controls.cpp b/src/game/controls.cpp index 72380e4..5496b3f 100644 --- a/src/game/controls.cpp +++ b/src/game/controls.cpp @@ -5,10 +5,11 @@ #include "ultramodern/ultramodern.hpp" // Arrays that hold the mappings for every input for keyboard and controller respectively. +// Now per-port: index by [port][input][binding]. 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{}; // Make the button value array, which maps a button index to its bit field. #define DEFINE_INPUT(name, value, readable) uint16_t(value##u), @@ -52,9 +53,15 @@ recomp::GameInput recomp::get_input_from_enum_name(const std::string_view enum_n return static_cast(find_it - input_enum_names.begin()); } -// Due to an RmlUi limitation this can't be const. Ideally it would return a const reference or even just a straight up copy. +// Backward-compat overload: port 0 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; + return get_input_binding(input, binding_index, device, 0); +} + +recomp::InputField& recomp::get_input_binding(GameInput input, size_t binding_index, recomp::InputDevice device, int port) { + input_mapping_array& device_mappings = (device == recomp::InputDevice::Controller) + ? controller_input_mappings[port] + : keyboard_input_mappings[port]; input_mapping& cur_input_mapping = device_mappings.at(static_cast(input)); if (binding_index < cur_input_mapping.size()) { @@ -66,8 +73,15 @@ recomp::InputField& recomp::get_input_binding(GameInput input, size_t binding_in } } +// Backward-compat overload: port 0 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; + set_input_binding(input, binding_index, device, value, 0); +} + +void recomp::set_input_binding(recomp::GameInput input, size_t binding_index, recomp::InputDevice device, recomp::InputField value, int port) { + input_mapping_array& device_mappings = (device == recomp::InputDevice::Controller) + ? controller_input_mappings[port] + : keyboard_input_mappings[port]; input_mapping& cur_input_mapping = device_mappings.at(static_cast(input)); if (binding_index < cur_input_mapping.size()) { @@ -79,33 +93,34 @@ bool recomp::get_n64_input(int controller_num, uint16_t* buttons_out, float* x_o 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()) { + input_mapping_array& kb_mappings = keyboard_input_mappings[0]; + input_mapping_array& ct_mappings = controller_input_mappings[0]; + 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(kb_mappings[input_index]) ? n64_button_values[i] : 0; + cur_buttons |= recomp::get_input_digital(ct_mappings[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(ct_mappings[(size_t)GameInput::X_AXIS_POS]) + - recomp::get_input_analog(ct_mappings[(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(ct_mappings[(size_t)GameInput::Y_AXIS_POS]) + - recomp::get_input_analog(ct_mappings[(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(kb_mappings[(size_t)GameInput::X_AXIS_POS]) + - recomp::get_input_analog(kb_mappings[(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(kb_mappings[(size_t)GameInput::Y_AXIS_POS]) + - recomp::get_input_analog(kb_mappings[(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 3ba0ea6..4a843d8 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -23,6 +23,28 @@ struct ControllerState { }; }; +// Per-port mode state +static std::array port_modes = { + recomp::ControllerPortMode::Keyboard, + recomp::ControllerPortMode::Off, + recomp::ControllerPortMode::Off, + recomp::ControllerPortMode::Off, +}; + +recomp::ControllerPortMode recomp::get_port_mode(int port) { + if (port < 0 || port >= recomp::max_ports) return recomp::ControllerPortMode::Off; + return port_modes[port]; +} + +void recomp::set_port_mode(int port, recomp::ControllerPortMode mode) { + if (port < 0 || port >= recomp::max_ports) return; + port_modes[port] = mode; +} + +int recomp::get_port_count() { + return recomp::max_ports; +} + static struct { const Uint8* keys = nullptr; SDL_Keymod keymod = SDL_Keymod::KMOD_NONE; From aecff0e18db33675e24aed1646adba0421ec10e1 Mon Sep 17 00:00:00 2001 From: Nick Benthem Date: Mon, 23 Mar 2026 22:30:59 -0400 Subject: [PATCH 2/5] Add per-port controller assignment, rumble, and P2 keyboard defaults - Auto-assign gamepads to ports on SDL connect, clear on disconnect - Per-port rumble state and per-port rumble update loop - Per-port get_connected_device_info based on port mode - get_port_controller_name returns SDL controller name for a port - Player 2 default keyboard mappings (arrow keys, numpad, RSHIFT/RCTRL/RALT) - get_default_keyboard_mappings_for_port: P0=WASD, P1=arrows, P2-3=empty --- include/recomp_input.h | 3 +- src/game/input.cpp | 150 ++++++++++++++++++++++++++++++++++------- 2 files changed, 129 insertions(+), 24 deletions(-) diff --git a/include/recomp_input.h b/include/recomp_input.h index b7dd49e..fe7fb4d 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -188,7 +188,8 @@ namespace recomp { void handle_events(); ultramodern::input::connected_device_info_t get_connected_device_info(int controller_num); - + std::string get_port_controller_name(int port); + // Rumble strength ranges from 0 to 100. int get_rumble_strength(); void set_rumble_strength(int strength); diff --git a/src/game/input.cpp b/src/game/input.cpp index 4a843d8..6f4ac68 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -53,15 +53,20 @@ static struct { std::mutex cur_controllers_mutex; std::vector cur_controllers{}; std::unordered_map controller_states; - + + // Per-port controller assignment + std::array port_controller_ids = {-1, -1, -1, -1}; + std::array port_controllers = {nullptr, nullptr, nullptr, nullptr}; + std::array rotation_delta{}; std::array mouse_delta{}; std::mutex pending_input_mutex; std::array pending_rotation_delta{}; std::array pending_mouse_delta{}; - float cur_rumble; - bool rumble_active; + // Per-port rumble state + std::array cur_rumble = {0.0f, 0.0f, 0.0f, 0.0f}; + std::array rumble_active = {false, false, false, false}; } InputState; static struct { @@ -158,14 +163,25 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) { SDL_GameController* controller = SDL_GameControllerOpen(controller_event->which); printf("Controller added: %d\n", controller_event->which); if (controller != nullptr) { - printf(" Instance ID: %d\n", SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))); - ControllerState& state = InputState.controller_states[SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))]; + SDL_JoystickID instance_id = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)); + printf(" Instance ID: %d\n", instance_id); + ControllerState& state = InputState.controller_states[instance_id]; state.controller = controller; if (SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_GYRO) && SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_ACCEL)) { SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_GYRO, SDL_TRUE); SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_ACCEL, SDL_TRUE); } + + // Assign to first port with mode=Controller and no assignment + for (int p = 0; p < recomp::max_ports; p++) { + if (port_modes[p] == recomp::ControllerPortMode::Controller && InputState.port_controllers[p] == nullptr) { + InputState.port_controller_ids[p] = instance_id; + InputState.port_controllers[p] = controller; + printf(" Assigned to port %d\n", p); + break; + } + } } } break; @@ -173,6 +189,17 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) { { SDL_ControllerDeviceEvent* controller_event = &event->cdevice; printf("Controller removed: %d\n", controller_event->which); + + // Clear port assignment for this controller + for (int p = 0; p < recomp::max_ports; p++) { + if (InputState.port_controller_ids[p] == controller_event->which) { + InputState.port_controller_ids[p] = -1; + InputState.port_controllers[p] = nullptr; + printf(" Cleared port %d assignment\n", p); + break; + } + } + InputState.controller_states.erase(controller_event->which); } break; @@ -485,6 +512,71 @@ const recomp::DefaultN64Mappings recomp::default_n64_controller_mappings = { } }; +// Port 1 default keyboard mappings: arrow keys + nearby keys +const recomp::DefaultN64Mappings default_n64_keyboard_mappings_p2 = { + .a = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RSHIFT} + }, + .b = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RCTRL} + }, + .l = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_PAGEUP} + }, + .r = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_PAGEDOWN} + }, + .z = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RALT} + }, + .start = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_KP_ENTER} + }, + .c_left = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_KP_4} + }, + .c_right = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_KP_6} + }, + .c_up = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_KP_8} + }, + .c_down = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_KP_2} + }, + .dpad_left = {}, + .dpad_right = {}, + .dpad_up = {}, + .dpad_down = {}, + .analog_left = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_LEFT} + }, + .analog_right = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RIGHT} + }, + .analog_up = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_UP} + }, + .analog_down = { + {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_DOWN} + }, + .toggle_menu = {}, + .accept_menu = {}, + .apply_menu = {} +}; + +const recomp::DefaultN64Mappings& recomp::get_default_keyboard_mappings_for_port(int port) { + switch (port) { + case 0: return recomp::default_n64_keyboard_mappings; + case 1: return default_n64_keyboard_mappings_p2; + default: { + // Ports 2-3: empty defaults + static const recomp::DefaultN64Mappings empty{}; + return empty; + } + } +} + void recomp::poll_inputs() { InputState.keys = SDL_GetKeyboardState(&InputState.numkeys); InputState.keymod = SDL_GetModState(); @@ -533,18 +625,19 @@ void recomp::poll_inputs() { } void recomp::set_rumble(int controller_num, bool on) { - if (controller_num == 0) { - InputState.rumble_active = on; + if (controller_num >= 0 && controller_num < recomp::max_ports) { + InputState.rumble_active[controller_num] = on; } } ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) { - switch (controller_num) { - case 0: + if (controller_num >= 0 && controller_num < recomp::max_ports) { + if (port_modes[controller_num] != recomp::ControllerPortMode::Off) { return ultramodern::input::connected_device_info_t { .connected_device = ultramodern::input::Device::Controller, .connected_pak = ultramodern::input::Pak::RumblePak, }; + } } return ultramodern::input::connected_device_info_t { @@ -553,6 +646,16 @@ ultramodern::input::connected_device_info_t recomp::get_connected_device_info(in }; } +std::string recomp::get_port_controller_name(int port) { + if (port >= 0 && port < recomp::max_ports && InputState.port_controllers[port] != nullptr) { + const char* name = SDL_GameControllerName(InputState.port_controllers[port]); + if (name != nullptr) { + return std::string(name); + } + } + return "None"; +} + static float smoothstep(float from, float to, float amount) { amount = (amount * amount) * (3.0f - 2.0f * amount); return std::lerp(from, to, amount); @@ -561,21 +664,22 @@ 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); - - 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) { + for (int p = 0; p < recomp::max_ports; p++) { + if (InputState.rumble_active[p]) { + InputState.cur_rumble[p] += 0.17f; + if (InputState.cur_rumble[p] > 1) InputState.cur_rumble[p] = 1; + } else { + InputState.cur_rumble[p] *= 0.92f; + InputState.cur_rumble[p] -= 0.01f; + if (InputState.cur_rumble[p] < 0) InputState.cur_rumble[p] = 0; + } + float smooth_rumble = smoothstep(0, 1, InputState.cur_rumble[p]); + uint16_t rumble_strength = smooth_rumble * (recomp::get_rumble_strength() * 0xFFFF / 100); + + // Apply rumble only to the assigned controller for this port + SDL_GameController* controller = InputState.port_controllers[p]; + if (controller != nullptr) { SDL_GameControllerRumble(controller, 0, rumble_strength, duration); } } From 6293227b6eeb86df06cd8474f30c6fdad5cb05b8 Mon Sep 17 00:00:00 2001 From: Nick Benthem Date: Mon, 23 Mar 2026 22:32:38 -0400 Subject: [PATCH 3/5] Add per-port input reading and rewrite get_n64_input for multi-port - Add controller_button_state_port/controller_axis_state_port that read only the gamepad assigned to a specific port - Add get_input_analog_port/get_input_digital_port public API that dispatches controller inputs through the per-port helpers - Rewrite get_n64_input to support all 4 ports: checks port mode, reads keyboard-only or controller-only bindings accordingly, and uses per-port gamepad isolation for Controller mode --- include/recomp_input.h | 5 +++ src/game/controls.cpp | 57 +++++++++++++++++-------- src/game/input.cpp | 97 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 17 deletions(-) diff --git a/include/recomp_input.h b/include/recomp_input.h index fe7fb4d..b7aaa18 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -74,6 +74,11 @@ namespace recomp { float get_input_analog(const std::span fields); bool get_input_digital(const InputField& field); bool get_input_digital(const std::span fields); + // Per-port versions: for controller inputs, read only from the port's assigned gamepad + float get_input_analog_port(const InputField& field, int port); + float get_input_analog_port(const std::span fields, int port); + bool get_input_digital_port(const InputField& field, int port); + bool get_input_digital_port(const std::span fields, int port); void get_gyro_deltas(float* x, float* y); void get_mouse_deltas(float* x, float* y); void get_right_analog(float* x, float* y); diff --git a/src/game/controls.cpp b/src/game/controls.cpp index 5496b3f..806b47e 100644 --- a/src/game/controls.cpp +++ b/src/game/controls.cpp @@ -94,33 +94,56 @@ bool recomp::get_n64_input(int controller_num, uint16_t* buttons_out, float* x_o float cur_x = 0.0f; float cur_y = 0.0f; - if (controller_num != 0) { + if (controller_num < 0 || controller_num >= recomp::max_ports) { + *buttons_out = 0; + *x_out = 0.0f; + *y_out = 0.0f; + return false; + } + + recomp::ControllerPortMode mode = recomp::get_port_mode(controller_num); + if (mode == recomp::ControllerPortMode::Off) { + *buttons_out = 0; + *x_out = 0.0f; + *y_out = 0.0f; return false; } if (!recomp::game_input_disabled()) { - input_mapping_array& kb_mappings = keyboard_input_mappings[0]; - input_mapping_array& ct_mappings = controller_input_mappings[0]; + input_mapping_array& kb_mappings = keyboard_input_mappings[controller_num]; + input_mapping_array& ct_mappings = controller_input_mappings[controller_num]; - 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(kb_mappings[input_index]) ? n64_button_values[i] : 0; - cur_buttons |= recomp::get_input_digital(ct_mappings[input_index]) ? n64_button_values[i] : 0; - } + if (mode == recomp::ControllerPortMode::Keyboard) { + // Keyboard mode: read only keyboard bindings for this port + 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(kb_mappings[input_index]) ? n64_button_values[i] : 0; + } - float joystick_x = recomp::get_input_analog(ct_mappings[(size_t)GameInput::X_AXIS_POS]) - - recomp::get_input_analog(ct_mappings[(size_t)GameInput::X_AXIS_NEG]); + cur_x = recomp::get_input_analog(kb_mappings[(size_t)GameInput::X_AXIS_POS]) + - recomp::get_input_analog(kb_mappings[(size_t)GameInput::X_AXIS_NEG]); - float joystick_y = recomp::get_input_analog(ct_mappings[(size_t)GameInput::Y_AXIS_POS]) - - recomp::get_input_analog(ct_mappings[(size_t)GameInput::Y_AXIS_NEG]); + cur_y = recomp::get_input_analog(kb_mappings[(size_t)GameInput::Y_AXIS_POS]) + - recomp::get_input_analog(kb_mappings[(size_t)GameInput::Y_AXIS_NEG]); + } + else if (mode == recomp::ControllerPortMode::Controller) { + // Controller mode: read only the gamepad assigned to this port + 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_port(ct_mappings[input_index], controller_num) ? n64_button_values[i] : 0; + } + + float joystick_x = recomp::get_input_analog_port(ct_mappings[(size_t)GameInput::X_AXIS_POS], controller_num) + - recomp::get_input_analog_port(ct_mappings[(size_t)GameInput::X_AXIS_NEG], controller_num); - recomp::apply_joystick_deadzone(joystick_x, joystick_y, &joystick_x, &joystick_y); + float joystick_y = recomp::get_input_analog_port(ct_mappings[(size_t)GameInput::Y_AXIS_POS], controller_num) + - recomp::get_input_analog_port(ct_mappings[(size_t)GameInput::Y_AXIS_NEG], controller_num); - cur_x = recomp::get_input_analog(kb_mappings[(size_t)GameInput::X_AXIS_POS]) - - recomp::get_input_analog(kb_mappings[(size_t)GameInput::X_AXIS_NEG]) + joystick_x; + recomp::apply_joystick_deadzone(joystick_x, joystick_y, &joystick_x, &joystick_y); - cur_y = recomp::get_input_analog(kb_mappings[(size_t)GameInput::Y_AXIS_POS]) - - recomp::get_input_analog(kb_mappings[(size_t)GameInput::Y_AXIS_NEG]) + joystick_y; + cur_x = joystick_x; + cur_y = joystick_y; + } } *buttons_out = cur_buttons; diff --git a/src/game/input.cpp b/src/game/input.cpp index 6f4ac68..ce257f0 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -701,6 +701,18 @@ bool controller_button_state(int32_t input_id) { return false; } +// Per-port version: reads only the assigned gamepad for the given port +bool controller_button_state_port(int32_t input_id, int port) { + if (input_id >= 0 && input_id < SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MAX) { + SDL_GameControllerButton button = (SDL_GameControllerButton)input_id; + SDL_GameController* controller = InputState.port_controllers[port]; + if (controller != nullptr) { + return SDL_GameControllerGetButton(controller, button) != 0; + } + } + return false; +} + static std::atomic_bool right_analog_suppressed = false; float controller_axis_state(int32_t input_id, bool allow_suppression) { @@ -731,6 +743,32 @@ float controller_axis_state(int32_t input_id, bool allow_suppression) { return false; } +// Per-port version: reads only the assigned gamepad for the given port +float controller_axis_state_port(int32_t input_id, bool allow_suppression, int port) { + 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; + + SDL_GameController* controller = InputState.port_controllers[port]; + if (controller != nullptr) { + float cur_val = SDL_GameControllerGetAxis(controller, axis) * (1/32768.0f); + if (negative_range) { + cur_val = -cur_val; + } + + if (allow_suppression && right_analog_suppressed.load() && + (axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX || axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY)) { + cur_val = 0; + } + ret = std::clamp(cur_val, 0.0f, 1.0f); + } + + return ret; + } + return false; +} + float recomp::get_input_analog(const recomp::InputField& field) { switch ((InputType)field.input_type) { case InputType::Keyboard: @@ -792,6 +830,65 @@ bool recomp::get_input_digital(const std::span fields) return ret; } +// Per-port versions: for controller inputs, read only from the port's assigned gamepad +float recomp::get_input_analog_port(const recomp::InputField& field, int port) { + switch ((InputType)field.input_type) { + case InputType::Keyboard: + if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) { + if (should_override_keystate(static_cast(field.input_id), InputState.keymod)) { + return 0.0f; + } + return InputState.keys[field.input_id] ? 1.0f : 0.0f; + } + return 0.0f; + case InputType::ControllerDigital: + return controller_button_state_port(field.input_id, port) ? 1.0f : 0.0f; + case InputType::ControllerAnalog: + return controller_axis_state_port(field.input_id, true, port); + case InputType::Mouse: + return 0.0f; + case InputType::None: + return false; + } +} + +float recomp::get_input_analog_port(const std::span fields, int port) { + float ret = 0.0f; + for (const auto& field : fields) { + ret += get_input_analog_port(field, port); + } + return std::clamp(ret, 0.0f, 1.0f); +} + +bool recomp::get_input_digital_port(const recomp::InputField& field, int port) { + switch ((InputType)field.input_type) { + case InputType::Keyboard: + if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) { + if (should_override_keystate(static_cast(field.input_id), InputState.keymod)) { + return false; + } + return InputState.keys[field.input_id] != 0; + } + return false; + case InputType::ControllerDigital: + return controller_button_state_port(field.input_id, port); + case InputType::ControllerAnalog: + return controller_axis_state_port(field.input_id, true, port) >= axis_threshold; + case InputType::Mouse: + return false; + case InputType::None: + return false; + } +} + +bool recomp::get_input_digital_port(const std::span fields, int port) { + bool ret = 0; + for (const auto& field : fields) { + ret |= get_input_digital_port(field, port); + } + return ret; +} + void recomp::get_gyro_deltas(float* x, float* y) { std::array cur_rotation_delta = InputState.rotation_delta; float sensitivity = (float)recomp::get_gyro_sensitivity() / 100.0f; From 4bc9f87c17b99efec44fba15bd5d9ab93f62aa21 Mon Sep 17 00:00:00 2001 From: Nick Benthem Date: Mon, 23 Mar 2026 22:35:38 -0400 Subject: [PATCH 4/5] Add per-port config persistence and UI port selector - Per-port save/load: new JSON format with "ports" array containing mode + keyboard/controller bindings per port - Backward compatibility: old flat JSON format loads as port 0 - Port-aware reset overloads for all reset functions - RmlUi port selector (P1-P4 buttons) and mode selector (Off/Keyboard/Controller) in controls config menu - Data bindings for selected_port, port_mode, assigned_controller_name - All UI callbacks (scan, reset, clear) pass selected_port --- assets/config_menu/controls.rml | 66 ++++++++++++ include/zelda_config.h | 4 + src/game/config.cpp | 175 ++++++++++++++++++++++---------- src/ui/ui_config.cpp | 48 +++++++-- 4 files changed, 232 insertions(+), 61 deletions(-) diff --git a/assets/config_menu/controls.rml b/assets/config_menu/controls.rml index d947ae8..ba0df31 100644 --- a/assets/config_menu/controls.rml +++ b/assets/config_menu/controls.rml @@ -3,6 +3,72 @@
+ +
+ + + + +
+ +
+ + + + +
@@ -17,7 +17,7 @@ class="button" data-class-button--success="selected_port == 1" data-event-click="select_port(1)" - style="min-width: 48dp;" + data-attr-style="port_1_style" >
P2
@@ -25,7 +25,7 @@ class="button" data-class-button--success="selected_port == 2" data-event-click="select_port(2)" - style="min-width: 48dp;" + data-attr-style="port_2_style" >
P3
@@ -33,18 +33,18 @@ class="button" data-class-button--success="selected_port == 3" data-event-click="select_port(3)" - style="min-width: 48dp;" + data-attr-style="port_3_style" >
P4
-
+
@@ -52,7 +52,7 @@ class="button" data-class-button--success="port_mode == 'Keyboard'" data-event-click="set_port_mode('Keyboard')" - style="min-width: 80dp;" + data-attr-style="port_mode == 'Keyboard' ? 'min-width: 80dp; opacity: 1.0;' : 'min-width: 80dp; opacity: 0.5;'" >
Keyboard
@@ -60,41 +60,26 @@ class="button" data-class-button--success="port_mode == 'Controller'" data-event-click="set_port_mode('Controller')" - style="min-width: 80dp;" + data-attr-style="port_mode == 'Controller' ? 'min-width: 80dp; opacity: 1.0;' : 'min-width: 80dp; opacity: 0.5;'" >
Controller
- -
-
-
- -
-
- -
+ +
@@ -107,7 +92,7 @@ data-for="input_bindings, i : inputs.array" data-event-mouseover="set_input_row_focus(i)" data-class-control-option--active="get_input_enum_name(i)==cur_input_row" - data-if="!input_device_is_keyboard || (get_input_enum_name(i) != 'TOGGLE_MENU' && get_input_enum_name(i) != 'ACCEPT_MENU' && get_input_enum_name(i) != 'APPLY_MENU')" + data-if="(!is_multiplayer_port || get_input_enum_name(i) == 'A' || get_input_enum_name(i) == 'B' || get_input_enum_name(i) == 'START' || get_input_enum_name(i) == 'X_AXIS_NEG' || get_input_enum_name(i) == 'X_AXIS_POS' || get_input_enum_name(i) == 'Y_AXIS_NEG' || get_input_enum_name(i) == 'Y_AXIS_POS') && (!input_device_is_keyboard || (get_input_enum_name(i) != 'TOGGLE_MENU' && get_input_enum_name(i) != 'ACCEPT_MENU' && get_input_enum_name(i) != 'APPLY_MENU'))" >