rawinput: modifiers (key combinations) (#813)

## Link to GitHub Issue or related Pull Request, if one exists
Fixes #635

## Description of change
Adds optional **button modifiers**, allowing a binding to require one or
more "modifier" buttons to be held before it activates.

- New "Modifiers" controller page in the overlay config to bind the
Modifier 1–4 source buttons.
- Each binding gains a 4-bit modifier_mask (Modifier 1-4). It is
persisted as an optional modifiers XML attribute on button nodes and
controller-preset entries. The attribute is optional and defaults to 0,
so existing config files and presets load unchanged and the feature is
off unless the user opts in.
- The button Edit properties popup gains a "Modifiers" dropdown to pick
which modifiers a binding requires. Doesn't apply to MIDI though.
- Input evaluation skips a binding whose required modifiers are not
held, falling through to its alternatives; velocity reporting uses the
same gated path.
- Controller-preset templates gain a "Modifiers" group for importing and
exporting.

## Testing
This commit is contained in:
bicarus
2026-07-21 00:26:36 -07:00
committed by GitHub
parent 623e1e3998
commit ed7318c270
11 changed files with 393 additions and 48 deletions
+66 -11
View File
@@ -3,7 +3,9 @@
#include <cassert>
#include <optional>
#include "games/io.h"
#include "launcher/superexit.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "rawinput/piuio.h"
#include "util/time.h"
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
return sorted;
}
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
namespace GameAPI::Buttons {
static State get_button_state(
rawinput::RawInputManager *manager,
Button &button,
bool check_alts,
bool check_modifiers);
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
}
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
const auto modifier_mask = button.getModifierMask();
if (modifier_mask == 0) {
return true;
}
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
if (!modifier_buttons) {
return false;
}
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
(index >= modifier_buttons->size() ||
get_button_state(manager, modifier_buttons->at(index), true, false) !=
GameAPI::Buttons::BUTTON_PRESSED)) {
return false;
}
}
return true;
}
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
rawinput::RawInputManager *manager,
Button &_button,
bool check_alts,
bool check_modifiers) {
// check override
if (_button.override_enabled) {
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
std::optional<bool> window_has_focus;
while (true) {
// skip bindings whose required modifiers are not held
//
// note that modifiers cannot process MIDI as the logic is written
// below, since MIDI buttons are event-based (on event, off event, etc)
// and cannot be correctly handled by a simple early return
// there is no explicit check for MIDI here, but the UI should have
// prevented it
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
button_count++;
if (!alternatives || alternatives->empty() ||
button_count - 1 >= alternatives->size()) {
return BUTTON_NOT_PRESSED;
}
current_button = &alternatives->at(button_count - 1);
continue;
}
// naive behavior
if (current_button->isNaive()) {
GameAPI::Buttons::State state;
@@ -469,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
}
}
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
return get_button_state(manager, button, check_alts, true);
}
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
if (manager) {
return getState(manager.get(), button, check_alts);
@@ -484,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
return button.override_velocity;
}
// naive behavior
if (button.isNaive()) {
if (button.getInvert()) {
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
} else {
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
}
}
// get button state
Buttons::State button_state = Buttons::getState(manager, button, false);
const auto button_state = Buttons::getState(manager, button, false);
// naive bindings report their digital state as full or zero velocity
if (button.isNaive()) {
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
}
// check if button isn't being pressed
if (button_state != Buttons::BUTTON_PRESSED) {
+21 -1
View File
@@ -299,7 +299,27 @@ std::string Button::getMidiNoteString() {
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
}
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
auto binding_display = this->getBindingDisplayString(manager);
auto modifiers = this->getModifierMask();
if (binding_display.empty() || modifiers == 0) {
return binding_display;
}
std::string modifier_display;
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
if ((modifiers & UINT8_C(1)) == 0) {
continue;
}
if (!modifier_display.empty()) {
modifier_display += "+";
}
modifier_display += fmt::format("Mod{}", index);
}
return modifier_display + "+" + binding_display;
}
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
// get VKey string
auto vKey = (uint16_t) this->getVKey();
+11
View File
@@ -49,6 +49,7 @@ private:
std::string name;
std::string device_identifier = "";
unsigned short vKey = INVALID_VKEY;
uint8_t modifier_mask = 0;
// default bindings are always naive
unsigned short vKey_default = INVALID_VKEY;
@@ -65,6 +66,7 @@ private:
unsigned short velocity_threshold = 0;
std::string getMidiNoteString();
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
public:
std::string getVKeyString();
@@ -111,6 +113,7 @@ public:
device_identifier = "";
analog_type = BAT_NONE;
bat_threshold = 0;
modifier_mask = 0;
}
std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -123,6 +126,14 @@ public:
return this->name;
}
inline uint8_t getModifierMask() const {
return this->modifier_mask;
}
inline void setModifierMask(uint8_t new_modifier_mask) {
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
}
inline const std::string &getDeviceIdentifier() const {
return this->device_identifier;
}
+12
View File
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
int bat_threshold = 0;
int velocity_threshold = 0;
bool invert = false;
unsigned int modifier_mask = 0;
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
const char *devid = gameButtonNode->Attribute("devid");
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
gameButtonsNode->DeleteChild(gameButtonNode);
gameButtonNode = this->configFile.NewElement("button");
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
gameButtonNode->SetAttribute("invert", invert);
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
gameButtonsNode->InsertEndChild(gameButtonNode);
} else {
button->setVKey(static_cast<unsigned short int>(vKey));
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
button->setBatThreshold(bat_threshold);
button->setVelocityThreshold(velocity_threshold);
button->setInvert(invert);
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button->setDeviceIdentifier(devid);
}
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode);
}
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode);
}
@@ -552,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", button.getInvert());
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
break;
}
}
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", 0);
gameButtonNode->SetAttribute("invert", false);
gameButtonNode->SetAttribute("devid", "");
gameButtonNode->SetAttribute("modifiers", 0);
gameButtonsNode->InsertEndChild(gameButtonNode);
}
}
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
int bat_threshold = 0;
int velocity_threshold = 0;
bool invert = false;
unsigned int modifier_mask = 0;
gameButtonNode->QueryIntAttribute("vkey", &vKey);
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
@@ -971,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
const char *devid = gameButtonNode->Attribute("devid");
// find alternative
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
alt.setBatThreshold(bat_threshold);
alt.setVelocityThreshold(velocity_threshold);
alt.setInvert(invert);
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
alt.setDeviceIdentifier(std::string(devid));
}
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
button.setBatThreshold(bat_threshold);
button.setVelocityThreshold(velocity_threshold);
button.setInvert(invert);
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button.setDeviceIdentifier(devid);
}
+46
View File
@@ -26,6 +26,7 @@ namespace overlay::windows {
el->SetAttribute("debounce_down", entry.debounce_down);
el->SetAttribute("bat_threshold", entry.bat_threshold);
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
el->SetAttribute("modifiers", entry.modifier_mask);
parent->InsertEndChild(el);
}
@@ -54,6 +55,10 @@ namespace overlay::windows {
el->QueryIntAttribute("bat_threshold", &bat);
entry.bat_threshold = bat;
unsigned int modifier_mask = 0;
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
return entry;
}
@@ -160,6 +165,28 @@ namespace overlay::windows {
}
}
// modifier buttons
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
if (mod_el) {
auto *btn_el = mod_el->FirstChildElement("button");
while (btn_el) {
const char *btn_name = btn_el->Attribute("name");
std::string btn_name_str = btn_name ? btn_name : "";
TemplateButtonBinding *binding = nullptr;
for (auto &b : tmpl.modifier_buttons) {
if (b.name == btn_name_str) { binding = &b; break; }
}
if (!binding) {
tmpl.modifier_buttons.push_back(
{btn_name_str, read_button_entry(btn_el), {}});
} else {
binding->alternatives.push_back(read_button_entry(btn_el));
}
btn_el = btn_el->NextSiblingElement("button");
}
}
// keypad buttons
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
if (kp_el) {
@@ -244,6 +271,7 @@ namespace overlay::windows {
bool save_user_template(
const ControllerTemplate &tmpl,
const bool save_buttons,
const bool save_modifiers,
const bool save_keypads,
const bool save_analogs,
const bool save_lights) {
@@ -300,6 +328,24 @@ namespace overlay::windows {
}
tmpl_el->InsertEndChild(buttons_el);
// modifier buttons - skip unbound entries
if (!tmpl.modifier_buttons.empty()) {
auto *mod_el = doc.NewElement("modifier_buttons");
if (save_modifiers) {
for (auto &btn : tmpl.modifier_buttons) {
if (!btn.primary.is_unbound()) {
write_button_entry(doc, mod_el, btn.name, btn.primary);
}
for (auto &alt : btn.alternatives) {
if (!alt.is_unbound()) {
write_button_entry(doc, mod_el, btn.name, alt);
}
}
}
}
tmpl_el->InsertEndChild(mod_el);
}
// keypad buttons — skip unbound entries
if (!tmpl.keypad_buttons.empty()) {
auto *kp_el = doc.NewElement("keypad_buttons");
+32
View File
@@ -44,6 +44,7 @@ namespace games {
static bool IO_INITIALIZED = false;
static std::vector<std::string> games;
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_modifiers;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
static robin_hood::unordered_map<std::string, std::string> buttons_help;
@@ -365,6 +366,37 @@ namespace games {
return it->second;
}
static std::vector<Button> gen_buttons_modifiers(const std::string &game) {
auto modifier_buttons = GameAPI::Buttons::getButtons(game);
const std::vector<std::string> names {
"Modifier 1",
"Modifier 2",
"Modifier 3",
"Modifier 4",
};
modifier_buttons = GameAPI::Buttons::sortButtons(modifier_buttons, names);
for (auto &modifier : modifier_buttons) {
modifier.setModifierMask(0);
for (auto &alternative : modifier.getAlternatives()) {
alternative.setModifierMask(0);
}
}
return modifier_buttons;
}
std::vector<Button> *get_buttons_modifiers(const std::string &game) {
initialize();
auto it = buttons_modifiers.find(game);
if (it == buttons_modifiers.end()) {
if (game.empty()) {
return nullptr;
}
buttons_modifiers[game] = gen_buttons_modifiers(game);
return &buttons_modifiers[game];
}
return &it->second;
}
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
auto buttons = GameAPI::Buttons::getButtons(game);
std::vector<std::string> names;
+11
View File
@@ -5,6 +5,16 @@
namespace games {
namespace ModifierButtons {
enum {
Modifier1,
Modifier2,
Modifier3,
Modifier4,
Size,
};
}
namespace OverlayButtons {
enum {
Screenshot,
@@ -59,6 +69,7 @@ namespace games {
const std::vector<std::string> &get_games();
std::vector<Button> *get_buttons(const std::string &game);
std::vector<Button> *get_buttons_modifiers(const std::string &game);
std::string get_buttons_help(const std::string &game);
std::string get_analogs_help(const std::string &game);
std::vector<Button> *get_buttons_keypads(const std::string &game);
+173 -39
View File
@@ -85,6 +85,7 @@ namespace overlay::windows {
static const std::vector<std::pair<const char *, ControllerPage>> CONTROLLER_PAGE_GROUPS = {
{ "Buttons", ControllerPage::CONTROLLER_PAGE_BUTTONS },
{ "Keypads", ControllerPage::CONTROLLER_PAGE_KEYPADS },
{ "Modifiers", ControllerPage::CONTROLLER_PAGE_MODIFIERS },
{ "Analogs", ControllerPage::CONTROLLER_PAGE_ANALOGS },
{ "Overlay", ControllerPage::CONTROLLER_PAGE_OVERLAY },
{ "Lights", ControllerPage::CONTROLLER_PAGE_LIGHTS },
@@ -616,6 +617,21 @@ namespace overlay::windows {
}
break;
}
case ControllerPage::CONTROLLER_PAGE_MODIFIERS: {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Button Modifiers");
ImGui::Spacing();
ImGui::TextWrapped(
"Bind modifier buttons below, then open Edit properties on any button binding "
"(except MIDI) to select the required modifiers. This allows you to configure "
"button combinations.\n\n"
"For example, if you bind the Start button as Modifier 1, and then bind "
"the A button for Service Coin and set it to require Modifier 1, you press "
"Start + A to trigger Service Coin.");
ImGui::TextUnformatted("");
this->build_buttons(
"Modifier", games::get_buttons_modifiers(this->games_selected_name));
break;
}
case ControllerPage::CONTROLLER_PAGE_ANALOGS: {
// help text for binding analog, if the game has one
@@ -1256,7 +1272,8 @@ namespace overlay::windows {
if (open_edit) {
ImGui::OpenPopup(edit_name.c_str());
}
edit_button_popup(edit_name, button_display, button, button_velocity, alt_index);
edit_button_popup(
edit_name, button_display, button, button_velocity, alt_index, name != "Modifier");
// clean up
ImGui::PopID();
@@ -1275,6 +1292,7 @@ namespace overlay::windows {
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setInvert(false);
button->setModifierMask(0);
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
button->setLastVelocity(0);
button->setTemporary(false);
@@ -1370,6 +1388,7 @@ namespace overlay::windows {
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(BAT_NONE);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1);
ImGui::CloseCurrentPopup();
@@ -1402,6 +1421,7 @@ namespace overlay::windows {
button->setDeviceIdentifier(device->name);
button->setVKey(vkey);
button->setAnalogType(BAT_NONE);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1);
ImGui::CloseCurrentPopup();
@@ -1537,6 +1557,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1582,6 +1603,7 @@ namespace overlay::windows {
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setModifierMask(0);
// same idea as setMidiVKey - keep velocity threshold consistent
button->setVelocityThreshold(
device->midiInfo->v2_velocity_threshold[button->getVKey()]);
@@ -1609,6 +1631,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1636,6 +1659,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1663,6 +1687,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1689,6 +1714,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1710,6 +1736,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1740,6 +1767,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1816,6 +1844,7 @@ namespace overlay::windows {
if (RI_MGR->XINPUT_MGR->get_any_button_pressed(xinput)) {
button->setDeviceIdentifier(xinput::get_device_desc(xinput.player));
button->setVKey(static_cast<uint16_t>(xinput.button));
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1);
inc_buttons_many_index(button_it_max);
@@ -1872,6 +1901,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1890,37 +1920,83 @@ namespace overlay::windows {
}
}
bool Config::build_modifier_picker(Button &button) {
bool changed = false;
auto modifier_mask = button.getModifierMask();
// collect live states and build the closed dropdown summary
uint8_t pressed_modifier_mask = 0;
std::string modifier_preview;
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
const uint8_t modifier_bit = UINT8_C(1) << index;
if (modifier_buttons && index < modifier_buttons->size() &&
GameAPI::Buttons::getState(RI_MGR, modifier_buttons->at(index))) {
pressed_modifier_mask |= modifier_bit;
}
if ((modifier_mask & modifier_bit) != 0) {
modifier_preview += modifier_preview.empty() ? "Mod " : "+";
modifier_preview += std::to_string(index + 1);
}
}
if (modifier_preview.empty()) {
modifier_preview = "None";
}
// color the combo when all requirements are held, but keep its label unchanged
const bool all_modifiers_pressed = modifier_mask != 0 &&
(pressed_modifier_mask & modifier_mask) == modifier_mask;
if (all_modifiers_pressed) {
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
}
const bool combo_open = ImGui::BeginCombo("##Modifiers", modifier_preview.c_str());
if (all_modifiers_pressed) {
ImGui::PopStyleColor();
}
// update requirements while highlighting modifier sources that are currently pressed
if (combo_open) {
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
const uint8_t modifier_bit = UINT8_C(1) << index;
bool required = (modifier_mask & modifier_bit) != 0;
const auto label = fmt::format("Modifier {}", index + 1);
const bool modifier_pressed = (pressed_modifier_mask & modifier_bit) != 0;
if (modifier_pressed) {
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
}
const bool requirement_changed = ImGui::Checkbox(label.c_str(), &required);
if (modifier_pressed) {
ImGui::PopStyleColor();
}
if (requirement_changed) {
modifier_mask ^= modifier_bit;
button.setModifierMask(modifier_mask);
changed = true;
}
}
ImGui::EndCombo();
}
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::TextUnformatted("Modifiers");
return changed;
}
void Config::edit_button_popup(
const std::string &edit_name,
const std::string &button_display,
Button *button,
const float button_velocity,
const int alt_index) {
const int alt_index,
const bool allow_modifiers) {
const auto button_state = GameAPI::Buttons::getState(RI_MGR, *button, false);
if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
bool dirty = false;
auto device = RI_MGR->devices_get(button->getDeviceIdentifier());
// binding
ImGui::Text("Binding");
// combo for devices
std::string device_desc = (device != nullptr) ? device->desc : "Empty (Naive)";
if (ImGui::BeginCombo("Device Identifier", device_desc.c_str())) {
if (ImGui::Selectable("Empty (Naive)", button->isNaive())) {
button->setDeviceIdentifier("");
dirty = true;
}
if (button->isNaive()) {
ImGui::SetItemDefaultFocus();
}
for (auto &device : RI_MGR->devices_get()) {
bool selected = button->getDeviceIdentifier() == device.name.c_str();
const auto device_desc = fmt::format("{}##{}", device.desc, device.name);
if (ImGui::Selectable(device_desc.c_str(), selected)) {
button->setDeviceIdentifier(device.name);
// reset controls when switching devices
auto switch_device = [&](const std::string &device_identifier) {
button->setDeviceIdentifier(device_identifier);
button->setAnalogType(ButtonAnalogType::BAT_NONE);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
@@ -1928,7 +2004,28 @@ namespace overlay::windows {
button->setVelocityThreshold(0);
button->setVKey(0);
button->setInvert(false);
button->setModifierMask(0);
dirty = true;
};
// binding
ImGui::Text("Binding");
ImGui::Separator();
// combo for devices
std::string device_desc = (device != nullptr) ? device->desc : "Empty (Naive)";
if (ImGui::BeginCombo("Device Identifier", device_desc.c_str())) {
if (ImGui::Selectable("Empty (Naive)", button->isNaive()) && !button->isNaive()) {
switch_device("");
}
if (button->isNaive()) {
ImGui::SetItemDefaultFocus();
}
for (auto &device : RI_MGR->devices_get()) {
bool selected = button->getDeviceIdentifier() == device.name.c_str();
const auto device_desc = fmt::format("{}##{}", device.desc, device.name);
if (ImGui::Selectable(device_desc.c_str(), selected) && !selected) {
switch_device(device.name);
}
if (selected) {
ImGui::SetItemDefaultFocus();
@@ -1936,6 +2033,7 @@ namespace overlay::windows {
}
ImGui::EndCombo();
}
device = RI_MGR->devices_get(button->getDeviceIdentifier());
// analog type (only for HID)
const auto bat = button->getAnalogType();
@@ -2227,21 +2325,22 @@ namespace overlay::windows {
}
}
// modifiers
{
const bool is_midi = device != nullptr && device->type == rawinput::MIDI;
if (allow_modifiers && button->isValid() && !is_midi) {
dirty |= build_modifier_picker(*button);
}
}
// preview
if (!button_display.empty()) {
ImGui::TextUnformatted("\nPreview");
ImGui::Separator();
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f));
}
// this doesn't account for utf-8 but whatever
if (button_display.size() >= 40) {
ImGui::Text("%.37s...", button_display.c_str());
ImGui::SameLine();
ImGui::HelpMarker(button_display.c_str());
} else {
ImGui::TextUnformatted(button_display.c_str());
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
}
ImGui::TextTruncated(button_display, ImGui::GetContentRegionAvail().x);
ImGui::TextUnformatted("\n");
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
ImGui::PopStyleColor();
@@ -2250,8 +2349,10 @@ namespace overlay::windows {
ImGui::TextUnformatted("");
}
// options
ImGui::Text("Options");
ImGui::TextUnformatted("Options");
ImGui::Separator();
// check for debounce
if (button->getDebounceUp() || button->getDebounceDown()
@@ -2370,6 +2471,7 @@ namespace overlay::windows {
ImGui::ProgressBar(button_velocity);
} else {
ImGui::Text("State");
ImGui::Separator();
ImGui::ProgressBar(button_velocity);
}
@@ -5490,6 +5592,12 @@ namespace overlay::windows {
tmpl.buttons.emplace_back(btn);
}
}
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
if (modifier_buttons) {
for (auto &modifier : *modifier_buttons) {
tmpl.modifier_buttons.emplace_back(modifier);
}
}
// capture keypad buttons
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
@@ -5545,6 +5653,17 @@ namespace overlay::windows {
}
}
// clear all modifier buttons
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
if (modifier_buttons) {
for (auto &modifier : *modifier_buttons) {
for (int ai = (int)modifier.getAlternatives().size() - 1; ai >= 0; ai--) {
clear_button(&modifier.getAlternatives()[ai], ai + 1);
}
clear_button(&modifier, -1);
}
}
// clear all keypad buttons
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
if (keypad_buttons) {
@@ -5638,6 +5757,7 @@ namespace overlay::windows {
btn.setDebounceDown(entry->debounce_down);
btn.setBatThreshold(entry->bat_threshold);
btn.setVelocityThreshold(entry->velocity_threshold);
btn.setModifierMask(entry->modifier_mask);
::Config::getInstance().updateBinding(game, btn, -1);
} else {
Button alt_btn(btn.getName());
@@ -5649,6 +5769,7 @@ namespace overlay::windows {
alt_btn.setDebounceDown(entry->debounce_down);
alt_btn.setBatThreshold(entry->bat_threshold);
alt_btn.setVelocityThreshold(entry->velocity_threshold);
alt_btn.setModifierMask(entry->modifier_mask);
alt_btn.setTemporary(true);
btn.getAlternatives().push_back(alt_btn);
::Config::getInstance().updateBinding(
@@ -5667,6 +5788,11 @@ namespace overlay::windows {
if (this->apply_buttons) {
apply_buttons(tmpl.buttons, games::get_buttons(this->games_selected_name));
}
if (this->apply_modifiers) {
apply_buttons(
tmpl.modifier_buttons,
games::get_buttons_modifiers(this->games_selected_name));
}
if (this->apply_keypads) {
apply_buttons(tmpl.keypad_buttons, games::get_buttons_keypads(this->games_selected_name));
}
@@ -5908,7 +6034,7 @@ namespace overlay::windows {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::HelpMarker(
"Clears all game button, analog, and light bindings.");
"Clears all game button, modifier, keypad, analog, and light bindings.");
if (this->all_cleared) {
ImGui::SameLine();
ImGui::TextUnformatted("Done.");
@@ -5923,6 +6049,8 @@ namespace overlay::windows {
bool selection_changed = false;
selection_changed |= ImGui::Checkbox("Buttons", &this->apply_buttons);
ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Modifiers", &this->apply_modifiers);
ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Keypads", &this->apply_keypads);
ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Analogs", &this->apply_analogs);
@@ -5932,7 +6060,7 @@ namespace overlay::windows {
std::fill(this->template_is_applied.begin(), this->template_is_applied.end(), false);
}
if (!this->apply_buttons && !this->apply_keypads &&
if (!this->apply_buttons && !this->apply_modifiers && !this->apply_keypads &&
!this->apply_analogs && !this->apply_lights) {
ImGui::TextUnformatted("\nYou must select at least one group to apply.\n\n");
}
@@ -5972,7 +6100,8 @@ namespace overlay::windows {
}
}
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) {
if (!apply_buttons && !apply_modifiers && !apply_keypads &&
!apply_analogs && !apply_lights) {
ImGui::BeginDisabled();
}
@@ -6141,7 +6270,8 @@ namespace overlay::windows {
ImGui::EndTable();
}
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) {
if (!apply_buttons && !apply_modifiers && !apply_keypads &&
!apply_analogs && !apply_lights) {
ImGui::EndDisabled();
}
}
@@ -6220,12 +6350,14 @@ namespace overlay::windows {
ImGui::TextUnformatted("Pick which groups to save:");
ImGui::Checkbox("Buttons", &this->save_buttons);
ImGui::SameLine();
ImGui::Checkbox("Modifiers", &this->save_modifiers);
ImGui::SameLine();
ImGui::Checkbox("Keypads", &this->save_keypads);
ImGui::SameLine();
ImGui::Checkbox("Analogs", &this->save_analogs);
ImGui::SameLine();
ImGui::Checkbox("Lights", &this->save_lights);
if (!this->save_analogs && !this->save_keypads &&
if (!this->save_buttons && !this->save_modifiers && !this->save_keypads &&
!this->save_analogs && !this->save_lights) {
ImGui::TextUnformatted("\nYou must select at least one group to save.\n\n");
}
@@ -6277,7 +6409,8 @@ namespace overlay::windows {
ImGui::BeginDisabled(
!all_labels_set ||
(!this->save_buttons && !this->save_keypads && !this->save_analogs && !this->save_lights));
(!this->save_buttons && !this->save_modifiers && !this->save_keypads &&
!this->save_analogs && !this->save_lights));
if (ImGui::Button("Save")) {
// replace device IDs with labels in the template
@@ -6286,7 +6419,8 @@ namespace overlay::windows {
template_save_sources[si], template_save_labels[si]);
}
if (save_user_template(template_pending_save,
this->save_buttons, this->save_keypads, this->save_analogs, this->save_lights)) {
this->save_buttons, this->save_modifiers, this->save_keypads,
this->save_analogs, this->save_lights)) {
template_save_name[0] = '\0';
templates_cache_dirty = true;
}
@@ -6368,7 +6502,7 @@ namespace overlay::windows {
t.rename_source(template_save_sources[si], template_save_labels[si]);
}
}
save_user_template(t, true, true, true, true);
save_user_template(t, true, true, true, true, true);
templates_cache_dirty = true;
template_save_sources.clear();
template_save_labels.clear();
+6 -1
View File
@@ -26,6 +26,7 @@ namespace overlay::windows {
CONTROLLER_PAGE_INVALID,
CONTROLLER_PAGE_BUTTONS,
CONTROLLER_PAGE_KEYPADS,
CONTROLLER_PAGE_MODIFIERS,
CONTROLLER_PAGE_ANALOGS,
CONTROLLER_PAGE_OVERLAY,
CONTROLLER_PAGE_LIGHTS,
@@ -119,10 +120,12 @@ namespace overlay::windows {
std::vector<std::string> template_save_sources;
std::vector<std::string> template_save_labels;
bool apply_buttons = true;
bool apply_modifiers = true;
bool apply_keypads = true;
bool apply_analogs = true;
bool apply_lights = true;
bool save_buttons = true;
bool save_modifiers = true;
bool save_keypads = true;
bool save_analogs = true;
bool save_lights = true;
@@ -168,12 +171,14 @@ namespace overlay::windows {
void bind_button_popup(const std::string &bind_name, Button *button, const int button_it_max, const int alt_index);
void naive_button_popup(const std::string &naive_string, Button *button, const int button_it_max, const int alt_index);
bool build_modifier_picker(Button &button);
void edit_button_popup(
const std::string &edit_name,
const std::string &button_display,
Button *button,
const float button_velocity,
const int alt_index);
const int alt_index,
const bool allow_modifiers);
void clear_button(Button *button, const int alt_index, std::optional<unsigned short> vKey_default = std::nullopt);
void reset_button_to_default(Button *button, unsigned short vKey_default);
unsigned int get_keypad_top_row(const Button &button);
@@ -6,7 +6,7 @@
namespace overlay::windows {
// helpers - iterate both buttons and keypad_buttons
// helpers - iterate all button groups
static void count_buttons_in(const std::vector<TemplateButtonBinding> &btns, int &naive, int &device) {
for (auto &btn : btns) {
if (btn.primary.is_naive()) naive++;
@@ -21,6 +21,7 @@ namespace overlay::windows {
int ControllerTemplate::count_naive_buttons() const {
int naive = 0, device = 0;
count_buttons_in(buttons, naive, device);
count_buttons_in(modifier_buttons, naive, device);
count_buttons_in(keypad_buttons, naive, device);
return naive;
}
@@ -28,6 +29,7 @@ namespace overlay::windows {
int ControllerTemplate::count_device_buttons() const {
int naive = 0, device = 0;
count_buttons_in(buttons, naive, device);
count_buttons_in(modifier_buttons, naive, device);
count_buttons_in(keypad_buttons, naive, device);
return device;
}
@@ -64,6 +66,7 @@ namespace overlay::windows {
std::set<std::string> ControllerTemplate::get_used_devices() const {
std::set<std::string> devices;
collect_devices_from(buttons, devices);
collect_devices_from(modifier_buttons, devices);
collect_devices_from(keypad_buttons, devices);
for (auto &a : analogs) {
if (a.is_device()) devices.insert(a.device_identifier);
@@ -94,6 +97,7 @@ namespace overlay::windows {
bool has_naive = false;
collect_sources_from(buttons, sources_set, has_naive);
collect_sources_from(modifier_buttons, sources_set, has_naive);
collect_sources_from(keypad_buttons, sources_set, has_naive);
for (auto &a : analogs) {
if (a.is_device()) {
@@ -131,6 +135,7 @@ namespace overlay::windows {
}
};
rename_in_buttons(buttons);
rename_in_buttons(modifier_buttons);
rename_in_buttons(keypad_buttons);
for (auto &a : analogs) {
if (a.device_identifier == old_id) a.device_identifier = new_id;
@@ -181,6 +186,16 @@ namespace overlay::windows {
}
}
// modifier buttons
std::vector<std::string> modifier_lines;
collect_btn_lines(modifier_buttons, modifier_lines);
if (!modifier_lines.empty()) {
result += "Modifiers:\n";
for (auto &line : modifier_lines) {
result += " " + line + "\n";
}
}
// keypad buttons
std::vector<std::string> kp_lines;
collect_btn_lines(keypad_buttons, kp_lines);
@@ -19,6 +19,7 @@ namespace overlay::windows {
double debounce_down = 0.0;
int bat_threshold = 0;
unsigned short velocity_threshold = 0;
uint8_t modifier_mask = 0;
bool is_naive() const { return device_identifier.empty() && vKey != INVALID_VKEY; }
bool is_device() const { return !device_identifier.empty(); }
@@ -34,6 +35,7 @@ namespace overlay::windows {
e.debounce_down = btn.getDebounceDown();
e.bat_threshold = btn.getBatThreshold();
e.velocity_threshold = btn.getVelocityThreshold();
e.modifier_mask = btn.getModifierMask();
return e;
}
};
@@ -123,6 +125,7 @@ namespace overlay::windows {
std::string game_name;
bool is_builtin = false;
std::vector<TemplateButtonBinding> buttons;
std::vector<TemplateButtonBinding> modifier_buttons;
std::vector<TemplateButtonBinding> keypad_buttons;
std::vector<TemplateAnalogBinding> analogs;
std::vector<TemplateLightBinding> lights;
@@ -150,6 +153,7 @@ namespace overlay::windows {
bool save_user_template(
const ControllerTemplate &tmpl,
const bool save_buttons,
const bool save_modifiers,
const bool save_keypads,
const bool save_analogs,
const bool save_lights);