From aae80eff3b427bf621ef39a69fa92428b62681f6 Mon Sep 17 00:00:00 2001 From: briaguya <70942617+briaguya0@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:08:19 -0400 Subject: [PATCH 1/2] WIP: Add press-to-join multiplayer controller system Part 1: Extend ConnectedPhysicalDeviceManager with multiplayer state and press-to-join polling logic. Part 2: Add C bridge functions for game code to call. Co-Authored-By: Claude Opus 4.6 (1M context) --- CMakeLists.txt | 1 + include/libultraship/bridge.h | 3 +- .../libultraship/bridge/multiplayerbridge.h | 21 ++ .../ConnectedPhysicalDeviceManager.h | 25 ++ src/CMakeLists.txt | 4 + src/libultraship/bridge/multiplayerbridge.cpp | 27 ++ .../controller/controldeck/ControlDeck.cpp | 4 + .../ConnectedPhysicalDeviceManager.cpp | 253 +++++++++++++++++- 8 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 include/libultraship/bridge/multiplayerbridge.h create mode 100644 src/libultraship/bridge/multiplayerbridge.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f2e09ae2..cce82de18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ set(ADDITIONAL_LIB_INCLUDES "") # ========= Configuration Options ========= option(INCLUDE_MPQ_SUPPORT "Enable StormLib and MPQ archive support" OFF) option(GBI_UCODE "Specify the GBI ucode version" F3DEX_GBI_2) +option(ENABLE_PRESS_TO_JOIN "Enable press-to-join multiplayer controller assignment" OFF) # =========== Dependencies ============= if (CMAKE_SYSTEM_NAME STREQUAL "Windows") diff --git a/include/libultraship/bridge.h b/include/libultraship/bridge.h index 37d70041f..7920c0ab2 100644 --- a/include/libultraship/bridge.h +++ b/include/libultraship/bridge.h @@ -8,4 +8,5 @@ #include "libultraship/bridge/crashhandlerbridge.h" #include "libultraship/bridge/gfxdebuggerbridge.h" #include "libultraship/bridge/gfxbridge.h" -#include "libultraship/bridge/eventsbridge.h" \ No newline at end of file +#include "libultraship/bridge/eventsbridge.h" +#include "libultraship/bridge/multiplayerbridge.h" \ No newline at end of file diff --git a/include/libultraship/bridge/multiplayerbridge.h b/include/libultraship/bridge/multiplayerbridge.h new file mode 100644 index 000000000..ec3dbaa5a --- /dev/null +++ b/include/libultraship/bridge/multiplayerbridge.h @@ -0,0 +1,21 @@ +#pragma once + +#ifdef ENABLE_PRESS_TO_JOIN + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void MultiplayerStart(uint8_t portCount); +void MultiplayerStopPressToJoin(void); +void MultiplayerStartPressToJoin(void); +void MultiplayerStop(void); +int8_t MultiplayerGetPortStatus(uint8_t port); + +#ifdef __cplusplus +}; +#endif + +#endif // ENABLE_PRESS_TO_JOIN diff --git a/include/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.h b/include/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.h index 89c05b55b..aa139d4b6 100644 --- a/include/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.h +++ b/include/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.h @@ -8,6 +8,10 @@ namespace Ship { +#ifdef ENABLE_PRESS_TO_JOIN +static constexpr int32_t KEYBOARD_PSEUDO_INSTANCE_ID = -2; +#endif + class ConnectedPhysicalDeviceManager { public: ConnectedPhysicalDeviceManager(); @@ -24,9 +28,30 @@ class ConnectedPhysicalDeviceManager { void HandlePhysicalDeviceDisconnect(int32_t sdlJoystickInstanceId); void RefreshConnectedSDLGamepads(); +#ifdef ENABLE_PRESS_TO_JOIN + void StartMultiplayer(uint8_t portCount); + void StopPressToJoin(); + void StartPressToJoin(); + void StopMultiplayer(); + int8_t GetPortDeviceStatus(uint8_t port); + void PollPressToJoin(); +#endif + private: std::unordered_map mConnectedSDLGamepads; std::unordered_map mConnectedSDLGamepadNames; std::unordered_map> mIgnoredInstanceIds; + +#ifdef ENABLE_PRESS_TO_JOIN + bool mMultiplayerActive = false; + bool mPressToJoinActive = false; + uint8_t mMultiplayerPortCount = 1; + std::unordered_map mPressToJoinAssignments; + + void AssignDeviceToPort(int32_t instanceId, uint8_t port); + void UnassignPort(uint8_t port); + void RebuildIgnoreListsFromAssignments(); + bool PortHasKeyboardMappings(uint8_t port); +#endif }; } // namespace Ship diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4809c32b3..822635fa3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -113,6 +113,10 @@ target_link_libraries(libultraship PUBLIC prism) target_compile_definitions(libultraship PRIVATE ${GBI_UCODE}) +if (ENABLE_PRESS_TO_JOIN) + target_compile_definitions(libultraship PUBLIC ENABLE_PRESS_TO_JOIN) +endif() + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") target_compile_definitions(${PROJECT_NAME} PRIVATE WIN32 diff --git a/src/libultraship/bridge/multiplayerbridge.cpp b/src/libultraship/bridge/multiplayerbridge.cpp new file mode 100644 index 000000000..9333bf2b9 --- /dev/null +++ b/src/libultraship/bridge/multiplayerbridge.cpp @@ -0,0 +1,27 @@ +#ifdef ENABLE_PRESS_TO_JOIN + +#include "libultraship/bridge/multiplayerbridge.h" +#include "ship/controller/controldeck/ControlDeck.h" +#include "ship/Context.h" + +void MultiplayerStart(uint8_t portCount) { + Ship::Context::GetInstance()->GetControlDeck()->GetConnectedPhysicalDeviceManager()->StartMultiplayer(portCount); +} + +void MultiplayerStopPressToJoin(void) { + Ship::Context::GetInstance()->GetControlDeck()->GetConnectedPhysicalDeviceManager()->StopPressToJoin(); +} + +void MultiplayerStartPressToJoin(void) { + Ship::Context::GetInstance()->GetControlDeck()->GetConnectedPhysicalDeviceManager()->StartPressToJoin(); +} + +void MultiplayerStop(void) { + Ship::Context::GetInstance()->GetControlDeck()->GetConnectedPhysicalDeviceManager()->StopMultiplayer(); +} + +int8_t MultiplayerGetPortStatus(uint8_t port) { + return Ship::Context::GetInstance()->GetControlDeck()->GetConnectedPhysicalDeviceManager()->GetPortDeviceStatus(port); +} + +#endif // ENABLE_PRESS_TO_JOIN diff --git a/src/libultraship/controller/controldeck/ControlDeck.cpp b/src/libultraship/controller/controldeck/ControlDeck.cpp index c7c96f632..2380400b9 100644 --- a/src/libultraship/controller/controldeck/ControlDeck.cpp +++ b/src/libultraship/controller/controldeck/ControlDeck.cpp @@ -57,6 +57,10 @@ void ControlDeck::WriteToOSContPad(OSContPad* pad) { SDL_PumpEvents(); Ship::WheelHandler::GetInstance()->Update(); +#ifdef ENABLE_PRESS_TO_JOIN + GetConnectedPhysicalDeviceManager()->PollPressToJoin(); +#endif + if (AllGameInputBlocked()) { return; } diff --git a/src/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.cpp b/src/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.cpp index 7f0397e25..c2a93ab26 100644 --- a/src/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.cpp +++ b/src/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.cpp @@ -1,5 +1,12 @@ #include "ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.h" #include +#ifdef ENABLE_PRESS_TO_JOIN +#include "ship/Context.h" +#include "ship/window/Window.h" +#include "ship/controller/controldeck/ControlDeck.h" +#include "ship/controller/controldevice/controller/mapping/ControllerMapping.h" +#include "libultraship/bridge/consolevariablebridge.h" +#endif namespace Ship { ConnectedPhysicalDeviceManager::ConnectedPhysicalDeviceManager() { @@ -46,6 +53,17 @@ void ConnectedPhysicalDeviceManager::HandlePhysicalDeviceConnect(int32_t sdlDevi } void ConnectedPhysicalDeviceManager::HandlePhysicalDeviceDisconnect(int32_t sdlJoystickInstanceId) { +#ifdef ENABLE_PRESS_TO_JOIN + // If press-to-join is active, free any port that had this device so it can be re-joined + if (mPressToJoinActive) { + for (auto& [port, assignedId] : mPressToJoinAssignments) { + if (assignedId == sdlJoystickInstanceId) { + UnassignPort(port); + break; + } + } + } +#endif RefreshConnectedSDLGamepads(); } @@ -100,9 +118,242 @@ void ConnectedPhysicalDeviceManager::RefreshConnectedSDLGamepads() { mConnectedSDLGamepads[instanceId] = gamepad; mConnectedSDLGamepadNames[instanceId] = gamepadName; - for (uint8_t port = 1; port < 4; port++) { +#ifdef ENABLE_PRESS_TO_JOIN + if (!mMultiplayerActive) { +#endif + for (uint8_t port = 1; port < 4; port++) { + mIgnoredInstanceIds[port].insert(instanceId); + } +#ifdef ENABLE_PRESS_TO_JOIN + } +#endif + } + +#ifdef ENABLE_PRESS_TO_JOIN + if (mMultiplayerActive) { + RebuildIgnoreListsFromAssignments(); + } +#endif +} +#ifdef ENABLE_PRESS_TO_JOIN +static const char* CVAR_PRESS_TO_JOIN_ENABLED = "gPressToJoinEnabled"; + +void ConnectedPhysicalDeviceManager::StartMultiplayer(uint8_t portCount) { + if (!CVarGetInteger(CVAR_PRESS_TO_JOIN_ENABLED, 1)) { + return; + } + + mMultiplayerActive = true; + mPressToJoinActive = true; + mMultiplayerPortCount = portCount; + mPressToJoinAssignments.clear(); + + // Ignore all devices on all active ports — everyone starts unassigned + for (uint8_t port = 0; port < mMultiplayerPortCount; port++) { + for (const auto& [instanceId, gamepad] : mConnectedSDLGamepads) { mIgnoredInstanceIds[port].insert(instanceId); } } + + SPDLOG_INFO("Press-to-join: started multiplayer with {} ports", portCount); +} + +void ConnectedPhysicalDeviceManager::StopPressToJoin() { + if (!mMultiplayerActive) { + return; + } + + mPressToJoinActive = false; + SPDLOG_INFO("Press-to-join: stopped (assignments locked)"); +} + +void ConnectedPhysicalDeviceManager::StartPressToJoin() { + if (!mMultiplayerActive) { + return; + } + + if (!CVarGetInteger(CVAR_PRESS_TO_JOIN_ENABLED, 1)) { + return; + } + + mPressToJoinActive = true; + + // Free any ports whose devices have disconnected + std::vector portsToFree; + for (const auto& [port, instanceId] : mPressToJoinAssignments) { + if (instanceId == KEYBOARD_PSEUDO_INSTANCE_ID) { + continue; + } + if (!mConnectedSDLGamepads.contains(instanceId)) { + portsToFree.push_back(port); + } + } + for (auto port : portsToFree) { + UnassignPort(port); + } + + SPDLOG_INFO("Press-to-join: re-enabled"); +} + +void ConnectedPhysicalDeviceManager::StopMultiplayer() { + mMultiplayerActive = false; + mPressToJoinActive = false; + mMultiplayerPortCount = 1; + mPressToJoinAssignments.clear(); + + // Restore default all-on-port-0 behavior + RefreshConnectedSDLGamepads(); + + SPDLOG_INFO("Press-to-join: stopped multiplayer, restored single player mode"); +} + +int8_t ConnectedPhysicalDeviceManager::GetPortDeviceStatus(uint8_t port) { + if (!mPressToJoinAssignments.contains(port)) { + return 0; // unassigned + } + + auto instanceId = mPressToJoinAssignments[port]; + if (instanceId == KEYBOARD_PSEUDO_INSTANCE_ID) { + return 1; // keyboard is always "connected" + } + + if (mConnectedSDLGamepads.contains(instanceId)) { + return 1; // assigned and connected + } + + return -1; // assigned but disconnected +} + +void ConnectedPhysicalDeviceManager::PollPressToJoin() { + if (!mPressToJoinActive) { + return; + } + + static constexpr int32_t AXIS_DEADZONE = 16000; + + for (uint8_t port = 0; port < mMultiplayerPortCount; port++) { + if (mPressToJoinAssignments.contains(port)) { + continue; // port already filled + } + + // Check keyboard for this port + if (PortHasKeyboardMappings(port)) { + auto lastScancode = Context::GetInstance()->GetWindow()->GetLastScancode(); + if (lastScancode != -1) { + AssignDeviceToPort(KEYBOARD_PSEUDO_INSTANCE_ID, port); + SPDLOG_INFO("Press-to-join: keyboard assigned to port {}", port); + continue; + } + } + + // Check unassigned SDL gamepads + for (const auto& [instanceId, gamepad] : mConnectedSDLGamepads) { + // Skip if already assigned to any port + bool alreadyAssigned = false; + for (const auto& [assignedPort, assignedId] : mPressToJoinAssignments) { + if (assignedId == instanceId) { + alreadyAssigned = true; + break; + } + } + if (alreadyAssigned) { + continue; + } + + // Check any button pressed + bool inputDetected = false; + for (int btn = 0; btn < SDL_CONTROLLER_BUTTON_MAX; btn++) { + if (SDL_GameControllerGetButton(gamepad, static_cast(btn))) { + inputDetected = true; + break; + } + } + + // Check any axis past deadzone + if (!inputDetected) { + for (int axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) { + auto value = SDL_GameControllerGetAxis(gamepad, static_cast(axis)); + if (abs(value) > AXIS_DEADZONE) { + inputDetected = true; + break; + } + } + } + + if (inputDetected) { + AssignDeviceToPort(instanceId, port); + SPDLOG_INFO("Press-to-join: gamepad {} assigned to port {}", instanceId, port); + break; // one assignment per frame per port + } + } + } +} + +void ConnectedPhysicalDeviceManager::AssignDeviceToPort(int32_t instanceId, uint8_t port) { + mPressToJoinAssignments[port] = instanceId; + + if (instanceId != KEYBOARD_PSEUDO_INSTANCE_ID) { + // Unignore this device on the target port + UnignoreInstanceIdForPort(port, instanceId); + + // Ignore it on all other active ports + for (uint8_t otherPort = 0; otherPort < mMultiplayerPortCount; otherPort++) { + if (otherPort != port) { + IgnoreInstanceIdForPort(otherPort, instanceId); + } + } + } +} + +void ConnectedPhysicalDeviceManager::UnassignPort(uint8_t port) { + if (!mPressToJoinAssignments.contains(port)) { + return; + } + + auto instanceId = mPressToJoinAssignments[port]; + mPressToJoinAssignments.erase(port); + + if (instanceId != KEYBOARD_PSEUDO_INSTANCE_ID) { + // Re-ignore this device on the port it was assigned to + IgnoreInstanceIdForPort(port, instanceId); + } + + SPDLOG_INFO("Press-to-join: port {} unassigned", port); +} + +void ConnectedPhysicalDeviceManager::RebuildIgnoreListsFromAssignments() { + // Clear ignore lists for all active ports + for (uint8_t port = 0; port < mMultiplayerPortCount; port++) { + mIgnoredInstanceIds[port].clear(); + } + + // For each active port, ignore all devices except the one assigned to it + for (uint8_t port = 0; port < mMultiplayerPortCount; port++) { + for (const auto& [instanceId, gamepad] : mConnectedSDLGamepads) { + if (mPressToJoinAssignments.contains(port) && + mPressToJoinAssignments[port] == instanceId) { + continue; // don't ignore the assigned device + } + mIgnoredInstanceIds[port].insert(instanceId); + } + } +} + +bool ConnectedPhysicalDeviceManager::PortHasKeyboardMappings(uint8_t port) { + auto controller = Context::GetInstance()->GetControlDeck()->GetControllerByPort(port); + if (controller == nullptr) { + return false; + } + + for (const auto& [bitmask, button] : controller->GetAllButtons()) { + for (const auto& [id, mapping] : button->GetAllButtonMappings()) { + if (mapping->GetMappingType() == MAPPING_TYPE_KEYBOARD) { + return true; + } + } + } + + return false; } +#endif } // namespace Ship From 0b8e8476e0b8b083eb48c69cbdcb99de75b84192 Mon Sep 17 00:00:00 2001 From: briaguya <70942617+briaguya0@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:41:53 -0400 Subject: [PATCH 2/2] Input editor toggle, per-port defaults, release-edge assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Press-to-join checkbox in the input editor, bound to gPressToJoinEnabled. When on, per-device-per-port checkboxes are disabled (LUS manages assignments); when off, users get full manual control. Apply SDLGamepad defaults to ports 1-3 at init so press-to-join has mappings to route input through. Flagged as POC — a better API for games to express per-port defaults would replace the hardcoded loop. Release-edge detection in PollPressToJoin: track which devices had input last frame, assign when they transition to no-input. Fires on the release of a tap rather than the press, so ReadToPad reads zero input on the assign frame and the trigger press isn't also consumed as a character-select confirm. StopMultiplayer wipes mIgnoredInstanceIds before refresh so port 0 doesn't linger with its multiplayer-era "ignore everyone except X" set. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controller/controldeck/ControlDeck.cpp | 11 ++ .../ConnectedPhysicalDeviceManager.cpp | 103 ++++++++++++------ src/ship/window/gui/InputEditorWindow.cpp | 20 ++++ 3 files changed, 101 insertions(+), 33 deletions(-) diff --git a/src/ship/controller/controldeck/ControlDeck.cpp b/src/ship/controller/controldeck/ControlDeck.cpp index b5298b84f..156d95188 100644 --- a/src/ship/controller/controldeck/ControlDeck.cpp +++ b/src/ship/controller/controldeck/ControlDeck.cpp @@ -38,6 +38,17 @@ void ControlDeck::Init(uint8_t* controllerBits) { mPorts[0]->GetConnectedController()->AddDefaultMappings(PhysicalDeviceType::Mouse); mPorts[0]->GetConnectedController()->AddDefaultMappings(PhysicalDeviceType::SDLGamepad); } + +#ifdef ENABLE_PRESS_TO_JOIN + // POC: apply SDLGamepad defaults to all other ports so press-to-join works + // out of the box. A proper solution would let games define per-port defaults + // via ControllerDefaultMappings rather than hardcoding this loop in library code. + for (size_t i = 1; i < mPorts.size(); i++) { + if (!mPorts[i]->GetConnectedController()->HasConfig()) { + mPorts[i]->GetConnectedController()->AddDefaultMappings(PhysicalDeviceType::SDLGamepad); + } + } +#endif } bool ControlDeck::ProcessKeyboardEvent(KbEventType eventType, KbScancode scancode) { diff --git a/src/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.cpp b/src/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.cpp index c2a93ab26..48fd3ad43 100644 --- a/src/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.cpp +++ b/src/ship/controller/physicaldevice/ConnectedPhysicalDeviceManager.cpp @@ -201,7 +201,10 @@ void ConnectedPhysicalDeviceManager::StopMultiplayer() { mMultiplayerPortCount = 1; mPressToJoinAssignments.clear(); - // Restore default all-on-port-0 behavior + // Wipe multiplayer-era ignore state so port 0 doesn't keep ignoring devices + // that weren't the one assigned. RefreshConnectedSDLGamepads refills ports + // 1-3 with every connected device to restore the all-on-port-0 default. + mIgnoredInstanceIds.clear(); RefreshConnectedSDLGamepads(); SPDLOG_INFO("Press-to-join: stopped multiplayer, restored single player mode"); @@ -225,18 +228,70 @@ int8_t ConnectedPhysicalDeviceManager::GetPortDeviceStatus(uint8_t port) { } void ConnectedPhysicalDeviceManager::PollPressToJoin() { + // Fire assignment on the release edge of a press rather than the press itself. + // If we assigned on press, ReadToPad immediately after would read the held button + // and the game would treat the join press as a confirm/select on the same frame. + static constexpr int32_t AXIS_DEADZONE = 16000; + static bool wasActiveLastFrame = false; + static uint32_t framesSinceActivation = 0; + static std::unordered_set lastFrameInputHeld; + + if (mPressToJoinActive && !wasActiveLastFrame) { + framesSinceActivation = 0; + lastFrameInputHeld.clear(); + } + wasActiveLastFrame = mPressToJoinActive; + if (!mPressToJoinActive) { return; } - static constexpr int32_t AXIS_DEADZONE = 16000; + // Snapshot who has any input held right now + std::unordered_set currentInputHeld; + for (const auto& [instanceId, gamepad] : mConnectedSDLGamepads) { + bool hasInput = false; + for (int btn = 0; btn < SDL_CONTROLLER_BUTTON_MAX; btn++) { + if (SDL_GameControllerGetButton(gamepad, static_cast(btn))) { + hasInput = true; + break; + } + } + if (!hasInput) { + for (int axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) { + auto value = SDL_GameControllerGetAxis(gamepad, static_cast(axis)); + if (abs(value) > AXIS_DEADZONE) { + hasInput = true; + break; + } + } + } + if (hasInput) { + currentInputHeld.insert(instanceId); + } + } + + // During the post-activation grace period, track input state but don't assign yet + if (framesSinceActivation < 15) { + framesSinceActivation++; + lastFrameInputHeld = currentInputHeld; + return; + } + + // Release edge = had input last frame, no input now + std::unordered_set releasedThisFrame; + for (auto instanceId : lastFrameInputHeld) { + if (!currentInputHeld.contains(instanceId)) { + releasedThisFrame.insert(instanceId); + } + } for (uint8_t port = 0; port < mMultiplayerPortCount; port++) { if (mPressToJoinAssignments.contains(port)) { - continue; // port already filled + continue; } - // Check keyboard for this port + // Keyboard uses GetLastScancode which is already event-based (fires once per press), + // so it doesn't need release-edge treatment. if (PortHasKeyboardMappings(port)) { auto lastScancode = Context::GetInstance()->GetWindow()->GetLastScancode(); if (lastScancode != -1) { @@ -246,9 +301,8 @@ void ConnectedPhysicalDeviceManager::PollPressToJoin() { } } - // Check unassigned SDL gamepads - for (const auto& [instanceId, gamepad] : mConnectedSDLGamepads) { - // Skip if already assigned to any port + int32_t pickedInstanceId = -1; + for (auto instanceId : releasedThisFrame) { bool alreadyAssigned = false; for (const auto& [assignedPort, assignedId] : mPressToJoinAssignments) { if (assignedId == instanceId) { @@ -259,34 +313,17 @@ void ConnectedPhysicalDeviceManager::PollPressToJoin() { if (alreadyAssigned) { continue; } - - // Check any button pressed - bool inputDetected = false; - for (int btn = 0; btn < SDL_CONTROLLER_BUTTON_MAX; btn++) { - if (SDL_GameControllerGetButton(gamepad, static_cast(btn))) { - inputDetected = true; - break; - } - } - - // Check any axis past deadzone - if (!inputDetected) { - for (int axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) { - auto value = SDL_GameControllerGetAxis(gamepad, static_cast(axis)); - if (abs(value) > AXIS_DEADZONE) { - inputDetected = true; - break; - } - } - } - - if (inputDetected) { - AssignDeviceToPort(instanceId, port); - SPDLOG_INFO("Press-to-join: gamepad {} assigned to port {}", instanceId, port); - break; // one assignment per frame per port - } + pickedInstanceId = instanceId; + break; + } + if (pickedInstanceId != -1) { + AssignDeviceToPort(pickedInstanceId, port); + SPDLOG_INFO("Press-to-join: gamepad {} assigned to port {}", pickedInstanceId, port); + releasedThisFrame.erase(pickedInstanceId); } } + + lastFrameInputHeld = currentInputHeld; } void ConnectedPhysicalDeviceManager::AssignDeviceToPort(int32_t instanceId, uint8_t port) { diff --git a/src/ship/window/gui/InputEditorWindow.cpp b/src/ship/window/gui/InputEditorWindow.cpp index 27455bac2..86da1406e 100644 --- a/src/ship/window/gui/InputEditorWindow.cpp +++ b/src/ship/window/gui/InputEditorWindow.cpp @@ -6,6 +6,10 @@ #include "ship/controller/controldevice/controller/mapping/sdl/SDLAxisDirectionToButtonMapping.h" #include "ship/controller/controldeck/ControlDeck.h" #include "libultraship/libultra/controller.h" +#ifdef ENABLE_PRESS_TO_JOIN +#include "libultraship/bridge/consolevariablebridge.h" +#define CVAR_PRESS_TO_JOIN_ENABLED "gPressToJoinEnabled" +#endif #define SCALE_IMGUI_SIZE(value) ((value / 13.0f) * ImGui::GetFontSize()) @@ -1168,6 +1172,16 @@ void InputEditorWindow::DrawGyroSection(uint8_t port) { } void InputEditorWindow::DrawDeviceToggles(uint8_t portIndex) { +#ifdef ENABLE_PRESS_TO_JOIN + bool pressToJoinEnabled = CVarGetInteger(CVAR_PRESS_TO_JOIN_ENABLED, 1); + if (ImGui::Checkbox("Press-to-Join (auto-assign devices on character select)", &pressToJoinEnabled)) { + CVarSetInteger(CVAR_PRESS_TO_JOIN_ENABLED, pressToJoinEnabled); + } + if (pressToJoinEnabled) { + ImGui::BeginDisabled(); + } +#endif + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); auto keyboardButtonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); @@ -1214,6 +1228,12 @@ void InputEditorWindow::DrawDeviceToggles(uint8_t portIndex) { ImGui::PopStyleColor(); ImGui::PopItemFlag(); } + +#ifdef ENABLE_PRESS_TO_JOIN + if (pressToJoinEnabled) { + ImGui::EndDisabled(); + } +#endif } void InputEditorWindow::DrawClearAllButton(uint8_t portIndex) {