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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ jobs:
clang-format:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: clang-format
run: |
docker run --rm -v ${PWD}:/src ghcr.io/wiiu-env/clang-format:13.0.0-2 -r ./source
build-binary:
runs-on: ubuntu-22.04
needs: clang-format
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: create version.h
run: |
git_hash=$(git rev-parse --short "$GITHUB_SHA")
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ jobs:
clang-format:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: clang-format
run: |
docker run --rm -v ${PWD}:/src ghcr.io/wiiu-env/clang-format:13.0.0-2 -r ./source
check-build-with-logging:
runs-on: ubuntu-22.04
needs: clang-format
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: build binary with logging
run: |
docker build . -t builder
Expand All @@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-22.04
needs: clang-format
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: create version.h
run: |
git_hash=$(git rev-parse --short "${{ github.event.pull_request.head.sha }}")
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ cmake-build-debug/
CMakeLists.txt
*.wms
*.zip
docs/
10 changes: 5 additions & 5 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:20260225

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
COPY --from=ghcr.io/wiiu-env/libbuttoncombo:20260331 /artifacts $DEVKITPRO
COPY --from=ghcr.io/wiiu-env/libfunctionpatcher:20260331 /artifacts $DEVKITPRO
COPY --from=ghcr.io/wiiu-env/wiiumodulesystem:20260418 /artifacts $DEVKITPRO

WORKDIR project
WORKDIR /project
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ INCLUDES := source
#-------------------------------------------------------------------------------
# options for code generation
#-------------------------------------------------------------------------------
CFLAGS := -Wall -Wextra -O2 -ffunction-sections\
CFLAGS := -Wall -Wextra -Werror -Os -ffunction-sections\
$(MACHDEP)

