diff --git a/src/common/heterogeneous_containers.h b/src/common/heterogeneous_containers.h index 8f7a3a9d7..ad769f49c 100644 --- a/src/common/heterogeneous_containers.h +++ b/src/common/heterogeneous_containers.h @@ -72,6 +72,32 @@ UnorderedStringMapFind(UnorderedStringMap<ValueType>& map, const KeyType& key) { return map.find(key); } +template<typename KeyType, typename ValueType> +ALWAYS_INLINE typename UnorderedStringMultimap<ValueType>::const_iterator +UnorderedStringMultiMapFind(const UnorderedStringMultimap<ValueType>& map, const KeyType& key) +{ + return map.find(key); +} +template<typename KeyType, typename ValueType> +ALWAYS_INLINE std::pair<typename UnorderedStringMultimap<ValueType>::const_iterator, + typename UnorderedStringMultimap<ValueType>::const_iterator> +UnorderedStringMultiMapEqualRange(const UnorderedStringMultimap<ValueType>& map, const KeyType& key) +{ + return map.equal_range(key); +} +template<typename KeyType, typename ValueType> +ALWAYS_INLINE typename UnorderedStringMultimap<ValueType>::iterator +UnorderedStringMultiMapFind(UnorderedStringMultimap<ValueType>& map, const KeyType& key) +{ + return map.find(key); +} +template<typename KeyType, typename ValueType> +ALWAYS_INLINE std::pair<typename UnorderedStringMultimap<ValueType>::iterator, + typename UnorderedStringMultimap<ValueType>::iterator> +UnorderedStringMultiMapEqualRange(UnorderedStringMultimap<ValueType>& map, const KeyType& key) +{ + return map.equal_range(key); +} #else template<typename ValueType> using UnorderedStringMap = std::unordered_map<std::string, ValueType>; @@ -81,15 +107,43 @@ using UnorderedStringSet = std::unordered_set<std::string>; using UnorderedStringMultiSet = std::unordered_multiset<std::string>; template<typename KeyType, typename ValueType> -ALWAYS_INLINE typename UnorderedStringMap<ValueType>::const_iterator UnorderedStringMapFind(const UnorderedStringMap<ValueType>& map, const KeyType& key) +ALWAYS_INLINE typename UnorderedStringMap<ValueType>::const_iterator +UnorderedStringMapFind(const UnorderedStringMap<ValueType>& map, const KeyType& key) { return map.find(std::string(key)); } template<typename KeyType, typename ValueType> -ALWAYS_INLINE typename UnorderedStringMap<ValueType>::iterator UnorderedStringMapFind(UnorderedStringMap<ValueType>& map, const KeyType& key) +ALWAYS_INLINE typename UnorderedStringMap<ValueType>::iterator +UnorderedStringMapFind(UnorderedStringMap<ValueType>& map, const KeyType& key) { return map.find(std::string(key)); } +template<typename KeyType, typename ValueType> +ALWAYS_INLINE typename UnorderedStringMultimap<ValueType>::const_iterator +UnorderedStringMultiMapFind(const UnorderedStringMultimap<ValueType>& map, const KeyType& key) +{ + return map.find(std::string(key)); +} +template<typename KeyType, typename ValueType> +ALWAYS_INLINE std::pair<typename UnorderedStringMultimap<ValueType>::const_iterator, + typename UnorderedStringMultimap<ValueType>::const_iterator> +UnorderedStringMultiMapEqualRange(const UnorderedStringMultimap<ValueType>& map, const KeyType& key) +{ + return map.equal_range(std::string(key)); +} +template<typename KeyType, typename ValueType> +ALWAYS_INLINE typename UnorderedStringMultimap<ValueType>::iterator +UnorderedStringMultiMapFind(UnorderedStringMultimap<ValueType>& map, const KeyType& key) +{ + return map.find(std::string(key)); +} +template<typename KeyType, typename ValueType> +ALWAYS_INLINE std::pair<typename UnorderedStringMultimap<ValueType>::iterator, + typename UnorderedStringMultimap<ValueType>::iterator> +UnorderedStringMultiMapEqualRange(UnorderedStringMultimap<ValueType>& map, const KeyType& key) +{ + return map.equal_range(std::string(key)); +} #endif template<typename ValueType> diff --git a/src/common/layered_settings_interface.cpp b/src/common/layered_settings_interface.cpp index 6ce655d28..d56994985 100644 --- a/src/common/layered_settings_interface.cpp +++ b/src/common/layered_settings_interface.cpp @@ -3,6 +3,7 @@ #include "layered_settings_interface.h" #include "common/assert.h" +#include <unordered_set> LayeredSettingsInterface::LayeredSettingsInterface() = default; @@ -190,3 +191,35 @@ bool LayeredSettingsInterface::AddToStringList(const char* section, const char* Panic("Attempt to call AddToStringList() on layered settings interface"); return true; } + +std::vector<std::pair<std::string, std::string>> LayeredSettingsInterface::GetKeyValueList(const char* section) const +{ + std::unordered_set<std::string_view> seen; + std::vector<std::pair<std::string, std::string>> ret; + for (u32 layer = FIRST_LAYER; layer <= LAST_LAYER; layer++) + { + if (SettingsInterface* sif = m_layers[layer]) + { + const size_t newly_added_begin = ret.size(); + std::vector<std::pair<std::string, std::string>> entries = sif->GetKeyValueList(section); + for (std::pair<std::string, std::string>& entry : entries) + { + if (seen.find(entry.first) != seen.end()) + continue; + ret.push_back(std::move(entry)); + } + + // Mark keys as seen after processing all entries in case the layer has multiple entries for a specific key + for (auto cur = ret.begin() + newly_added_begin, end = ret.end(); cur < end; cur++) + seen.insert(cur->first); + } + } + + return ret; +} + +void LayeredSettingsInterface::SetKeyValueList(const char* section, + const std::vector<std::pair<std::string, std::string>>& items) +{ + Panic("Attempt to call SetKeyValueList() on layered settings interface"); +} diff --git a/src/common/layered_settings_interface.h b/src/common/layered_settings_interface.h index b94d15fe6..ec46ca56a 100644 --- a/src/common/layered_settings_interface.h +++ b/src/common/layered_settings_interface.h @@ -49,6 +49,9 @@ public: bool RemoveFromStringList(const char* section, const char* key, const char* item) override; bool AddToStringList(const char* section, const char* key, const char* item) override; + std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const override; + void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) override; + // default parameter overloads using SettingsInterface::GetBoolValue; using SettingsInterface::GetDoubleValue; @@ -62,4 +65,4 @@ private: static constexpr Layer LAST_LAYER = LAYER_BASE; std::array<SettingsInterface*, NUM_LAYERS> m_layers{}; -}; +}; \ No newline at end of file diff --git a/src/common/memory_settings_interface.cpp b/src/common/memory_settings_interface.cpp index 05a19c98a..e580599a1 100644 --- a/src/common/memory_settings_interface.cpp +++ b/src/common/memory_settings_interface.cpp @@ -21,7 +21,7 @@ void MemorySettingsInterface::Clear() bool MemorySettingsInterface::GetIntValue(const char* section, const char* key, s32* value) const { - const auto sit = m_sections.find(section); + const auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return false; @@ -39,11 +39,11 @@ bool MemorySettingsInterface::GetIntValue(const char* section, const char* key, bool MemorySettingsInterface::GetUIntValue(const char* section, const char* key, u32* value) const { - const auto sit = m_sections.find(section); + const auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return false; - const auto iter = sit->second.find(key); + const auto iter = UnorderedStringMultiMapFind(sit->second, key); if (iter == sit->second.end()) return false; @@ -57,11 +57,11 @@ bool MemorySettingsInterface::GetUIntValue(const char* section, const char* key, bool MemorySettingsInterface::GetFloatValue(const char* section, const char* key, float* value) const { - const auto sit = m_sections.find(section); + const auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return false; - const auto iter = sit->second.find(key); + const auto iter = UnorderedStringMultiMapFind(sit->second, key); if (iter == sit->second.end()) return false; @@ -75,11 +75,11 @@ bool MemorySettingsInterface::GetFloatValue(const char* section, const char* key bool MemorySettingsInterface::GetDoubleValue(const char* section, const char* key, double* value) const { - const auto sit = m_sections.find(section); + const auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return false; - const auto iter = sit->second.find(key); + const auto iter = UnorderedStringMultiMapFind(sit->second, key); if (iter == sit->second.end()) return false; @@ -93,11 +93,11 @@ bool MemorySettingsInterface::GetDoubleValue(const char* section, const char* ke bool MemorySettingsInterface::GetBoolValue(const char* section, const char* key, bool* value) const { - const auto sit = m_sections.find(section); + const auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return false; - const auto iter = sit->second.find(key); + const auto iter = UnorderedStringMultiMapFind(sit->second, key); if (iter == sit->second.end()) return false; @@ -111,11 +111,11 @@ bool MemorySettingsInterface::GetBoolValue(const char* section, const char* key, bool MemorySettingsInterface::GetStringValue(const char* section, const char* key, std::string* value) const { - const auto sit = m_sections.find(section); + const auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return false; - const auto iter = sit->second.find(key); + const auto iter = UnorderedStringMultiMapFind(sit->second, key); if (iter == sit->second.end()) return false; @@ -153,13 +153,34 @@ void MemorySettingsInterface::SetStringValue(const char* section, const char* ke SetValue(section, key, value); } +std::vector<std::pair<std::string, std::string>> MemorySettingsInterface::GetKeyValueList(const char* section) const +{ + std::vector<std::pair<std::string, std::string>> output; + auto sit = UnorderedStringMapFind(m_sections, section); + if (sit != m_sections.end()) + { + for (const auto& it : sit->second) + output.emplace_back(it.first, it.second); + } + return output; +} + +void MemorySettingsInterface::SetKeyValueList(const char* section, + const std::vector<std::pair<std::string, std::string>>& items) +{ + auto sit = UnorderedStringMapFind(m_sections, section); + sit->second.clear(); + for (const auto& [key, value] : items) + sit->second.emplace(key, value); +} + void MemorySettingsInterface::SetValue(const char* section, const char* key, std::string value) { - auto sit = m_sections.find(section); + auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first; - const auto range = sit->second.equal_range(key); + const auto range = UnorderedStringMultiMapEqualRange(sit->second, key); if (range.first == sit->second.end()) { sit->second.emplace(std::string(key), std::move(value)); @@ -182,10 +203,10 @@ std::vector<std::string> MemorySettingsInterface::GetStringList(const char* sect { std::vector<std::string> ret; - const auto sit = m_sections.find(section); + const auto sit = UnorderedStringMapFind(m_sections, section); if (sit != m_sections.end()) { - const auto range = sit->second.equal_range(key); + const auto range = UnorderedStringMultiMapEqualRange(sit->second, key); for (auto iter = range.first; iter != range.second; ++iter) ret.emplace_back(iter->second); } @@ -195,11 +216,11 @@ std::vector<std::string> MemorySettingsInterface::GetStringList(const char* sect void MemorySettingsInterface::SetStringList(const char* section, const char* key, const std::vector<std::string>& items) { - auto sit = m_sections.find(section); + auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first; - const auto range = sit->second.equal_range(key); + const auto range = UnorderedStringMultiMapEqualRange(sit->second, key); for (auto iter = range.first; iter != range.second;) sit->second.erase(iter++); @@ -210,11 +231,11 @@ void MemorySettingsInterface::SetStringList(const char* section, const char* key bool MemorySettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item) { - auto sit = m_sections.find(section); + auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first; - const auto range = sit->second.equal_range(key); + const auto range = UnorderedStringMultiMapEqualRange(sit->second, key); bool result = false; for (auto iter = range.first; iter != range.second;) { @@ -234,11 +255,11 @@ bool MemorySettingsInterface::RemoveFromStringList(const char* section, const ch bool MemorySettingsInterface::AddToStringList(const char* section, const char* key, const char* item) { - auto sit = m_sections.find(section); + auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first; - const auto range = sit->second.equal_range(key); + const auto range = UnorderedStringMultiMapEqualRange(sit->second, key); for (auto iter = range.first; iter != range.second; ++iter) { if (iter->second == item) @@ -251,7 +272,7 @@ bool MemorySettingsInterface::AddToStringList(const char* section, const char* k bool MemorySettingsInterface::ContainsValue(const char* section, const char* key) const { - const auto sit = m_sections.find(section); + const auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return false; @@ -260,18 +281,18 @@ bool MemorySettingsInterface::ContainsValue(const char* section, const char* key void MemorySettingsInterface::DeleteValue(const char* section, const char* key) { - auto sit = m_sections.find(section); + auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return; - const auto range = sit->second.equal_range(key); + const auto range = UnorderedStringMultiMapEqualRange(sit->second, key); for (auto iter = range.first; iter != range.second;) sit->second.erase(iter++); } void MemorySettingsInterface::ClearSection(const char* section) { - auto sit = m_sections.find(section); + auto sit = UnorderedStringMapFind(m_sections, section); if (sit == m_sections.end()) return; diff --git a/src/common/memory_settings_interface.h b/src/common/memory_settings_interface.h index 49fe3e117..c0a3a85f3 100644 --- a/src/common/memory_settings_interface.h +++ b/src/common/memory_settings_interface.h @@ -29,6 +29,10 @@ public: void SetDoubleValue(const char* section, const char* key, double value) override; void SetBoolValue(const char* section, const char* key, bool value) override; void SetStringValue(const char* section, const char* key, const char* value) override; + + std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const override; + void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) override; + bool ContainsValue(const char* section, const char* key) const override; void DeleteValue(const char* section, const char* key) override; void ClearSection(const char* section) override; diff --git a/src/common/settings_interface.h b/src/common/settings_interface.h index 591979a91..9ba4c96b7 100644 --- a/src/common/settings_interface.h +++ b/src/common/settings_interface.h @@ -35,6 +35,9 @@ public: virtual bool RemoveFromStringList(const char* section, const char* key, const char* item) = 0; virtual bool AddToStringList(const char* section, const char* key, const char* item) = 0; + virtual std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const = 0; + virtual void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) = 0; + virtual bool ContainsValue(const char* section, const char* key) const = 0; virtual void DeleteValue(const char* section, const char* key) = 0; virtual void ClearSection(const char* section) = 0; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 29c4b22d6..31f4d449f 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -59,6 +59,7 @@ add_library(core host_interface_progress_callback.cpp host_interface_progress_callback.h host_settings.h + input_types.h interrupt_controller.cpp interrupt_controller.h libcrypt_serials.cpp diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp index 044b54abd..1d0328570 100644 --- a/src/core/analog_controller.cpp +++ b/src/core/analog_controller.cpp @@ -789,12 +789,12 @@ std::unique_ptr<AnalogController> AnalogController::Create(u32 index) static const Controller::ControllerBindingInfo s_binding_info[] = { #define BUTTON(name, display_name, button, genb) \ { \ - name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb \ + name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb \ } #define AXIS(name, display_name, halfaxis, genb) \ { \ name, display_name, static_cast<u32>(AnalogController::Button::Count) + static_cast<u32>(halfaxis), \ - Controller::ControllerBindingType::HalfAxis, genb \ + InputBindingInfo::Type::HalfAxis, genb \ } BUTTON("Up", "D-Pad Up", AnalogController::Button::Up, GenericInputBinding::DPadUp), @@ -862,11 +862,11 @@ static const SettingInfo s_settings[] = { "functioning, try increasing this value."), "8", "0", "255", "1", "%d", nullptr, 1.0f}, {SettingInfo::Type::IntegerList, "InvertLeftStick", TRANSLATABLE("AnalogController", "Invert Left Stick"), - TRANSLATABLE("AnalogController", "Inverts the direction of the left analog stick."), - "0", "0", "3", nullptr, nullptr, s_invert_settings, 0.0f}, + TRANSLATABLE("AnalogController", "Inverts the direction of the left analog stick."), "0", "0", "3", nullptr, nullptr, + s_invert_settings, 0.0f}, {SettingInfo::Type::IntegerList, "InvertRightStick", TRANSLATABLE("AnalogController", "Invert Right Stick"), - TRANSLATABLE("AnalogController", "Inverts the direction of the right analog stick."), - "0", "0", "3", nullptr, nullptr, s_invert_settings, 0.0f}, + TRANSLATABLE("AnalogController", "Inverts the direction of the right analog stick."), "0", "0", "3", nullptr, + nullptr, s_invert_settings, 0.0f}, }; const Controller::ControllerInfo AnalogController::INFO = {ControllerType::AnalogController, diff --git a/src/core/analog_joystick.cpp b/src/core/analog_joystick.cpp index a07332cbc..82082fe7c 100644 --- a/src/core/analog_joystick.cpp +++ b/src/core/analog_joystick.cpp @@ -335,12 +335,12 @@ std::unique_ptr<AnalogJoystick> AnalogJoystick::Create(u32 index) static const Controller::ControllerBindingInfo s_binding_info[] = { #define BUTTON(name, display_name, button, genb) \ { \ - name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb \ + name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb \ } #define AXIS(name, display_name, halfaxis, genb) \ { \ name, display_name, static_cast<u32>(AnalogJoystick::Button::Count) + static_cast<u32>(halfaxis), \ - Controller::ControllerBindingType::HalfAxis, genb \ + InputBindingInfo::Type::HalfAxis, genb \ } BUTTON("Up", "D-Pad Up", AnalogJoystick::Button::Up, GenericInputBinding::DPadUp), @@ -391,11 +391,11 @@ static const SettingInfo s_settings[] = { "controllers, e.g. DualShock 4, Xbox One Controller."), "1.33f", "0.01f", "2.00f", "0.01f", "%.0f%%", nullptr, 100.0f}, {SettingInfo::Type::IntegerList, "InvertLeftStick", TRANSLATABLE("AnalogJoystick", "Invert Left Stick"), - TRANSLATABLE("AnalogJoystick", "Inverts the direction of the left analog stick."), - "0", "0", "3", nullptr, nullptr, s_invert_settings, 0.0f}, + TRANSLATABLE("AnalogJoystick", "Inverts the direction of the left analog stick."), "0", "0", "3", nullptr, nullptr, + s_invert_settings, 0.0f}, {SettingInfo::Type::IntegerList, "InvertRightStick", TRANSLATABLE("AnalogJoystick", "Invert Right Stick"), - TRANSLATABLE("AnalogJoystick", "Inverts the direction of the right analog stick."), - "0", "0", "3", nullptr, nullptr, s_invert_settings, 0.0f}, + TRANSLATABLE("AnalogJoystick", "Inverts the direction of the right analog stick."), "0", "0", "3", nullptr, nullptr, + s_invert_settings, 0.0f}, }; const Controller::ControllerInfo AnalogJoystick::INFO = {ControllerType::AnalogJoystick, diff --git a/src/core/controller.cpp b/src/core/controller.cpp index cc25cd0cf..757025a23 100644 --- a/src/core/controller.cpp +++ b/src/core/controller.cpp @@ -148,7 +148,7 @@ std::vector<std::string> Controller::GetControllerBinds(const std::string_view& for (u32 i = 0; i < info->num_bindings; i++) { const ControllerBindingInfo& bi = info->bindings[i]; - if (bi.type == ControllerBindingType::Unknown || bi.type == ControllerBindingType::Motor) + if (bi.type == InputBindingInfo::Type::Unknown || bi.type == InputBindingInfo::Type::Motor) continue; ret.emplace_back(info->bindings[i].name); @@ -168,7 +168,7 @@ std::vector<std::string> Controller::GetControllerBinds(ControllerType type) for (u32 i = 0; i < info->num_bindings; i++) { const ControllerBindingInfo& bi = info->bindings[i]; - if (bi.type == ControllerBindingType::Unknown || bi.type == ControllerBindingType::Motor) + if (bi.type == InputBindingInfo::Type::Unknown || bi.type == InputBindingInfo::Type::Motor) continue; ret.emplace_back(info->bindings[i].name); diff --git a/src/core/controller.h b/src/core/controller.h index dbc04441b..282894c59 100644 --- a/src/core/controller.h +++ b/src/core/controller.h @@ -3,6 +3,7 @@ #pragma once #include "common/image.h" +#include "input_types.h" #include "settings.h" #include "types.h" #include <memory> @@ -16,21 +17,9 @@ class SettingsInterface; class StateWrapper; class HostInterface; -enum class GenericInputBinding : u8; - class Controller { public: - enum class ControllerBindingType : u8 - { - Unknown, - Button, - Axis, - HalfAxis, - Motor, - Macro - }; - enum class VibrationCapabilities : u8 { NoVibration, @@ -44,7 +33,7 @@ public: const char* name; const char* display_name; u32 bind_index; - ControllerBindingType type; + InputBindingInfo::Type type; GenericInputBinding generic_mapping; }; diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index af3ec2215..fc2d34c2a 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -128,6 +128,7 @@ <ClInclude Include="host_display.h" /> <ClInclude Include="host_interface_progress_callback.h" /> <ClInclude Include="host_settings.h" /> + <ClInclude Include="input_types.h" /> <ClInclude Include="interrupt_controller.h" /> <ClInclude Include="libcrypt_serials.h" /> <ClInclude Include="mdec.h" /> diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index f809c9330..b0951bd14 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -124,5 +124,6 @@ <ClInclude Include="host_settings.h" /> <ClInclude Include="achievements.h" /> <ClInclude Include="game_database.h" /> + <ClInclude Include="input_types.h" /> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/core/digital_controller.cpp b/src/core/digital_controller.cpp index 888032422..33c4357da 100644 --- a/src/core/digital_controller.cpp +++ b/src/core/digital_controller.cpp @@ -146,7 +146,7 @@ std::unique_ptr<DigitalController> DigitalController::Create(u32 index) static const Controller::ControllerBindingInfo s_binding_info[] = { #define BUTTON(name, display_name, button, genb) \ { \ - name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb \ + name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb \ } BUTTON("Up", "D-Pad Up", DigitalController::Button::Up, GenericInputBinding::DPadUp), diff --git a/src/core/guncon.cpp b/src/core/guncon.cpp index eca985e67..d80efd24a 100644 --- a/src/core/guncon.cpp +++ b/src/core/guncon.cpp @@ -208,7 +208,7 @@ std::unique_ptr<GunCon> GunCon::Create(u32 index) static const Controller::ControllerBindingInfo s_binding_info[] = { #define BUTTON(name, display_name, button, genb) \ { \ - name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb \ + name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb \ } BUTTON("Trigger", "Trigger", GunCon::Button::Trigger, GenericInputBinding::R2), diff --git a/src/core/host.h b/src/core/host.h index dda90ee85..c1eb545a1 100644 --- a/src/core/host.h +++ b/src/core/host.h @@ -23,49 +23,6 @@ class CDImage; /// Marks a core string as being translatable. #define TRANSLATABLE(context, str) str -/// Generic input bindings. These roughly match a DualShock 4 or XBox One controller. -/// They are used for automatic binding to PS2 controller types, and for big picture mode navigation. -enum class GenericInputBinding : u8 -{ - Unknown, - - DPadUp, - DPadRight, - DPadLeft, - DPadDown, - - LeftStickUp, - LeftStickRight, - LeftStickDown, - LeftStickLeft, - L3, - - RightStickUp, - RightStickRight, - RightStickDown, - RightStickLeft, - R3, - - Triangle, // Y on XBox pads. - Circle, // B on XBox pads. - Cross, // A on XBox pads. - Square, // X on XBox pads. - - Select, // Share on DS4, View on XBox pads. - Start, // Options on DS4, Menu on XBox pads. - System, // PS button on DS4, Guide button on XBox pads. - - L1, // LB on Xbox pads. - L2, // Left trigger on XBox pads. - R1, // RB on XBox pads. - R2, // Right trigger on Xbox pads. - - SmallMotor, // High frequency vibration. - LargeMotor, // Low frequency vibration. - - Count, -}; - namespace Host { /// Reads a file from the resources directory of the application. /// This may be outside of the "normal" filesystem on platforms such as Mac. diff --git a/src/core/input_types.h b/src/core/input_types.h new file mode 100644 index 000000000..8e8957454 --- /dev/null +++ b/src/core/input_types.h @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022-2023 Connor McLaughlin <stenzek@gmail.com> +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once +#include "types.h" + +enum class GenericInputBinding : u8; + +struct InputBindingInfo +{ + enum class Type : u8 + { + Unknown, + Button, + Axis, + HalfAxis, + Motor, + Pointer, // Receive relative mouse movement events, bind_index is offset by the axis. + Macro, + }; + + const char* name; + const char* display_name; + Type bind_type; + u16 bind_index; + GenericInputBinding generic_mapping; +}; + +/// Generic input bindings. These roughly match a DualShock 4 or XBox One controller. +/// They are used for automatic binding to PS2 controller types, and for big picture mode navigation. +enum class GenericInputBinding : u8 +{ + Unknown, + + DPadUp, + DPadRight, + DPadLeft, + DPadDown, + + LeftStickUp, + LeftStickRight, + LeftStickDown, + LeftStickLeft, + L3, + + RightStickUp, + RightStickRight, + RightStickDown, + RightStickLeft, + R3, + + Triangle, // Y on XBox pads. + Circle, // B on XBox pads. + Cross, // A on XBox pads. + Square, // X on XBox pads. + + Select, // Share on DS4, View on XBox pads. + Start, // Options on DS4, Menu on XBox pads. + System, // PS button on DS4, Guide button on XBox pads. + + L1, // LB on Xbox pads. + L2, // Left trigger on XBox pads. + R1, // RB on XBox pads. + R2, // Right trigger on Xbox pads. + + SmallMotor, // High frequency vibration. + LargeMotor, // Low frequency vibration. + + Count, +}; + diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp index 765511b29..a1756c84a 100644 --- a/src/core/negcon.cpp +++ b/src/core/negcon.cpp @@ -228,12 +228,12 @@ std::unique_ptr<NeGcon> NeGcon::Create(u32 index) static const Controller::ControllerBindingInfo s_binding_info[] = { #define BUTTON(name, display_name, button, genb) \ { \ - name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb \ + name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb \ } #define AXIS(name, display_name, halfaxis, genb) \ { \ name, display_name, static_cast<u32>(NeGcon::Button::Count) + static_cast<u32>(halfaxis), \ - Controller::ControllerBindingType::HalfAxis, genb \ + InputBindingInfo::Type::HalfAxis, genb \ } BUTTON("Up", "D-Pad Up", NeGcon::Button::Up, GenericInputBinding::DPadUp), diff --git a/src/core/playstation_mouse.cpp b/src/core/playstation_mouse.cpp index c620b3902..ae01ef32e 100644 --- a/src/core/playstation_mouse.cpp +++ b/src/core/playstation_mouse.cpp @@ -179,7 +179,7 @@ std::unique_ptr<PlayStationMouse> PlayStationMouse::Create(u32 index) static const Controller::ControllerBindingInfo s_binding_info[] = { #define BUTTON(name, display_name, button, genb) \ { \ - name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb \ + name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb \ } BUTTON("Left", "Left Button", PlayStationMouse::Button::Left, GenericInputBinding::Cross), diff --git a/src/duckstation-qt/controllerbindingwidgets.cpp b/src/duckstation-qt/controllerbindingwidgets.cpp index 00e576796..8bc208485 100644 --- a/src/duckstation-qt/controllerbindingwidgets.cpp +++ b/src/duckstation-qt/controllerbindingwidgets.cpp @@ -370,7 +370,7 @@ ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* pare for (u32 i = 0; i < cinfo->num_bindings; i++) { const Controller::ControllerBindingInfo& bi = cinfo->bindings[i]; - if (bi.type == Controller::ControllerBindingType::Motor) + if (bi.type == InputBindingInfo::Type::Motor) continue; QListWidgetItem* item = new QListWidgetItem(); @@ -383,7 +383,8 @@ ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* pare m_frequency = dialog->getIntValue(section.c_str(), fmt::format("Macro{}Frequency", index + 1u).c_str(), 0); updateFrequencyText(); - m_ui.trigger->initialize(dialog->getProfileSettingsInterface(), section, fmt::format("Macro{}", index + 1u)); + m_ui.trigger->initialize(dialog->getProfileSettingsInterface(), InputBindingInfo::Type::Macro, section, + fmt::format("Macro{}", index + 1u)); connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); }); connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); }); @@ -453,7 +454,7 @@ void ControllerMacroEditWidget::updateBinds() for (u32 i = 0, bind_index = 0; i < cinfo->num_bindings; i++) { const Controller::ControllerBindingInfo& bi = cinfo->bindings[i]; - if (bi.type == Controller::ControllerBindingType::Motor) + if (bi.type == InputBindingInfo::Type::Motor) continue; const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index)); @@ -740,17 +741,18 @@ void ControllerBindingWidget_Base::initBindingWidgets() for (u32 i = 0; i < cinfo->num_bindings; i++) { const Controller::ControllerBindingInfo& bi = cinfo->bindings[i]; - if (bi.type == Controller::ControllerBindingType::Unknown || bi.type == Controller::ControllerBindingType::Motor) - continue; - - InputBindingWidget* widget = findChild<InputBindingWidget*>(QString::fromUtf8(bi.name)); - if (!widget) + if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis || + bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer) { - Log_ErrorPrintf("No widget found for '%s' (%s)", bi.name, cinfo->name); - continue; - } + InputBindingWidget* widget = findChild<InputBindingWidget*>(QString::fromUtf8(bi.name)); + if (!widget) + { + Log_ErrorPrintf("No widget found for '%s' (%s)", bi.name, cinfo->name); + continue; + } - widget->initialize(sif, config_section, bi.name); + widget->initialize(sif, bi.type, config_section, bi.name); + } } switch (cinfo->vibration_caps) diff --git a/src/duckstation-qt/controllersettingsdialog.cpp b/src/duckstation-qt/controllersettingsdialog.cpp index f7e5c97be..ac07b3e2c 100644 --- a/src/duckstation-qt/controllersettingsdialog.cpp +++ b/src/duckstation-qt/controllersettingsdialog.cpp @@ -237,7 +237,7 @@ void ControllerSettingsDialog::onVibrationMotorsEnumerated(const QList<InputBind for (const InputBindingKey key : motors) { - const std::string key_str(InputManager::ConvertInputBindingKeyToString(key)); + const std::string key_str(InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type::Motor, key)); if (!key_str.empty()) m_vibration_motors.push_back(QString::fromStdString(key_str)); } diff --git a/src/duckstation-qt/hotkeysettingswidget.cpp b/src/duckstation-qt/hotkeysettingswidget.cpp index 50fcec6e8..a7a968a50 100644 --- a/src/duckstation-qt/hotkeysettingswidget.cpp +++ b/src/duckstation-qt/hotkeysettingswidget.cpp @@ -73,8 +73,8 @@ void HotkeySettingsWidget::createButtons() QLabel* label = new QLabel(qApp->translate("Hotkeys", hotkey->display_name), m_container); layout->addWidget(label, target_row, 0); - InputBindingWidget* bind = - new InputBindingWidget(m_container, m_dialog->getProfileSettingsInterface(), "Hotkeys", hotkey->name); + InputBindingWidget* bind = new InputBindingWidget(m_container, m_dialog->getProfileSettingsInterface(), + InputBindingInfo::Type::Button, "Hotkeys", hotkey->name); bind->setMinimumWidth(300); layout->addWidget(bind, target_row, 1); } diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp index de1c7946e..da5bd12a8 100644 --- a/src/duckstation-qt/inputbindingdialog.cpp +++ b/src/duckstation-qt/inputbindingdialog.cpp @@ -11,10 +11,11 @@ #include <QtGui/QMouseEvent> #include <QtGui/QWheelEvent> -InputBindingDialog::InputBindingDialog(SettingsInterface* sif, std::string section_name, std::string key_name, +InputBindingDialog::InputBindingDialog(SettingsInterface* sif, InputBindingInfo::Type bind_type, + std::string section_name, std::string key_name, std::vector<std::string> bindings, QWidget* parent) - : QDialog(parent), m_sif(sif), m_section_name(std::move(section_name)), m_key_name(std::move(key_name)), - m_bindings(std::move(bindings)) + : QDialog(parent), m_sif(sif), m_bind_type(bind_type), m_section_name(std::move(section_name)), + m_key_name(std::move(key_name)), m_bindings(std::move(bindings)) { m_ui.setupUi(this); m_ui.title->setText( @@ -53,8 +54,9 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick) { // double clicks get triggered if we click bind, then click again quickly. - unsigned button_index = CountTrailingZeros(static_cast<u32>(static_cast<const QMouseEvent*>(event)->button())); - m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index)); + unsigned long button_index; + if (_BitScanForward(&button_index, static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()))) + m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index)); return true; } else if (event_type == QEvent::Wheel) @@ -64,7 +66,7 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) if (dx != 0.0f) { InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelX)); - key.negative = (dx < 0.0f); + key.modifier = dx < 0.0f ? InputModifier::Negate : InputModifier::None; m_new_bindings.push_back(key); } @@ -72,7 +74,7 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) if (dy != 0.0f) { InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelY)); - key.negative = (dy < 0.0f); + key.modifier = dy < 0.0f ? InputModifier::Negate : InputModifier::None; m_new_bindings.push_back(key); } @@ -89,20 +91,20 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) // if we've moved more than a decent distance from the center of the widget, bind it. // this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad. static constexpr const s32 THRESHOLD = 50; - const QPointF diff(static_cast<QMouseEvent*>(event)->globalPosition() - m_input_listen_start_position); + const QPoint diff(static_cast<QMouseEvent*>(event)->globalPosition().toPoint() - m_input_listen_start_position); bool has_one = false; if (std::abs(diff.x()) >= THRESHOLD) { InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X)); - key.negative = (diff.x() < 0); + key.modifier = diff.x() < 0 ? InputModifier::Negate : InputModifier::None; m_new_bindings.push_back(key); has_one = true; } if (std::abs(diff.y()) >= THRESHOLD) { InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y)); - key.negative = (diff.y() < 0); + key.modifier = diff.y() < 0 ? InputModifier::Negate : InputModifier::None; m_new_bindings.push_back(key); has_one = true; } @@ -132,6 +134,7 @@ void InputBindingDialog::onInputListenTimerTimeout() void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds) { + m_value_ranges.clear(); m_new_bindings.clear(); m_mouse_mapping_enabled = InputBindingWidget::isMouseMappingEnabled(); m_input_listen_start_position = QCursor::pos(); @@ -179,7 +182,7 @@ void InputBindingDialog::addNewBinding() return; const std::string new_binding( - InputManager::ConvertInputBindingKeysToString(m_new_bindings.data(), m_new_bindings.size())); + InputManager::ConvertInputBindingKeysToString(m_bind_type, m_new_bindings.data(), m_new_bindings.size())); if (!new_binding.empty()) { if (std::find(m_bindings.begin(), m_bindings.end(), new_binding) != m_bindings.end()) @@ -248,14 +251,37 @@ void InputBindingDialog::saveListToSettings() void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float value) { - const float abs_value = std::abs(value); + if (!isListeningForInput()) + return; - for (InputBindingKey other_key : m_new_bindings) + float initial_value = value; + float min_value = value; + auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(), + [key](const auto& it) { return it.first.bits == key.bits; }); + if (it != m_value_ranges.end()) + { + initial_value = it->second.first; + min_value = it->second.second = std::min(it->second.second, value); + } + else + { + m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); + } + + const float abs_value = std::abs(value); + const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f); + + for (InputBindingKey& other_key : m_new_bindings) { if (other_key.MaskDirection() == key.MaskDirection()) { - if (abs_value < 0.5f) + // for pedals, we wait for it to go back to near its starting point to commit the binding + if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f))) { + // did we go the full range? + if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f) + other_key.modifier = InputModifier::FullAxis; + // if this key is in our new binding list, it's a "release", and we're done addNewBinding(); stopListeningForInput(); @@ -268,10 +294,11 @@ void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float val } // new binding, add it to the list, but wait for a decent distance first, and then wait for release - if (abs_value >= 0.5f) + if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f))) { InputBindingKey key_to_add = key; - key_to_add.negative = (value < 0.0f); + key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None; + key_to_add.invert = reverse_threshold; m_new_bindings.push_back(key_to_add); } } diff --git a/src/duckstation-qt/inputbindingdialog.h b/src/duckstation-qt/inputbindingdialog.h index 2ac4c0a51..4747e0328 100644 --- a/src/duckstation-qt/inputbindingdialog.h +++ b/src/duckstation-qt/inputbindingdialog.h @@ -17,8 +17,8 @@ class InputBindingDialog : public QDialog Q_OBJECT public: - InputBindingDialog(SettingsInterface* sif, std::string section_name, std::string key_name, - std::vector<std::string> bindings, QWidget* parent); + InputBindingDialog(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name, + std::string key_name, std::vector<std::string> bindings, QWidget* parent); ~InputBindingDialog(); protected Q_SLOTS: @@ -51,13 +51,15 @@ protected: Ui::InputBindingDialog m_ui; SettingsInterface* m_sif; + InputBindingInfo::Type m_bind_type; std::string m_section_name; std::string m_key_name; std::vector<std::string> m_bindings; std::vector<InputBindingKey> m_new_bindings; + std::vector<std::pair<InputBindingKey, std::pair<float, float>>> m_value_ranges; QTimer* m_input_listen_timer = nullptr; u32 m_input_listen_remaining_seconds = 0; - QPointF m_input_listen_start_position{}; + QPoint m_input_listen_start_position{}; bool m_mouse_mapping_enabled = false; }; diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp index e1a8b0554..a1a9ac749 100644 --- a/src/duckstation-qt/inputbindingwidgets.cpp +++ b/src/duckstation-qt/inputbindingwidgets.cpp @@ -22,8 +22,8 @@ InputBindingWidget::InputBindingWidget(QWidget* parent) : QPushButton(parent) connect(this, &QPushButton::clicked, this, &InputBindingWidget::onClicked); } -InputBindingWidget::InputBindingWidget(QWidget* parent, SettingsInterface* sif, std::string section_name, - std::string key_name) +InputBindingWidget::InputBindingWidget(QWidget* parent, SettingsInterface* sif, InputBindingInfo::Type bind_type, + std::string section_name, std::string key_name) : QPushButton(parent) { setMinimumWidth(225); @@ -31,7 +31,7 @@ InputBindingWidget::InputBindingWidget(QWidget* parent, SettingsInterface* sif, connect(this, &QPushButton::clicked, this, &InputBindingWidget::onClicked); - initialize(sif, std::move(section_name), std::move(key_name)); + initialize(sif, bind_type, std::move(section_name), std::move(key_name)); } InputBindingWidget::~InputBindingWidget() @@ -44,9 +44,11 @@ bool InputBindingWidget::isMouseMappingEnabled() return Host::GetBaseBoolSettingValue("UI", "EnableMouseMapping", false); } -void InputBindingWidget::initialize(SettingsInterface* sif, std::string section_name, std::string key_name) +void InputBindingWidget::initialize(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name, + std::string key_name) { m_sif = sif; + m_bind_type = bind_type; m_section_name = std::move(section_name); m_key_name = std::move(key_name); reloadBinding(); @@ -109,8 +111,9 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event) else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick) { // double clicks get triggered if we click bind, then click again quickly. - const u32 button_index = CountTrailingZeros(static_cast<u32>(static_cast<const QMouseEvent*>(event)->button())); - m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index)); + unsigned long button_index; + if (_BitScanForward(&button_index, static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()))) + m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index)); return true; } else if (event_type == QEvent::Wheel) @@ -120,7 +123,7 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event) if (dx != 0.0f) { InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelX)); - key.negative = (dx < 0.0f); + key.modifier = dx < 0.0f ? InputModifier::Negate : InputModifier::None; m_new_bindings.push_back(key); } @@ -128,7 +131,7 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event) if (dy != 0.0f) { InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelY)); - key.negative = (dy < 0.0f); + key.modifier = dy < 0.0f ? InputModifier::Negate : InputModifier::None; m_new_bindings.push_back(key); } @@ -145,20 +148,20 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event) // if we've moved more than a decent distance from the center of the widget, bind it. // this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad. static constexpr const s32 THRESHOLD = 50; - const QPointF diff(static_cast<QMouseEvent*>(event)->globalPosition() - m_input_listen_start_position); + const QPoint diff(static_cast<QMouseEvent*>(event)->globalPosition().toPoint() - m_input_listen_start_position); bool has_one = false; if (std::abs(diff.x()) >= THRESHOLD) { InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X)); - key.negative = (diff.x() < 0); + key.modifier = diff.x() < 0 ? InputModifier::Negate : InputModifier::None; m_new_bindings.push_back(key); has_one = true; } if (std::abs(diff.y()) >= THRESHOLD) { InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y)); - key.negative = (diff.y() < 0); + key.modifier = diff.y() < 0 ? InputModifier::Negate : InputModifier::None; m_new_bindings.push_back(key); has_one = true; } @@ -205,8 +208,8 @@ void InputBindingWidget::setNewBinding() if (m_new_bindings.empty()) return; - const std::string new_binding( - InputManager::ConvertInputBindingKeysToString(m_new_bindings.data(), m_new_bindings.size())); + std::string new_binding( + InputManager::ConvertInputBindingKeysToString(m_bind_type, m_new_bindings.data(), m_new_bindings.size())); if (!new_binding.empty()) { if (m_sif) @@ -280,6 +283,7 @@ void InputBindingWidget::onInputListenTimerTimeout() void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds) { + m_value_ranges.clear(); m_new_bindings.clear(); m_mouse_mapping_enabled = isMouseMappingEnabled(); m_input_listen_start_position = QCursor::pos(); @@ -315,14 +319,37 @@ void InputBindingWidget::stopListeningForInput() void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float value) { - const float abs_value = std::abs(value); + if (!isListeningForInput()) + return; - for (InputBindingKey other_key : m_new_bindings) + float initial_value = value; + float min_value = value; + auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(), + [key](const auto& it) { return it.first.bits == key.bits; }); + if (it != m_value_ranges.end()) + { + initial_value = it->second.first; + min_value = it->second.second = std::min(it->second.second, value); + } + else + { + m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); + } + + const float abs_value = std::abs(value); + const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f); + + for (InputBindingKey& other_key : m_new_bindings) { if (other_key.MaskDirection() == key.MaskDirection()) { - if (abs_value < 0.5f) + // for pedals, we wait for it to go back to near its starting point to commit the binding + if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f))) { + // did we go the full range? + if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f) + other_key.modifier = InputModifier::FullAxis; + // if this key is in our new binding list, it's a "release", and we're done setNewBinding(); stopListeningForInput(); @@ -335,10 +362,11 @@ void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float val } // new binding, add it to the list, but wait for a decent distance first, and then wait for release - if (abs_value >= 0.5f) + if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f))) { InputBindingKey key_to_add = key; - key_to_add.negative = (value < 0.0f); + key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None; + key_to_add.invert = reverse_threshold; m_new_bindings.push_back(key_to_add); } } @@ -359,7 +387,8 @@ void InputBindingWidget::unhookInputManager() void InputBindingWidget::openDialog() { - InputBindingDialog binding_dialog(m_sif, m_section_name, m_key_name, m_bindings, QtUtils::GetRootWidget(this)); + InputBindingDialog binding_dialog(m_sif, m_bind_type, m_section_name, m_key_name, m_bindings, + QtUtils::GetRootWidget(this)); binding_dialog.exec(); reloadBinding(); } diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h index 8e182263e..9b331480b 100644 --- a/src/duckstation-qt/inputbindingwidgets.h +++ b/src/duckstation-qt/inputbindingwidgets.h @@ -18,12 +18,14 @@ class InputBindingWidget : public QPushButton public: InputBindingWidget(QWidget* parent); - InputBindingWidget(QWidget* parent, SettingsInterface* sif, std::string section_name, std::string key_name); + InputBindingWidget(QWidget* parent, SettingsInterface* sif, InputBindingInfo::Type bind_type, + std::string section_name, std::string key_name); ~InputBindingWidget(); static bool isMouseMappingEnabled(); - void initialize(SettingsInterface* sif, std::string section_name, std::string key_name); + void initialize(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name, + std::string key_name); public Q_SLOTS: void clearBinding(); @@ -57,13 +59,15 @@ protected: void unhookInputManager(); SettingsInterface* m_sif = nullptr; + InputBindingInfo::Type m_bind_type = InputBindingInfo::Type::Unknown; std::string m_section_name; std::string m_key_name; std::vector<std::string> m_bindings; std::vector<InputBindingKey> m_new_bindings; + std::vector<std::pair<InputBindingKey, std::pair<float, float>>> m_value_ranges; QTimer* m_input_listen_timer = nullptr; u32 m_input_listen_remaining_seconds = 0; - QPointF m_input_listen_start_position{}; + QPoint m_input_listen_start_position{}; bool m_mouse_mapping_enabled = false; }; diff --git a/src/frontend-common/dinput_source.cpp b/src/frontend-common/dinput_source.cpp index 03b091489..44ddd268b 100644 --- a/src/frontend-common/dinput_source.cpp +++ b/src/frontend-common/dinput_source.cpp @@ -56,7 +56,7 @@ std::string DInputSource::GetDeviceIdentifier(u32 index) } static constexpr std::array<const char*, DInputSource::NUM_HAT_DIRECTIONS> s_hat_directions = { - {"Up", "Right", "Down", "Left"}}; + {"Up", "Down", "Left", "Right"}}; bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) { @@ -88,14 +88,15 @@ bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex // need to release the lock while we're enumerating, because we call winId(). settings_lock.unlock(); - const std::optional<WindowInfo> toplevel_wi(Host::GetTopLevelWindowInfo()); - if (!toplevel_wi.has_value() || toplevel_wi->type != WindowInfo::Type::Win32) - { - Log_ErrorPrintf("Missing top level window, cannot add DInput devices."); - return false; - } + const std::optional<WindowInfo> toplevel_wi(Host::GetTopLevelWindowInfo()); settings_lock.lock(); + if (!toplevel_wi.has_value() || toplevel_wi->type != WindowInfo::Type::Win32) + { + Log_ErrorPrintf("Missing top level window, cannot add DInput devices."); + return false; + } + m_toplevel_window = static_cast<HWND>(toplevel_wi->window_handle); ReloadDevices(); return true; @@ -106,15 +107,6 @@ void DInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::m // noop } -void DInputSource::Shutdown() -{ - while (!m_controllers.empty()) - { - Host::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(m_controllers.size() - 1))); - m_controllers.pop_back(); - } -} - static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCEW lpddi, LPVOID pvRef) { static_cast<std::vector<DIDEVICEINSTANCEW>*>(pvRef)->push_back(*lpddi); @@ -126,6 +118,7 @@ bool DInputSource::ReloadDevices() // detect any removals PollEvents(); + // look for new devices std::vector<DIDEVICEINSTANCEW> devices; m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY); @@ -156,7 +149,7 @@ bool DInputSource::ReloadDevices() { const u32 index = static_cast<u32>(m_controllers.size()); m_controllers.push_back(std::move(cd)); - Host::OnInputDeviceConnected(GetDeviceIdentifier(index), name); + InputManager::OnInputDeviceConnected(GetDeviceIdentifier(index), name); changed = true; } } @@ -164,6 +157,15 @@ bool DInputSource::ReloadDevices() return changed; } +void DInputSource::Shutdown() +{ + while (!m_controllers.empty()) + { + InputManager::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(m_controllers.size() - 1))); + m_controllers.pop_back(); + } +} + bool DInputSource::AddDevice(ControllerData& cd, const std::string& name) { HRESULT hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE); @@ -204,11 +206,11 @@ bool DInputSource::AddDevice(ControllerData& cd, const std::string& name) cd.num_buttons = caps.dwButtons; - static constexpr auto axis_offsets = - make_array(offsetof(DIJOYSTATE, lX), offsetof(DIJOYSTATE, lY), offsetof(DIJOYSTATE, lZ), offsetof(DIJOYSTATE, lRz), - offsetof(DIJOYSTATE, lRx), offsetof(DIJOYSTATE, lRy), offsetof(DIJOYSTATE, rglSlider[0]), - offsetof(DIJOYSTATE, rglSlider[1])); - for (const auto offset : axis_offsets) + static constexpr const u32 axis_offsets[] = {offsetof(DIJOYSTATE, lX), offsetof(DIJOYSTATE, lY), + offsetof(DIJOYSTATE, lZ), offsetof(DIJOYSTATE, lRz), + offsetof(DIJOYSTATE, lRx), offsetof(DIJOYSTATE, lRy), + offsetof(DIJOYSTATE, rglSlider[0]), offsetof(DIJOYSTATE, rglSlider[1])}; + for (const u32 offset : axis_offsets) { // ask for 16 bits of axis range DIPROPRANGE range = {}; @@ -222,9 +224,7 @@ bool DInputSource::AddDevice(ControllerData& cd, const std::string& name) // did it apply? if (SUCCEEDED(cd.device->GetProperty(DIPROP_RANGE, &range.diph))) - { - cd.axis_offsets.push_back(static_cast<u32>(offset)); - } + cd.axis_offsets.push_back(offset); } cd.num_hats = caps.dwPOVs; @@ -263,7 +263,7 @@ void DInputSource::PollEvents() if (hr != DI_OK) { - Host::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(i))); + InputManager::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(i))); m_controllers.erase(m_controllers.begin() + i); continue; } @@ -336,13 +336,28 @@ std::optional<InputBindingKey> DInputSource::ParseKeyString(const std::string_vi if (StringUtil::StartsWith(binding, "+Axis") || StringUtil::StartsWith(binding, "-Axis")) { - const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5)); + std::string_view end; + const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5), 10, &end); if (!axis_index.has_value()) return std::nullopt; key.source_subtype = InputSubclass::ControllerAxis; key.data = axis_index.value(); - key.negative = (binding[0] == '-'); + key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; + key.invert = (end == "~"); + return key; + } + else if (StringUtil::StartsWith(binding, "FullAxis")) + { + std::string_view end; + const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(8), 10, &end); + if (!axis_index.has_value()) + return std::nullopt; + + key.source_subtype = InputSubclass::ControllerAxis; + key.data = axis_index.value(); + key.modifier = InputModifier::FullAxis; + key.invert = (end == "~"); return key; } else if (StringUtil::StartsWith(binding, "Hat")) @@ -388,7 +403,9 @@ std::string DInputSource::ConvertKeyToString(InputBindingKey key) { if (key.source_subtype == InputSubclass::ControllerAxis) { - ret = fmt::format("DInput-{}/{}Axis{}", u32(key.source_index), key.negative ? '-' : '+', u32(key.data)); + const char* modifier = + (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+")); + ret = fmt::format("DInput-{}/{}Axis{}{}", u32(key.source_index), modifier, u32(key.data), key.invert ? "~" : ""); } else if (key.source_subtype == InputSubclass::ControllerButton && key.data >= MAX_NUM_BUTTONS) { diff --git a/src/frontend-common/dinput_source.h b/src/frontend-common/dinput_source.h index 1d3872ba5..9151a26be 100644 --- a/src/frontend-common/dinput_source.h +++ b/src/frontend-common/dinput_source.h @@ -4,8 +4,8 @@ #pragma once #define DIRECTINPUT_VERSION 0x0800 #include "common/windows_headers.h" -#include "input_source.h" #include "core/types.h" +#include "input_source.h" #include <array> #include <dinput.h> #include <functional> @@ -80,7 +80,7 @@ private: ControllerDataArray m_controllers; HMODULE m_dinput_module{}; - LPCDIDATAFORMAT m_joystick_data_format{}; ComPtr<IDirectInput8W> m_dinput; + LPCDIDATAFORMAT m_joystick_data_format{}; HWND m_toplevel_window = NULL; }; diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index d62a0b2b0..3ec2dec5d 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -44,6 +44,8 @@ #include <bitset> #include <thread> #include <unordered_map> +#include <utility> +#include <vector> Log_SetChannel(FullscreenUI); #ifdef WITH_CHEEVOS @@ -362,11 +364,10 @@ static void PopulateGraphicsAdapterList(); static void PopulateGameListDirectoryCache(SettingsInterface* si); static void PopulatePostProcessingChain(); static void SavePostProcessingChain(); -static void BeginInputBinding(SettingsInterface* bsi, Controller::ControllerBindingType type, - const std::string_view& section, const std::string_view& key, - const std::string_view& display_name); +static void BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, const std::string_view& section, + const std::string_view& key, const std::string_view& display_name); static void DrawInputBindingWindow(); -static void DrawInputBindingButton(SettingsInterface* bsi, Controller::ControllerBindingType type, const char* section, +static void DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section, const char* name, const char* display_name, bool show_type = true); static void ClearInputBindingVariables(); static void StartAutomaticBinding(u32 port); @@ -381,11 +382,12 @@ static FrontendCommon::PostProcessingChain s_postprocessing_chain; static std::vector<const HotkeyInfo*> s_hotkey_list_cache; static std::atomic_bool s_settings_changed{false}; static std::atomic_bool s_game_settings_changed{false}; -static Controller::ControllerBindingType s_input_binding_type = Controller::ControllerBindingType::Unknown; +static InputBindingInfo::Type s_input_binding_type = InputBindingInfo::Type::Unknown; static std::string s_input_binding_section; static std::string s_input_binding_key; static std::string s_input_binding_display_name; static std::vector<InputBindingKey> s_input_binding_new_bindings; +static std::vector<std::pair<InputBindingKey, std::pair<float, float>>> s_input_binding_value_ranges; static Common::Timer s_input_binding_timer; ////////////////////////////////////////////////////////////////////////// @@ -788,7 +790,7 @@ void FullscreenUI::Render() if (s_about_window_open) DrawAboutWindow(); - if (s_input_binding_type != Controller::ControllerBindingType::Unknown) + if (s_input_binding_type != InputBindingInfo::Type::Unknown) DrawInputBindingWindow(); ImGuiFullscreen::EndLayout(); @@ -1277,9 +1279,8 @@ std::string FullscreenUI::GetEffectiveStringSetting(SettingsInterface* bsi, cons return ret; } -void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::ControllerBindingType type, - const char* section, const char* name, const char* display_name, - bool show_type) +void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section, + const char* name, const char* display_name, bool show_type) { TinyString title; title.Fmt("{}/{}", section, name); @@ -1299,17 +1300,17 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::Co { switch (type) { - case Controller::ControllerBindingType::Button: + case InputBindingInfo::Type::Button: title = fmt::format(ICON_FA_DOT_CIRCLE " {}", display_name); break; - case Controller::ControllerBindingType::Axis: - case Controller::ControllerBindingType::HalfAxis: + case InputBindingInfo::Type::Axis: + case InputBindingInfo::Type::HalfAxis: title = fmt::format(ICON_FA_BULLSEYE " {}", display_name); break; - case Controller::ControllerBindingType::Motor: + case InputBindingInfo::Type::Motor: title = fmt::format(ICON_FA_BELL " {}", display_name); break; - case Controller::ControllerBindingType::Macro: + case InputBindingInfo::Type::Macro: title = fmt::format(ICON_FA_PIZZA_SLICE " {}", display_name); break; default: @@ -1343,18 +1344,19 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::Co void FullscreenUI::ClearInputBindingVariables() { - s_input_binding_type = Controller::ControllerBindingType::Unknown; + s_input_binding_type = InputBindingInfo::Type::Unknown; s_input_binding_section = {}; s_input_binding_key = {}; s_input_binding_display_name = {}; s_input_binding_new_bindings = {}; + s_input_binding_value_ranges = {}; } -void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::ControllerBindingType type, +void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, const std::string_view& section, const std::string_view& key, const std::string_view& display_name) { - if (s_input_binding_type != Controller::ControllerBindingType::Unknown) + if (s_input_binding_type != InputBindingInfo::Type::Unknown) { InputManager::RemoveHook(); ClearInputBindingVariables(); @@ -1365,25 +1367,50 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control s_input_binding_key = key; s_input_binding_display_name = display_name; s_input_binding_new_bindings = {}; + s_input_binding_value_ranges = {}; s_input_binding_timer.Reset(); - InputManager::SetHook([game_settings = IsEditingGameSettings(bsi)]( - InputBindingKey key, float value) -> InputInterceptHook::CallbackResult { + const bool game_settings = IsEditingGameSettings(bsi); + + InputManager::SetHook([game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult { + if (s_input_binding_type == InputBindingInfo::Type::Unknown) + return InputInterceptHook::CallbackResult::StopProcessingEvent; + // holding the settings lock here will protect the input binding list auto lock = Host::GetSettingsLock(); - const float abs_value = std::abs(value); - - for (InputBindingKey other_key : s_input_binding_new_bindings) + float initial_value = value; + float min_value = value; + auto it = std::find_if(s_input_binding_value_ranges.begin(), s_input_binding_value_ranges.end(), + [key](const auto& it) { return it.first.bits == key.bits; }); + if (it != s_input_binding_value_ranges.end()) { + initial_value = it->second.first; + min_value = it->second.second = std::min(it->second.second, value); + } + else + { + s_input_binding_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); + } + + const float abs_value = std::abs(value); + const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f); + + for (InputBindingKey& other_key : s_input_binding_new_bindings) + { + // if this key is in our new binding list, it's a "release", and we're done if (other_key.MaskDirection() == key.MaskDirection()) { - if (abs_value < 0.5f) + // for pedals, we wait for it to go back to near its starting point to commit the binding + if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f))) { - // if this key is in our new binding list, it's a "release", and we're done + // did we go the full range? + if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f) + other_key.modifier = InputModifier::FullAxis; + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); const std::string new_binding(InputManager::ConvertInputBindingKeysToString( - s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size())); + s_input_binding_type, s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size())); bsi->SetStringValue(s_input_binding_section.c_str(), s_input_binding_key.c_str(), new_binding.c_str()); SetSettingsChanged(bsi); ClearInputBindingVariables(); @@ -1396,10 +1423,11 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control } // new binding, add it to the list, but wait for a decent distance first, and then wait for release - if (abs_value >= 0.5f) + if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f))) { InputBindingKey key_to_add = key; - key_to_add.negative = (value < 0.0f); + key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None; + key_to_add.invert = reverse_threshold; s_input_binding_new_bindings.push_back(key_to_add); } @@ -1409,7 +1437,7 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control void FullscreenUI::DrawInputBindingWindow() { - DebugAssert(s_input_binding_type != Controller::ControllerBindingType::Unknown); + DebugAssert(s_input_binding_type != InputBindingInfo::Type::Unknown); const double time_remaining = INPUT_BINDING_TIMEOUT_SECONDS - s_input_binding_timer.GetTimeSeconds(); if (time_remaining <= 0.0) @@ -3180,7 +3208,7 @@ void FullscreenUI::DrawControllerSettingsPage() for (u32 macro_index = 0; macro_index < InputManager::NUM_MACRO_BUTTONS_PER_CONTROLLER; macro_index++) { - DrawInputBindingButton(bsi, Controller::ControllerBindingType::Macro, section.c_str(), + DrawInputBindingButton(bsi, InputBindingInfo::Type::Macro, section.c_str(), fmt::format("Macro{}", macro_index + 1).c_str(), fmt::format("Macro {} Trigger", macro_index + 1).c_str()); @@ -3194,9 +3222,8 @@ void FullscreenUI::DrawControllerSettingsPage() for (u32 i = 0; i < ci->num_bindings; i++) { const Controller::ControllerBindingInfo& bi = ci->bindings[i]; - if (bi.type != Controller::ControllerBindingType::Button && - bi.type != Controller::ControllerBindingType::Axis && - bi.type != Controller::ControllerBindingType::HalfAxis) + if (bi.type != InputBindingInfo::Type::Button && bi.type != InputBindingInfo::Type::Axis && + bi.type != InputBindingInfo::Type::HalfAxis) { continue; } @@ -3350,8 +3377,7 @@ void FullscreenUI::DrawHotkeySettingsPage() last_category = hotkey; } - DrawInputBindingButton(bsi, Controller::ControllerBindingType::Button, "Hotkeys", hotkey->name, - hotkey->display_name, false); + DrawInputBindingButton(bsi, InputBindingInfo::Type::Button, "Hotkeys", hotkey->name, hotkey->display_name, false); } EndMenuButtons(); @@ -5831,9 +5857,12 @@ void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry) void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry) { ImGuiFullscreen::ChoiceDialogOptions options = { - {ICON_FA_WRENCH " Game Properties", false}, {ICON_FA_PLAY " Resume Game", false}, - {ICON_FA_UNDO " Load State", false}, {ICON_FA_COMPACT_DISC " Default Boot", false}, - {ICON_FA_LIGHTBULB " Fast Boot", false}, {ICON_FA_MAGIC " Slow Boot", false}, + {ICON_FA_WRENCH " Game Properties", false}, + {ICON_FA_PLAY " Resume Game", false}, + {ICON_FA_UNDO " Load State", false}, + {ICON_FA_COMPACT_DISC " Default Boot", false}, + {ICON_FA_LIGHTBULB " Fast Boot", false}, + {ICON_FA_MAGIC " Slow Boot", false}, {ICON_FA_FOLDER_MINUS " Reset Play Time", false}, {ICON_FA_WINDOW_CLOSE " Close Menu", false}, }; diff --git a/src/frontend-common/imgui_overlays.cpp b/src/frontend-common/imgui_overlays.cpp index b5a8ae9a8..0a8c2d368 100644 --- a/src/frontend-common/imgui_overlays.cpp +++ b/src/frontend-common/imgui_overlays.cpp @@ -523,8 +523,8 @@ void ImGuiManager::DrawInputsOverlay() const Controller::ControllerBindingInfo& bi = cinfo->bindings[bind]; switch (bi.type) { - case Controller::ControllerBindingType::Axis: - case Controller::ControllerBindingType::HalfAxis: + case InputBindingInfo::Type::Axis: + case InputBindingInfo::Type::HalfAxis: { // axes are always shown const float value = controller->GetBindState(bi.bind_index); @@ -535,7 +535,7 @@ void ImGuiManager::DrawInputsOverlay() } break; - case Controller::ControllerBindingType::Button: + case InputBindingInfo::Type::Button: { // buttons only shown when active const float value = controller->GetBindState(bi.bind_index); @@ -544,9 +544,10 @@ void ImGuiManager::DrawInputsOverlay() } break; - case Controller::ControllerBindingType::Motor: - case Controller::ControllerBindingType::Macro: - case Controller::ControllerBindingType::Unknown: + case InputBindingInfo::Type::Motor: + case InputBindingInfo::Type::Macro: + case InputBindingInfo::Type::Unknown: + case InputBindingInfo::Type::Pointer: default: break; } diff --git a/src/frontend-common/input_manager.cpp b/src/frontend-common/input_manager.cpp index ef51637f5..98893e08b 100644 --- a/src/frontend-common/input_manager.cpp +++ b/src/frontend-common/input_manager.cpp @@ -281,48 +281,72 @@ bool InputManager::ParseBindingAndGetSource(const std::string_view& binding, Inp return false; } -std::string InputManager::ConvertInputBindingKeyToString(InputBindingKey key) +std::string InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key) { - if (key.source_type == InputSourceType::Keyboard) + if (binding_type == InputBindingInfo::Type::Pointer) { - const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data)); - if (str.has_value() && !str->empty()) - return fmt::format("Keyboard/{}", str->c_str()); - } - else if (key.source_type == InputSourceType::Pointer) - { - if (key.source_subtype == InputSubclass::PointerButton) + // pointer and device bindings don't have a data part + if (key.source_type == InputSourceType::Pointer) { - if (key.data < s_pointer_button_names.size()) - return fmt::format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_names[key.data]); - else - return fmt::format("Pointer-{}/Button{}", u32{key.source_index}, key.data); + return GetPointerDeviceName(key.data); } - else if (key.source_subtype == InputSubclass::PointerAxis) + else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)]) { - return fmt::format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data], - key.negative ? '-' : '+'); + // This assumes that it always follows the Type/Binding form. + std::string keystr(s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key)); + std::string::size_type pos = keystr.find('/'); + if (pos != std::string::npos) + keystr.erase(pos); + return keystr; } } - else if (key.source_type == InputSourceType::Sensor) + else { - if (key.source_subtype == InputSubclass::SensorAccelerometer && key.data < s_sensor_accelerometer_names.size()) - return fmt::format("Sensor/{}", s_sensor_accelerometer_names[key.data]); - } - else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)]) - { - return s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key); + if (key.source_type == InputSourceType::Keyboard) + { + const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data)); + if (str.has_value() && !str->empty()) + return fmt::format("Keyboard/{}", str->c_str()); + } + else if (key.source_type == InputSourceType::Pointer) + { + if (key.source_subtype == InputSubclass::PointerButton) + { + if (key.data < s_pointer_button_names.size()) + return fmt::format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_names[key.data]); + else + return fmt::format("Pointer-{}/Button{}", u32{key.source_index}, key.data); + } + else if (key.source_subtype == InputSubclass::PointerAxis) + { + return fmt::format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data], + key.modifier == InputModifier::Negate ? '-' : '+'); + } + } + else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)]) + { + return s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key); + } } return {}; } -std::string InputManager::ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys) +std::string InputManager::ConvertInputBindingKeysToString(InputBindingInfo::Type binding_type, + const InputBindingKey* keys, size_t num_keys) { + // can't have a chord of devices/pointers + if (binding_type == InputBindingInfo::Type::Pointer) + { + // so only take the first + if (num_keys > 0) + return ConvertInputBindingKeyToString(binding_type, keys[0]); + } + std::stringstream ss; for (size_t i = 0; i < num_keys; i++) { - const std::string keystr(ConvertInputBindingKeyToString(keys[i])); + const std::string keystr(ConvertInputBindingKeyToString(binding_type, keys[i])); if (keystr.empty()) return std::string(); @@ -574,9 +598,9 @@ std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_v const std::string_view dir_part(sub_binding.substr(std::strlen(s_pointer_axis_names[i]))); if (dir_part == "+") - key.negative = false; + key.modifier = InputModifier::None; else if (dir_part == "-") - key.negative = true; + key.modifier = InputModifier::Negate; else return std::nullopt; @@ -597,6 +621,23 @@ std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_v return std::nullopt; } +std::optional<u32> InputManager::GetIndexFromPointerBinding(const std::string_view& source) +{ + if (!StringUtil::StartsWith(source, "Pointer-")) + return std::nullopt; + + const std::optional<s32> pointer_index = StringUtil::FromChars<s32>(source.substr(8)); + if (!pointer_index.has_value() || pointer_index.value() < 0) + return std::nullopt; + + return static_cast<u32>(pointer_index.value()); +} + +std::string InputManager::GetPointerDeviceName(u32 pointer_index) +{ + return fmt::format("Pointer-{}", pointer_index); +} + std::optional<InputBindingKey> InputManager::ParseSensorKey(const std::string_view& source, const std::string_view& sub_binding) { @@ -616,9 +657,9 @@ std::optional<InputBindingKey> InputManager::ParseSensorKey(const std::string_vi const std::string_view dir_part(sub_binding.substr(std::strlen(s_sensor_accelerometer_names[i]))); if (dir_part == "+") - key.negative = false; + key.modifier = InputModifier::None; else if (dir_part == "-") - key.negative = true; + key.modifier = InputModifier::Negate; else return std::nullopt; @@ -777,7 +818,6 @@ bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBi for (auto it = range.first; it != range.second; ++it) { InputBinding* binding = it->second.get(); - // find the key which matches us for (u32 i = 0; i < binding->num_keys; i++) { @@ -785,11 +825,26 @@ bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBi continue; const u8 bit = static_cast<u8>(1) << i; - const bool negative = binding->keys[i].negative; + const bool negative = binding->keys[i].modifier == InputModifier::Negate; const bool new_state = (negative ? (value < 0.0f) : (value > 0.0f)); + float value_to_pass = 0.0f; + switch (binding->keys[i].modifier) + { + case InputModifier::None: + if (value > 0.0f) + value_to_pass = value; + break; + case InputModifier::Negate: + if (value < 0.0f) + value_to_pass = -value; + break; + case InputModifier::FullAxis: + value_to_pass = value * 0.5f + 0.5f; + break; + } - // invert if we're negative, since the handler expects 0..1 - const float value_to_pass = (negative ? ((value < 0.0f) ? -value : 0.0f) : (value > 0.0f) ? value : 0.0f); + // handle inverting, needed for some wheels. + value_to_pass = binding->keys[i].invert ? (1.0f - value_to_pass) : value_to_pass; // axes are fired regardless of a state change, unless they're zero // (but going from not-zero to zero will still fire, because of the full state) @@ -1130,6 +1185,16 @@ std::vector<std::string> InputManager::GetInputProfileNames() return ret; } +void InputManager::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name) +{ + Host::OnInputDeviceConnected(identifier, device_name); +} + +void InputManager::OnInputDeviceDisconnected(const std::string_view& identifier) +{ + Host::OnInputDeviceDisconnected(identifier); +} + // ------------------------------------------------------------------------ // Vibration // ------------------------------------------------------------------------ diff --git a/src/frontend-common/input_manager.h b/src/frontend-common/input_manager.h index 65950278e..41a506a0c 100644 --- a/src/frontend-common/input_manager.h +++ b/src/frontend-common/input_manager.h @@ -14,6 +14,8 @@ #include "common/types.h" #include "common/window_info.h" +#include "core/input_types.h" + /// Class, or source of an input event. enum class InputSourceType : u32 { @@ -47,12 +49,20 @@ enum class InputSubclass : u32 ControllerButton = 0, ControllerAxis = 1, - ControllerMotor = 2, - ControllerHaptic = 3, + ControllerHat = 2, + ControllerMotor = 3, + ControllerHaptic = 4, SensorAccelerometer = 0, }; +enum class InputModifier : u32 +{ + None = 0, + Negate, ///< Input * -1, gets the negative side of the axis + FullAxis, ///< (Input * 0.5) + 0.5, uses both the negative and positive side of the axis together +}; + /// A composite type representing a full input key which is part of an event. union InputBindingKey { @@ -60,9 +70,10 @@ union InputBindingKey { InputSourceType source_type : 4; u32 source_index : 8; ///< controller number - InputSubclass source_subtype : 2; ///< if 1, binding is for an axis and not a button (used for controllers) - u32 negative : 1; ///< if 1, binding is for the negative side of the axis - u32 unused : 17; + InputSubclass source_subtype : 3; ///< if 1, binding is for an axis and not a button (used for controllers) + InputModifier modifier : 2; + u32 invert : 1; ///< if 1, value is inverted prior to being sent to the sink + u32 unused : 14; u32 data; }; @@ -77,7 +88,8 @@ union InputBindingKey { InputBindingKey r; r.bits = bits; - r.negative = false; + r.modifier = InputModifier::None; + r.invert = 0; return r; } }; @@ -181,6 +193,12 @@ bool GetInputSourceDefaultEnabled(InputSourceType type); /// Parses an input class string. std::optional<InputSourceType> ParseInputSourceString(const std::string_view& str); +/// Parses a pointer device string, i.e. tells you which pointer is specified. +std::optional<u32> GetIndexFromPointerBinding(const std::string_view& str); + +/// Returns the device name for a pointer index (e.g. Pointer-0). +std::string GetPointerDeviceName(u32 pointer_index); + /// Converts a key code from a human-readable string to an identifier. std::optional<u32> ConvertHostKeyboardStringToCode(const std::string_view& str); @@ -204,10 +222,11 @@ InputBindingKey MakeSensorAxisKey(InputSubclass sensor, u32 axis); std::optional<InputBindingKey> ParseInputBindingKey(const std::string_view& binding); /// Converts a input key to a string. -std::string ConvertInputBindingKeyToString(InputBindingKey key); +std::string ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key); /// Converts a chord of binding keys to a string. -std::string ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys); +std::string ConvertInputBindingKeysToString(InputBindingInfo::Type binding_type, const InputBindingKey* keys, + size_t num_keys); /// Returns a list of all hotkeys. std::vector<const HotkeyInfo*> GetHotkeyList(); @@ -263,7 +282,7 @@ void AddVibrationBinding(u32 pad_index, const InputBindingKey* motor_0_binding, /// Updates internal state for any binds for this key, and fires callbacks as needed. /// Returns true if anything was bound to this key, otherwise false. -bool InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key); +bool InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key = GenericInputBinding::Unknown); /// Sets a hook which can be used to intercept events before they're processed by the normal bindings. /// This is typically used when binding new controls to detect what gets pressed. @@ -315,6 +334,12 @@ bool MapController(SettingsInterface& si, u32 controller, /// Returns a list of input profiles available. std::vector<std::string> GetInputProfileNames(); + +/// Called when a new input device is connected. +void OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name); + +/// Called when an input device is disconnected. +void OnInputDeviceDisconnected(const std::string_view& identifier); } // namespace InputManager namespace Host { diff --git a/src/frontend-common/input_source.cpp b/src/frontend-common/input_source.cpp index c8baf60c6..c89b7a91e 100644 --- a/src/frontend-common/input_source.cpp +++ b/src/frontend-common/input_source.cpp @@ -38,6 +38,17 @@ InputBindingKey InputSource::MakeGenericControllerButtonKey(InputSourceType claz return key; } +InputBindingKey InputSource::MakeGenericControllerHatKey(InputSourceType clazz, u32 controller_index, s32 hat_index, + u8 hat_direction, u32 num_directions) +{ + InputBindingKey key = {}; + key.source_type = clazz; + key.source_index = controller_index; + key.source_subtype = InputSubclass::ControllerHat; + key.data = static_cast<u32>(hat_index) * num_directions + hat_direction; + return key; +} + InputBindingKey InputSource::MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index) { InputBindingKey key = {}; @@ -81,12 +92,21 @@ std::optional<InputBindingKey> InputSource::ParseGenericControllerKey(InputSourc key.data = static_cast<u32>(axis_number.value()); if (sub_binding[0] == '+') - key.negative = false; + key.modifier = InputModifier::None; else if (sub_binding[0] == '-') - key.negative = true; + key.modifier = InputModifier::Negate; else return std::nullopt; } + else if (StringUtil::StartsWith(sub_binding, "FullAxis")) + { + const std::optional<s32> axis_number = StringUtil::FromChars<s32>(sub_binding.substr(8)); + if (!axis_number.has_value() || axis_number.value() < 0) + return std::nullopt; + key.source_subtype = InputSubclass::ControllerAxis; + key.data = static_cast<u32>(axis_number.value()); + key.modifier = InputModifier::FullAxis; + } else if (StringUtil::StartsWith(sub_binding, "Button")) { const std::optional<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6)); @@ -108,8 +128,21 @@ std::string InputSource::ConvertGenericControllerKeyToString(InputBindingKey key { if (key.source_subtype == InputSubclass::ControllerAxis) { - return StringUtil::StdStringFromFormat("%s-%u/%cAxis%u", InputManager::InputSourceToString(key.source_type), - key.source_index, key.negative ? '+' : '-', key.data); + const char* modifier = ""; + switch (key.modifier) + { + case InputModifier::None: + modifier = "+"; + break; + case InputModifier::Negate: + modifier = "-"; + break; + case InputModifier::FullAxis: + modifier = "Full"; + break; + } + return StringUtil::StdStringFromFormat("%s-%u/%sAxis%u", InputManager::InputSourceToString(key.source_type), + key.source_index, modifier, key.data); } else if (key.source_subtype == InputSubclass::ControllerButton) { @@ -120,4 +153,4 @@ std::string InputSource::ConvertGenericControllerKeyToString(InputBindingKey key { return {}; } -} +} \ No newline at end of file diff --git a/src/frontend-common/input_source.h b/src/frontend-common/input_source.h index 7749c94e5..0a5784924 100644 --- a/src/frontend-common/input_source.h +++ b/src/frontend-common/input_source.h @@ -55,6 +55,10 @@ public: /// Creates a key for a generic controller button event. static InputBindingKey MakeGenericControllerButtonKey(InputSourceType clazz, u32 controller_index, s32 button_index); + /// Creates a key for a generic controller hat event. + static InputBindingKey MakeGenericControllerHatKey(InputSourceType clazz, u32 controller_index, s32 hat_index, + u8 hat_direction, u32 num_directions); + /// Creates a key for a generic controller motor event. static InputBindingKey MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index); diff --git a/src/frontend-common/sdl_input_source.cpp b/src/frontend-common/sdl_input_source.cpp index ad175c78c..bc7d2ffa0 100644 --- a/src/frontend-common/sdl_input_source.cpp +++ b/src/frontend-common/sdl_input_source.cpp @@ -14,7 +14,7 @@ #endif Log_SetChannel(SDLInputSource); -static const char* s_sdl_axis_names[] = { +static constexpr const char* s_sdl_axis_names[] = { "LeftX", // SDL_CONTROLLER_AXIS_LEFTX "LeftY", // SDL_CONTROLLER_AXIS_LEFTY "RightX", // SDL_CONTROLLER_AXIS_RIGHTX @@ -22,7 +22,7 @@ static const char* s_sdl_axis_names[] = { "LeftTrigger", // SDL_CONTROLLER_AXIS_TRIGGERLEFT "RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT }; -static const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = { +static constexpr const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = { {GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight}, // SDL_CONTROLLER_AXIS_LEFTX {GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown}, // SDL_CONTROLLER_AXIS_LEFTY {GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // SDL_CONTROLLER_AXIS_RIGHTX @@ -31,7 +31,7 @@ static const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = { {GenericInputBinding::Unknown, GenericInputBinding::R2}, // SDL_CONTROLLER_AXIS_TRIGGERRIGHT }; -static const char* s_sdl_button_names[] = { +static constexpr const char* s_sdl_button_names[] = { "A", // SDL_CONTROLLER_BUTTON_A "B", // SDL_CONTROLLER_BUTTON_B "X", // SDL_CONTROLLER_BUTTON_X @@ -54,7 +54,7 @@ static const char* s_sdl_button_names[] = { "Paddle4", // SDL_CONTROLLER_BUTTON_PADDLE4 "Touchpad", // SDL_CONTROLLER_BUTTON_TOUCHPAD }; -static const GenericInputBinding s_sdl_generic_binding_button_mapping[] = { +static constexpr const GenericInputBinding s_sdl_generic_binding_button_mapping[] = { GenericInputBinding::Cross, // SDL_CONTROLLER_BUTTON_A GenericInputBinding::Circle, // SDL_CONTROLLER_BUTTON_B GenericInputBinding::Square, // SDL_CONTROLLER_BUTTON_X @@ -78,11 +78,20 @@ static const GenericInputBinding s_sdl_generic_binding_button_mapping[] = { GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_TOUCHPAD }; +static constexpr const char* s_sdl_hat_direction_names[] = { + // clang-format off + "North", + "East", + "South", + "West", + // clang-format on +}; + SDLInputSource::SDLInputSource() = default; SDLInputSource::~SDLInputSource() { - DebugAssert(m_controllers.empty()); + Assert(m_controllers.empty()); } bool SDLInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) @@ -123,6 +132,13 @@ void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std: } } +bool SDLInputSource::ReloadDevices() +{ + // We'll get a GC added/removed event here. + PollEvents(); + return false; +} + void SDLInputSource::Shutdown() { ShutdownSubsystem(); @@ -131,36 +147,32 @@ void SDLInputSource::Shutdown() void SDLInputSource::LoadSettings(SettingsInterface& si) { m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false); -} - -bool SDLInputSource::ReloadDevices() -{ - // We'll get a GC added/removed event here. - PollEvents(); - return false; + m_sdl_hints = si.GetKeyValueList("SDLHints"); } void SDLInputSource::SetHints() { SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0"); + // Enable Wii U Pro Controller support + // New as of SDL 2.26, so use string + SDL_SetHint("SDL_JOYSTICK_HIDAPI_WII", "1"); +#ifndef _WIN32 + // Gets us pressure sensitive button support on Linux + // Apparently doesn't work on Windows, so leave it off there + // New as of SDL 2.26, so use string + SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS3", "1"); +#endif + + for (const std::pair<std::string, std::string>& hint : m_sdl_hints) + SDL_SetHint(hint.first.c_str(), hint.second.c_str()); } bool SDLInputSource::InitializeSubsystem() { - int result; -#ifdef __APPLE__ - // On macOS, SDL_InitSubSystem runs a main-thread-only call to some GameController framework method - // So send this to be run on the main thread - dispatch_sync_f(dispatch_get_main_queue(), &result, [](void* ctx) { - *static_cast<int*>(ctx) = SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC); - }); -#else - result = SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC); -#endif - if (result < 0) + if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0) { - Log_ErrorPrintf("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed"); + Log_ErrorPrint("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed"); return false; } @@ -172,16 +184,11 @@ bool SDLInputSource::InitializeSubsystem() void SDLInputSource::ShutdownSubsystem() { while (!m_controllers.empty()) - CloseGameController(m_controllers.begin()->joystick_id); + CloseDevice(m_controllers.begin()->joystick_id); if (m_sdl_subsystem_initialized) { -#ifdef __APPLE__ - dispatch_sync_f(dispatch_get_main_queue(), nullptr, - [](void*) { SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC); }); -#else SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC); -#endif m_sdl_subsystem_initialized = false; } } @@ -206,7 +213,7 @@ std::vector<std::pair<std::string, std::string>> SDLInputSource::EnumerateDevice { std::string id(StringUtil::StdStringFromFormat("SDL-%d", cd.player_id)); - const char* name = SDL_GameControllerName(cd.game_controller); + const char* name = cd.game_controller ? SDL_GameControllerName(cd.game_controller) : SDL_JoystickName(cd.joystick); if (name) ret.emplace_back(std::move(id), name); else @@ -258,6 +265,19 @@ std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_ { // likely an axis const std::string_view axis_name(binding.substr(1)); + + if (StringUtil::StartsWith(axis_name, "Axis")) + { + std::string_view end; + if (auto value = StringUtil::FromChars<u32>(axis_name.substr(4), 10, &end)) + { + key.source_subtype = InputSubclass::ControllerAxis; + key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names)); + key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; + key.invert = (end == "~"); + return key; + } + } for (u32 i = 0; i < std::size(s_sdl_axis_names); i++) { if (axis_name == s_sdl_axis_names[i]) @@ -265,14 +285,51 @@ std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_ // found an axis! key.source_subtype = InputSubclass::ControllerAxis; key.data = i; - key.negative = (binding[0] == '-'); + key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; return key; } } } + else if (StringUtil::StartsWith(binding, "FullAxis")) + { + std::string_view end; + if (auto value = StringUtil::FromChars<u32>(binding.substr(8), 10, &end)) + { + key.source_subtype = InputSubclass::ControllerAxis; + key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names)); + key.modifier = InputModifier::FullAxis; + key.invert = (end == "~"); + return key; + } + } + else if (StringUtil::StartsWith(binding, "Hat")) + { + std::string_view hat_dir; + if (auto value = StringUtil::FromChars<u32>(binding.substr(3), 10, &hat_dir); value.has_value() && !hat_dir.empty()) + { + for (u8 dir = 0; dir < static_cast<u8>(std::size(s_sdl_hat_direction_names)); dir++) + { + if (hat_dir == s_sdl_hat_direction_names[dir]) + { + key.source_subtype = InputSubclass::ControllerHat; + key.data = value.value() * static_cast<u32>(std::size(s_sdl_hat_direction_names)) + dir; + return key; + } + } + } + } else { // must be a button + if (StringUtil::StartsWith(binding, "Button")) + { + if (auto value = StringUtil::FromChars<u32>(binding.substr(6))) + { + key.source_subtype = InputSubclass::ControllerButton; + key.data = *value + static_cast<u32>(std::size(s_sdl_button_names)); + return key; + } + } for (u32 i = 0; i < std::size(s_sdl_button_names); i++) { if (binding == s_sdl_button_names[i]) @@ -294,14 +351,39 @@ std::string SDLInputSource::ConvertKeyToString(InputBindingKey key) if (key.source_type == InputSourceType::SDL) { - if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_sdl_axis_names)) + if (key.source_subtype == InputSubclass::ControllerAxis) { - ret = StringUtil::StdStringFromFormat("SDL-%u/%c%s", key.source_index, key.negative ? '-' : '+', - s_sdl_axis_names[key.data]); + const char* modifier = + (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+")); + if (key.data < std::size(s_sdl_axis_names)) + { + ret = StringUtil::StdStringFromFormat("SDL-%u/%s%s", key.source_index, modifier, s_sdl_axis_names[key.data]); + } + else + { + ret = StringUtil::StdStringFromFormat("SDL-%u/%sAxis%u%s", key.source_index, modifier, + key.data - static_cast<u32>(std::size(s_sdl_axis_names)), + key.invert ? "~" : ""); + } } - else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_sdl_button_names)) + else if (key.source_subtype == InputSubclass::ControllerButton) { - ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]); + if (key.data < std::size(s_sdl_button_names)) + { + ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]); + } + else + { + ret = StringUtil::StdStringFromFormat("SDL-%u/Button%u", key.source_index, + key.data - static_cast<u32>(std::size(s_sdl_button_names))); + } + } + else if (key.source_subtype == InputSubclass::ControllerHat) + { + const u32 hat_index = key.data / static_cast<u32>(std::size(s_sdl_hat_direction_names)); + const u32 hat_direction = key.data % static_cast<u32>(std::size(s_sdl_hat_direction_names)); + ret = StringUtil::StdStringFromFormat("SDL-%u/Hat%u%s", key.source_index, hat_index, + s_sdl_hat_direction_names[hat_direction]); } else if (key.source_subtype == InputSubclass::ControllerMotor) { @@ -323,14 +405,37 @@ bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event) case SDL_CONTROLLERDEVICEADDED: { Log_InfoPrintf("(SDLInputSource) Controller %d inserted", event->cdevice.which); - OpenGameController(event->cdevice.which); + OpenDevice(event->cdevice.which, true); return true; } case SDL_CONTROLLERDEVICEREMOVED: { Log_InfoPrintf("(SDLInputSource) Controller %d removed", event->cdevice.which); - CloseGameController(event->cdevice.which); + CloseDevice(event->cdevice.which); + return true; + } + + case SDL_JOYDEVICEADDED: + { + // Let game controller handle.. well.. game controllers. + if (SDL_IsGameController(event->jdevice.which)) + return false; + + Log_InfoPrintf("(SDLInputSource) Joystick %d inserted", event->jdevice.which); + OpenDevice(event->cdevice.which, false); + return true; + } + break; + + case SDL_JOYDEVICEREMOVED: + { + if (auto it = GetControllerDataForJoystickId(event->cdevice.which); + it != m_controllers.end() && it->game_controller) + return false; + + Log_InfoPrintf("(SDLInputSource) Joystick %d removed", event->jdevice.which); + CloseDevice(event->cdevice.which); return true; } @@ -341,11 +446,37 @@ bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event) case SDL_CONTROLLERBUTTONUP: return HandleControllerButtonEvent(&event->cbutton); + case SDL_JOYAXISMOTION: + return HandleJoystickAxisEvent(&event->jaxis); + + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + return HandleJoystickButtonEvent(&event->jbutton); + + case SDL_JOYHATMOTION: + return HandleJoystickHatEvent(&event->jhat); + default: return false; } } +SDL_Joystick* SDLInputSource::GetJoystickForDevice(const std::string_view& device) +{ + if (!StringUtil::StartsWith(device, "SDL-")) + return nullptr; + + const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4)); + if (!player_id.has_value() || player_id.value() < 0) + return nullptr; + + auto it = GetControllerDataForPlayerId(player_id.value()); + if (it == m_controllers.end()) + return nullptr; + + return it->joystick; +} + SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id) { return std::find_if(m_controllers.begin(), m_controllers.end(), @@ -375,11 +506,23 @@ int SDLInputSource::GetFreePlayerId() const return 0; } -bool SDLInputSource::OpenGameController(int index) +bool SDLInputSource::OpenDevice(int index, bool is_gamecontroller) { - SDL_GameController* gcontroller = SDL_GameControllerOpen(index); - SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr; - if (!gcontroller || !joystick) + SDL_GameController* gcontroller; + SDL_Joystick* joystick; + + if (is_gamecontroller) + { + gcontroller = SDL_GameControllerOpen(index); + joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr; + } + else + { + gcontroller = nullptr; + joystick = SDL_JoystickOpen(index); + } + + if (!gcontroller && !joystick) { Log_ErrorPrintf("(SDLInputSource) Failed to open controller %d", index); if (gcontroller) @@ -389,7 +532,7 @@ bool SDLInputSource::OpenGameController(int index) } const int joystick_id = SDL_JoystickInstanceID(joystick); - int player_id = SDL_GameControllerGetPlayerIndex(gcontroller); + int player_id = gcontroller ? SDL_GameControllerGetPlayerIndex(gcontroller) : SDL_JoystickGetPlayerIndex(joystick); if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end()) { const int free_player_id = GetFreePlayerId(); @@ -399,20 +542,49 @@ bool SDLInputSource::OpenGameController(int index) player_id = free_player_id; } - Log_InfoPrintf("(SDLInputSource) Opened controller %d (instance id %d, player id %d): %s", index, joystick_id, - player_id, SDL_GameControllerName(gcontroller)); + const char* name = gcontroller ? SDL_GameControllerName(gcontroller) : SDL_JoystickName(joystick); + if (!name) + name = "Unknown Device"; + + Log_VerbosePrintf("(SDLInputSource) Opened %s %d (instance id %d, player id %d): %s", + is_gamecontroller ? "game controller" : "joystick", index, joystick_id, player_id, name); ControllerData cd = {}; cd.player_id = player_id; cd.joystick_id = joystick_id; cd.haptic_left_right_effect = -1; cd.game_controller = gcontroller; + cd.joystick = joystick; - cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0); + if (gcontroller) + { + const int num_axes = SDL_JoystickNumAxes(joystick); + const int num_buttons = SDL_JoystickNumButtons(joystick); + cd.joy_axis_used_in_gc.resize(num_axes, false); + cd.joy_button_used_in_gc.resize(num_buttons, false); + auto mark_bind = [&](SDL_GameControllerButtonBind bind) { + if (bind.bindType == SDL_CONTROLLER_BINDTYPE_AXIS && bind.value.axis < num_axes) + cd.joy_axis_used_in_gc[bind.value.axis] = true; + if (bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON && bind.value.button < num_buttons) + cd.joy_button_used_in_gc[bind.value.button] = true; + }; + for (size_t i = 0; i < std::size(s_sdl_axis_names); i++) + mark_bind(SDL_GameControllerGetBindForAxis(gcontroller, static_cast<SDL_GameControllerAxis>(i))); + for (size_t i = 0; i < std::size(s_sdl_button_names); i++) + mark_bind(SDL_GameControllerGetBindForButton(gcontroller, static_cast<SDL_GameControllerButton>(i))); + } + else + { + // GC doesn't have the concept of hats, so we only need to do this for joysticks. + const int num_hats = SDL_JoystickNumHats(joystick); + if (num_hats > 0) + cd.last_hat_state.resize(static_cast<size_t>(num_hats), u8(0)); + } + + cd.use_game_controller_rumble = (gcontroller && SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0); if (cd.use_game_controller_rumble) { - Log_DevPrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller", - SDL_GameControllerName(gcontroller)); + Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller", name); } else { @@ -431,7 +603,7 @@ bool SDLInputSource::OpenGameController(int index) } else { - Log_WarningPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError()); + Log_ErrorPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError()); if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0) { cd.haptic = haptic; @@ -445,37 +617,43 @@ bool SDLInputSource::OpenGameController(int index) } if (cd.haptic) - Log_DevPrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller)); + Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", name); } if (!cd.haptic && !cd.use_game_controller_rumble) - Log_WarningPrintf("(SDLInputSource) Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller)); + Log_VerbosePrintf("(SDLInputSource) Rumble is not supported on '%s'", name); m_controllers.push_back(std::move(cd)); - const char* name = SDL_GameControllerName(cd.game_controller); - Host::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name ? name : "Unknown Device"); + InputManager::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name); return true; } -bool SDLInputSource::CloseGameController(int joystick_index) +bool SDLInputSource::CloseDevice(int joystick_index) { auto it = GetControllerDataForJoystickId(joystick_index); if (it == m_controllers.end()) return false; + InputManager::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("SDL-%d", it->player_id)); + if (it->haptic) - SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic)); + SDL_HapticClose(it->haptic); - SDL_GameControllerClose(static_cast<SDL_GameController*>(it->game_controller)); + if (it->game_controller) + SDL_GameControllerClose(it->game_controller); + else + SDL_JoystickClose(it->joystick); - const int player_id = it->player_id; m_controllers.erase(it); - - Host::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("SDL-%d", player_id)); return true; } +static float NormalizeS16(s16 value) +{ + return static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f); +} + bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev) { auto it = GetControllerDataForJoystickId(ev->which); @@ -483,8 +661,8 @@ bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev return false; const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, ev->axis)); - const float value = static_cast<float>(ev->value) / (ev->value < 0 ? 32768.0f : 32767.0f); - return InputManager::InvokeEvents(key, value, GenericInputBinding::Unknown); + InputManager::InvokeEvents(key, NormalizeS16(ev->value)); + return true; } bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev) @@ -497,7 +675,62 @@ bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ? s_sdl_generic_binding_button_mapping[ev->button] : GenericInputBinding::Unknown; - return InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key); + InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key); + return true; +} + +bool SDLInputSource::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev) +{ + auto it = GetControllerDataForJoystickId(ev->which); + if (it == m_controllers.end()) + return false; + if (ev->axis < it->joy_axis_used_in_gc.size() && it->joy_axis_used_in_gc[ev->axis]) + return false; // Will get handled by GC event + const u32 axis = ev->axis + static_cast<u32>(std::size(s_sdl_axis_names)); // Ensure we don't conflict with GC axes + const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, axis)); + InputManager::InvokeEvents(key, NormalizeS16(ev->value)); + return true; +} + +bool SDLInputSource::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev) +{ + auto it = GetControllerDataForJoystickId(ev->which); + if (it == m_controllers.end()) + return false; + if (ev->button < it->joy_button_used_in_gc.size() && it->joy_button_used_in_gc[ev->button]) + return false; // Will get handled by GC event + const u32 button = + ev->button + static_cast<u32>(std::size(s_sdl_button_names)); // Ensure we don't conflict with GC buttons + const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, button)); + InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f); + return true; +} + +bool SDLInputSource::HandleJoystickHatEvent(const SDL_JoyHatEvent* ev) +{ + auto it = GetControllerDataForJoystickId(ev->which); + if (it == m_controllers.end() || ev->hat >= it->last_hat_state.size()) + return false; + + const unsigned long last_direction = it->last_hat_state[ev->hat]; + it->last_hat_state[ev->hat] = ev->value; + + unsigned long changed_direction = last_direction ^ ev->value; + while (changed_direction != 0) + { + unsigned long pos; + _BitScanForward(&pos, changed_direction); + + const unsigned long mask = (1u << pos); + changed_direction &= ~mask; + + const InputBindingKey key(MakeGenericControllerHatKey(InputSourceType::SDL, it->player_id, ev->hat, + static_cast<u8>(pos), + static_cast<u32>(std::size(s_sdl_hat_direction_names)))); + InputManager::InvokeEvents(key, (last_direction & mask) ? 0.0f : 1.0f); + } + + return true; } std::vector<InputBindingKey> SDLInputSource::EnumerateMotors() @@ -581,7 +814,7 @@ bool SDLInputSource::GetGenericBindingMapping(const std::string_view& device, Ge } else { - // joysticks, which we haven't implemented yet anyway. + // joysticks have arbitrary axis numbers, so automapping isn't going to work here. return false; } } diff --git a/src/frontend-common/sdl_input_source.h b/src/frontend-common/sdl_input_source.h index 3ecdd3c8c..5daf1d341 100644 --- a/src/frontend-common/sdl_input_source.h +++ b/src/frontend-common/sdl_input_source.h @@ -36,22 +36,26 @@ public: bool ProcessSDLEvent(const SDL_Event* event); -private: - enum : int - { - MAX_NUM_AXES = 7, - MAX_NUM_BUTTONS = 16, - }; + SDL_Joystick* GetJoystickForDevice(const std::string_view& device); +private: struct ControllerData { SDL_Haptic* haptic; SDL_GameController* game_controller; + SDL_Joystick* joystick; u16 rumble_intensity[2]; int haptic_left_right_effect; int joystick_id; int player_id; bool use_game_controller_rumble; + + // Used to disable Joystick controls that are used in GameController inputs so we don't get double events + std::vector<bool> joy_button_used_in_gc; + std::vector<bool> joy_axis_used_in_gc; + + // Track last hat state so we can send "unpressed" events. + std::vector<u8> last_hat_state; }; using ControllerDataVector = std::vector<ControllerData>; @@ -65,14 +69,18 @@ private: ControllerDataVector::iterator GetControllerDataForPlayerId(int id); int GetFreePlayerId() const; - bool OpenGameController(int index); - bool CloseGameController(int joystick_index); - bool HandleControllerAxisEvent(const SDL_ControllerAxisEvent* event); - bool HandleControllerButtonEvent(const SDL_ControllerButtonEvent* event); + bool OpenDevice(int index, bool is_gamecontroller); + bool CloseDevice(int joystick_index); + bool HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev); + bool HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev); + bool HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev); + bool HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev); + bool HandleJoystickHatEvent(const SDL_JoyHatEvent* ev); void SendRumbleUpdate(ControllerData* cd); ControllerDataVector m_controllers; bool m_sdl_subsystem_initialized = false; bool m_controller_enhanced_mode = false; + std::vector<std::pair<std::string, std::string>> m_sdl_hints; }; diff --git a/src/frontend-common/xinput_source.cpp b/src/frontend-common/xinput_source.cpp index ee271f466..b461281af 100644 --- a/src/frontend-common/xinput_source.cpp +++ b/src/frontend-common/xinput_source.cpp @@ -260,7 +260,7 @@ std::optional<InputBindingKey> XInputSource::ParseKeyString(const std::string_vi // found an axis! key.source_subtype = InputSubclass::ControllerAxis; key.data = i; - key.negative = (binding[0] == '-'); + key.modifier = binding[0] == '-' ? InputModifier::Negate : InputModifier::None; return key; } } @@ -291,8 +291,8 @@ std::string XInputSource::ConvertKeyToString(InputBindingKey key) { if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_axis_names)) { - ret = StringUtil::StdStringFromFormat("XInput-%u/%c%s", key.source_index, key.negative ? '-' : '+', - s_axis_names[key.data]); + const char modifier = key.modifier == InputModifier::Negate ? '-' : '+'; + ret = StringUtil::StdStringFromFormat("XInput-%u/%c%s", key.source_index, modifier, s_axis_names[key.data]); } else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_button_names)) { @@ -382,16 +382,15 @@ void XInputSource::HandleControllerConnection(u32 index) cd.has_small_motor = caps.Vibration.wRightMotorSpeed != 0; cd.last_state = {}; - Host::OnInputDeviceConnected(StringUtil::StdStringFromFormat("XInput-%u", index), - StringUtil::StdStringFromFormat("XInput Controller %u", index)); + InputManager::OnInputDeviceConnected(StringUtil::StdStringFromFormat("XInput-%u", index), + StringUtil::StdStringFromFormat("XInput Controller %u", index)); } void XInputSource::HandleControllerDisconnection(u32 index) { Log_InfoPrintf("XInput controller %u disconnected.", index); + InputManager::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("XInput-%u", index)); m_controllers[index] = {}; - - Host::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("XInput-%u", index)); } void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state) diff --git a/src/util/ini_settings_interface.cpp b/src/util/ini_settings_interface.cpp index 48ce74dc9..758546e1d 100644 --- a/src/util/ini_settings_interface.cpp +++ b/src/util/ini_settings_interface.cpp @@ -186,28 +186,28 @@ bool INISettingsInterface::GetStringValue(const char* section, const char* key, return true; } -void INISettingsInterface::SetIntValue(const char* section, const char* key, int value) +void INISettingsInterface::SetIntValue(const char* section, const char* key, s32 value) { m_dirty = true; - m_ini.SetLongValue(section, key, static_cast<long>(value), nullptr, false, true); + m_ini.SetValue(section, key, StringUtil::ToChars(value).c_str(), nullptr, true); } void INISettingsInterface::SetUIntValue(const char* section, const char* key, u32 value) { m_dirty = true; - m_ini.SetLongValue(section, key, static_cast<long>(value), nullptr, false, true); + m_ini.SetValue(section, key, StringUtil::ToChars(value).c_str(), nullptr, true); } void INISettingsInterface::SetFloatValue(const char* section, const char* key, float value) { m_dirty = true; - m_ini.SetDoubleValue(section, key, static_cast<double>(value), nullptr, true); + m_ini.SetValue(section, key, StringUtil::ToChars(value).c_str(), nullptr, true); } void INISettingsInterface::SetDoubleValue(const char* section, const char* key, double value) { m_dirty = true; - m_ini.SetDoubleValue(section, key, value, nullptr, true); + m_ini.SetValue(section, key, StringUtil::ToChars(value).c_str(), nullptr, true); } void INISettingsInterface::SetBoolValue(const char* section, const char* key, bool value) @@ -282,3 +282,40 @@ bool INISettingsInterface::AddToStringList(const char* section, const char* key, m_ini.SetValue(section, key, item, nullptr, false); return true; } + +std::vector<std::pair<std::string, std::string>> INISettingsInterface::GetKeyValueList(const char* section) const +{ + using Entry = CSimpleIniA::Entry; + using KVEntry = std::pair<const char*, Entry>; + std::vector<KVEntry> entries; + std::vector<std::pair<std::string, std::string>> output; + std::list<Entry> keys, values; + if (m_ini.GetAllKeys(section, keys)) + { + for (Entry& key : keys) + { + if (!m_ini.GetAllValues(section, key.pItem, values)) // [[unlikely]] + { + Log_ErrorPrintf("Got no values for a key returned from GetAllKeys!"); + continue; + } + for (const Entry& value : values) + entries.emplace_back(key.pItem, value); + } + } + + std::sort(entries.begin(), entries.end(), + [](const KVEntry& a, const KVEntry& b) { return a.second.nOrder < b.second.nOrder; }); + for (const KVEntry& entry : entries) + output.emplace_back(entry.first, entry.second.pItem); + + return output; +} + +void INISettingsInterface::SetKeyValueList(const char* section, + const std::vector<std::pair<std::string, std::string>>& items) +{ + m_ini.Delete(section, nullptr); + for (const std::pair<std::string, std::string>& item : items) + m_ini.SetValue(section, item.first.c_str(), item.second.c_str(), nullptr, false); +} diff --git a/src/util/ini_settings_interface.h b/src/util/ini_settings_interface.h index 64d095451..8aa178db3 100644 --- a/src/util/ini_settings_interface.h +++ b/src/util/ini_settings_interface.h @@ -45,6 +45,9 @@ public: bool RemoveFromStringList(const char* section, const char* key, const char* item) override; bool AddToStringList(const char* section, const char* key, const char* item) override; + std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const override; + void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) override; + // default parameter overloads using SettingsInterface::GetBoolValue; using SettingsInterface::GetDoubleValue;