diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 65c599af7..9c196f679 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -101,4 +101,8 @@ bool ReadbackLinearImagesEnabled() { return g_config->readback_linear_images; } +const Keymap& GetKeymap() { + return g_config->keymap; +} + } // namespace Config diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 3b7f1d2fc..d8bf2709e 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -5,6 +5,8 @@ #include "common/subsystems.h" #include +#include +#include namespace Config { @@ -18,6 +20,8 @@ enum class ProfilerDirection { None, Network }; enum class OutputDirection { Silent, Console, File }; +using Keymap = std::vector; + constexpr uint32_t DEFAULT_CONSOLE_LANGUAGE = 1; constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29; @@ -41,6 +45,7 @@ struct ConfigOptions { bool spirv_debug_printf_enabled = false; bool renderdoc_enabled = false; bool readback_linear_images = false; + Keymap keymap; }; void Load(const ConfigOptions& cfg); @@ -72,6 +77,8 @@ bool SpirvDebugPrintfEnabled(); bool RenderDocEnabled(); bool ReadbackLinearImagesEnabled(); +const Keymap& GetKeymap(); + } // namespace Config #endif /* KYTY_COMMON_EMULATOR_CONFIG_H_ */ diff --git a/src/graphics/presentation/window/hostInput.cpp b/src/graphics/presentation/window/hostInput.cpp new file mode 100644 index 000000000..29c046294 --- /dev/null +++ b/src/graphics/presentation/window/hostInput.cpp @@ -0,0 +1,256 @@ +#include "graphics/presentation/window/hostInput.h" + +#include "SDL_keyboard.h" +#include "SDL_keycode.h" +#include "SDL_mouse.h" +#include "common/assert.h" +#include "common/emulatorConfig.h" +#include "libs/controller.h" + +#include +#include +#include +#include +#include +#include + +namespace Libs::Graphics { + +namespace { + +struct Binding { + SDL_Keycode key = SDLK_UNKNOWN; + uint8_t mouse_button = 0; + uint32_t pad_button = 0; +}; + +static constexpr std::array PAD_BUTTONS = { + std::pair {std::string_view("L3"), Controller::PAD_BUTTON_L3}, + std::pair {std::string_view("R3"), Controller::PAD_BUTTON_R3}, + std::pair {std::string_view("Options"), Controller::PAD_BUTTON_OPTIONS}, + std::pair {std::string_view("Up"), Controller::PAD_BUTTON_UP}, + std::pair {std::string_view("Right"), Controller::PAD_BUTTON_RIGHT}, + std::pair {std::string_view("Down"), Controller::PAD_BUTTON_DOWN}, + std::pair {std::string_view("Left"), Controller::PAD_BUTTON_LEFT}, + std::pair {std::string_view("L2"), Controller::PAD_BUTTON_L2}, + std::pair {std::string_view("R2"), Controller::PAD_BUTTON_R2}, + std::pair {std::string_view("L1"), Controller::PAD_BUTTON_L1}, + std::pair {std::string_view("R1"), Controller::PAD_BUTTON_R1}, + std::pair {std::string_view("Triangle"), Controller::PAD_BUTTON_TRIANGLE}, + std::pair {std::string_view("Circle"), Controller::PAD_BUTTON_CIRCLE}, + std::pair {std::string_view("Cross"), Controller::PAD_BUTTON_CROSS}, + std::pair {std::string_view("Square"), Controller::PAD_BUTTON_SQUARE}, + std::pair {std::string_view("TouchPad"), Controller::PAD_BUTTON_TOUCH_PAD}, +}; + +uint32_t PadButtonFromName(std::string_view name) { + const auto button = std::find_if(PAD_BUTTONS.begin(), PAD_BUTTONS.end(), + [name](const auto& item) { return item.first == name; }); + return button != PAD_BUTTONS.end() ? button->second : 0; +} + +SDL_Keycode NormalizeKey(SDL_Keycode key) { + switch (key) { + case SDLK_RSHIFT: return SDLK_LSHIFT; + case SDLK_RCTRL: return SDLK_LCTRL; + case SDLK_RALT: return SDLK_LALT; + case SDLK_RGUI: return SDLK_LGUI; + default: return key; + } +} + +uint8_t MouseButtonFromName(std::string_view name) { + static constexpr std::array buttons = { + std::pair {std::string_view("Mouse:Left"), uint8_t {SDL_BUTTON_LEFT}}, + std::pair {std::string_view("Mouse:Middle"), uint8_t {SDL_BUTTON_MIDDLE}}, + std::pair {std::string_view("Mouse:Right"), uint8_t {SDL_BUTTON_RIGHT}}, + std::pair {std::string_view("Mouse:X1"), uint8_t {SDL_BUTTON_X1}}, + std::pair {std::string_view("Mouse:X2"), uint8_t {SDL_BUTTON_X2}}, + }; + + const auto button = std::find_if(buttons.begin(), buttons.end(), + [name](const auto& item) { return item.first == name; }); + return button != buttons.end() ? button->second : 0; +} + +bool Conflicts(const Binding& first, const Binding& second) { + return first.pad_button == second.pad_button || + (first.key != SDLK_UNKNOWN && first.key == second.key) || + (first.mouse_button != 0 && first.mouse_button == second.mouse_button); +} + +class InputMap { +public: + InputMap(): m_custom(!Config::GetKeymap().empty()) { + for (const auto& value: Config::GetKeymap()) { + const std::string_view entry = value; + const auto split = entry.find('='); + + Binding binding; + if (split != std::string_view::npos) { + binding.pad_button = PadButtonFromName(entry.substr(0, split)); + const auto host_input = entry.substr(split + 1); + binding.mouse_button = MouseButtonFromName(host_input); + if (binding.mouse_button == 0) { + binding.key = NormalizeKey(SDL_GetKeyFromName(std::string(host_input).c_str())); + } + } + + const bool reserved = + binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1; + if (binding.pad_button == 0 || reserved || + (binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) { + EXIT("Invalid input mapping: %s\n", value.c_str()); + } + Add(binding); + } + } + [[nodiscard]] bool Custom() const { return m_custom; } + + [[nodiscard]] uint32_t FindKey(int key_code) const { + key_code = NormalizeKey(static_cast(key_code)); + const auto binding = + std::find_if(m_bindings.begin(), m_bindings.begin() + m_size, + [key_code](const auto& item) { return item.key == key_code; }); + return binding != m_bindings.begin() + m_size ? binding->pad_button : 0; + } + + [[nodiscard]] uint32_t FindMouseButton(uint8_t mouse_button) const { + const auto binding = std::find_if( + m_bindings.begin(), m_bindings.begin() + m_size, + [mouse_button](const auto& item) { return item.mouse_button == mouse_button; }); + return binding != m_bindings.begin() + m_size ? binding->pad_button : 0; + } + +private: + void Add(const Binding& binding) { + for (std::size_t index = 0; index < m_size;) { + if (Conflicts(m_bindings[index], binding)) { + m_bindings[index] = m_bindings[--m_size]; + } else { + index++; + } + } + EXIT_IF(m_size >= m_bindings.size()); + m_bindings[m_size++] = binding; + } + + std::array m_bindings {}; + std::size_t m_size = 0; + bool m_custom; +}; + +const InputMap& GetInputMap() { + static const InputMap map; + return map; +} + +void SetButton(uint32_t button, bool down) { + if (button == Controller::PAD_BUTTON_L2) { + Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID, + Controller::Axis::TriggerLeft, down ? 255 : 0); + } else if (button == Controller::PAD_BUTTON_R2) { + Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID, + Controller::Axis::TriggerRight, down ? 255 : 0); + } else if (button != 0) { + Controller::ControllerButton(Controller::HOST_INPUT_CONTROLLER_ID, button, down); + } +} + +uint32_t DefaultKeyboardButton(int key_code) { + switch (NormalizeKey(static_cast(key_code))) { + case SDLK_UP: return Controller::PAD_BUTTON_UP; + case SDLK_LEFT: return Controller::PAD_BUTTON_LEFT; + case SDLK_DOWN: return Controller::PAD_BUTTON_DOWN; + case SDLK_RIGHT: return Controller::PAD_BUTTON_RIGHT; + case SDLK_j: return Controller::PAD_BUTTON_CROSS; + case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE; + case SDLK_k: return Controller::PAD_BUTTON_SQUARE; + case SDLK_l: return Controller::PAD_BUTTON_CIRCLE; + case SDLK_q: return Controller::PAD_BUTTON_L1; + case SDLK_e: return Controller::PAD_BUTTON_R1; + case SDLK_LSHIFT: return Controller::PAD_BUTTON_L3; + case SDLK_LCTRL: return Controller::PAD_BUTTON_R3; + case SDLK_RETURN: + case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS; + case SDLK_BACKSPACE: + case SDLK_TAB: return Controller::PAD_BUTTON_TOUCH_PAD; + default: return 0; + } +} + +struct StickKeys { + bool left = false; + bool right = false; + bool up = false; + bool down = false; +}; + +void SetStickAxis(Controller::Axis axis, bool negative, bool positive) { + const int value = negative == positive ? 128 : negative ? 0 : 255; + Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID, axis, value); +} + +void DefaultKeyboardInput(int key_code, bool down) { + static StickKeys left; + static StickKeys right; + + switch (NormalizeKey(static_cast(key_code))) { + case SDLK_a: + left.left = down; + SetStickAxis(Controller::Axis::LeftX, left.left, left.right); + return; + case SDLK_d: + left.right = down; + SetStickAxis(Controller::Axis::LeftX, left.left, left.right); + return; + case SDLK_w: + left.up = down; + SetStickAxis(Controller::Axis::LeftY, left.up, left.down); + return; + case SDLK_s: + left.down = down; + SetStickAxis(Controller::Axis::LeftY, left.up, left.down); + return; + case SDLK_f: + right.left = down; + SetStickAxis(Controller::Axis::RightX, right.left, right.right); + return; + case SDLK_h: + right.right = down; + SetStickAxis(Controller::Axis::RightX, right.left, right.right); + return; + case SDLK_t: + right.up = down; + SetStickAxis(Controller::Axis::RightY, right.up, right.down); + return; + case SDLK_g: + right.down = down; + SetStickAxis(Controller::Axis::RightY, right.up, right.down); + return; + default: SetButton(DefaultKeyboardButton(key_code), down); return; + } +} +} // namespace + +void HostInputInit() { + GetInputMap(); +} + +void HostInputKey(int key_code, bool down) { + const auto& map = GetInputMap(); + if (map.Custom()) { + SetButton(map.FindKey(key_code), down); + } else { + DefaultKeyboardInput(key_code, down); + } +} + +void HostInputMouseButton(uint8_t mouse_button, bool down) { + const auto& map = GetInputMap(); + if (map.Custom() && mouse_button != 0) { + SetButton(map.FindMouseButton(mouse_button), down); + } +} + +} // namespace Libs::Graphics diff --git a/src/graphics/presentation/window/hostInput.h b/src/graphics/presentation/window/hostInput.h new file mode 100644 index 000000000..92121c446 --- /dev/null +++ b/src/graphics/presentation/window/hostInput.h @@ -0,0 +1,14 @@ +#ifndef KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_ +#define KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_ + +#include + +namespace Libs::Graphics { + +void HostInputInit(); +void HostInputKey(int key_code, bool down); +void HostInputMouseButton(uint8_t mouse_button, bool down); + +} // namespace Libs::Graphics + +#endif /* KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_ */ diff --git a/src/graphics/presentation/window/window.cpp b/src/graphics/presentation/window/window.cpp index 59d0b2d91..89793eb24 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -32,6 +32,7 @@ #include "graphics/host_gpu/vma.h" #include "graphics/host_gpu/vulkanCommon.h" #include "graphics/presentation/renderDoc.h" +#include "graphics/presentation/window/hostInput.h" #include "graphics/presentation/window/windowInternal.h" #include "kytyGitVersion.h" #include "libs/controller.h" @@ -59,8 +60,6 @@ namespace Libs::Graphics { -constexpr int KEYBOARD_CONTROLLER_ID = -1000; - struct EventKeyboard { bool down; bool up; @@ -73,26 +72,6 @@ struct EventKeyboard { double timestamp_seconds; }; -static uint32_t KeyboardKeyToPadButton(int key_code) { - switch (key_code) { - case SDLK_w: return Controller::PAD_BUTTON_UP; - case SDLK_a: return Controller::PAD_BUTTON_LEFT; - case SDLK_s: return Controller::PAD_BUTTON_DOWN; - case SDLK_d: return Controller::PAD_BUTTON_RIGHT; - case SDLK_j: return Controller::PAD_BUTTON_CROSS; - case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE; - case SDLK_k: return Controller::PAD_BUTTON_SQUARE; - case SDLK_l: return Controller::PAD_BUTTON_CIRCLE; - case SDLK_q: return Controller::PAD_BUTTON_L1; - case SDLK_e: return Controller::PAD_BUTTON_R1; - case SDLK_RETURN: - case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS; - case SDLK_BACKSPACE: - case SDLK_TAB: return Controller::PAD_BUTTON_TOUCH_PAD; - default: return 0; - } -} - static uint32_t ControllerButtonToPadButton(int button) { switch (button) { case SDL_CONTROLLER_BUTTON_A: return Controller::PAD_BUTTON_CROSS; @@ -261,16 +240,11 @@ static void GameEventKeyboard(WindowLoopState& game, const EventKeyboard& key) { } } - const auto button = KeyboardKeyToPadButton(key.key_code); - if (button != 0 && (key.down || key.up) && !key.repeat) { - static bool keyboard_connected = false; - if (!keyboard_connected) { - Controller::ControllerConnect(KEYBOARD_CONTROLLER_ID); - keyboard_connected = true; - } - Controller::ControllerButton(KEYBOARD_CONTROLLER_ID, button, key.down); - } #endif + + if ((key.down || key.up) && !key.repeat) { + HostInputKey(key.key_code, key.down); + } } static void GameEventMouse([[maybe_unused]] const EventMouse& mb) { @@ -292,6 +266,23 @@ static void GameEventMouse([[maybe_unused]] const EventMouse& mb) { mb.x, mb.y); } #endif + + if (mb.down || mb.up) { + uint8_t mouse_button = 0; + if (mb.left) { + mouse_button = SDL_BUTTON_LEFT; + } else if (mb.middle) { + mouse_button = SDL_BUTTON_MIDDLE; + } else if (mb.right) { + mouse_button = SDL_BUTTON_RIGHT; + } else if (mb.x1) { + mouse_button = SDL_BUTTON_X1; + } else if (mb.x2) { + mouse_button = SDL_BUTTON_X2; + } + + HostInputMouseButton(mouse_button, mb.down); + } } static void GameEventFinger([[maybe_unused]] const EventFinger& f) { @@ -795,6 +786,7 @@ static void WindowCreate(WindowContext& context) { if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { EXIT("%s\n", SDL_GetError()); } + HostInputInit(); LOGF("WindowCreate(): width = %d, height = %d\n", width, height); diff --git a/src/launcher/CMakeLists.txt b/src/launcher/CMakeLists.txt index 1c3665493..45b2bbbf6 100644 --- a/src/launcher/CMakeLists.txt +++ b/src/launcher/CMakeLists.txt @@ -16,6 +16,7 @@ file(GLOB launcher_forms add_executable(launcher WIN32 ${launcher_src} ${launcher_forms} + resources.qrc ) diff --git a/src/launcher/forms/configuration_list_widget.ui b/src/launcher/forms/configuration_list_widget.ui index 0be1d1c4d..54f1fac53 100644 --- a/src/launcher/forms/configuration_list_widget.ui +++ b/src/launcher/forms/configuration_list_widget.ui @@ -69,6 +69,31 @@ + + + + + 36 + 36 + + + + Edit input mapping + + + + + + + 32 + 32 + + + + true + + + diff --git a/src/launcher/icons/edit-configuration.svg b/src/launcher/icons/edit-configuration.svg new file mode 100644 index 000000000..6223a1bda --- /dev/null +++ b/src/launcher/icons/edit-configuration.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/launcher/icons/global-settings.svg b/src/launcher/icons/global-settings.svg new file mode 100644 index 000000000..5f704d44d --- /dev/null +++ b/src/launcher/icons/global-settings.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/launcher/icons/input-mapping.svg b/src/launcher/icons/input-mapping.svg new file mode 100644 index 000000000..659fbdf0b --- /dev/null +++ b/src/launcher/icons/input-mapping.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/launcher/icons/remove-configuration.svg b/src/launcher/icons/remove-configuration.svg new file mode 100644 index 000000000..4a53ef64d --- /dev/null +++ b/src/launcher/icons/remove-configuration.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 81033583c..ca4a122cc 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -99,6 +99,7 @@ public: QString printf_output_file = "_kyty.txt"; ProfilerDirection profiler_direction = ProfilerDirection::None; bool renderdoc_enabled = false; + QStringList host_input_mapping; QString elf = QStringLiteral("eboot.bin"); @@ -118,6 +119,7 @@ public: printf_output_file = other.printf_output_file; profiler_direction = other.profiler_direction; renderdoc_enabled = other.renderdoc_enabled; + host_input_mapping = other.host_input_mapping; } void CopyFrom(const Configuration& other) { @@ -154,6 +156,7 @@ public: KYTY_CFG_SET(printf_output_file); KYTY_CFG_SET(profiler_direction); KYTY_CFG_SET(renderdoc_enabled); + s->setValue("host_input_mapping", host_input_mapping); KYTY_CFG_SET(elf); } @@ -180,7 +183,8 @@ public: KYTY_CFG_GET(printf_output_file); KYTY_CFG_GET(profiler_direction); KYTY_CFG_GET(renderdoc_enabled); - elf = s->value("elf", elf).toString(); + host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList(); + elf = s->value("elf", elf).toString(); } }; diff --git a/src/launcher/include/configurationListWidget.h b/src/launcher/include/configurationListWidget.h index 9f28ac2e3..ca0061170 100644 --- a/src/launcher/include/configurationListWidget.h +++ b/src/launcher/include/configurationListWidget.h @@ -38,7 +38,10 @@ public: [[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; } [[nodiscard]] const QStringList& GetGameDirectories() const { return m_game_dirs; } - [[nodiscard]] bool CanViewSelectedTrophies() const; + [[nodiscard]] const QStringList& GetHostInputMapping() const { + return m_global_info.host_input_mapping; + } + [[nodiscard]] bool CanViewSelectedTrophies() const; bool EnsureGameDirectory(); void ScanGameDirectory(); @@ -58,6 +61,7 @@ protected slots: void edit_configuration(); void delete_configuartion(); void edit_global_settings(); + void edit_input_mapping(); void run_configuration(); void list_currentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); void list_itemDoubleClicked(QTreeWidgetItem* witem, int column); diff --git a/src/launcher/include/inputMappingDialog.h b/src/launcher/include/inputMappingDialog.h new file mode 100644 index 000000000..280b3a0aa --- /dev/null +++ b/src/launcher/include/inputMappingDialog.h @@ -0,0 +1,31 @@ +#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ +#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ + +#include +#include + +class QPushButton; +class QTreeWidget; +class QTreeWidgetItem; +class QWidget; + +class InputMappingDialog final: public QDialog { +public: + explicit InputMappingDialog(const QStringList& mapping, QWidget* parent = nullptr); + + [[nodiscard]] QStringList Mapping() const; + +private: + void ChangeBinding(); + void ClearBinding(); + void RestoreDefaults(); + void SetBinding(QTreeWidgetItem* item, const QString& binding); + void UpdateButtons(); + + QTreeWidget* m_bindings = nullptr; + QPushButton* m_change_button = nullptr; + QPushButton* m_clear_button = nullptr; + bool m_customized = false; +}; + +#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */ diff --git a/src/launcher/resources.qrc b/src/launcher/resources.qrc new file mode 100644 index 000000000..b57e0d7db --- /dev/null +++ b/src/launcher/resources.qrc @@ -0,0 +1,8 @@ + + + icons/edit-configuration.svg + icons/global-settings.svg + icons/input-mapping.svg + icons/remove-configuration.svg + + diff --git a/src/launcher/src/configurationListWidget.cpp b/src/launcher/src/configurationListWidget.cpp index 0eeb039a7..aeb4aa61e 100644 --- a/src/launcher/src/configurationListWidget.cpp +++ b/src/launcher/src/configurationListWidget.cpp @@ -6,6 +6,7 @@ #include "configurationEditDialog.h" #include "configurationItem.h" #include "gameListTreeWidget.h" +#include "inputMappingDialog.h" #include "mainDialog.h" #include "patchesDialog.h" #include "trophyViewerDialog.h" @@ -169,6 +170,7 @@ static void ApplyGameListStyle(Ui::ConfigurationListWidget* ui) { "QToolButton:disabled { background: transparent; }")); ui->edit_button->setStyleSheet(ui->global_settings_button->styleSheet()); ui->delete_button->setStyleSheet(ui->global_settings_button->styleSheet()); + ui->input_mapping_button->setStyleSheet(ui->global_settings_button->styleSheet()); } static void AddSaveDataDir(QStringList* dirs, QSet* seen, const QString& root, @@ -226,10 +228,12 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent) m_ui->setupUi(this); ApplyGameListStyle(m_ui); - m_ui->global_settings_button->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView)); + m_ui->global_settings_button->setIcon(QIcon(QStringLiteral(":/icons/global-settings.svg"))); m_ui->global_settings_button->setToolTip(tr("Edit global settings and game folders")); - m_ui->edit_button->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); - m_ui->delete_button->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); + m_ui->input_mapping_button->setIcon(QIcon(QStringLiteral(":/icons/input-mapping.svg"))); + m_ui->input_mapping_button->setToolTip(tr("Edit global input mapping")); + m_ui->edit_button->setIcon(QIcon(QStringLiteral(":/icons/edit-configuration.svg"))); + m_ui->delete_button->setIcon(QIcon(QStringLiteral(":/icons/remove-configuration.svg"))); m_ui->delete_button->setEnabled(false); m_ui->edit_button->setEnabled(false); @@ -249,6 +253,8 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent) connect(m_ui->global_settings_button, &QToolButton::clicked, this, &ConfigurationListWidget::edit_global_settings); + connect(m_ui->input_mapping_button, &QToolButton::clicked, this, + &ConfigurationListWidget::edit_input_mapping); connect(m_ui->edit_button, &QToolButton::clicked, this, &ConfigurationListWidget::edit_configuration); connect(m_ui->delete_button, &QToolButton::clicked, this, @@ -717,6 +723,14 @@ void ConfigurationListWidget::edit_global_settings() { } } +void ConfigurationListWidget::edit_input_mapping() { + InputMappingDialog dialog(m_global_info.host_input_mapping, this); + if (dialog.exec() == QDialog::Accepted) { + m_global_info.host_input_mapping = dialog.Mapping(); + WriteSettings(); + } +} + void ConfigurationListWidget::ClearCustomSettings(ConfigurationItem* item) { delete m_custom_infos.take(item->GetInfo().game_path); } diff --git a/src/launcher/src/inputMappingDialog.cpp b/src/launcher/src/inputMappingDialog.cpp new file mode 100644 index 000000000..402e199e3 --- /dev/null +++ b/src/launcher/src/inputMappingDialog.cpp @@ -0,0 +1,311 @@ +#include "inputMappingDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int BINDING_COLUMN = 1; + +struct PadButton { + const char* id; + const char* label; + const char* default_binding; +}; + +constexpr PadButton PAD_BUTTONS[] = { + {"Up", "D-pad Up", "Up"}, + {"Down", "D-pad Down", "Down"}, + {"Left", "D-pad Left", "Left"}, + {"Right", "D-pad Right", "Right"}, + {"Triangle", "Triangle", "I"}, + {"Circle", "Circle", "L"}, + {"Cross", "Cross", "J"}, + {"Square", "Square", "K"}, + {"L1", "L1", "Q"}, + {"R1", "R1", "E"}, + {"L2", "L2", ""}, + {"R2", "R2", ""}, + {"L3", "L3", "Left Shift"}, + {"R3", "R3", "Left Ctrl"}, + {"Options", "Options", "Return"}, + {"TouchPad", "Touch pad", "Backspace"}, +}; + +QString KeypadName(int key) { + if (key >= Qt::Key_0 && key <= Qt::Key_9) { + return QStringLiteral("Keypad %1").arg(key - Qt::Key_0); + } + + switch (key) { + case Qt::Key_Return: + case Qt::Key_Enter: return QStringLiteral("Keypad Enter"); + case Qt::Key_Slash: return QStringLiteral("Keypad /"); + case Qt::Key_Asterisk: return QStringLiteral("Keypad *"); + case Qt::Key_Minus: return QStringLiteral("Keypad -"); + case Qt::Key_Plus: return QStringLiteral("Keypad +"); + case Qt::Key_Period: return QStringLiteral("Keypad ."); + case Qt::Key_Equal: return QStringLiteral("Keypad ="); + case Qt::Key_Comma: return QStringLiteral("Keypad ,"); + default: return {}; + } +} + +QString KeyName(const QKeyEvent& event) { + const int key = event.key(); + if (event.modifiers().testFlag(Qt::KeypadModifier)) { + return KeypadName(key); + } + if (key >= Qt::Key_A && key <= Qt::Key_Z) { + return QChar(key); + } + if (key >= Qt::Key_0 && key <= Qt::Key_9) { + return QChar(key); + } + if (key >= Qt::Key_F1 && key <= Qt::Key_F24) { + return QStringLiteral("F%1").arg(key - Qt::Key_F1 + 1); + } + + switch (key) { + case Qt::Key_Return: + case Qt::Key_Enter: return QStringLiteral("Return"); + case Qt::Key_Backspace: return QStringLiteral("Backspace"); + case Qt::Key_Tab: return QStringLiteral("Tab"); + case Qt::Key_Shift: return QStringLiteral("Left Shift"); + case Qt::Key_Control: return QStringLiteral("Left Ctrl"); + case Qt::Key_Alt: return QStringLiteral("Left Alt"); + case Qt::Key_Meta: return QStringLiteral("Left GUI"); + case Qt::Key_Insert: return QStringLiteral("Insert"); + case Qt::Key_Delete: return QStringLiteral("Delete"); + case Qt::Key_Home: return QStringLiteral("Home"); + case Qt::Key_End: return QStringLiteral("End"); + case Qt::Key_PageUp: return QStringLiteral("PageUp"); + case Qt::Key_PageDown: return QStringLiteral("PageDown"); + case Qt::Key_Left: return QStringLiteral("Left"); + case Qt::Key_Right: return QStringLiteral("Right"); + case Qt::Key_Up: return QStringLiteral("Up"); + case Qt::Key_Down: return QStringLiteral("Down"); + case Qt::Key_CapsLock: return QStringLiteral("CapsLock"); + case Qt::Key_NumLock: return QStringLiteral("Numlock"); + case Qt::Key_ScrollLock: return QStringLiteral("ScrollLock"); + case Qt::Key_Pause: return QStringLiteral("Pause"); + case Qt::Key_Print: return QStringLiteral("PrintScreen"); + default: break; + } + + if (event.modifiers() != Qt::NoModifier) { + return {}; + } + const auto name = QKeySequence(key).toString(QKeySequence::PortableText); + return name.size() == 1 ? name : QString(); +} + +class InputCaptureDialog final: public QDialog { +public: + explicit InputCaptureDialog(QWidget* parent): QDialog(parent) { + setWindowTitle(tr("Set Binding")); + setModal(true); + setMinimumWidth(360); + + auto* layout = new QVBoxLayout(this); + m_label = new QLabel( + tr("Press a key or mouse button.\nSpace and F1 are reserved; Esc cancels."), this); + m_label->setAlignment(Qt::AlignCenter); + layout->addWidget(m_label); + } + + [[nodiscard]] const QString& Binding() const { return m_binding; } + +protected: + void keyPressEvent(QKeyEvent* event) override { + if (event->isAutoRepeat()) { + return; + } + if (event->key() == Qt::Key_Escape) { + reject(); + return; + } + if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1) { + m_label->setText(tr("That key is reserved by the emulator.")); + return; + } + + m_binding = KeyName(*event); + if (!m_binding.isEmpty()) { + accept(); + } else { + m_label->setText(tr("That key is not supported.")); + } + } + + void mousePressEvent(QMouseEvent* event) override { + switch (event->button()) { + case Qt::LeftButton: m_binding = QStringLiteral("Mouse:Left"); break; + case Qt::RightButton: m_binding = QStringLiteral("Mouse:Right"); break; + case Qt::MiddleButton: m_binding = QStringLiteral("Mouse:Middle"); break; + case Qt::BackButton: m_binding = QStringLiteral("Mouse:X1"); break; + case Qt::ForwardButton: m_binding = QStringLiteral("Mouse:X2"); break; + default: return; + } + accept(); + } + +private: + QLabel* m_label = nullptr; + QString m_binding; +}; + +QHash ParseMapping(const QStringList& mapping) { + QHash result; + for (const auto& entry: mapping) { + const auto separator = entry.indexOf(QLatin1Char('=')); + if (separator > 0 && separator + 1 < entry.size()) { + const auto binding = entry.mid(separator + 1); + for (auto item = result.begin(); item != result.end();) { + if (item.value().compare(binding, Qt::CaseInsensitive) == 0) { + item = result.erase(item); + } else { + ++item; + } + } + result.insert(entry.left(separator), binding); + } + } + return result; +} + +} // namespace + +InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent) + : QDialog(parent), m_customized(!mapping.isEmpty()) { + setWindowTitle(tr("Input Mapping")); + resize(440, 520); + + auto* layout = new QVBoxLayout(this); + layout->addWidget(new QLabel(tr("Map keyboard or mouse input to DualSense buttons.\n" + "Defaults: left stick W/A/S/D, right stick T/F/G/H."), + this)); + + m_bindings = new QTreeWidget(this); + m_bindings->setColumnCount(2); + m_bindings->setHeaderLabels({tr("DualSense button"), tr("Host input")}); + m_bindings->setRootIsDecorated(false); + m_bindings->setSelectionMode(QAbstractItemView::SingleSelection); + m_bindings->header()->setSectionResizeMode(0, QHeaderView::Stretch); + m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch); + layout->addWidget(m_bindings); + + const auto parsed = ParseMapping(mapping); + for (const auto& button: PAD_BUTTONS) { + auto* item = new QTreeWidgetItem(m_bindings); + item->setText(0, tr(button.label)); + item->setData(0, Qt::UserRole, QString::fromLatin1(button.id)); + SetBinding(item, m_customized ? parsed.value(QString::fromLatin1(button.id)) + : QString::fromLatin1(button.default_binding)); + } + m_bindings->setCurrentItem(m_bindings->topLevelItem(0)); + + auto* controls = new QHBoxLayout; + m_change_button = new QPushButton(tr("Change..."), this); + m_clear_button = new QPushButton(tr("Clear"), this); + auto* defaults = new QPushButton(tr("Defaults"), this); + controls->addWidget(m_change_button); + controls->addWidget(m_clear_button); + controls->addWidget(defaults); + controls->addStretch(); + layout->addLayout(controls); + + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + layout->addWidget(buttons); + + connect(m_bindings, &QTreeWidget::itemDoubleClicked, this, + [this](QTreeWidgetItem*, int) { ChangeBinding(); }); + connect(m_bindings, &QTreeWidget::itemSelectionChanged, this, [this]() { UpdateButtons(); }); + connect(m_change_button, &QPushButton::clicked, this, [this]() { ChangeBinding(); }); + connect(m_clear_button, &QPushButton::clicked, this, [this]() { ClearBinding(); }); + connect(defaults, &QPushButton::clicked, this, [this]() { RestoreDefaults(); }); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + UpdateButtons(); +} + +QStringList InputMappingDialog::Mapping() const { + QStringList result; + if (!m_customized) { + return result; + } + for (int index = 0; index < m_bindings->topLevelItemCount(); index++) { + const auto* item = m_bindings->topLevelItem(index); + const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString(); + if (!binding.isEmpty()) { + result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding); + } + } + return result; +} + +void InputMappingDialog::ChangeBinding() { + auto* item = m_bindings->currentItem(); + if (item == nullptr) { + return; + } + + InputCaptureDialog dialog(this); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + for (int index = 0; index < m_bindings->topLevelItemCount(); index++) { + auto* other = m_bindings->topLevelItem(index); + if (other != item && other->data(BINDING_COLUMN, Qt::UserRole) + .toString() + .compare(dialog.Binding(), Qt::CaseInsensitive) == 0) { + SetBinding(other, {}); + } + } + SetBinding(item, dialog.Binding()); + m_customized = true; +} + +void InputMappingDialog::ClearBinding() { + SetBinding(m_bindings->currentItem(), {}); + m_customized = true; +} + +void InputMappingDialog::RestoreDefaults() { + for (int index = 0; index < m_bindings->topLevelItemCount(); index++) { + SetBinding(m_bindings->topLevelItem(index), + QString::fromLatin1(PAD_BUTTONS[index].default_binding)); + } + m_customized = false; +} + +void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) { + if (item == nullptr) { + return; + } + item->setData(BINDING_COLUMN, Qt::UserRole, binding); + item->setText(BINDING_COLUMN, binding.isEmpty() ? tr("None") : binding); + UpdateButtons(); +} + +void InputMappingDialog::UpdateButtons() { + if (m_change_button == nullptr) { + return; + } + const auto* item = m_bindings->currentItem(); + m_change_button->setEnabled(item != nullptr); + m_clear_button->setEnabled(item != nullptr && + !item->data(BINDING_COLUMN, Qt::UserRole).toString().isEmpty()); +} diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 25aa39543..9a3d7dd30 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -218,6 +218,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { args << "--printf-output-file" << info.printf_output_file; args << "--profiler-direction" << EnumToText(info.profiler_direction); args << "--spirv-debug-printf" << "false"; + for (const auto& binding: info.host_input_mapping) { + args << "--keymap" << binding; + } if (info.renderdoc_enabled) { args << "--rd"; } @@ -428,7 +431,10 @@ void MainDialogPrivate::Run() { m_running_item->SetRunning(true); - m_main_dialog->RunInterpreter(&m_process, m_running_item->GetInfo()); + Configuration info; + info.CopyFrom(m_running_item->GetInfo()); + info.host_input_mapping = m_ui->widget->GetHostInputMapping(); + m_main_dialog->RunInterpreter(&m_process, info); Update(); } diff --git a/src/libs/controller.cpp b/src/libs/controller.cpp index 2b9cecd6d..853244231 100644 --- a/src/libs/controller.cpp +++ b/src/libs/controller.cpp @@ -85,8 +85,6 @@ private: static GameController* g_controller = nullptr; -static constexpr int KEYBOARD_CONTROLLER_ID = -1000; - static uint8_t pad_connected_count_to_u8(int connected_count) { return static_cast(connected_count > 255 ? 255 : connected_count); } @@ -117,7 +115,7 @@ KYTY_SUBSYSTEM_INIT(Controller) { EXIT_IF(g_controller != nullptr); g_controller = new GameController; - g_controller->Connect(KEYBOARD_CONTROLLER_ID); + g_controller->Connect(HOST_INPUT_CONTROLLER_ID); } KYTY_SUBSYSTEM_UNEXPECTED_SHUTDOWN(Controller) {} @@ -155,7 +153,7 @@ void GameController::CheckActive() { if (!m_connected_ids.empty()) { new_active_id = m_connected_ids[0]; for (const auto id: m_connected_ids) { - if (id != KEYBOARD_CONTROLLER_ID) { + if (id != HOST_INPUT_CONTROLLER_ID) { new_active_id = id; break; } diff --git a/src/libs/controller.h b/src/libs/controller.h index 37c5cdf34..46224fbdb 100644 --- a/src/libs/controller.h +++ b/src/libs/controller.h @@ -9,6 +9,8 @@ namespace Libs::Controller { KYTY_SUBSYSTEM_DEFINE(Controller); +constexpr int HOST_INPUT_CONTROLLER_ID = -1000; + constexpr uint32_t PAD_BUTTON_L3 = 0x00000002; constexpr uint32_t PAD_BUTTON_R3 = 0x00000004; constexpr uint32_t PAD_BUTTON_OPTIONS = 0x00000008; diff --git a/src/main.cpp b/src/main.cpp index 21ad8bf02..88e9f02d7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -64,6 +64,7 @@ static void PrintUsage() { ::printf(" --spirv-debug-printf Enable SPIR-V debug printf.\n"); ::printf( " --readback-linear-images Read back writable linear images on submit.\n"); + ::printf(" --keymap DualSense mapping; may be repeated.\n"); ::printf(" --rd Enable RenderDoc capture.\n"); } @@ -247,6 +248,13 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } + } else if (arg == "--keymap") { + const auto split = value.find('='); + if (split == std::string::npos || split == 0 || split + 1 == value.size()) { + ::printf("invalid keymap: %s\n", value.c_str()); + return false; + } + options.config.keymap.push_back(value); } else { ::printf("unknown option: %s\n", arg.c_str()); return false;