CFLAGS += $(INCLUDE) -D__WIIU__ -D__WUT__
Expand Down
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**.
10 changes: 5 additions & 5 deletions source/ButtonComboInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ bool ButtonComboInfoIF::getMetaOptions(const ButtonComboModule_MetaOptionsOut &o

void ButtonComboInfoIF::setMetaOptions(const ButtonComboModule_MetaOptions options) {
mLabel = options.label;
DEBUG_FUNCTION_LINE("Updated label to: \"%s\", for %08X", mLabel.c_str(), getHandle().handle);
DEBUG_FUNCTION_LINE("Updated label to: \"%s\", for %p", mLabel.c_str(), getHandle().handle);
}

ButtonComboModule_CallbackOptions ButtonComboInfoIF::getCallbackOptions() const {
Expand All @@ -52,15 +52,15 @@ ButtonComboModule_CallbackOptions ButtonComboInfoIF::getCallbackOptions() const
void ButtonComboInfoIF::setCallbackOptions(const ButtonComboModule_CallbackOptions options) {
mCallback = options.callback;
mContext = options.context;
DEBUG_FUNCTION_LINE("Updated callback to: %08X(%08X), for %s %08X", mCallback, mContext, mLabel.c_str(), getHandle().handle);
DEBUG_FUNCTION_LINE("Updated callback to: %p(%p), for %s handle: %p", mCallback, mContext, mLabel.c_str(), getHandle().handle);
}

uint32_t ButtonComboInfoIF::getCombo() const {
return mCombo;
}
void ButtonComboInfoIF::setCombo(const ButtonComboModule_Buttons combo) {
mCombo = combo;
DEBUG_FUNCTION_LINE("Updated combo to: %08X, for %s %08X", mCombo, mLabel.c_str(), getHandle().handle);
DEBUG_FUNCTION_LINE("Updated combo to: %08X, for %s handle: %p", mCombo, mLabel.c_str(), getHandle().handle);
resetPrevInput();
}

Expand All @@ -70,7 +70,7 @@ ButtonComboModule_ComboStatus ButtonComboInfoIF::getStatus() const {

void ButtonComboInfoIF::setStatus(const ButtonComboModule_ComboStatus status) {
mStatus = status;
DEBUG_FUNCTION_LINE("Updated status to: %08X, for %s %08X", mStatus, mLabel.c_str(), getHandle().handle);
DEBUG_FUNCTION_LINE("Updated status to: %08X, for %s handle: %p", mStatus, mLabel.c_str(), getHandle().handle);
}

ButtonComboModule_ControllerTypes ButtonComboInfoIF::getControllerMask() const {
Expand All @@ -79,7 +79,7 @@ ButtonComboModule_ControllerTypes ButtonComboInfoIF::getControllerMask() const {

void ButtonComboInfoIF::setControllerMask(const ButtonComboModule_ControllerTypes mask) {
mControllerMask = mask;
DEBUG_FUNCTION_LINE("Updated controllerMask to: %08X, for %s %08X", mControllerMask, mLabel.c_str(), getHandle().handle);
DEBUG_FUNCTION_LINE("Updated controllerMask to: %08X, for %s handle: %p", mControllerMask, mLabel.c_str(), getHandle().handle);
resetPrevInput();
}

Expand Down
6 changes: 3 additions & 3 deletions source/ButtonComboInfoDown.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ void ButtonComboInfoDown::UpdateInput(

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 [%08X]", mCombo, controller, pressedButtons.size(), pressedButtons.back(), mLabel.c_str(), getHandle().handle);
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) {
const bool prevButtonsIncludedCombo = (prevButtonCombo & mCombo) == mCombo; // Make sure the combo can't be triggered on releasing
Expand All @@ -39,10 +39,10 @@ void ButtonComboInfoDown::UpdateInput(

if (buttonsPressedChanged && buttonsPressedMatchCombo && !prevButtonsIncludedCombo) {
if (mCallback != nullptr) {
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);
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);
} 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
8 changes: 4 additions & 4 deletions source/ButtonComboInfoHold.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ void ButtonComboInfoHold::UpdateInput(const ButtonComboModule_ControllerTypes co
auto &holdInformation = mHoldInformation[chanIndex];
const auto latestButtonPress = pressedButtons.back();

DEBUG_FUNCTION_LINE_VERBOSE("[HOLD ] Check button combo %08X on controller %08X (lastItem im pressedButtons (size %d) is %08X) for %s [%08X]", mCombo, controller, pressedButtons.size(), latestButtonPress, mLabel.c_str(), getHandle().handle);
DEBUG_FUNCTION_LINE_VERBOSE("[HOLD ] Check button combo %08X on controller %08X (lastItem im pressedButtons (size %d) is %08X) for %s [%p]", mCombo, controller, pressedButtons.size(), latestButtonPress, mLabel.c_str(), getHandle().handle);

const bool prevButtonsIncludedCombo = (holdInformation.prevButtonCombo & mCombo) == mCombo; // Make sure the combo can't be triggered on releasing
const bool buttonsPressedChanged = holdInformation.prevButtonCombo != latestButtonPress; // Avoid "holding" the combo
Expand All @@ -51,11 +51,11 @@ void ButtonComboInfoHold::UpdateInput(const ButtonComboModule_ControllerTypes co

if (intervalInMs > mTargetDurationInMs && !holdInformation.callbackTriggered) {
if (mCallback != nullptr) {
DEBUG_FUNCTION_LINE("Calling callback [%08X](controller: %08X context: %08X) for \"%s\" [handle: %08X], hold %08X for %d ms", mCallback, controller, mContext, mLabel.c_str(), getHandle().handle, mCombo, intervalInMs);
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);

} 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 All @@ -66,7 +66,7 @@ void ButtonComboInfoHold::UpdateInput(const ButtonComboModule_ControllerTypes co
}

ButtonComboModule_Error ButtonComboInfoHold::setHoldDuration(const uint32_t holdDurationInMs) {
DEBUG_FUNCTION_LINE("Setting holdDurationInMs to %d for %s [%08X]", holdDurationInMs, mLabel.c_str(), getHandle().handle);
DEBUG_FUNCTION_LINE("Setting holdDurationInMs to %d for %s [%p]", holdDurationInMs, mLabel.c_str(), getHandle().handle);
mTargetDurationInMs = holdDurationInMs;
return BUTTON_COMBO_MODULE_ERROR_SUCCESS;
}
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
Loading