diff --git a/Makefile b/Makefile index bdfc792..263b1a0 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CFLAGS := -Wall -Wextra -Werror -Os -ffunction-sections\ CFLAGS += $(INCLUDE) -D__WIIU__ -D__WUT__ -CXXFLAGS := $(CFLAGS) -std=c++20 +CXXFLAGS := $(CFLAGS) -std=c++23 ASFLAGS := -g $(ARCH) LDFLAGS = -g $(ARCH) $(RPXSPECS) -Wl,-Map,$(notdir $*.map) $(WUMSSPECS) diff --git a/source/ButtonComboInfo.h b/source/ButtonComboInfo.h index 192e9a9..5428861 100644 --- a/source/ButtonComboInfo.h +++ b/source/ButtonComboInfo.h @@ -17,7 +17,8 @@ class ButtonComboInfoIF { void *context, bool observer); virtual ~ButtonComboInfoIF(); - virtual void UpdateInput(ButtonComboModule_ControllerTypes controller, std::span pressedButtons) = 0; + // Note: return the index of the sample that activated the combo, or -1. + virtual int UpdateInput(ButtonComboModule_ControllerTypes controller, std::span pressedButtons) = 0; [[nodiscard]] bool isObserver() const; diff --git a/source/ButtonComboInfoDown.cpp b/source/ButtonComboInfoDown.cpp index 068bb1d..2e3411a 100644 --- a/source/ButtonComboInfoDown.cpp +++ b/source/ButtonComboInfoDown.cpp @@ -1,6 +1,7 @@ #include "ButtonComboInfoDown.h" #include "logger.h" +#include ButtonComboInfoDown::ButtonComboInfoDown( std::string label, @@ -16,23 +17,24 @@ ButtonComboInfoDown::~ButtonComboInfoDown() { DEBUG_FUNCTION_LINE_INFO("Deleted ButtonComboInfoDown: \"%s\", combo: %08X, controllerMask: %08X. Observer %d", mLabel.c_str(), mCombo, mControllerMask, mIsObserver); } -void ButtonComboInfoDown::UpdateInput( +int ButtonComboInfoDown::UpdateInput( const ButtonComboModule_ControllerTypes controller, const std::span pressedButtons) { if ((mControllerMask & controller) == 0) { - return; + return -1; } const auto chanIndex = ControllerTypeToChanIndex(controller); if (chanIndex < 0 || static_cast(chanIndex) >= std::size(mHoldInformation)) { DEBUG_FUNCTION_LINE_WARN("ChanIndex is out of bounds %d", chanIndex); - return; + return -1; } auto &[prevButtonCombo] = mHoldInformation[chanIndex]; DEBUG_FUNCTION_LINE_VERBOSE("[PRESS DOWN] Check button combo %08X on controller %08X (lastItem im pressedButtons (size %d) is %08X) for %s [%p]", mCombo, controller, pressedButtons.size(), pressedButtons.back(), mLabel.c_str(), getHandle().handle); - for (const auto &pressedButton : pressedButtons) { + int activatedIndex = -1; + for (auto [index, pressedButton] : std::views::enumerate(pressedButtons)) { const bool prevButtonsIncludedCombo = (prevButtonCombo & mCombo) == mCombo; // Make sure the combo can't be triggered on releasing const bool buttonsPressedChanged = prevButtonCombo != pressedButton; // Avoid "holding" the combo const bool buttonsPressedMatchCombo = pressedButton == mCombo; // detect the actual combo @@ -41,12 +43,14 @@ void ButtonComboInfoDown::UpdateInput( if (mCallback != nullptr) { DEBUG_FUNCTION_LINE("Calling callback [%p](controller: %08X, context: %p) for \"%s\" [handle: %p], pressed down %08X", mCallback, controller, mContext, mLabel.c_str(), getHandle().handle, mCombo); mCallback(controller, getHandle(), mContext); + activatedIndex = index; } else { DEBUG_FUNCTION_LINE_WARN("Callback was null for combo %p", getHandle().handle); } } prevButtonCombo = pressedButton; } + return activatedIndex; } ButtonComboModule_Error ButtonComboInfoDown::setHoldDuration(uint32_t) { diff --git a/source/ButtonComboInfoDown.h b/source/ButtonComboInfoDown.h index b7f2b4e..1a4541b 100644 --- a/source/ButtonComboInfoDown.h +++ b/source/ButtonComboInfoDown.h @@ -23,7 +23,7 @@ class ButtonComboInfoDown final : public ButtonComboInfoIF { uint32_t prevButtonCombo; } HoldInformation; - void UpdateInput(ButtonComboModule_ControllerTypes controller, std::span pressedButtons) override; + int UpdateInput(ButtonComboModule_ControllerTypes controller, std::span pressedButtons) override; ButtonComboModule_Error setHoldDuration(uint32_t uint32) override; @@ -33,4 +33,4 @@ class ButtonComboInfoDown final : public ButtonComboInfoIF { private: HoldInformation mHoldInformation[9] = {}; // one for each controller -}; \ No newline at end of file +}; diff --git a/source/ButtonComboInfoHold.cpp b/source/ButtonComboInfoHold.cpp index d656a47..30facfb 100644 --- a/source/ButtonComboInfoHold.cpp +++ b/source/ButtonComboInfoHold.cpp @@ -22,16 +22,18 @@ ButtonComboInfoHold::~ButtonComboInfoHold() { DEBUG_FUNCTION_LINE_INFO("Deleted ButtonComboInfoHold: \"%s\", combo: %08X, targetDurationInMs: %d ms, controllerMask: %08X", mLabel.c_str(), mCombo, mTargetDurationInMs, mControllerMask); } -void ButtonComboInfoHold::UpdateInput(const ButtonComboModule_ControllerTypes controller, const std::span pressedButtons) { +int ButtonComboInfoHold::UpdateInput(const ButtonComboModule_ControllerTypes controller, const std::span pressedButtons) { if ((mControllerMask & controller) == 0) { - return; + return -1; } const auto chanIndex = ControllerTypeToChanIndex(controller); if (chanIndex < 0 || static_cast(chanIndex) >= std::size(mHoldInformation)) { DEBUG_FUNCTION_LINE_WARN("ChanIndex is out of bounds %d", chanIndex); - return; + return -1; } + int activatedIndex = -1; + auto &holdInformation = mHoldInformation[chanIndex]; const auto latestButtonPress = pressedButtons.back(); @@ -53,7 +55,7 @@ void ButtonComboInfoHold::UpdateInput(const ButtonComboModule_ControllerTypes co if (mCallback != nullptr) { DEBUG_FUNCTION_LINE("Calling callback [%p](controller: %08X context: %p) for \"%s\" [handle: %p], hold %08X for %d ms", mCallback, controller, mContext, mLabel.c_str(), getHandle().handle, mCombo, intervalInMs); mCallback(controller, getHandle(), mContext); - + activatedIndex = pressedButtons.size() - 1; } else { DEBUG_FUNCTION_LINE_WARN("Callback was null for combo %p", getHandle().handle); } @@ -63,6 +65,7 @@ void ButtonComboInfoHold::UpdateInput(const ButtonComboModule_ControllerTypes co holdInformation.callbackTriggered = false; holdInformation.holdStartedAt = 0; } + return activatedIndex; } ButtonComboModule_Error ButtonComboInfoHold::setHoldDuration(const uint32_t holdDurationInMs) { diff --git a/source/ButtonComboInfoHold.h b/source/ButtonComboInfoHold.h index 3c7bdbb..ad843eb 100644 --- a/source/ButtonComboInfoHold.h +++ b/source/ButtonComboInfoHold.h @@ -25,7 +25,7 @@ class ButtonComboInfoHold final : public ButtonComboInfoIF { ~ButtonComboInfoHold() override; private: - void UpdateInput(ButtonComboModule_ControllerTypes controller, std::span pressedButtons) override; + int UpdateInput(ButtonComboModule_ControllerTypes controller, std::span pressedButtons) override; ButtonComboModule_Error setHoldDuration(uint32_t holdDurationInMs) override; diff --git a/source/ButtonComboManager.cpp b/source/ButtonComboManager.cpp index f9368da..80a45f2 100644 --- a/source/ButtonComboManager.cpp +++ b/source/ButtonComboManager.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -395,12 +396,12 @@ ButtonComboModule_Error ButtonComboManager::GetButtonComboStatus(const ButtonCom return BUTTON_COMBO_MODULE_ERROR_SUCCESS; } -void ButtonComboManager::UpdateInputVPAD(const VPADChan chan, const VPADStatus *buffer, const uint32_t bufferSize, const VPADReadError *error) { +void ButtonComboManager::UpdateInputVPAD(const VPADChan chan, std::span buffer, const VPADReadError *error) { if (chan < VPAD_CHAN_0 || chan > VPAD_CHAN_1) { DEBUG_FUNCTION_LINE_ERR("Invalid VPADChan"); return; } - if (buffer == nullptr || !error || *error != VPAD_READ_SUCCESS) { + if (!buffer.data() || buffer.empty() || !error || *error != VPAD_READ_SUCCESS) { DEBUG_FUNCTION_LINE_ERR("Invalid buffer or error state"); return; } @@ -410,25 +411,43 @@ void ButtonComboManager::UpdateInputVPAD(const VPADChan chan, const VPADStatus * return; } + // When button proc mode is loose, only the most recent button state matters. + bool buttonsAreLoose = VPADGetButtonProcMode(chan) == 0; + int comboStatus = -1; + { std::lock_guard lock(mMutex); - const auto controller = convert(chan); - uint32_t usedBufferSize = 1; + const auto controller = convert(chan); + // Fix games like TP HD - if (VPADGetButtonProcMode(chan) == 1) { - usedBufferSize = bufferSize; + mVPADButtonBuffer.resize(buttonsAreLoose ? 1 : buffer.size()); + // The order of buffer samples is new -> old, but we want it to be in old -> new. + for (auto [dst, src] : std::views::zip(mVPADButtonBuffer | std::views::reverse, + buffer)) { + dst = remapVPADButtons(src.hold); } - if (usedBufferSize > mVPADButtonBuffer.size()) { - mVPADButtonBuffer.resize(usedBufferSize); - } + comboStatus = UpdateInputsLocked(controller, mVPADButtonBuffer); + } + + // Begin button suppression logic. + auto &suppressed = mVPADSuppressed[static_cast(chan)]; - // the order of the "buffer" data is new -> old, but we want it to be in old -> new - for (uint32_t i = 0; i < usedBufferSize; i++) { - mVPADButtonBuffer[usedBufferSize - i - 1] = remapVPADButtons(buffer[i].hold); + // Check every buffer entry, from old to new. + for (int i = buffer.size() - 1; i >= 0; --i) { + auto& entry = buffer[i]; + if ((buttonsAreLoose && comboStatus != -1) || i == comboStatus) { + // This is the entry that activated a combo, the triggers tell which buttons + // to suppress. + suppressed |= entry.trigger; } + // Re-enable all buttons released. + suppressed &= ~entry.release; - UpdateInputsLocked(controller, std::span(mVPADButtonBuffer.data(), usedBufferSize)); + // Don't let the application see the suppressed buttons + entry.hold &= ~suppressed; + entry.trigger &= ~suppressed; + entry.release &= ~suppressed; } } @@ -438,14 +457,20 @@ void ButtonComboManager::UpdateTVMenuBlocking() { VPADSetTVMenuInvalid(VPAD_CHAN_1, block); } -void ButtonComboManager::UpdateInputsLocked(const ButtonComboModule_ControllerTypes controller, const std::span pressedButtons) { +int ButtonComboManager::UpdateInputsLocked(const ButtonComboModule_ControllerTypes controller, const std::span pressedButtons) { + int when_triggered = -1; std::lock_guard lock(mMutex); mIsIterating++; for (const auto &combo : mCombos) { if (combo->getStatus() != BUTTON_COMBO_MODULE_COMBO_STATUS_VALID) { continue; } - combo->UpdateInput(controller, pressedButtons); + int idx = combo->UpdateInput(controller, pressedButtons); + if (idx != -1) { + if (when_triggered == -1 || idx < when_triggered) { + when_triggered = idx; + } + } } mIsIterating--; @@ -459,6 +484,7 @@ void ButtonComboManager::UpdateInputsLocked(const ButtonComboModule_ControllerTy // Update TV Menu blocking status once after all removals UpdateTVMenuBlocking(); } + return when_triggered; } void ButtonComboManager::UpdateInputWPAD(const WPADChan chan, WPADStatus *data) { @@ -471,6 +497,12 @@ void ButtonComboManager::UpdateInputWPAD(const WPADChan chan, WPADStatus *data) return; } + unsigned ctrlIdx = static_cast(chan); + auto &coreBtnTracker = mWPADCoreBtns[ctrlIdx]; + auto &extBtnTracker = mWPADExtBtns[ctrlIdx]; + + coreBtnTracker.update(data->buttons); + // Do not check for combos while the combo detection is active if (mInButtonComboDetection) { return; @@ -490,18 +522,49 @@ void ButtonComboManager::UpdateInputWPAD(const WPADChan chan, WPADStatus *data) case WPAD_EXT_MPLUS_CLASSIC: { const auto classic = reinterpret_cast(data); pressedButtons = remapClassicButtons(classic->buttons); + extBtnTracker.update(classic->buttons); break; } case WPAD_EXT_PRO_CONTROLLER: { const auto proController = reinterpret_cast(data); pressedButtons = remapProButtons(proController->buttons); + extBtnTracker.update(proController->buttons); break; } default: return; } - UpdateInputsLocked(controller, std::span(&pressedButtons, 1)); + // Begin button suppression logic. + int comboStatus = UpdateInputsLocked(controller, std::span(&pressedButtons, 1)); + if (comboStatus != -1) { + // A combo was activated, let's suppress all trigger buttons. + coreBtnTracker.blockTriggered(); + extBtnTracker.blockTriggered(); + } + + // For both core and extensions: + // - Re-enable all buttons released. + // - Don't let the application see the suppressed buttons. + + coreBtnTracker.unblockReleased(); + coreBtnTracker.suppressButtons(data->buttons); + switch (data->extensionType) { + case WPAD_EXT_CLASSIC: + case WPAD_EXT_MPLUS_CLASSIC: { + extBtnTracker.unblockReleased(); + const auto classic = reinterpret_cast(data); + extBtnTracker.suppressButtons(classic->buttons); + break; + } + case WPAD_EXT_PRO_CONTROLLER: { + extBtnTracker.unblockReleased(); + const auto proController = reinterpret_cast(data); + extBtnTracker.suppressButtons(proController->buttons); + break; + } + } + // Finish button suppression logic. } ButtonComboInfoIF *ButtonComboManager::GetComboInfoForHandle(const ButtonComboModule_ComboHandle handle) const { @@ -675,6 +738,7 @@ ButtonComboModule_Error ButtonComboManager::DetectButtonCombo_Blocking(const But ButtonComboModule_Error result = BUTTON_COMBO_MODULE_ERROR_UNKNOWN_ERROR; while (true) { uint32_t buttonsHold = 0; + [[maybe_unused]] uint32_t buttonsHoldAbort = 0; for (int i = 0; i < 2; i++) { VPADReadError vpad_error = VPAD_READ_UNINITIALIZED; diff --git a/source/ButtonComboManager.h b/source/ButtonComboManager.h index 65b3454..27e936b 100644 --- a/source/ButtonComboManager.h +++ b/source/ButtonComboManager.h @@ -4,14 +4,17 @@ #include #include +#include #include #include #include +#include #include #include #include +#include "ButtonTracker.h" class ButtonComboManager { public: @@ -19,7 +22,7 @@ class ButtonComboManager { static std::optional> CreateComboInfo(const ButtonComboModule_ComboOptions &options, ButtonComboModule_Error &err); - void UpdateInputVPAD(VPADChan chan, const VPADStatus *buffer, uint32_t bufferSize, const VPADReadError *error); + void UpdateInputVPAD(VPADChan chan, std::span buffer, const VPADReadError *error); void UpdateTVMenuBlocking(); void UpdateInputWPAD(WPADChan chan, WPADStatus *data); @@ -55,7 +58,7 @@ class ButtonComboManager { private: [[nodiscard]] ButtonComboInfoIF *GetComboInfoForHandle(ButtonComboModule_ComboHandle handle) const; - void UpdateInputsLocked(ButtonComboModule_ControllerTypes controller, std::span pressedButtons); + int UpdateInputsLocked(ButtonComboModule_ControllerTypes controller, std::span pressedButtons); ButtonComboModule_ComboStatus CheckComboStatus(const ButtonComboInfoIF &other); @@ -66,4 +69,8 @@ class ButtonComboManager { mutable std::recursive_mutex mMutex; std::recursive_mutex mDetectButtonsMutex; bool mInButtonComboDetection = false; -}; \ No newline at end of file + + std::array mVPADSuppressed{}; + std::array, 7> mWPADCoreBtns; + std::array, 7> mWPADExtBtns; +}; diff --git a/source/ButtonTracker.h b/source/ButtonTracker.h new file mode 100644 index 0000000..65971ec --- /dev/null +++ b/source/ButtonTracker.h @@ -0,0 +1,39 @@ +#pragma once + +// Simple class to track buttons triggered and released; also perform suppression logic. + +template +struct ButtonTracker { + void reset() noexcept { + hold = 0; + trigger = 0; + release = 0; + suppress = 0; + } + + void update(T buttons) noexcept { + T changed = buttons ^ hold; + hold = buttons; + trigger = changed & buttons; + release = changed & ~buttons; + } + + void blockTriggered() noexcept { + suppress |= trigger; + } + + void unblockReleased() noexcept { + suppress &= ~release; + } + + template + void suppressButtons(U &buttons) { + buttons &= ~suppress; + } + +private: + T hold = 0; + T trigger = 0; + T release = 0; + T suppress = 0; +}; diff --git a/source/function_patches.cpp b/source/function_patches.cpp index 89ed590..c0835eb 100644 --- a/source/function_patches.cpp +++ b/source/function_patches.cpp @@ -14,7 +14,7 @@ DECL_FUNCTION(int32_t, VPADRead, VPADChan chan, VPADStatus *buffer, uint32_t buf if (result > 0 && real_error == VPAD_READ_SUCCESS) { if (const auto comboManager = gButtonComboManager; comboManager) { - comboManager->UpdateInputVPAD(chan, buffer, result > static_cast(buffer_size) ? buffer_size : result, error); + comboManager->UpdateInputVPAD(chan, std::span(buffer, result), error); } } if (error) {