Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM ghcr.io/wiiu-env/devkitppc:20241128
FROM ghcr.io/wiiu-env/devkitppc:20250608

COPY --from=ghcr.io/wiiu-env/libbuttoncombo:20250125-cb22627 /artifacts $DEVKITPRO
COPY --from=ghcr.io/wiiu-env/libfunctionpatcher:20241012 /artifacts $DEVKITPRO
COPY --from=ghcr.io/wiiu-env/wiiumodulesystem:20240424 /artifacts $DEVKITPRO

WORKDIR project
WORKDIR /project
72 changes: 56 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,61 @@
# ButtonComboModule

[![CI-Release](https://github.com/wiiu-env/ButtonComboModule/actions/workflows/ci.yml/badge.svg)](https://github.com/wiiu-env/ButtonComboModule/actions/workflows/ci.yml)

**ButtonComboModule** is a Wii U Module System (WUMS) module that provides system-wide button combination detection. It
allows other homebrew applications and modules to easily register callbacks for specific button presses or holds across
various controllers (GamePad, Pro Controller, Wii Remote, etc.).

## Usage
(`[ENVIRONMENT]` is a placeholder for the actual environment name.)

1. Copy the file `ButtonComboModule.wms` into `sd:/wiiu/environments/[ENVIRONMENT]/modules`.
2. Requires the [WUMSLoader](https://github.com/wiiu-env/WUMSLoader) in `sd:/wiiu/environments/[ENVIRONMENT]/modules/setup`.
3. Requires the [FunctionPatcherModule](https://github.com/wiiu-env/FunctionPatcherModule) in `sd:/wiiu/environments/[ENVIRONMENT]/modules`.
(`[ENVIRONMENT]` is a placeholder for the actual environment name, e.g., `tiramisu` or `aroma`.)

1. Copy the file `ButtonComboModule.wms` into `sd:/wiiu/environments/[ENVIRONMENT]/modules`.
2. Requires the [WUMSLoader](https://github.com/wiiu-env/WUMSLoader) in
`sd:/wiiu/environments/[ENVIRONMENT]/modules/setup`.
3. Requires the [FunctionPatcherModule](https://github.com/wiiu-env/FunctionPatcherModule) in
`sd:/wiiu/environments/[ENVIRONMENT]/modules`.

## Development

### Homebrew Applications (.rpx / .wuhb)

If you are developing a standard homebrew application and want to use system-wide button combos, you should use the
**libbuttoncombo** client library.

* **Repository**: [wiiu-env/libbuttoncombo](https://github.com/wiiu-env/libbuttoncombo)

### WUPS Plugins (.wps)

If you are developing a plugin for the Wii U Plugin System (WUPS), you **should not** use `libbuttoncombo`.

## Buildflags
The [WiiUPluginSystem](https://github.com/wiiu-env/WiiUPluginSystem) (WUPS) library already provides built-in wrappers
for the ButtonComboModule. You can use the WUPS API directly to register combos without linking an external library.

### Logging
Building via `make` only logs errors (via OSReport). To enable logging via the [LoggingModule](https://github.com/wiiu-env/LoggingModule) set `DEBUG` to `1` or `VERBOSE`.
## Building

`make` Logs errors only (via OSReport).
`make DEBUG=1` Enables information and error logging via [LoggingModule](https://github.com/wiiu-env/LoggingModule).
`make DEBUG=VERBOSE` Enables verbose information and error logging via [LoggingModule](https://github.com/wiiu-env/LoggingModule).
To build this module, you need **devkitPro** installed with `wut` and `wums`. You also need the `libfunctionpatcher`
libraries installed.

If the [LoggingModule](https://github.com/wiiu-env/LoggingModule) is not present, it'll fallback to UDP (Port 4405) and [CafeOS](https://github.com/wiiu-env/USBSerialLoggingModule) logging.
### Build Flags (Logging)

## Building using the Dockerfile
Building via `make` only logs critical errors via OSReport by default. To enable verbose logging via
the [LoggingModule](https://github.com/wiiu-env/LoggingModule), set `DEBUG` to `1` or `VERBOSE`.

It's possible to use a docker image for building. This way you don't need anything installed on your host system.
* `make`: Logs errors only (via OSReport).
* `make DEBUG=1`: Enables information and error logging via [LoggingModule](https://github.com/wiiu-env/LoggingModule).
* `make DEBUG=VERBOSE`: Enables verbose information and error logging
via [LoggingModule](https://github.com/wiiu-env/LoggingModule).

If the [LoggingModule](https://github.com/wiiu-env/LoggingModule) is not present, it will fallback to UDP (Port 4405)
and [USBSerialLoggingModule](https://github.com/wiiu-env/USBSerialLoggingModule) logging.

### Building using Docker

It is possible to use a Docker image for building. This way, you don't need anything installed on your host system.

```
# Build docker image (only needed once)
# Build Docker image (only needed once)
docker build . -t buttoncombomodule-builder

# make
Expand All @@ -33,6 +65,14 @@ docker run -it --rm -v ${PWD}:/project buttoncombomodule-builder make
docker run -it --rm -v ${PWD}:/project buttoncombomodule-builder make clean
```

## Format the code via docker
## Formatting

You can format the code via Docker:

```
docker run --rm -v ${PWD}:/src ghcr.io/wiiu-env/clang-format:13.0.0-2 -r ./source -i
```

## License

`docker run --rm -v ${PWD}:/src ghcr.io/wiiu-env/clang-format:13.0.0-2 -r ./source -i`
This module is licensed under the **GPL-3.0**.
2 changes: 1 addition & 1 deletion source/ButtonComboInfoDown.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ void ButtonComboInfoDown::UpdateInput(
DEBUG_FUNCTION_LINE("Calling callback [%08X](controller: %08X, context: %08X) for \"%s\" [handle: %08X], pressed down %08X", mCallback, controller, mContext, mLabel.c_str(), getHandle().handle, mCombo);
mCallback(controller, getHandle(), mContext);
} else {
DEBUG_FUNCTION_LINE_WARN("Callback was null for combo %08X", getHandle());
DEBUG_FUNCTION_LINE_WARN("Callback was null for combo %p", getHandle().handle);
}
}
prevButtonCombo = pressedButton;
Expand Down
2 changes: 1 addition & 1 deletion source/ButtonComboInfoHold.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ void ButtonComboInfoHold::UpdateInput(const ButtonComboModule_ControllerTypes co
mCallback(controller, getHandle(), mContext);

} else {
DEBUG_FUNCTION_LINE_WARN("Callback was null for combo %08X", getHandle());
DEBUG_FUNCTION_LINE_WARN("Callback was null for combo %p", getHandle().handle);
}
holdInformation.callbackTriggered = true;
}
Expand Down
72 changes: 45 additions & 27 deletions source/ButtonComboManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <padscore/kpad.h>

#include <algorithm>
#include <span>
#include <vector>

namespace {
Expand Down Expand Up @@ -367,20 +368,21 @@ void ButtonComboManager::AddCombo(std::shared_ptr<ButtonComboInfoIF> newComboInf
outHandle = newComboInfo->getHandle();
mCombos.emplace_front(std::move(newComboInfo));

const auto block = hasActiveComboWithTVButton();
VPADSetTVMenuInvalid(VPAD_CHAN_0, block);
VPADSetTVMenuInvalid(VPAD_CHAN_1, block);
UpdateTVMenuBlocking();
}

ButtonComboModule_Error ButtonComboManager::RemoveCombo(ButtonComboModule_ComboHandle handle) {
std::lock_guard lock(mMutex);

if (mIsIterating) {
mCombosToRemove.push_back(handle);
return BUTTON_COMBO_MODULE_ERROR_SUCCESS;
}

if (!remove_first_if(mCombos, [handle](const auto &combo) { return combo->getHandle() == handle; })) {
DEBUG_FUNCTION_LINE_WARN("Failed to remove combo by handle %08X", handle);
DEBUG_FUNCTION_LINE_WARN("Failed to remove combo by handle %p", handle.handle);
} else {
const auto block = hasActiveComboWithTVButton();

VPADSetTVMenuInvalid(VPAD_CHAN_0, block);
VPADSetTVMenuInvalid(VPAD_CHAN_1, block);
UpdateTVMenuBlocking();
}

return BUTTON_COMBO_MODULE_ERROR_SUCCESS;
Expand Down Expand Up @@ -431,12 +433,36 @@ void ButtonComboManager::UpdateInputVPAD(const VPADChan chan, const VPADStatus *
mVPADButtonBuffer[usedBufferSize - i - 1] = remapVPADButtons(buffer[i].hold);
}

for (const auto &combo : mCombos) {
if (combo->getStatus() != BUTTON_COMBO_MODULE_COMBO_STATUS_VALID) {
continue;
}
combo->UpdateInput(controller, std::span(mVPADButtonBuffer.data(), usedBufferSize));
UpdateInputsLocked(controller, std::span(mVPADButtonBuffer.data(), usedBufferSize));
}
}

void ButtonComboManager::UpdateTVMenuBlocking() {
const auto block = hasActiveComboWithTVButton();
VPADSetTVMenuInvalid(VPAD_CHAN_0, block);
VPADSetTVMenuInvalid(VPAD_CHAN_1, block);
}

void ButtonComboManager::UpdateInputsLocked(const ButtonComboModule_ControllerTypes controller, const std::span<uint32_t> pressedButtons) {
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);
}
mIsIterating--;

// Remove pending removals if existing
if (mIsIterating == 0 && !mCombosToRemove.empty()) {
for (auto handle : mCombosToRemove) {
remove_first_if(mCombos, [handle](const auto &combo) { return combo->getHandle() == handle; });
}
mCombosToRemove.clear();

// Update TV Menu blocking status once after all removals
UpdateTVMenuBlocking();
}
}

Expand Down Expand Up @@ -479,18 +505,12 @@ void ButtonComboManager::UpdateInputWPAD(const WPADChan chan, WPADStatus *data)
default:
return;
}
{
std::lock_guard lock(mMutex);
for (const auto &combo : mCombos) {
if (combo->getStatus() != BUTTON_COMBO_MODULE_COMBO_STATUS_VALID) {
continue;
}
combo->UpdateInput(controller, std::span(&pressedButtons, 1));
}
}

UpdateInputsLocked(controller, std::span(&pressedButtons, 1));
}

ButtonComboInfoIF *ButtonComboManager::GetComboInfoForHandle(const ButtonComboModule_ComboHandle handle) const {
std::lock_guard lock(mMutex);
for (const auto &combo : mCombos) {
if (combo->getHandle() == handle) {
return combo.get();
Expand Down Expand Up @@ -554,9 +574,7 @@ ButtonComboModule_Error ButtonComboManager::UpdateButtonCombo(const ButtonComboM
comboInfo->setStatus(CheckComboStatus(*comboInfo));
outComboStatus = comboInfo->getStatus();

const auto block = hasActiveComboWithTVButton();
VPADSetTVMenuInvalid(VPAD_CHAN_0, block);
VPADSetTVMenuInvalid(VPAD_CHAN_1, block);
UpdateTVMenuBlocking();

return BUTTON_COMBO_MODULE_ERROR_SUCCESS;
}
Expand Down Expand Up @@ -603,7 +621,7 @@ ButtonComboModule_Error ButtonComboManager::GetButtonComboInfoEx(const ButtonCom
std::lock_guard lock(mMutex);
const auto *comboInfo = GetComboInfoForHandle(handle);
if (!comboInfo) {
DEBUG_FUNCTION_LINE_ERR("ButtonComboModule_GetButtonComboInfo failed to get manager for handle %08X", handle);
DEBUG_FUNCTION_LINE_ERR("ButtonComboModule_GetButtonComboInfo failed to get manager for handle %p", handle.handle);
return BUTTON_COMBO_MODULE_ERROR_INVALID_ARGUMENT;
}
outOptions = comboInfo->getComboInfoEx();
Expand All @@ -630,7 +648,7 @@ ButtonComboModule_Error ButtonComboManager::DetectButtonCombo_Blocking(const But
}

if (options.holdComboForInMs == 0 || options.holdAbortForInMs == 0 || options.abortButtonCombo == 0) {
DEBUG_FUNCTION_LINE_WARN("Failed to detect button combo: Invalid params. holdComboFor: %s ms, holdAbortFor: %d ms, abortButtonCombo: %08X", options.holdComboForInMs, options.holdAbortForInMs, options.abortButtonCombo);
DEBUG_FUNCTION_LINE_WARN("Failed to detect button combo: Invalid params. holdComboFor: %d ms, holdAbortFor: %d ms, abortButtonCombo: %08X", options.holdComboForInMs, options.holdAbortForInMs, options.abortButtonCombo);
return BUTTON_COMBO_MODULE_ERROR_INVALID_ARGUMENT;
}

Expand Down
17 changes: 11 additions & 6 deletions source/ButtonComboManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,13 @@ class ButtonComboManager {

static std::optional<std::shared_ptr<ButtonComboInfoIF>> CreateComboInfo(const ButtonComboModule_ComboOptions &options, ButtonComboModule_Error &err);

[[nodiscard]] ButtonComboInfoIF *GetComboInfoForHandle(ButtonComboModule_ComboHandle handle) const;

void UpdateInputVPAD(VPADChan chan, const VPADStatus *buffer, uint32_t bufferSize, const VPADReadError *error);
void UpdateTVMenuBlocking();

void UpdateInputWPAD(WPADChan chan, WPADStatus *data);

bool hasActiveComboWithTVButton();

ButtonComboModule_ComboStatus CheckComboStatus(const ButtonComboInfoIF &other);

void AddCombo(std::shared_ptr<ButtonComboInfoIF> newComboInfo, ButtonComboModule_ComboHandle &outHandle, ButtonComboModule_ComboStatus &outStatus);

ButtonComboModule_Error RemoveCombo(ButtonComboModule_ComboHandle handle);
Expand Down Expand Up @@ -56,9 +53,17 @@ class ButtonComboManager {
ButtonComboModule_Error DetectButtonCombo_Blocking(const ButtonComboModule_DetectButtonComboOptions &options, ButtonComboModule_Buttons &outButtonCombo);

private:
[[nodiscard]] ButtonComboInfoIF *GetComboInfoForHandle(ButtonComboModule_ComboHandle handle) const;

void UpdateInputsLocked(ButtonComboModule_ControllerTypes controller, std::span<uint32_t> pressedButtons);

ButtonComboModule_ComboStatus CheckComboStatus(const ButtonComboInfoIF &other);

std::forward_list<std::shared_ptr<ButtonComboInfoIF>> mCombos;
std::vector<ButtonComboModule_ComboHandle> mCombosToRemove;
int mIsIterating = 0;
std::vector<uint32_t> mVPADButtonBuffer;
std::mutex mMutex;
std::mutex mDetectButtonsMutex;
mutable std::recursive_mutex mMutex;
std::recursive_mutex mDetectButtonsMutex;
bool mInButtonComboDetection = false;
};
Loading