mirror of https://github.com/PCSX2/pcsx2.git
InputManager: Add InputManager implementation
This commit is contained in:
parent
b511a54445
commit
14c9cfb310
|
@ -63,6 +63,12 @@ endif()
|
||||||
if(TARGET PkgConfig::SDL2)
|
if(TARGET PkgConfig::SDL2)
|
||||||
target_compile_definitions(PCSX2_FLAGS INTERFACE SDL_BUILD)
|
target_compile_definitions(PCSX2_FLAGS INTERFACE SDL_BUILD)
|
||||||
target_link_libraries(PCSX2_FLAGS INTERFACE PkgConfig::SDL2)
|
target_link_libraries(PCSX2_FLAGS INTERFACE PkgConfig::SDL2)
|
||||||
|
if(PCSX2_CORE)
|
||||||
|
target_sources(PCSX2 PRIVATE
|
||||||
|
Frontend/SDLInputSource.cpp
|
||||||
|
Frontend/SDLInputSource.h
|
||||||
|
)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
#ifndef PCSX2_CORE
|
#ifndef PCSX2_CORE
|
||||||
#include "gui/App.h"
|
#include "gui/App.h"
|
||||||
#else
|
#else
|
||||||
|
#include "PAD/Host/PAD.h"
|
||||||
#include "VMManager.h"
|
#include "VMManager.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -552,6 +553,11 @@ static __fi void VSyncStart(u32 sCycle)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef PCSX2_CORE
|
||||||
|
// Update vibration at the end of a frame.
|
||||||
|
PAD::Update();
|
||||||
|
#endif
|
||||||
|
|
||||||
frameLimit(); // limit FPS
|
frameLimit(); // limit FPS
|
||||||
gsPostVsyncStart(); // MUST be after framelimit; doing so before causes funk with frame times!
|
gsPostVsyncStart(); // MUST be after framelimit; doing so before causes funk with frame times!
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,929 @@
|
||||||
|
/* PCSX2 - PS2 Emulator for PCs
|
||||||
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||||
|
*
|
||||||
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||||
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||||
|
* ation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "PrecompiledHeader.h"
|
||||||
|
#include "Frontend/InputManager.h"
|
||||||
|
#include "Frontend/InputSource.h"
|
||||||
|
#include "PAD/Host/PAD.h"
|
||||||
|
#include "common/StringUtil.h"
|
||||||
|
#include "common/Timer.h"
|
||||||
|
#include "VMManager.h"
|
||||||
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <sstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Constants
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
enum : u32
|
||||||
|
{
|
||||||
|
MAX_KEYS_PER_BINDING = 4,
|
||||||
|
MAX_MOTORS_PER_PAD = 2,
|
||||||
|
FIRST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Mouse) + 1u,
|
||||||
|
LAST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Count),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Event Handler Type
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// This class acts as an adapter to convert from normalized values to
|
||||||
|
// binary values when the callback is a binary/button handler. That way
|
||||||
|
// you don't need to convert float->bool in your callbacks.
|
||||||
|
|
||||||
|
class InputEventHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputEventHandler()
|
||||||
|
{
|
||||||
|
new (&u.button) InputButtonEventHandler;
|
||||||
|
is_axis = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputEventHandler(InputButtonEventHandler button)
|
||||||
|
{
|
||||||
|
new (&u.button) InputButtonEventHandler(std::move(button));
|
||||||
|
is_axis = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputEventHandler(InputAxisEventHandler axis)
|
||||||
|
{
|
||||||
|
new (&u.axis) InputAxisEventHandler(std::move(axis));
|
||||||
|
is_axis = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputEventHandler(const InputEventHandler& copy)
|
||||||
|
{
|
||||||
|
if (copy.is_axis)
|
||||||
|
new (&u.axis) InputAxisEventHandler(copy.u.axis);
|
||||||
|
else
|
||||||
|
new (&u.button) InputButtonEventHandler(copy.u.button);
|
||||||
|
is_axis = copy.is_axis;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputEventHandler(InputEventHandler&& move)
|
||||||
|
{
|
||||||
|
if (move.is_axis)
|
||||||
|
new (&u.axis) InputAxisEventHandler(std::move(move.u.axis));
|
||||||
|
else
|
||||||
|
new (&u.button) InputButtonEventHandler(std::move(move.u.button));
|
||||||
|
is_axis = move.is_axis;
|
||||||
|
}
|
||||||
|
|
||||||
|
~InputEventHandler()
|
||||||
|
{
|
||||||
|
// call the right destructor... :D
|
||||||
|
if (is_axis)
|
||||||
|
u.axis.InputAxisEventHandler::~InputAxisEventHandler();
|
||||||
|
else
|
||||||
|
u.button.InputButtonEventHandler::~InputButtonEventHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
InputEventHandler& operator=(const InputEventHandler& copy)
|
||||||
|
{
|
||||||
|
InputEventHandler::~InputEventHandler();
|
||||||
|
|
||||||
|
if (copy.is_axis)
|
||||||
|
new (&u.axis) InputAxisEventHandler(copy.u.axis);
|
||||||
|
else
|
||||||
|
new (&u.button) InputButtonEventHandler(copy.u.button);
|
||||||
|
is_axis = copy.is_axis;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputEventHandler& operator=(InputEventHandler&& move)
|
||||||
|
{
|
||||||
|
InputEventHandler::~InputEventHandler();
|
||||||
|
|
||||||
|
if (move.is_axis)
|
||||||
|
new (&u.axis) InputAxisEventHandler(std::move(move.u.axis));
|
||||||
|
else
|
||||||
|
new (&u.button) InputButtonEventHandler(std::move(move.u.button));
|
||||||
|
is_axis = move.is_axis;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
__fi bool IsAxis() const { return is_axis; }
|
||||||
|
|
||||||
|
__fi void Invoke(float value) const
|
||||||
|
{
|
||||||
|
if (is_axis)
|
||||||
|
u.axis(value);
|
||||||
|
else
|
||||||
|
u.button(value > 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
union HandlerUnion
|
||||||
|
{
|
||||||
|
// constructor/destructor needs to be declared
|
||||||
|
HandlerUnion() {}
|
||||||
|
~HandlerUnion() {}
|
||||||
|
|
||||||
|
InputButtonEventHandler button;
|
||||||
|
InputAxisEventHandler axis;
|
||||||
|
} u;
|
||||||
|
|
||||||
|
bool is_axis;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Binding Type
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// This class tracks both the keys which make it up (for chords), as well
|
||||||
|
// as the state of all buttons. For button callbacks, it's fired when
|
||||||
|
// all keys go active, and for axis callbacks, when all are active and
|
||||||
|
// the value changes.
|
||||||
|
|
||||||
|
struct InputBinding
|
||||||
|
{
|
||||||
|
InputBindingKey keys[MAX_KEYS_PER_BINDING] = {};
|
||||||
|
InputEventHandler handler;
|
||||||
|
u8 num_keys = 0;
|
||||||
|
u8 full_mask = 0;
|
||||||
|
u8 current_mask = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadVibrationBinding
|
||||||
|
{
|
||||||
|
struct Motor
|
||||||
|
{
|
||||||
|
InputBindingKey binding;
|
||||||
|
u64 last_update_time;
|
||||||
|
InputSource* source;
|
||||||
|
float last_intensity;
|
||||||
|
};
|
||||||
|
|
||||||
|
u32 pad_index = 0;
|
||||||
|
Motor motors[MAX_MOTORS_PER_PAD] = {};
|
||||||
|
|
||||||
|
/// Returns true if the two motors are bound to the same host motor.
|
||||||
|
__fi bool AreMotorsCombined() const { return motors[0].binding == motors[1].binding; }
|
||||||
|
|
||||||
|
/// Returns the intensity when both motors are combined.
|
||||||
|
__fi float GetCombinedIntensity() const { return std::max(motors[0].last_intensity, motors[1].last_intensity); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Forward Declarations (for static qualifier)
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
namespace InputManager
|
||||||
|
{
|
||||||
|
static std::optional<InputBindingKey> ParseHostKeyboardKey(
|
||||||
|
const std::string_view& source, const std::string_view& sub_binding);
|
||||||
|
static std::optional<InputBindingKey> ParseHostMouseKey(
|
||||||
|
const std::string_view& source, const std::string_view& sub_binding);
|
||||||
|
|
||||||
|
static std::vector<std::string_view> SplitChord(const std::string_view& binding);
|
||||||
|
static bool SplitBinding(const std::string_view& binding, std::string_view* source, std::string_view* sub_binding);
|
||||||
|
static void AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler);
|
||||||
|
static bool ParseBindingAndGetSource(const std::string_view& binding, InputBindingKey* key, InputSource** source);
|
||||||
|
|
||||||
|
static void AddHotkeyBindings(SettingsInterface& si);
|
||||||
|
static void AddPadBindings(SettingsInterface& si, u32 pad, const char* default_type);
|
||||||
|
static void UpdateContinuedVibration();
|
||||||
|
|
||||||
|
static bool DoEventHook(InputBindingKey key, float value);
|
||||||
|
} // namespace InputManager
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Local Variables
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// This is a multimap containing any binds related to the specified key.
|
||||||
|
using BindingMap = std::unordered_multimap<InputBindingKey, std::shared_ptr<InputBinding>, InputBindingKeyHash>;
|
||||||
|
using VibrationBindingArray = std::vector<PadVibrationBinding>;
|
||||||
|
static BindingMap s_binding_map;
|
||||||
|
static VibrationBindingArray s_pad_vibration_array;
|
||||||
|
static std::mutex s_binding_map_write_lock;
|
||||||
|
|
||||||
|
// Hooks/intercepting (for setting bindings)
|
||||||
|
static std::mutex m_event_intercept_mutex;
|
||||||
|
static InputInterceptHook::Callback m_event_intercept_callback;
|
||||||
|
|
||||||
|
// Input sources. Keyboard/mouse don't exist here.
|
||||||
|
static std::array<std::unique_ptr<InputSource>, static_cast<u32>(InputSourceType::Count)> s_input_sources;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Hotkeys
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
static const HotkeyInfo* const s_hotkey_list[] = {g_vm_manager_hotkeys, g_host_hotkeys};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Binding Parsing
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
std::vector<std::string_view> InputManager::SplitChord(const std::string_view& binding)
|
||||||
|
{
|
||||||
|
std::vector<std::string_view> parts;
|
||||||
|
|
||||||
|
// under an if for RVO
|
||||||
|
if (!binding.empty())
|
||||||
|
{
|
||||||
|
std::string_view::size_type last = 0;
|
||||||
|
std::string_view::size_type next;
|
||||||
|
while ((next = binding.find('&', last)) != std::string_view::npos)
|
||||||
|
{
|
||||||
|
if (last != next)
|
||||||
|
{
|
||||||
|
std::string_view part(StringUtil::StripWhitespace(binding.substr(last, next - last)));
|
||||||
|
if (!part.empty())
|
||||||
|
parts.push_back(std::move(part));
|
||||||
|
}
|
||||||
|
last = next + 1;
|
||||||
|
}
|
||||||
|
if (last < (binding.size() - 1))
|
||||||
|
{
|
||||||
|
std::string_view part(StringUtil::StripWhitespace(binding.substr(last)));
|
||||||
|
if (!part.empty())
|
||||||
|
parts.push_back(std::move(part));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputManager::SplitBinding(
|
||||||
|
const std::string_view& binding, std::string_view* source, std::string_view* sub_binding)
|
||||||
|
{
|
||||||
|
const std::string_view::size_type slash_pos = binding.find('/');
|
||||||
|
if (slash_pos == std::string_view::npos)
|
||||||
|
{
|
||||||
|
Console.Warning("Malformed binding: '%*s'", static_cast<int>(binding.size()), binding.data());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*source = std::string_view(binding).substr(0, slash_pos);
|
||||||
|
*sub_binding = std::string_view(binding).substr(slash_pos + 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InputBindingKey> InputManager::ParseInputBindingKey(const std::string_view& binding)
|
||||||
|
{
|
||||||
|
std::string_view source, sub_binding;
|
||||||
|
if (!SplitBinding(binding, &source, &sub_binding))
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
// lameee, string matching
|
||||||
|
if (StringUtil::StartsWith(source, "Keyboard"))
|
||||||
|
{
|
||||||
|
return ParseHostKeyboardKey(source, sub_binding);
|
||||||
|
}
|
||||||
|
else if (StringUtil::StartsWith(source, "Mouse"))
|
||||||
|
{
|
||||||
|
return ParseHostMouseKey(source, sub_binding);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
||||||
|
{
|
||||||
|
if (s_input_sources[i])
|
||||||
|
{
|
||||||
|
std::optional<InputBindingKey> key = s_input_sources[i]->ParseKeyString(source, sub_binding);
|
||||||
|
if (key.has_value())
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputManager::ParseBindingAndGetSource(const std::string_view& binding, InputBindingKey* key, InputSource** source)
|
||||||
|
{
|
||||||
|
std::string_view source_string, sub_binding;
|
||||||
|
if (!SplitBinding(binding, &source_string, &sub_binding))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
||||||
|
{
|
||||||
|
if (s_input_sources[i])
|
||||||
|
{
|
||||||
|
std::optional<InputBindingKey> parsed_key = s_input_sources[i]->ParseKeyString(source_string, sub_binding);
|
||||||
|
if (parsed_key.has_value())
|
||||||
|
{
|
||||||
|
*key = parsed_key.value();
|
||||||
|
*source = s_input_sources[i].get();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string InputManager::ConvertInputBindingKeyToString(InputBindingKey key)
|
||||||
|
{
|
||||||
|
if (key.source_type == InputSourceType::Keyboard)
|
||||||
|
{
|
||||||
|
const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data));
|
||||||
|
if (str.has_value() && !str->empty())
|
||||||
|
return StringUtil::StdStringFromFormat("Keyboard/%s", str->c_str());
|
||||||
|
}
|
||||||
|
else if (key.source_type == InputSourceType::Mouse)
|
||||||
|
{
|
||||||
|
if (key.source_subtype == InputSubclass::MouseButton)
|
||||||
|
return StringUtil::StdStringFromFormat("Mouse%u/Button%u", key.source_index, key.data);
|
||||||
|
else if (key.source_subtype == InputSubclass::MousePointer)
|
||||||
|
return StringUtil::StdStringFromFormat("Mouse%u/Pointer%u", key.source_index, key.data);
|
||||||
|
else if (key.source_subtype == InputSubclass::MouseWheel)
|
||||||
|
return StringUtil::StdStringFromFormat(
|
||||||
|
"Mouse%u/Wheel%u%c", key.source_index, key.data, key.negative ? '-' : '+');
|
||||||
|
}
|
||||||
|
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::stringstream ss;
|
||||||
|
for (size_t i = 0; i < num_keys; i++)
|
||||||
|
{
|
||||||
|
const std::string keystr(ConvertInputBindingKeyToString(keys[i]));
|
||||||
|
if (keystr.empty())
|
||||||
|
return std::string();
|
||||||
|
|
||||||
|
if (i > 0)
|
||||||
|
ss << " & ";
|
||||||
|
|
||||||
|
ss << keystr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputManager::AddBindings(const std::vector<std::string>& bindings, const InputEventHandler& handler)
|
||||||
|
{
|
||||||
|
for (const std::string& binding : bindings)
|
||||||
|
{
|
||||||
|
std::shared_ptr<InputBinding> ibinding;
|
||||||
|
const std::vector<std::string_view> chord_bindings(SplitChord(binding));
|
||||||
|
|
||||||
|
for (const std::string_view& chord_binding : chord_bindings)
|
||||||
|
{
|
||||||
|
std::optional<InputBindingKey> key = ParseInputBindingKey(chord_binding);
|
||||||
|
if (!key.has_value())
|
||||||
|
{
|
||||||
|
Console.WriteLn("Invalid binding: '%s'", binding.c_str());
|
||||||
|
ibinding.reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ibinding)
|
||||||
|
{
|
||||||
|
ibinding = std::make_shared<InputBinding>();
|
||||||
|
ibinding->handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ibinding->num_keys == MAX_KEYS_PER_BINDING)
|
||||||
|
{
|
||||||
|
Console.WriteLn("Too many chord parts, max is %u (%s)", MAX_KEYS_PER_BINDING, binding.c_str());
|
||||||
|
ibinding.reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ibinding->keys[ibinding->num_keys] = key.value();
|
||||||
|
ibinding->full_mask |= (static_cast<u8>(1) << ibinding->num_keys);
|
||||||
|
ibinding->num_keys++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ibinding)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// plop it in the input map for all the keys
|
||||||
|
for (u32 i = 0; i < ibinding->num_keys; i++)
|
||||||
|
s_binding_map.emplace(ibinding->keys[i].MaskDirection(), ibinding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Key Decoders
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
InputBindingKey InputManager::MakeHostKeyboardKey(s32 key_code)
|
||||||
|
{
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = InputSourceType::Keyboard;
|
||||||
|
key.data = static_cast<u32>(key_code);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputBindingKey InputManager::MakeHostMouseButtonKey(s32 button_index)
|
||||||
|
{
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = InputSourceType::Mouse;
|
||||||
|
key.source_subtype = InputSubclass::MouseButton;
|
||||||
|
key.data = static_cast<u32>(button_index);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputBindingKey InputManager::MakeHostMouseWheelKey(s32 axis_index)
|
||||||
|
{
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = InputSourceType::Mouse;
|
||||||
|
key.source_subtype = InputSubclass::MouseWheel;
|
||||||
|
key.data = static_cast<u32>(axis_index);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Bind Encoders
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static std::array<const char*, static_cast<u32>(InputSourceType::Count)> s_input_class_names = {{
|
||||||
|
"Keyboard",
|
||||||
|
"Mouse",
|
||||||
|
#ifdef _WIN32
|
||||||
|
"XInput",
|
||||||
|
#endif
|
||||||
|
#ifdef SDL_BUILD
|
||||||
|
"SDL",
|
||||||
|
#endif
|
||||||
|
}};
|
||||||
|
|
||||||
|
InputSource* InputManager::GetInputSourceInterface(InputSourceType type)
|
||||||
|
{
|
||||||
|
return s_input_sources[static_cast<u32>(type)].get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* InputManager::InputSourceToString(InputSourceType clazz)
|
||||||
|
{
|
||||||
|
return s_input_class_names[static_cast<u32>(clazz)];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InputSourceType> InputManager::ParseInputSourceString(const std::string_view& str)
|
||||||
|
{
|
||||||
|
for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)
|
||||||
|
{
|
||||||
|
if (str == s_input_class_names[i])
|
||||||
|
return static_cast<InputSourceType>(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InputBindingKey> InputManager::ParseHostKeyboardKey(
|
||||||
|
const std::string_view& source, const std::string_view& sub_binding)
|
||||||
|
{
|
||||||
|
if (source != "Keyboard")
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const std::optional<s32> code = ConvertHostKeyboardStringToCode(sub_binding);
|
||||||
|
if (!code.has_value())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = InputSourceType::Keyboard;
|
||||||
|
key.data = static_cast<u32>(code.value());
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InputBindingKey> InputManager::ParseHostMouseKey(
|
||||||
|
const std::string_view& source, const std::string_view& sub_binding)
|
||||||
|
{
|
||||||
|
if (source != "Mouse")
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = InputSourceType::Mouse;
|
||||||
|
|
||||||
|
if (StringUtil::StartsWith(sub_binding, "Button"))
|
||||||
|
{
|
||||||
|
const std::optional<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
|
||||||
|
if (!button_number.has_value() || button_number.value() < 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
key.source_subtype = InputSubclass::MouseButton;
|
||||||
|
key.data = static_cast<u32>(button_number.value());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Binding Enumeration
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
std::vector<const HotkeyInfo*> InputManager::GetHotkeyList()
|
||||||
|
{
|
||||||
|
std::vector<const HotkeyInfo*> ret;
|
||||||
|
for (const HotkeyInfo* hotkey_list : s_hotkey_list)
|
||||||
|
{
|
||||||
|
for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
|
||||||
|
ret.push_back(hotkey);
|
||||||
|
}
|
||||||
|
std::sort(ret.begin(), ret.end(), [](const HotkeyInfo* left, const HotkeyInfo* right)
|
||||||
|
{
|
||||||
|
// category -> display name
|
||||||
|
if (const int res = StringUtil::Strcasecmp(left->category, right->category); res != 0)
|
||||||
|
return (res < 0);
|
||||||
|
return (StringUtil::Strcasecmp(left->display_name, right->display_name) < 0);
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputManager::AddHotkeyBindings(SettingsInterface& si)
|
||||||
|
{
|
||||||
|
for (const HotkeyInfo* hotkey_list : s_hotkey_list)
|
||||||
|
{
|
||||||
|
for (const HotkeyInfo* hotkey = hotkey_list; hotkey->name != nullptr; hotkey++)
|
||||||
|
{
|
||||||
|
const std::vector<std::string> bindings(si.GetStringList("Hotkeys", hotkey->name));
|
||||||
|
if (bindings.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AddBindings(bindings, InputButtonEventHandler{hotkey->handler});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputManager::AddPadBindings(SettingsInterface& si, u32 pad_index, const char* default_type)
|
||||||
|
{
|
||||||
|
const std::string section(StringUtil::StdStringFromFormat("Pad%u", pad_index + 1));
|
||||||
|
const std::string type(si.GetStringValue(section.c_str(), "Type", default_type));
|
||||||
|
if (type.empty() || type == "None")
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::vector<std::string> bind_names = PAD::GetControllerBinds(type);
|
||||||
|
if (!bind_names.empty())
|
||||||
|
{
|
||||||
|
for (u32 bind_index = 0; bind_index < static_cast<u32>(bind_names.size()); bind_index++)
|
||||||
|
{
|
||||||
|
const std::string& bind_name = bind_names[bind_index];
|
||||||
|
const std::vector<std::string> bindings(si.GetStringList(section.c_str(), bind_name.c_str()));
|
||||||
|
if (!bindings.empty())
|
||||||
|
{
|
||||||
|
// we use axes for all pad bindings to simplify things, and because they are pressure sensitive
|
||||||
|
AddBindings(bindings, InputAxisEventHandler{ [pad_index, bind_index, bind_names](float value) {
|
||||||
|
PAD::SetControllerState(pad_index, bind_index, value);
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAD::VibrationCapabilities vibcaps = PAD::GetControllerVibrationCapabilities(type);
|
||||||
|
if (vibcaps != PAD::VibrationCapabilities::NoVibration)
|
||||||
|
{
|
||||||
|
PadVibrationBinding vib;
|
||||||
|
vib.pad_index = pad_index;
|
||||||
|
|
||||||
|
bool has_any_bindings = false;
|
||||||
|
switch (vibcaps)
|
||||||
|
{
|
||||||
|
case PAD::VibrationCapabilities::LargeSmallMotors:
|
||||||
|
{
|
||||||
|
const std::string large_binding(si.GetStringValue(section.c_str(), "LargeMotor"));
|
||||||
|
const std::string small_binding(si.GetStringValue(section.c_str(), "SmallMotor"));
|
||||||
|
has_any_bindings |= ParseBindingAndGetSource(large_binding, &vib.motors[0].binding, &vib.motors[0].source);
|
||||||
|
has_any_bindings |= ParseBindingAndGetSource(small_binding, &vib.motors[1].binding, &vib.motors[1].source);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PAD::VibrationCapabilities::SingleMotor:
|
||||||
|
{
|
||||||
|
const std::string binding(si.GetStringValue(section.c_str(), "Motor"));
|
||||||
|
has_any_bindings |= ParseBindingAndGetSource(binding, &vib.motors[0].binding, &vib.motors[0].source);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_any_bindings)
|
||||||
|
s_pad_vibration_array.push_back(std::move(vib));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Event Handling
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool InputManager::HasAnyBindingsForKey(InputBindingKey key)
|
||||||
|
{
|
||||||
|
std::unique_lock lock(s_binding_map_write_lock);
|
||||||
|
return (s_binding_map.find(key.MaskDirection()) != s_binding_map.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputManager::InvokeEvents(InputBindingKey key, float value)
|
||||||
|
{
|
||||||
|
if (DoEventHook(key, value))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// find all the bindings associated with this key
|
||||||
|
const InputBindingKey masked_key = key.MaskDirection();
|
||||||
|
const auto range = s_binding_map.equal_range(masked_key);
|
||||||
|
if (range.first == s_binding_map.end())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
if (binding->keys[i].MaskDirection() != masked_key)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const u8 bit = static_cast<u8>(1) << i;
|
||||||
|
const bool negative = binding->keys[i].negative;
|
||||||
|
const bool new_state = (negative ? (value < 0.0f) : (value > 0.0f));
|
||||||
|
|
||||||
|
// update state based on whether the whole chord was activated
|
||||||
|
const u8 new_mask = (new_state ? (binding->current_mask | bit) : (binding->current_mask & ~bit));
|
||||||
|
const bool prev_full_state = (binding->current_mask == binding->full_mask);
|
||||||
|
const bool new_full_state = (new_mask == binding->full_mask);
|
||||||
|
binding->current_mask = new_mask;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// axes are fired regardless of a state change, unless they're zero
|
||||||
|
// for buttons, we can use the state of the last chord key, because it'll be 1 on press,
|
||||||
|
// and 0 on release (when the full state changes).
|
||||||
|
if (prev_full_state != new_full_state || (binding->handler.IsAxis() && value_to_pass > 0.0f))
|
||||||
|
{
|
||||||
|
binding->handler.Invoke(value_to_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bail out, since we shouldn't have the same key twice in the chord
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Vibration
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void InputManager::SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity)
|
||||||
|
{
|
||||||
|
for (PadVibrationBinding& pad : s_pad_vibration_array)
|
||||||
|
{
|
||||||
|
if (pad.pad_index != pad_index)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
PadVibrationBinding::Motor& large_motor = pad.motors[0];
|
||||||
|
PadVibrationBinding::Motor& small_motor = pad.motors[1];
|
||||||
|
if (large_motor.last_intensity == large_or_single_motor_intensity && small_motor.last_intensity == small_motor_intensity)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pad.AreMotorsCombined())
|
||||||
|
{
|
||||||
|
// if the motors are combined, we need to adjust to the maximum of both
|
||||||
|
const float report_intensity = std::max(large_or_single_motor_intensity, small_motor_intensity);
|
||||||
|
if (large_motor.source)
|
||||||
|
{
|
||||||
|
large_motor.last_update_time = Common::Timer::GetCurrentValue();
|
||||||
|
large_motor.source->UpdateMotorState(large_motor.binding, report_intensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (large_motor.source == small_motor.source)
|
||||||
|
{
|
||||||
|
// both motors are bound to the same source, do an optimal update
|
||||||
|
large_motor.last_update_time = Common::Timer::GetCurrentValue();
|
||||||
|
large_motor.source->UpdateMotorState(large_motor.binding, small_motor.binding, large_or_single_motor_intensity, small_motor_intensity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// update motors independently
|
||||||
|
if (large_motor.source && large_motor.last_intensity != large_or_single_motor_intensity)
|
||||||
|
{
|
||||||
|
large_motor.last_update_time = Common::Timer::GetCurrentValue();
|
||||||
|
large_motor.source->UpdateMotorState(large_motor.binding, large_or_single_motor_intensity);
|
||||||
|
}
|
||||||
|
if (small_motor.source && small_motor.last_intensity != small_motor_intensity)
|
||||||
|
{
|
||||||
|
small_motor.last_update_time = Common::Timer::GetCurrentValue();
|
||||||
|
small_motor.source->UpdateMotorState(small_motor.binding, small_motor_intensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
large_motor.last_intensity = large_or_single_motor_intensity;
|
||||||
|
small_motor.last_intensity = small_motor_intensity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputManager::PauseVibration()
|
||||||
|
{
|
||||||
|
for (PadVibrationBinding& binding : s_pad_vibration_array)
|
||||||
|
{
|
||||||
|
for (u32 motor_index = 0; motor_index < MAX_MOTORS_PER_PAD; motor_index++)
|
||||||
|
{
|
||||||
|
PadVibrationBinding::Motor& motor = binding.motors[motor_index];
|
||||||
|
if (!motor.source || motor.last_intensity == 0.0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// we deliberately don't zero the intensity here, so it can resume later
|
||||||
|
motor.last_update_time = 0;
|
||||||
|
motor.source->UpdateMotorState(motor.binding, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputManager::UpdateContinuedVibration()
|
||||||
|
{
|
||||||
|
// update vibration intensities, so if the game does a long effect, it continues
|
||||||
|
const u64 current_time = Common::Timer::GetCurrentValue();
|
||||||
|
for (PadVibrationBinding& pad : s_pad_vibration_array)
|
||||||
|
{
|
||||||
|
if (pad.AreMotorsCombined())
|
||||||
|
{
|
||||||
|
// motors are combined
|
||||||
|
PadVibrationBinding::Motor& large_motor = pad.motors[0];
|
||||||
|
if (!large_motor.source)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// so only check the first one
|
||||||
|
const double dt = Common::Timer::ConvertValueToSeconds(current_time - large_motor.last_update_time);
|
||||||
|
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// but take max of both motors for the intensity
|
||||||
|
const float intensity = pad.GetCombinedIntensity();
|
||||||
|
if (intensity == 0.0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
large_motor.last_update_time = current_time;
|
||||||
|
large_motor.source->UpdateMotorState(large_motor.binding, intensity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// independent motor control
|
||||||
|
for (u32 i = 0; i < MAX_MOTORS_PER_PAD; i++)
|
||||||
|
{
|
||||||
|
PadVibrationBinding::Motor& motor = pad.motors[i];
|
||||||
|
if (!motor.source || motor.last_intensity == 0.0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const double dt = Common::Timer::ConvertValueToSeconds(current_time - motor.last_update_time);
|
||||||
|
if (dt < VIBRATION_UPDATE_INTERVAL_SECONDS)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// re-notify the source of the continued effect
|
||||||
|
motor.last_update_time = current_time;
|
||||||
|
motor.source->UpdateMotorState(motor.binding, motor.last_intensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Hooks/Event Intercepting
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void InputManager::SetHook(InputInterceptHook::Callback callback)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||||
|
pxAssert(!m_event_intercept_callback);
|
||||||
|
m_event_intercept_callback = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputManager::RemoveHook()
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||||
|
if (m_event_intercept_callback)
|
||||||
|
m_event_intercept_callback = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputManager::HasHook()
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||||
|
return (bool)m_event_intercept_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputManager::DoEventHook(InputBindingKey key, float value)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||||
|
if (!m_event_intercept_callback)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const InputInterceptHook::CallbackResult action = m_event_intercept_callback(key, value);
|
||||||
|
if (action == InputInterceptHook::CallbackResult::StopMonitoring)
|
||||||
|
m_event_intercept_callback = {};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Binding Updater
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// TODO(Stenzek): Find a better place for this. Maybe in PAD?
|
||||||
|
static constexpr std::array<const char*, InputManager::MAX_PAD_NUMBER> s_default_pad_types = {{
|
||||||
|
"DualShock2", // Pad 1
|
||||||
|
"None" // Pad 2
|
||||||
|
}};
|
||||||
|
|
||||||
|
void InputManager::ReloadBindings(SettingsInterface& si)
|
||||||
|
{
|
||||||
|
PauseVibration();
|
||||||
|
|
||||||
|
std::unique_lock lock(s_binding_map_write_lock);
|
||||||
|
|
||||||
|
s_binding_map.clear();
|
||||||
|
s_pad_vibration_array.clear();
|
||||||
|
|
||||||
|
AddHotkeyBindings(si);
|
||||||
|
|
||||||
|
for (u32 pad = 0; pad < MAX_PAD_NUMBER; pad++)
|
||||||
|
AddPadBindings(si, pad, s_default_pad_types[pad]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Source Management
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void InputManager::CloseSources()
|
||||||
|
{
|
||||||
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
||||||
|
{
|
||||||
|
if (s_input_sources[i])
|
||||||
|
{
|
||||||
|
s_input_sources[i]->Shutdown();
|
||||||
|
s_input_sources[i].reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputManager::PollSources()
|
||||||
|
{
|
||||||
|
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
|
||||||
|
{
|
||||||
|
if (s_input_sources[i])
|
||||||
|
s_input_sources[i]->PollEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VMManager::GetState() == VMState::Running && !s_pad_vibration_array.empty())
|
||||||
|
UpdateContinuedVibration();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static void UpdateInputSourceState(SettingsInterface& si, InputSourceType type, bool default_state)
|
||||||
|
{
|
||||||
|
const bool old_state = (s_input_sources[static_cast<u32>(type)] != nullptr);
|
||||||
|
const bool new_state = si.GetBoolValue("InputSources", InputManager::InputSourceToString(type), default_state);
|
||||||
|
if (old_state == new_state)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (new_state)
|
||||||
|
{
|
||||||
|
std::unique_ptr<InputSource> source = std::make_unique<T>();
|
||||||
|
if (!source->Initialize(si))
|
||||||
|
{
|
||||||
|
Console.Error("(InputManager) Source '%s' failed to initialize.", InputManager::InputSourceToString(type));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_input_sources[static_cast<u32>(type)] = std::move(source);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s_input_sources[static_cast<u32>(type)]->Shutdown();
|
||||||
|
s_input_sources[static_cast<u32>(type)].reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include "Frontend/XInputSource.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef SDL_BUILD
|
||||||
|
#include "Frontend/SDLInputSource.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void InputManager::ReloadSources(SettingsInterface& si)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
UpdateInputSourceState<XInputSource>(si, InputSourceType::XInput, false);
|
||||||
|
#endif
|
||||||
|
#ifdef SDL_BUILD
|
||||||
|
UpdateInputSourceState<SDLInputSource>(si, InputSourceType::SDL, true);
|
||||||
|
#endif
|
||||||
|
}
|
|
@ -0,0 +1,210 @@
|
||||||
|
/* PCSX2 - PS2 Emulator for PCs
|
||||||
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||||
|
*
|
||||||
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||||
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||||
|
* ation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <variant>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "common/Pcsx2Types.h"
|
||||||
|
#include "common/SettingsInterface.h"
|
||||||
|
|
||||||
|
/// Class, or source of an input event.
|
||||||
|
enum class InputSourceType : u32
|
||||||
|
{
|
||||||
|
Keyboard,
|
||||||
|
Mouse,
|
||||||
|
#ifdef _WIN32
|
||||||
|
//DInput,
|
||||||
|
XInput,
|
||||||
|
#endif
|
||||||
|
#ifdef SDL_BUILD
|
||||||
|
SDL,
|
||||||
|
#endif
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Subtype of a key for an input source.
|
||||||
|
enum class InputSubclass : u32
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
MouseButton = 0,
|
||||||
|
MousePointer = 1,
|
||||||
|
MouseWheel = 2,
|
||||||
|
|
||||||
|
ControllerButton = 0,
|
||||||
|
ControllerAxis = 1,
|
||||||
|
ControllerMotor = 2,
|
||||||
|
ControllerHaptic = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A composite type representing a full input key which is part of an event.
|
||||||
|
union InputBindingKey
|
||||||
|
{
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
u32 data;
|
||||||
|
};
|
||||||
|
|
||||||
|
u64 bits;
|
||||||
|
|
||||||
|
bool operator==(const InputBindingKey& k) const { return bits == k.bits; }
|
||||||
|
bool operator!=(const InputBindingKey& k) const { return bits != k.bits; }
|
||||||
|
|
||||||
|
/// Removes the direction bit from the key, which is used to look up the bindings for it.
|
||||||
|
/// This is because negative bindings should still fire when they reach zero again.
|
||||||
|
InputBindingKey MaskDirection() const
|
||||||
|
{
|
||||||
|
InputBindingKey r;
|
||||||
|
r.bits = bits;
|
||||||
|
r.negative = false;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(InputBindingKey) == sizeof(u64), "Input binding key is 64 bits");
|
||||||
|
|
||||||
|
/// Hashability for InputBindingKey
|
||||||
|
struct InputBindingKeyHash
|
||||||
|
{
|
||||||
|
std::size_t operator()(const InputBindingKey& k) const { return std::hash<u64>{}(k.bits); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Callback type for a binary event. Usually used for hotkeys.
|
||||||
|
using InputButtonEventHandler = std::function<void(bool value)>;
|
||||||
|
|
||||||
|
/// Callback types for a normalized event. Usually used for pads.
|
||||||
|
using InputAxisEventHandler = std::function<void(float value)>;
|
||||||
|
|
||||||
|
/// Input monitoring for external access.
|
||||||
|
struct InputInterceptHook
|
||||||
|
{
|
||||||
|
enum class CallbackResult
|
||||||
|
{
|
||||||
|
StopMonitoring,
|
||||||
|
ContinueMonitoring
|
||||||
|
};
|
||||||
|
|
||||||
|
using Callback = std::function<CallbackResult(InputBindingKey key, float value)>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Hotkeys are actions (e.g. toggle frame limit) which can be bound to keys or chords.
|
||||||
|
struct HotkeyInfo
|
||||||
|
{
|
||||||
|
const char* name;
|
||||||
|
const char* category;
|
||||||
|
const char* display_name;
|
||||||
|
void(*handler)(bool pressed);
|
||||||
|
};
|
||||||
|
#define DECLARE_HOTKEY_LIST(name) extern const HotkeyInfo name[]
|
||||||
|
#define BEGIN_HOTKEY_LIST(name) const HotkeyInfo name[] = {
|
||||||
|
#define DEFINE_HOTKEY(name, category, display_name, handler) {(name), (category), (display_name), (handler)},
|
||||||
|
#define END_HOTKEY_LIST() {nullptr, nullptr, nullptr, nullptr} };
|
||||||
|
|
||||||
|
DECLARE_HOTKEY_LIST(g_vm_manager_hotkeys);
|
||||||
|
DECLARE_HOTKEY_LIST(g_host_hotkeys);
|
||||||
|
|
||||||
|
/// External input source class.
|
||||||
|
class InputSource;
|
||||||
|
|
||||||
|
namespace InputManager
|
||||||
|
{
|
||||||
|
/// Number of emulated pads. TODO: Multitap support.
|
||||||
|
static constexpr u32 MAX_PAD_NUMBER = 2;
|
||||||
|
|
||||||
|
/// Minimum interval between vibration updates when the effect is continuous.
|
||||||
|
static constexpr double VIBRATION_UPDATE_INTERVAL_SECONDS = 0.5; // 500ms
|
||||||
|
|
||||||
|
/// Returns a pointer to the external input source class, if present.
|
||||||
|
InputSource* GetInputSourceInterface(InputSourceType type);
|
||||||
|
|
||||||
|
/// Converts an input class to a string.
|
||||||
|
const char* InputSourceToString(InputSourceType clazz);
|
||||||
|
|
||||||
|
/// Parses an input class string.
|
||||||
|
std::optional<InputSourceType> ParseInputSourceString(const std::string_view& str);
|
||||||
|
|
||||||
|
/// Converts a key code from a human-readable string to an identifier.
|
||||||
|
std::optional<u32> ConvertHostKeyboardStringToCode(const std::string_view& str);
|
||||||
|
|
||||||
|
/// Converts a key code from an identifier to a human-readable string.
|
||||||
|
std::optional<std::string> ConvertHostKeyboardCodeToString(u32 code);
|
||||||
|
|
||||||
|
/// Creates a key for a host-specific key code.
|
||||||
|
InputBindingKey MakeHostKeyboardKey(s32 key_code);
|
||||||
|
|
||||||
|
/// Creates a key for a host-specific button.
|
||||||
|
InputBindingKey MakeHostMouseButtonKey(s32 button_index);
|
||||||
|
|
||||||
|
/// Creates a key for a host-specific mouse wheel axis (0 = vertical, 1 = horizontal).
|
||||||
|
InputBindingKey MakeHostMouseWheelKey(s32 axis_index);
|
||||||
|
|
||||||
|
/// Parses an input binding key string.
|
||||||
|
std::optional<InputBindingKey> ParseInputBindingKey(const std::string_view& binding);
|
||||||
|
|
||||||
|
/// Converts a input key to a string.
|
||||||
|
std::string ConvertInputBindingKeyToString(InputBindingKey key);
|
||||||
|
|
||||||
|
/// Converts a chord of binding keys to a string.
|
||||||
|
std::string ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys);
|
||||||
|
|
||||||
|
/// Returns a list of all hotkeys.
|
||||||
|
std::vector<const HotkeyInfo*> GetHotkeyList();
|
||||||
|
|
||||||
|
/// Re-parses the config and registers all hotkey and pad bindings.
|
||||||
|
void ReloadBindings(SettingsInterface& si);
|
||||||
|
|
||||||
|
/// Re-parses the sources part of the config and initializes any backends.
|
||||||
|
void ReloadSources(SettingsInterface& si);
|
||||||
|
|
||||||
|
/// Shuts down any enabled input sources.
|
||||||
|
void CloseSources();
|
||||||
|
|
||||||
|
/// Polls input sources for events (e.g. external controllers).
|
||||||
|
void PollSources();
|
||||||
|
|
||||||
|
/// Returns true if any bindings exist for the specified key.
|
||||||
|
/// This is the only function which can be safely called on another thread.
|
||||||
|
bool HasAnyBindingsForKey(InputBindingKey key);
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
void SetHook(InputInterceptHook::Callback callback);
|
||||||
|
|
||||||
|
/// Removes any currently-active interception hook.
|
||||||
|
void RemoveHook();
|
||||||
|
|
||||||
|
/// Returns true if there is an interception hook present.
|
||||||
|
bool HasHook();
|
||||||
|
|
||||||
|
/// Internal method used by pads to dispatch vibration updates to input sources.
|
||||||
|
/// Intensity is normalized from 0 to 1.
|
||||||
|
void SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity);
|
||||||
|
|
||||||
|
/// Zeros all vibration intensities. Call when pausing.
|
||||||
|
/// The pad vibration state will internally remain, so that when emulation is unpaused, the effect resumes.
|
||||||
|
void PauseVibration();
|
||||||
|
} // namespace InputManager
|
|
@ -0,0 +1,134 @@
|
||||||
|
/* PCSX2 - PS2 Emulator for PCs
|
||||||
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||||
|
*
|
||||||
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||||
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||||
|
* ation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "PrecompiledHeader.h"
|
||||||
|
#include "Frontend/InputSource.h"
|
||||||
|
#include "common/StringUtil.h"
|
||||||
|
|
||||||
|
InputSource::InputSource() = default;
|
||||||
|
|
||||||
|
InputSource::~InputSource() = default;
|
||||||
|
|
||||||
|
void InputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity)
|
||||||
|
{
|
||||||
|
if (large_key.bits != 0)
|
||||||
|
UpdateMotorState(large_key, large_intensity);
|
||||||
|
if (small_key.bits != 0)
|
||||||
|
UpdateMotorState(small_key, small_intensity);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputBindingKey InputSource::MakeGenericControllerAxisKey(InputSourceType clazz, u32 controller_index, s32 axis_index)
|
||||||
|
{
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = clazz;
|
||||||
|
key.source_index = controller_index;
|
||||||
|
key.source_subtype = InputSubclass::ControllerAxis;
|
||||||
|
key.data = static_cast<u32>(axis_index);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputBindingKey InputSource::MakeGenericControllerButtonKey(
|
||||||
|
InputSourceType clazz, u32 controller_index, s32 button_index)
|
||||||
|
{
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = clazz;
|
||||||
|
key.source_index = controller_index;
|
||||||
|
key.source_subtype = InputSubclass::ControllerButton;
|
||||||
|
key.data = static_cast<u32>(button_index);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputBindingKey InputSource::MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index)
|
||||||
|
{
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = clazz;
|
||||||
|
key.source_index = controller_index;
|
||||||
|
key.source_subtype = InputSubclass::ControllerMotor;
|
||||||
|
key.data = static_cast<u32>(motor_index);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InputBindingKey> InputSource::ParseGenericControllerKey(
|
||||||
|
InputSourceType clazz, const std::string_view& source, const std::string_view& sub_binding)
|
||||||
|
{
|
||||||
|
// try to find the number, this function doesn't care about whether it's xinput or sdl or whatever
|
||||||
|
std::string_view::size_type pos = 0;
|
||||||
|
while (pos < source.size())
|
||||||
|
{
|
||||||
|
if (source[pos] >= '0' && source[pos] <= '9')
|
||||||
|
break;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (pos == source.size())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const std::optional<s32> source_index = StringUtil::FromChars<s32>(source.substr(pos));
|
||||||
|
if (source_index.has_value() || source_index.value() < 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = clazz;
|
||||||
|
key.source_index = source_index.value();
|
||||||
|
|
||||||
|
if (StringUtil::StartsWith(sub_binding, "+Axis") || StringUtil::StartsWith(sub_binding, "-Axis"))
|
||||||
|
{
|
||||||
|
const std::optional<s32> axis_number = StringUtil::FromChars<s32>(sub_binding.substr(5));
|
||||||
|
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());
|
||||||
|
|
||||||
|
if (sub_binding[0] == '+')
|
||||||
|
key.negative = false;
|
||||||
|
else if (sub_binding[0] == '-')
|
||||||
|
key.negative = true;
|
||||||
|
else
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
else if (StringUtil::StartsWith(sub_binding, "Button"))
|
||||||
|
{
|
||||||
|
const std::optional<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
|
||||||
|
if (!button_number.has_value() || button_number.value() < 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
key.source_subtype = InputSubclass::ControllerButton;
|
||||||
|
key.data = static_cast<u32>(button_number.value());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
else if (key.source_subtype == InputSubclass::ControllerButton)
|
||||||
|
{
|
||||||
|
return StringUtil::StdStringFromFormat(
|
||||||
|
"%s%u/Button%u", InputManager::InputSourceToString(key.source_type), key.source_index, key.data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/* PCSX2 - PS2 Emulator for PCs
|
||||||
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||||
|
*
|
||||||
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||||
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||||
|
* ation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/Pcsx2Defs.h"
|
||||||
|
#include "Frontend/InputManager.h"
|
||||||
|
|
||||||
|
class SettingsInterface;
|
||||||
|
|
||||||
|
class InputSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputSource();
|
||||||
|
virtual ~InputSource();
|
||||||
|
|
||||||
|
virtual bool Initialize(SettingsInterface& si) = 0;
|
||||||
|
virtual void Shutdown() = 0;
|
||||||
|
|
||||||
|
virtual void PollEvents() = 0;
|
||||||
|
|
||||||
|
virtual std::optional<InputBindingKey> ParseKeyString(
|
||||||
|
const std::string_view& device, const std::string_view& binding) = 0;
|
||||||
|
virtual std::string ConvertKeyToString(InputBindingKey key) = 0;
|
||||||
|
|
||||||
|
/// Enumerates available vibration motors at the time of call.
|
||||||
|
virtual std::vector<InputBindingKey> EnumerateMotors() = 0;
|
||||||
|
|
||||||
|
/// Informs the source of a new vibration motor state. Changes may not take effect until the next PollEvents() call.
|
||||||
|
virtual void UpdateMotorState(InputBindingKey key, float intensity) = 0;
|
||||||
|
|
||||||
|
/// Concurrently update both motors where possible, to avoid redundant packets.
|
||||||
|
virtual void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity);
|
||||||
|
|
||||||
|
/// Creates a key for a generic controller axis event.
|
||||||
|
static InputBindingKey MakeGenericControllerAxisKey(InputSourceType clazz, u32 controller_index, s32 axis_index);
|
||||||
|
|
||||||
|
/// 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 motor event.
|
||||||
|
static InputBindingKey MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index);
|
||||||
|
|
||||||
|
/// Parses a generic controller key string.
|
||||||
|
static std::optional<InputBindingKey> ParseGenericControllerKey(
|
||||||
|
InputSourceType clazz, const std::string_view& source, const std::string_view& sub_binding);
|
||||||
|
|
||||||
|
/// Converts a generic controller key to a string.
|
||||||
|
static std::string ConvertGenericControllerKeyToString(InputBindingKey key);
|
||||||
|
};
|
|
@ -0,0 +1,513 @@
|
||||||
|
/* PCSX2 - PS2 Emulator for PCs
|
||||||
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||||
|
*
|
||||||
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||||
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||||
|
* ation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "PrecompiledHeader.h"
|
||||||
|
#include "Frontend/SDLInputSource.h"
|
||||||
|
#include "Frontend/InputManager.h"
|
||||||
|
#include "Host.h"
|
||||||
|
#include "HostSettings.h"
|
||||||
|
#include "common/Assertions.h"
|
||||||
|
#include "common/StringUtil.h"
|
||||||
|
#include "common/Console.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
static const char* s_sdl_axis_names[] = {
|
||||||
|
"LeftX", // SDL_CONTROLLER_AXIS_LEFTX
|
||||||
|
"LeftY", // SDL_CONTROLLER_AXIS_LEFTY
|
||||||
|
"RightX", // SDL_CONTROLLER_AXIS_RIGHTX
|
||||||
|
"RightY", // SDL_CONTROLLER_AXIS_RIGHTY
|
||||||
|
"LeftTrigger", // SDL_CONTROLLER_AXIS_TRIGGERLEFT
|
||||||
|
"RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* s_sdl_button_names[] = {
|
||||||
|
"A", // SDL_CONTROLLER_BUTTON_A
|
||||||
|
"B", // SDL_CONTROLLER_BUTTON_B
|
||||||
|
"X", // SDL_CONTROLLER_BUTTON_X
|
||||||
|
"Y", // SDL_CONTROLLER_BUTTON_Y
|
||||||
|
"Back", // SDL_CONTROLLER_BUTTON_BACK
|
||||||
|
"Guide", // SDL_CONTROLLER_BUTTON_GUIDE
|
||||||
|
"Start", // SDL_CONTROLLER_BUTTON_START
|
||||||
|
"LeftStick", // SDL_CONTROLLER_BUTTON_LEFTSTICK
|
||||||
|
"RightStick", // SDL_CONTROLLER_BUTTON_RIGHTSTICK
|
||||||
|
"LeftShoulder", // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
|
||||||
|
"RightShoulder", // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
|
||||||
|
"DPadUp", // SDL_CONTROLLER_BUTTON_DPAD_UP
|
||||||
|
"DPadDown", // SDL_CONTROLLER_BUTTON_DPAD_DOWN
|
||||||
|
"DPadLeft", // SDL_CONTROLLER_BUTTON_DPAD_LEFT
|
||||||
|
"DPadRight", // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
|
||||||
|
"Misc1", // SDL_CONTROLLER_BUTTON_MISC1
|
||||||
|
"Paddle1", // SDL_CONTROLLER_BUTTON_PADDLE1
|
||||||
|
"Paddle2", // SDL_CONTROLLER_BUTTON_PADDLE2
|
||||||
|
"Paddle3", // SDL_CONTROLLER_BUTTON_PADDLE3
|
||||||
|
"Paddle4", // SDL_CONTROLLER_BUTTON_PADDLE4
|
||||||
|
"Touchpad", // SDL_CONTROLLER_BUTTON_TOUCHPAD
|
||||||
|
};
|
||||||
|
|
||||||
|
SDLInputSource::SDLInputSource() = default;
|
||||||
|
|
||||||
|
SDLInputSource::~SDLInputSource() { pxAssert(m_controllers.empty()); }
|
||||||
|
|
||||||
|
bool SDLInputSource::Initialize(SettingsInterface& si)
|
||||||
|
{
|
||||||
|
std::optional<std::vector<u8>> controller_db_data = Host::ReadResourceFile("game_controller_db.txt");
|
||||||
|
if (controller_db_data.has_value())
|
||||||
|
{
|
||||||
|
SDL_RWops* ops = SDL_RWFromConstMem(controller_db_data->data(), static_cast<int>(controller_db_data->size()));
|
||||||
|
if (SDL_GameControllerAddMappingsFromRW(ops, true) < 0)
|
||||||
|
Console.Error("SDL_GameControllerAddMappingsFromRW() failed: %s", SDL_GetError());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error("Controller database resource is missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool ds4_rumble_enabled = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
|
||||||
|
if (ds4_rumble_enabled)
|
||||||
|
{
|
||||||
|
Console.WriteLn("Enabling PS4/PS5 enhanced mode.");
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4, "true");
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "true");
|
||||||
|
#endif
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 16)
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5, "true");
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "true");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
|
||||||
|
{
|
||||||
|
Console.Error("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we should open the controllers as the connected events come in, so no need to do any more here
|
||||||
|
m_sdl_subsystem_initialized = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLInputSource::Shutdown()
|
||||||
|
{
|
||||||
|
while (!m_controllers.empty())
|
||||||
|
CloseGameController(m_controllers.begin()->joystick_id);
|
||||||
|
|
||||||
|
if (m_sdl_subsystem_initialized)
|
||||||
|
{
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
|
||||||
|
m_sdl_subsystem_initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLInputSource::PollEvents()
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
SDL_Event ev;
|
||||||
|
if (SDL_PollEvent(&ev))
|
||||||
|
ProcessSDLEvent(&ev);
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InputBindingKey> SDLInputSource::ParseKeyString(
|
||||||
|
const std::string_view& device, const std::string_view& binding)
|
||||||
|
{
|
||||||
|
if (!StringUtil::StartsWith(device, "SDL-") || binding.empty())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
|
||||||
|
if (!player_id.has_value() || player_id.value() < 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = InputSourceType::SDL;
|
||||||
|
key.source_index = static_cast<u32>(player_id.value());
|
||||||
|
|
||||||
|
if (StringUtil::EndsWith(binding, "Motor"))
|
||||||
|
{
|
||||||
|
key.source_subtype = InputSubclass::ControllerMotor;
|
||||||
|
if (binding == "LargeMotor")
|
||||||
|
{
|
||||||
|
key.data = 0;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
else if (binding == "SmallMotor")
|
||||||
|
{
|
||||||
|
key.data = 1;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (StringUtil::EndsWith(binding, "Haptic"))
|
||||||
|
{
|
||||||
|
key.source_subtype = InputSubclass::ControllerHaptic;
|
||||||
|
key.data = 0;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
else if (binding[0] == '+' || binding[0] == '-')
|
||||||
|
{
|
||||||
|
// likely an axis
|
||||||
|
const std::string_view axis_name(binding.substr(1));
|
||||||
|
for (u32 i = 0; i < std::size(s_sdl_axis_names); i++)
|
||||||
|
{
|
||||||
|
if (axis_name == s_sdl_axis_names[i])
|
||||||
|
{
|
||||||
|
// found an axis!
|
||||||
|
key.source_subtype = InputSubclass::ControllerAxis;
|
||||||
|
key.data = i;
|
||||||
|
key.negative = (binding[0] == '-');
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// must be a button
|
||||||
|
for (u32 i = 0; i < std::size(s_sdl_button_names); i++)
|
||||||
|
{
|
||||||
|
if (binding == s_sdl_button_names[i])
|
||||||
|
{
|
||||||
|
key.source_subtype = InputSubclass::ControllerButton;
|
||||||
|
key.data = i;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown axis/button
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SDLInputSource::ConvertKeyToString(InputBindingKey key)
|
||||||
|
{
|
||||||
|
std::string ret;
|
||||||
|
|
||||||
|
if (key.source_type == InputSourceType::SDL)
|
||||||
|
{
|
||||||
|
if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_sdl_axis_names))
|
||||||
|
{
|
||||||
|
ret = StringUtil::StdStringFromFormat(
|
||||||
|
"SDL-%u/%c%s", key.source_index, key.negative ? '-' : '+', s_sdl_axis_names[key.data]);
|
||||||
|
}
|
||||||
|
else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_sdl_button_names))
|
||||||
|
{
|
||||||
|
ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]);
|
||||||
|
}
|
||||||
|
else if (key.source_subtype == InputSubclass::ControllerMotor)
|
||||||
|
{
|
||||||
|
ret = StringUtil::StdStringFromFormat("SDL-%u/%sMotor", key.source_index, key.data ? "Large" : "Small");
|
||||||
|
}
|
||||||
|
else if (key.source_subtype == InputSubclass::ControllerHaptic)
|
||||||
|
{
|
||||||
|
ret = StringUtil::StdStringFromFormat("SDL-%u/Haptic", key.source_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
|
||||||
|
{
|
||||||
|
switch (event->type)
|
||||||
|
{
|
||||||
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
|
{
|
||||||
|
Console.WriteLn("(SDLInputSource) Controller %d inserted", event->cdevice.which);
|
||||||
|
OpenGameController(event->cdevice.which);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_CONTROLLERDEVICEREMOVED:
|
||||||
|
{
|
||||||
|
Console.WriteLn("(SDLInputSource) Controller %d removed", event->cdevice.which);
|
||||||
|
CloseGameController(event->cdevice.which);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_CONTROLLERAXISMOTION:
|
||||||
|
return HandleControllerAxisEvent(&event->caxis);
|
||||||
|
|
||||||
|
case SDL_CONTROLLERBUTTONDOWN:
|
||||||
|
case SDL_CONTROLLERBUTTONUP:
|
||||||
|
return HandleControllerButtonEvent(&event->cbutton);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id)
|
||||||
|
{
|
||||||
|
return std::find_if(
|
||||||
|
m_controllers.begin(), m_controllers.end(), [id](const ControllerData& cd) { return cd.joystick_id == id; });
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForPlayerId(int id)
|
||||||
|
{
|
||||||
|
return std::find_if(
|
||||||
|
m_controllers.begin(), m_controllers.end(), [id](const ControllerData& cd) { return cd.player_id == id; });
|
||||||
|
}
|
||||||
|
|
||||||
|
int SDLInputSource::GetFreePlayerId() const
|
||||||
|
{
|
||||||
|
for (int player_id = 0;; player_id++)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
for (i = 0; i < m_controllers.size(); i++)
|
||||||
|
{
|
||||||
|
if (m_controllers[i].player_id == player_id)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (i == m_controllers.size())
|
||||||
|
return player_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLInputSource::OpenGameController(int index)
|
||||||
|
{
|
||||||
|
SDL_GameController* gcontroller = SDL_GameControllerOpen(index);
|
||||||
|
SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
|
||||||
|
if (!gcontroller || !joystick)
|
||||||
|
{
|
||||||
|
Console.Error("(SDLInputSource) Failed to open controller %d", index);
|
||||||
|
if (gcontroller)
|
||||||
|
SDL_GameControllerClose(gcontroller);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int joystick_id = SDL_JoystickInstanceID(joystick);
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
int player_id = SDL_GameControllerGetPlayerIndex(gcontroller);
|
||||||
|
#else
|
||||||
|
int player_id = -1;
|
||||||
|
#endif
|
||||||
|
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
|
||||||
|
{
|
||||||
|
const int free_player_id = GetFreePlayerId();
|
||||||
|
Console.Warning("(SDLInputSource) Controller %d (joystick %d) returned player ID %d, which is invalid or in "
|
||||||
|
"use. Using ID %d instead.",
|
||||||
|
index, joystick_id, player_id, free_player_id);
|
||||||
|
player_id = free_player_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLn("(SDLInputSource) Opened controller %d (instance id %d, player id %d): %s", index, joystick_id,
|
||||||
|
player_id, SDL_GameControllerName(gcontroller));
|
||||||
|
|
||||||
|
ControllerData cd = {};
|
||||||
|
cd.player_id = player_id;
|
||||||
|
cd.joystick_id = joystick_id;
|
||||||
|
cd.haptic_left_right_effect = -1;
|
||||||
|
cd.game_controller = gcontroller;
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
|
||||||
|
#else
|
||||||
|
cd.use_game_controller_rumble = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (cd.use_game_controller_rumble)
|
||||||
|
{
|
||||||
|
Console.WriteLn(
|
||||||
|
"(SDLInputSource) Rumble is supported on '%s' via gamecontroller", SDL_GameControllerName(gcontroller));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
|
||||||
|
if (haptic)
|
||||||
|
{
|
||||||
|
SDL_HapticEffect ef = {};
|
||||||
|
ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
|
||||||
|
ef.leftright.length = 1000;
|
||||||
|
|
||||||
|
int ef_id = SDL_HapticNewEffect(haptic, &ef);
|
||||||
|
if (ef_id >= 0)
|
||||||
|
{
|
||||||
|
cd.haptic = haptic;
|
||||||
|
cd.haptic_left_right_effect = ef_id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError());
|
||||||
|
if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
|
||||||
|
{
|
||||||
|
cd.haptic = haptic;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error("(SDLInputSource) No haptic rumble supported: %s", SDL_GetError());
|
||||||
|
SDL_HapticClose(haptic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cd.haptic)
|
||||||
|
Console.WriteLn(
|
||||||
|
"(SDLInputSource) Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cd.haptic && !cd.use_game_controller_rumble)
|
||||||
|
Console.Warning("(SDLInputSource) Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller));
|
||||||
|
|
||||||
|
m_controllers.push_back(std::move(cd));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLInputSource::CloseGameController(int joystick_index)
|
||||||
|
{
|
||||||
|
auto it = GetControllerDataForJoystickId(joystick_index);
|
||||||
|
if (it == m_controllers.end())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (it->haptic)
|
||||||
|
SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));
|
||||||
|
|
||||||
|
SDL_GameControllerClose(static_cast<SDL_GameController*>(it->game_controller));
|
||||||
|
m_controllers.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
|
||||||
|
{
|
||||||
|
auto it = GetControllerDataForJoystickId(ev->which);
|
||||||
|
if (it == m_controllers.end())
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
|
||||||
|
{
|
||||||
|
auto it = GetControllerDataForJoystickId(ev->which);
|
||||||
|
if (it == m_controllers.end())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, ev->button));
|
||||||
|
return InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<InputBindingKey> SDLInputSource::EnumerateMotors()
|
||||||
|
{
|
||||||
|
std::vector<InputBindingKey> ret;
|
||||||
|
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = InputSourceType::SDL;
|
||||||
|
|
||||||
|
for (ControllerData& cd : m_controllers)
|
||||||
|
{
|
||||||
|
key.source_index = cd.player_id;
|
||||||
|
|
||||||
|
if (cd.use_game_controller_rumble || cd.haptic_left_right_effect)
|
||||||
|
{
|
||||||
|
// two motors
|
||||||
|
key.source_subtype = InputSubclass::ControllerMotor;
|
||||||
|
key.data = 0;
|
||||||
|
ret.push_back(key);
|
||||||
|
key.data = 1;
|
||||||
|
ret.push_back(key);
|
||||||
|
}
|
||||||
|
else if (cd.haptic)
|
||||||
|
{
|
||||||
|
// haptic effect
|
||||||
|
key.source_subtype = InputSubclass::ControllerHaptic;
|
||||||
|
key.data = 0;
|
||||||
|
ret.push_back(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLInputSource::UpdateMotorState(InputBindingKey key, float intensity)
|
||||||
|
{
|
||||||
|
if (key.source_subtype != InputSubclass::ControllerMotor && key.source_subtype != InputSubclass::ControllerHaptic)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto it = GetControllerDataForPlayerId(key.source_index);
|
||||||
|
if (it == m_controllers.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
it->rumble_intensity[key.data] = static_cast<u16>(intensity * 65535.0f);
|
||||||
|
SendRumbleUpdate(&(*it));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity)
|
||||||
|
{
|
||||||
|
if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
|
||||||
|
small_key.source_subtype != InputSubclass::ControllerMotor)
|
||||||
|
{
|
||||||
|
// bonkers config where they're mapped to different controllers... who would do such a thing?
|
||||||
|
UpdateMotorState(large_key, large_intensity);
|
||||||
|
UpdateMotorState(small_key, small_intensity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = GetControllerDataForPlayerId(large_key.source_index);
|
||||||
|
if (it == m_controllers.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
it->rumble_intensity[large_key.data] = static_cast<u16>(large_intensity * 65535.0f);
|
||||||
|
it->rumble_intensity[small_key.data] = static_cast<u16>(small_intensity * 65535.0f);
|
||||||
|
SendRumbleUpdate(&(*it));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLInputSource::SendRumbleUpdate(ControllerData* cd)
|
||||||
|
{
|
||||||
|
// we'll update before this duration is elapsed
|
||||||
|
static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
if (cd->use_game_controller_rumble)
|
||||||
|
{
|
||||||
|
SDL_GameControllerRumble(cd->game_controller, cd->rumble_intensity[0], cd->rumble_intensity[1], DURATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (cd->haptic_left_right_effect >= 0)
|
||||||
|
{
|
||||||
|
if ((static_cast<u32>(cd->rumble_intensity[0]) + static_cast<u32>(cd->rumble_intensity[1])) > 0)
|
||||||
|
{
|
||||||
|
SDL_HapticEffect ef;
|
||||||
|
ef.type = SDL_HAPTIC_LEFTRIGHT;
|
||||||
|
ef.leftright.large_magnitude = cd->rumble_intensity[0];
|
||||||
|
ef.leftright.small_magnitude = cd->rumble_intensity[1];
|
||||||
|
ef.leftright.length = DURATION;
|
||||||
|
SDL_HapticUpdateEffect(cd->haptic, cd->haptic_left_right_effect, &ef);
|
||||||
|
SDL_HapticRunEffect(cd->haptic, cd->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SDL_HapticStopEffect(cd->haptic, cd->haptic_left_right_effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const float strength = static_cast<float>(std::max(cd->rumble_intensity[0], cd->rumble_intensity[1])) * (1.0f / 65535.0f);
|
||||||
|
if (strength > 0.0f)
|
||||||
|
SDL_HapticRumblePlay(cd->haptic, strength, DURATION);
|
||||||
|
else
|
||||||
|
SDL_HapticRumbleStop(cd->haptic);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/* PCSX2 - PS2 Emulator for PCs
|
||||||
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||||
|
*
|
||||||
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||||
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||||
|
* ation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "Frontend/InputSource.h"
|
||||||
|
#include "SDL.h"
|
||||||
|
#include <array>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class SettingsInterface;
|
||||||
|
|
||||||
|
class SDLInputSource final : public InputSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SDLInputSource();
|
||||||
|
~SDLInputSource();
|
||||||
|
|
||||||
|
bool Initialize(SettingsInterface& si) override;
|
||||||
|
void Shutdown() override;
|
||||||
|
|
||||||
|
void PollEvents() override;
|
||||||
|
std::vector<InputBindingKey> EnumerateMotors() override;
|
||||||
|
void UpdateMotorState(InputBindingKey key, float intensity) override;
|
||||||
|
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity) override;
|
||||||
|
|
||||||
|
std::optional<InputBindingKey> ParseKeyString(const std::string_view& device, const std::string_view& binding) override;
|
||||||
|
std::string ConvertKeyToString(InputBindingKey key) override;
|
||||||
|
|
||||||
|
bool ProcessSDLEvent(const SDL_Event* event);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum : int
|
||||||
|
{
|
||||||
|
MAX_NUM_AXES = 7,
|
||||||
|
MAX_NUM_BUTTONS = 16,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ControllerData
|
||||||
|
{
|
||||||
|
SDL_Haptic* haptic;
|
||||||
|
SDL_GameController* game_controller;
|
||||||
|
u16 rumble_intensity[2];
|
||||||
|
int haptic_left_right_effect;
|
||||||
|
int joystick_id;
|
||||||
|
int player_id;
|
||||||
|
bool use_game_controller_rumble;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ControllerDataVector = std::vector<ControllerData>;
|
||||||
|
|
||||||
|
ControllerDataVector::iterator GetControllerDataForJoystickId(int id);
|
||||||
|
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);
|
||||||
|
void SendRumbleUpdate(ControllerData* cd);
|
||||||
|
|
||||||
|
ControllerDataVector m_controllers;
|
||||||
|
|
||||||
|
bool m_sdl_subsystem_initialized = false;
|
||||||
|
};
|
|
@ -0,0 +1,368 @@
|
||||||
|
/* PCSX2 - PS2 Emulator for PCs
|
||||||
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||||
|
*
|
||||||
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||||
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||||
|
* ation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "PrecompiledHeader.h"
|
||||||
|
#include "Frontend/XInputSource.h"
|
||||||
|
#include "Frontend/InputManager.h"
|
||||||
|
#include "HostSettings.h"
|
||||||
|
#include "common/Assertions.h"
|
||||||
|
#include "common/StringUtil.h"
|
||||||
|
#include "common/Console.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
const char* XInputSource::s_axis_names[XInputSource::NUM_AXES] = {
|
||||||
|
"LeftX", // AXIS_LEFTX
|
||||||
|
"LeftY", // AXIS_LEFTY
|
||||||
|
"RightX", // AXIS_RIGHTX
|
||||||
|
"RightY", // AXIS_RIGHTY
|
||||||
|
"LeftTrigger", // AXIS_TRIGGERLEFT
|
||||||
|
"RightTrigger", // AXIS_TRIGGERRIGHT
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* XInputSource::s_button_names[XInputSource::NUM_BUTTONS] = {
|
||||||
|
"DPadUp", // XINPUT_GAMEPAD_DPAD_UP
|
||||||
|
"DPadDown", // XINPUT_GAMEPAD_DPAD_DOWN
|
||||||
|
"DPadLeft", // XINPUT_GAMEPAD_DPAD_LEFT
|
||||||
|
"DPadRight", // XINPUT_GAMEPAD_DPAD_RIGHT
|
||||||
|
"Start", // XINPUT_GAMEPAD_START
|
||||||
|
"Back", // XINPUT_GAMEPAD_BACK
|
||||||
|
"LeftStick", // XINPUT_GAMEPAD_LEFT_THUMB
|
||||||
|
"RightStick", // XINPUT_GAMEPAD_RIGHT_THUMB
|
||||||
|
"LeftShoulder", // XINPUT_GAMEPAD_LEFT_SHOULDER
|
||||||
|
"RightShoulder", // XINPUT_GAMEPAD_RIGHT_SHOULDER
|
||||||
|
"A", // XINPUT_GAMEPAD_A
|
||||||
|
"B", // XINPUT_GAMEPAD_B
|
||||||
|
"X", // XINPUT_GAMEPAD_X
|
||||||
|
"Y", // XINPUT_GAMEPAD_Y
|
||||||
|
"Guide", // XINPUT_GAMEPAD_GUIDE
|
||||||
|
};
|
||||||
|
const u16 XInputSource::s_button_masks[XInputSource::NUM_BUTTONS] = {
|
||||||
|
XINPUT_GAMEPAD_DPAD_UP,
|
||||||
|
XINPUT_GAMEPAD_DPAD_DOWN,
|
||||||
|
XINPUT_GAMEPAD_DPAD_LEFT,
|
||||||
|
XINPUT_GAMEPAD_DPAD_RIGHT,
|
||||||
|
XINPUT_GAMEPAD_START,
|
||||||
|
XINPUT_GAMEPAD_BACK,
|
||||||
|
XINPUT_GAMEPAD_LEFT_THUMB,
|
||||||
|
XINPUT_GAMEPAD_RIGHT_THUMB,
|
||||||
|
XINPUT_GAMEPAD_LEFT_SHOULDER,
|
||||||
|
XINPUT_GAMEPAD_RIGHT_SHOULDER,
|
||||||
|
XINPUT_GAMEPAD_A,
|
||||||
|
XINPUT_GAMEPAD_B,
|
||||||
|
XINPUT_GAMEPAD_X,
|
||||||
|
XINPUT_GAMEPAD_Y,
|
||||||
|
0x400, // XINPUT_GAMEPAD_GUIDE
|
||||||
|
};
|
||||||
|
|
||||||
|
XInputSource::XInputSource() = default;
|
||||||
|
|
||||||
|
XInputSource::~XInputSource() = default;
|
||||||
|
|
||||||
|
bool XInputSource::Initialize(SettingsInterface& si)
|
||||||
|
{
|
||||||
|
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
|
||||||
|
m_xinput_module = LoadLibraryW(L"xinput1_4");
|
||||||
|
if (!m_xinput_module)
|
||||||
|
{
|
||||||
|
m_xinput_module = LoadLibraryW(L"xinput1_3");
|
||||||
|
}
|
||||||
|
if (!m_xinput_module)
|
||||||
|
{
|
||||||
|
m_xinput_module = LoadLibraryW(L"xinput9_1_0");
|
||||||
|
}
|
||||||
|
if (!m_xinput_module)
|
||||||
|
{
|
||||||
|
Console.Error("Failed to load XInput module.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the hidden version of XInputGetState(), which lets us query the guide button.
|
||||||
|
m_xinput_get_state =
|
||||||
|
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, reinterpret_cast<LPCSTR>(100)));
|
||||||
|
if (!m_xinput_get_state)
|
||||||
|
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, "XInputGetState"));
|
||||||
|
m_xinput_set_state =
|
||||||
|
reinterpret_cast<decltype(m_xinput_set_state)>(GetProcAddress(m_xinput_module, "XInputSetState"));
|
||||||
|
m_xinput_get_capabilities =
|
||||||
|
reinterpret_cast<decltype(m_xinput_get_capabilities)>(GetProcAddress(m_xinput_module, "XInputGetCapabilities"));
|
||||||
|
#else
|
||||||
|
m_xinput_get_state = XInputGetState;
|
||||||
|
m_xinput_set_state = XInputSetState;
|
||||||
|
m_xinput_get_capabilities = XInputGetCapabilities;
|
||||||
|
#endif
|
||||||
|
if (!m_xinput_get_state || !m_xinput_set_state || !m_xinput_get_capabilities)
|
||||||
|
{
|
||||||
|
Console.Error("Failed to get XInput function pointers.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XInputSource::Shutdown()
|
||||||
|
{
|
||||||
|
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
|
||||||
|
if (m_xinput_module)
|
||||||
|
{
|
||||||
|
FreeLibrary(m_xinput_module);
|
||||||
|
m_xinput_module = nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
m_xinput_get_state = nullptr;
|
||||||
|
m_xinput_set_state = nullptr;
|
||||||
|
m_xinput_get_capabilities = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XInputSource::PollEvents()
|
||||||
|
{
|
||||||
|
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
||||||
|
{
|
||||||
|
XINPUT_STATE new_state;
|
||||||
|
const DWORD result = m_xinput_get_state(i, &new_state);
|
||||||
|
const bool was_connected = m_controllers[i].connected;
|
||||||
|
if (result == ERROR_SUCCESS)
|
||||||
|
{
|
||||||
|
if (!was_connected)
|
||||||
|
HandleControllerConnection(i);
|
||||||
|
|
||||||
|
CheckForStateChanges(i, new_state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (result != ERROR_DEVICE_NOT_CONNECTED)
|
||||||
|
Console.Warning("XInputGetState(%u) failed: 0x%08X / 0x%08X", i, result, GetLastError());
|
||||||
|
|
||||||
|
if (was_connected)
|
||||||
|
HandleControllerDisconnection(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InputBindingKey> XInputSource::ParseKeyString(
|
||||||
|
const std::string_view& device, const std::string_view& binding)
|
||||||
|
{
|
||||||
|
if (!StringUtil::StartsWith(device, "XInput-") || binding.empty())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
|
||||||
|
if (!player_id.has_value() || player_id.value() < 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
InputBindingKey key = {};
|
||||||
|
key.source_type = InputSourceType::XInput;
|
||||||
|
key.source_index = static_cast<u32>(player_id.value());
|
||||||
|
|
||||||
|
if (StringUtil::EndsWith(binding, "Motor"))
|
||||||
|
{
|
||||||
|
key.source_subtype = InputSubclass::ControllerMotor;
|
||||||
|
if (binding == "LargeMotor")
|
||||||
|
{
|
||||||
|
key.data = 0;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
else if (binding == "SmallMotor")
|
||||||
|
{
|
||||||
|
key.data = 1;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (binding[0] == '+' || binding[0] == '-')
|
||||||
|
{
|
||||||
|
// likely an axis
|
||||||
|
const std::string_view axis_name(binding.substr(1));
|
||||||
|
for (u32 i = 0; i < std::size(s_axis_names); i++)
|
||||||
|
{
|
||||||
|
if (axis_name == s_axis_names[i])
|
||||||
|
{
|
||||||
|
// found an axis!
|
||||||
|
key.source_subtype = InputSubclass::ControllerAxis;
|
||||||
|
key.data = i;
|
||||||
|
key.negative = (binding[0] == '-');
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// must be a button
|
||||||
|
for (u32 i = 0; i < std::size(s_button_names); i++)
|
||||||
|
{
|
||||||
|
if (binding == s_button_names[i])
|
||||||
|
{
|
||||||
|
key.source_subtype = InputSubclass::ControllerButton;
|
||||||
|
key.data = i;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown axis/button
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string XInputSource::ConvertKeyToString(InputBindingKey key)
|
||||||
|
{
|
||||||
|
std::string ret;
|
||||||
|
|
||||||
|
if (key.source_type == InputSourceType::XInput)
|
||||||
|
{
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_button_names))
|
||||||
|
{
|
||||||
|
ret = StringUtil::StdStringFromFormat("XInput-%u/%s", key.source_index, s_button_names[key.data]);
|
||||||
|
}
|
||||||
|
else if (key.source_subtype == InputSubclass::ControllerMotor)
|
||||||
|
{
|
||||||
|
ret = StringUtil::StdStringFromFormat("XInput-%u/%sMotor", key.source_index, key.data ? "Large" : "Small");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<InputBindingKey> XInputSource::EnumerateMotors()
|
||||||
|
{
|
||||||
|
std::vector<InputBindingKey> ret;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
||||||
|
{
|
||||||
|
const ControllerData& cd = m_controllers[i];
|
||||||
|
if (!cd.connected)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (cd.has_large_motor)
|
||||||
|
ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 0));
|
||||||
|
|
||||||
|
if (cd.has_small_motor)
|
||||||
|
ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XInputSource::HandleControllerConnection(u32 index)
|
||||||
|
{
|
||||||
|
Console.WriteLn("XInput controller %u connected.", index);
|
||||||
|
|
||||||
|
XINPUT_CAPABILITIES caps = {};
|
||||||
|
if (m_xinput_get_capabilities(index, 0, &caps) != ERROR_SUCCESS)
|
||||||
|
Console.Warning("Failed to get XInput capabilities for controller %u", index);
|
||||||
|
|
||||||
|
ControllerData& cd = m_controllers[index];
|
||||||
|
cd.connected = true;
|
||||||
|
cd.has_large_motor = caps.Vibration.wLeftMotorSpeed != 0;
|
||||||
|
cd.has_small_motor = caps.Vibration.wRightMotorSpeed != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XInputSource::HandleControllerDisconnection(u32 index)
|
||||||
|
{
|
||||||
|
Console.WriteLn("XInput controller %u disconnected.", index);
|
||||||
|
m_controllers[index] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state)
|
||||||
|
{
|
||||||
|
ControllerData& cd = m_controllers[index];
|
||||||
|
if (new_state.dwPacketNumber == cd.last_state.dwPacketNumber)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cd.last_state.dwPacketNumber = new_state.dwPacketNumber;
|
||||||
|
|
||||||
|
XINPUT_GAMEPAD& ogp = cd.last_state.Gamepad;
|
||||||
|
const XINPUT_GAMEPAD& ngp = new_state.Gamepad;
|
||||||
|
|
||||||
|
#define CHECK_AXIS(field, axis, min_value, max_value) \
|
||||||
|
if (ogp.field != ngp.field) \
|
||||||
|
{ \
|
||||||
|
InputManager::InvokeEvents( \
|
||||||
|
MakeGenericControllerAxisKey(InputSourceType::XInput, index, axis), \
|
||||||
|
static_cast<float>(ngp.field) / ((ngp.field < 0) ? min_value : max_value)); \
|
||||||
|
ogp.field = ngp.field; \
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK_AXIS(sThumbLX, AXIS_LEFTX, -32768, 32767);
|
||||||
|
CHECK_AXIS(sThumbLY, AXIS_LEFTY, -32768, 32767);
|
||||||
|
CHECK_AXIS(sThumbRX, AXIS_RIGHTX, -32768, 32767);
|
||||||
|
CHECK_AXIS(sThumbRY, AXIS_RIGHTY, -32768, 32767);
|
||||||
|
CHECK_AXIS(bLeftTrigger, AXIS_LEFTTRIGGER, -128, 127);
|
||||||
|
CHECK_AXIS(bRightTrigger, AXIS_RIGHTTRIGGER, -128, 127);
|
||||||
|
|
||||||
|
#undef CHECK_AXIS
|
||||||
|
|
||||||
|
const u16 old_button_bits = ogp.wButtons;
|
||||||
|
const u16 new_button_bits = ngp.wButtons;
|
||||||
|
if (old_button_bits != new_button_bits)
|
||||||
|
{
|
||||||
|
for (u32 button = 0; button < NUM_BUTTONS; button++)
|
||||||
|
{
|
||||||
|
const u16 button_mask = s_button_masks[button];
|
||||||
|
if ((old_button_bits & button_mask) != (new_button_bits & button_mask))
|
||||||
|
{
|
||||||
|
InputManager::InvokeEvents(
|
||||||
|
MakeGenericControllerButtonKey(InputSourceType::XInput, index, button),
|
||||||
|
(new_button_bits & button_mask) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ogp.wButtons = ngp.wButtons;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XInputSource::UpdateMotorState(InputBindingKey key, float intensity)
|
||||||
|
{
|
||||||
|
if (key.source_subtype != InputSubclass::ControllerMotor || key.source_index >= NUM_CONTROLLERS)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ControllerData& cd = m_controllers[key.source_index];
|
||||||
|
if (!cd.connected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const u16 i_intensity = static_cast<u16>(intensity * 65535.0f);
|
||||||
|
if (key.data != 0)
|
||||||
|
cd.last_vibration.wRightMotorSpeed = i_intensity;
|
||||||
|
else
|
||||||
|
cd.last_vibration.wLeftMotorSpeed = i_intensity;
|
||||||
|
|
||||||
|
m_xinput_set_state(key.source_index, &cd.last_vibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
void XInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity)
|
||||||
|
{
|
||||||
|
if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
|
||||||
|
small_key.source_subtype != InputSubclass::ControllerMotor)
|
||||||
|
{
|
||||||
|
// bonkers config where they're mapped to different controllers... who would do such a thing?
|
||||||
|
UpdateMotorState(large_key, large_intensity);
|
||||||
|
UpdateMotorState(small_key, small_intensity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControllerData& cd = m_controllers[large_key.source_index];
|
||||||
|
if (!cd.connected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cd.last_vibration.wLeftMotorSpeed = static_cast<u16>(large_intensity * 65535.0f);
|
||||||
|
cd.last_vibration.wRightMotorSpeed = static_cast<u16>(small_intensity * 65535.0f);
|
||||||
|
m_xinput_set_state(large_key.source_index, &cd.last_vibration);
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
/* PCSX2 - PS2 Emulator for PCs
|
||||||
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||||
|
*
|
||||||
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||||
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||||
|
* ation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
|
* PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "Frontend/InputSource.h"
|
||||||
|
#include <Xinput.h>
|
||||||
|
#include <array>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class SettingsInterface;
|
||||||
|
|
||||||
|
class XInputSource final : public InputSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
XInputSource();
|
||||||
|
~XInputSource();
|
||||||
|
|
||||||
|
bool Initialize(SettingsInterface& si) override;
|
||||||
|
void Shutdown() override;
|
||||||
|
|
||||||
|
void PollEvents() override;
|
||||||
|
std::vector<InputBindingKey> EnumerateMotors() override;
|
||||||
|
void UpdateMotorState(InputBindingKey key, float intensity) override;
|
||||||
|
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity) override;
|
||||||
|
|
||||||
|
std::optional<InputBindingKey> ParseKeyString(const std::string_view& device, const std::string_view& binding) override;
|
||||||
|
std::string ConvertKeyToString(InputBindingKey key) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum : u32
|
||||||
|
{
|
||||||
|
NUM_CONTROLLERS = XUSER_MAX_COUNT, // 4
|
||||||
|
NUM_BUTTONS = 15,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum : u32
|
||||||
|
{
|
||||||
|
AXIS_LEFTX,
|
||||||
|
AXIS_LEFTY,
|
||||||
|
AXIS_RIGHTX,
|
||||||
|
AXIS_RIGHTY,
|
||||||
|
AXIS_LEFTTRIGGER,
|
||||||
|
AXIS_RIGHTTRIGGER,
|
||||||
|
NUM_AXES,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ControllerData
|
||||||
|
{
|
||||||
|
XINPUT_STATE last_state = {};
|
||||||
|
XINPUT_VIBRATION last_vibration = {};
|
||||||
|
bool connected = false;
|
||||||
|
bool has_large_motor = false;
|
||||||
|
bool has_small_motor = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ControllerDataArray = std::array<ControllerData, NUM_CONTROLLERS>;
|
||||||
|
|
||||||
|
void CheckForStateChanges(u32 index, const XINPUT_STATE& new_state);
|
||||||
|
void HandleControllerConnection(u32 index);
|
||||||
|
void HandleControllerDisconnection(u32 index);
|
||||||
|
|
||||||
|
ControllerDataArray m_controllers;
|
||||||
|
|
||||||
|
HMODULE m_xinput_module{};
|
||||||
|
DWORD(WINAPI* m_xinput_get_state)(DWORD, XINPUT_STATE*);
|
||||||
|
DWORD(WINAPI* m_xinput_set_state)(DWORD, XINPUT_VIBRATION*);
|
||||||
|
DWORD(WINAPI* m_xinput_get_capabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*);
|
||||||
|
|
||||||
|
static const char* s_axis_names[NUM_AXES];
|
||||||
|
static const char* s_button_names[NUM_BUTTONS];
|
||||||
|
static const u16 s_button_masks[NUM_BUTTONS];
|
||||||
|
};
|
|
@ -32,6 +32,7 @@ private:
|
||||||
u8 m_button_pressure[GAMEPAD_NUMBER][MAX_KEYS];
|
u8 m_button_pressure[GAMEPAD_NUMBER][MAX_KEYS];
|
||||||
PADAnalog m_analog[GAMEPAD_NUMBER];
|
PADAnalog m_analog[GAMEPAD_NUMBER];
|
||||||
float m_axis_scale[GAMEPAD_NUMBER];
|
float m_axis_scale[GAMEPAD_NUMBER];
|
||||||
|
float m_vibration_scale[GAMEPAD_NUMBER][2];
|
||||||
|
|
||||||
public:
|
public:
|
||||||
KeyStatus();
|
KeyStatus();
|
||||||
|
@ -39,7 +40,9 @@ public:
|
||||||
|
|
||||||
void Set(u32 pad, u32 index, float value);
|
void Set(u32 pad, u32 index, float value);
|
||||||
|
|
||||||
void SetAxisScale(u32 pad, float scale) { m_axis_scale[pad] = scale; }
|
__fi void SetAxisScale(u32 pad, float scale) { m_axis_scale[pad] = scale; }
|
||||||
|
__fi float GetVibrationScale(u32 pad, u32 motor) const { return m_vibration_scale[pad][motor]; }
|
||||||
|
__fi void SetVibrationScale(u32 pad, u32 motor, float scale) { m_vibration_scale[pad][motor] = scale; }
|
||||||
|
|
||||||
u16 GetButtons(u32 pad);
|
u16 GetButtons(u32 pad);
|
||||||
u8 GetPressure(u32 pad, u32 index);
|
u8 GetPressure(u32 pad, u32 index);
|
||||||
|
|
|
@ -167,11 +167,20 @@ void PAD::LoadConfig(const SettingsInterface& si)
|
||||||
{
|
{
|
||||||
const std::string section(StringUtil::StdStringFromFormat("Pad%u", i + 1u));
|
const std::string section(StringUtil::StdStringFromFormat("Pad%u", i + 1u));
|
||||||
const float axis_scale = si.GetFloatValue(section.c_str(), "AxisScale", 1.0f);
|
const float axis_scale = si.GetFloatValue(section.c_str(), "AxisScale", 1.0f);
|
||||||
|
const float large_motor_scale = si.GetFloatValue(section.c_str(), "LargeMotorScale", 1.0f);
|
||||||
|
const float small_motor_scale = si.GetFloatValue(section.c_str(), "SmallMotorScale", 1.0f);
|
||||||
|
|
||||||
g_key_status.SetAxisScale(i, axis_scale);
|
g_key_status.SetAxisScale(i, axis_scale);
|
||||||
|
g_key_status.SetVibrationScale(i, 0, large_motor_scale);
|
||||||
|
g_key_status.SetVibrationScale(i, 1, small_motor_scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PAD::Update()
|
||||||
|
{
|
||||||
|
Pad::rumble_all();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> PAD::GetControllerTypeNames()
|
std::vector<std::string> PAD::GetControllerTypeNames()
|
||||||
{
|
{
|
||||||
return {"DualShock2"};
|
return {"DualShock2"};
|
||||||
|
@ -211,6 +220,18 @@ std::vector<std::string> PAD::GetControllerBinds(const std::string_view& type)
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PAD::VibrationCapabilities PAD::GetControllerVibrationCapabilities(const std::string_view& type)
|
||||||
|
{
|
||||||
|
if (type == "DualShock2")
|
||||||
|
{
|
||||||
|
return VibrationCapabilities::LargeSmallMotors;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return VibrationCapabilities::NoVibration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PAD::SetControllerState(u32 controller, u32 bind, float value)
|
void PAD::SetControllerState(u32 controller, u32 bind, float value)
|
||||||
{
|
{
|
||||||
if (controller >= GAMEPAD_NUMBER || bind >= MAX_KEYS)
|
if (controller >= GAMEPAD_NUMBER || bind >= MAX_KEYS)
|
||||||
|
|
|
@ -35,15 +35,29 @@ u8 PADpoll(u8 value);
|
||||||
|
|
||||||
namespace PAD
|
namespace PAD
|
||||||
{
|
{
|
||||||
|
enum class VibrationCapabilities
|
||||||
|
{
|
||||||
|
NoVibration,
|
||||||
|
LargeSmallMotors,
|
||||||
|
SingleMotor,
|
||||||
|
Count
|
||||||
|
};
|
||||||
|
|
||||||
/// Reloads configuration.
|
/// Reloads configuration.
|
||||||
void LoadConfig(const SettingsInterface& si);
|
void LoadConfig(const SettingsInterface& si);
|
||||||
|
|
||||||
|
/// Updates vibration and other internal state. Called at the *end* of a frame.
|
||||||
|
void Update();
|
||||||
|
|
||||||
/// Returns a list of controller type names.
|
/// Returns a list of controller type names.
|
||||||
std::vector<std::string> GetControllerTypeNames();
|
std::vector<std::string> GetControllerTypeNames();
|
||||||
|
|
||||||
/// Returns the list of binds for the specified controller type.
|
/// Returns the list of binds for the specified controller type.
|
||||||
std::vector<std::string> GetControllerBinds(const std::string_view& type);
|
std::vector<std::string> GetControllerBinds(const std::string_view& type);
|
||||||
|
|
||||||
|
/// Returns the vibration configuration for the specified controller type.
|
||||||
|
VibrationCapabilities GetControllerVibrationCapabilities(const std::string_view& type);
|
||||||
|
|
||||||
/// Sets the specified bind on a controller to the specified pressure (normalized to 0..1).
|
/// Sets the specified bind on a controller to the specified pressure (normalized to 0..1).
|
||||||
void SetControllerState(u32 controller, u32 bind, float value);
|
void SetControllerState(u32 controller, u32 bind, float value);
|
||||||
} // namespace PAD
|
} // namespace PAD
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
#include "PAD/Host/StateManagement.h"
|
#include "PAD/Host/StateManagement.h"
|
||||||
#include "PAD/Host/KeyStatus.h"
|
#include "PAD/Host/KeyStatus.h"
|
||||||
|
#include "Frontend/InputManager.h"
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
static bool __fi test_bit(T& value, int bit)
|
static bool __fi test_bit(T& value, int bit)
|
||||||
|
@ -113,17 +114,15 @@ void Pad::reset()
|
||||||
|
|
||||||
void Pad::rumble(unsigned port)
|
void Pad::rumble(unsigned port)
|
||||||
{
|
{
|
||||||
for (unsigned motor = 0; motor < 2; motor++)
|
if (nextVibrate[0] == currentVibrate[0] && nextVibrate[1] == currentVibrate[1])
|
||||||
{
|
return;
|
||||||
// TODO: Probably be better to send all of these at once.
|
|
||||||
if (nextVibrate[motor] | currentVibrate[motor])
|
|
||||||
{
|
|
||||||
currentVibrate[motor] = nextVibrate[motor];
|
|
||||||
|
|
||||||
// TODO: Implement in InputManager
|
currentVibrate[0] = nextVibrate[0];
|
||||||
// Device::DoRumble(motor, port);
|
currentVibrate[1] = nextVibrate[1];
|
||||||
}
|
InputManager::SetPadVibrationIntensity(port,
|
||||||
}
|
std::min(static_cast<float>(currentVibrate[0]) * g_key_status.GetVibrationScale(port, 0) * (1.0f / 255.0f), 1.0f),
|
||||||
|
std::min(static_cast<float>(currentVibrate[1]) * g_key_status.GetVibrationScale(port, 1) * (1.0f / 255.0f), 1.0f)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pad::stop_vibrate_all()
|
void Pad::stop_vibrate_all()
|
||||||
|
|
Loading…
Reference in New Issue