From 1eac1b2b3fc47885514f470fcdfb040405bb9800 Mon Sep 17 00:00:00 2001 From: Rosalie Wanders Date: Sat, 10 Sep 2022 18:42:56 +0200 Subject: [PATCH] Introduce RMG-Input (for linux only) --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 8 + Source/RMG-Core/Settings/Settings.cpp | 243 ++++- Source/RMG-Core/Settings/SettingsID.hpp | 82 ++ Source/RMG-Input/CMakeLists.txt | 44 + Source/RMG-Input/Thread/SDLThread.cpp | 92 ++ Source/RMG-Input/Thread/SDLThread.hpp | 48 + .../RMG-Input/UserInterface/EventFilter.cpp | 37 + .../RMG-Input/UserInterface/EventFilter.hpp | 37 + Source/RMG-Input/UserInterface/MainDialog.cpp | 327 ++++++ Source/RMG-Input/UserInterface/MainDialog.hpp | 82 ++ Source/RMG-Input/UserInterface/MainDialog.ui | 95 ++ .../RMG-Input/UserInterface/OptionsDialog.cpp | 43 + .../RMG-Input/UserInterface/OptionsDialog.hpp | 34 + .../RMG-Input/UserInterface/OptionsDialog.ui | 91 ++ .../UserInterface/Resource/Controller.svg | 114 +++ .../Resource/Controller_AnalogStick.svg | 56 ++ .../Resource/Controller_NoAnalogStick.svg | 104 ++ .../Resource/Controller_Pressed_A.svg | 50 + .../Controller_Pressed_AnalogStick.svg | 50 + .../Resource/Controller_Pressed_B.svg | 50 + .../Controller_Pressed_CButtonDown.svg | 50 + .../Controller_Pressed_CButtonLeft.svg | 50 + .../Controller_Pressed_CButtonRight.svg | 50 + .../Resource/Controller_Pressed_CButtonUp.svg | 50 + .../Resource/Controller_Pressed_DpadDown.svg | 50 + .../Resource/Controller_Pressed_DpadLeft.svg | 50 + .../Resource/Controller_Pressed_DpadRight.svg | 50 + .../Resource/Controller_Pressed_DpadUp.svg | 50 + .../Controller_Pressed_LeftTrigger.svg | 50 + .../Controller_Pressed_RightTrigger.svg | 50 + .../Resource/Controller_Pressed_Start.svg | 50 + .../RMG-Input/UserInterface/Resource/RMG.png | Bin 0 -> 49144 bytes .../RMG-Input/UserInterface/UIResources.qrc | 20 + .../Widget/ControllerImageWidget.cpp | 229 +++++ .../Widget/ControllerImageWidget.hpp | 57 ++ .../UserInterface/Widget/ControllerWidget.cpp | 874 ++++++++++++++++ .../UserInterface/Widget/ControllerWidget.hpp | 130 +++ .../UserInterface/Widget/ControllerWidget.ui | 951 ++++++++++++++++++ .../UserInterface/Widget/CustomButton.cpp | 113 +++ .../UserInterface/Widget/CustomButton.hpp | 67 ++ Source/RMG-Input/Utilities/InputDevice.cpp | 175 ++++ Source/RMG-Input/Utilities/InputDevice.hpp | 85 ++ Source/RMG-Input/Utilities/QtKeyToSdl2Key.cpp | 329 ++++++ Source/RMG-Input/Utilities/QtKeyToSdl2Key.hpp | 12 + Source/RMG-Input/common.hpp | 60 ++ Source/RMG-Input/main.cpp | 605 +++++++++++ 47 files changed, 5944 insertions(+), 2 deletions(-) create mode 100644 Source/RMG-Input/CMakeLists.txt create mode 100644 Source/RMG-Input/Thread/SDLThread.cpp create mode 100644 Source/RMG-Input/Thread/SDLThread.hpp create mode 100644 Source/RMG-Input/UserInterface/EventFilter.cpp create mode 100644 Source/RMG-Input/UserInterface/EventFilter.hpp create mode 100644 Source/RMG-Input/UserInterface/MainDialog.cpp create mode 100644 Source/RMG-Input/UserInterface/MainDialog.hpp create mode 100644 Source/RMG-Input/UserInterface/MainDialog.ui create mode 100644 Source/RMG-Input/UserInterface/OptionsDialog.cpp create mode 100644 Source/RMG-Input/UserInterface/OptionsDialog.hpp create mode 100644 Source/RMG-Input/UserInterface/OptionsDialog.ui create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_AnalogStick.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_NoAnalogStick.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_A.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_AnalogStick.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_B.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonDown.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonLeft.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonRight.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonUp.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadDown.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadLeft.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadRight.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadUp.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_LeftTrigger.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_RightTrigger.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/Controller_Pressed_Start.svg create mode 100644 Source/RMG-Input/UserInterface/Resource/RMG.png create mode 100644 Source/RMG-Input/UserInterface/UIResources.qrc create mode 100644 Source/RMG-Input/UserInterface/Widget/ControllerImageWidget.cpp create mode 100644 Source/RMG-Input/UserInterface/Widget/ControllerImageWidget.hpp create mode 100644 Source/RMG-Input/UserInterface/Widget/ControllerWidget.cpp create mode 100644 Source/RMG-Input/UserInterface/Widget/ControllerWidget.hpp create mode 100644 Source/RMG-Input/UserInterface/Widget/ControllerWidget.ui create mode 100644 Source/RMG-Input/UserInterface/Widget/CustomButton.cpp create mode 100644 Source/RMG-Input/UserInterface/Widget/CustomButton.hpp create mode 100644 Source/RMG-Input/Utilities/InputDevice.cpp create mode 100644 Source/RMG-Input/Utilities/InputDevice.hpp create mode 100644 Source/RMG-Input/Utilities/QtKeyToSdl2Key.cpp create mode 100644 Source/RMG-Input/Utilities/QtKeyToSdl2Key.hpp create mode 100644 Source/RMG-Input/common.hpp create mode 100644 Source/RMG-Input/main.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23194f72..1a0fb36d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: run: | sudo apt-get -qq update sudo apt-get upgrade - sudo apt-get -y install cmake libhidapi-dev libsamplerate0-dev libminizip-dev libsdl2-dev libfreetype6-dev libgl1-mesa-dev libglu1-mesa-dev pkg-config zlib1g-dev binutils-dev libspeexdsp-dev qt5-default build-essential nasm git zip + sudo apt-get -y install cmake libhidapi-dev libsamplerate0-dev libminizip-dev libsdl2-dev libfreetype6-dev libgl1-mesa-dev libglu1-mesa-dev pkg-config zlib1g-dev binutils-dev libspeexdsp-dev qt5-default libqt5svg5-dev build-essential nasm git zip - name: Prepare Environment run: | echo "GIT_REVISION=$(git describe --tags --always)" >> $GITHUB_ENV diff --git a/CMakeLists.txt b/CMakeLists.txt index d1ec22e0..466224cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,12 +44,20 @@ endif() add_subdirectory(Source/RMG-Core) add_subdirectory(Source/RMG) add_subdirectory(Source/RMG-Audio) +if (NOT WIN32) + add_subdirectory(Source/RMG-Input) +endif() install(TARGETS RMG DESTINATION ${RMG_INSTALL_PATH} ) install(TARGETS RMG-Audio DESTINATION ${PLUGIN_INSTALL_PATH}/Audio ) +if (NOT WIN32) + install(TARGETS RMG-Input + DESTINATION ${PLUGIN_INSTALL_PATH}/Input + ) +endif() if (WIN32) add_subdirectory(Source/Installer) diff --git a/Source/RMG-Core/Settings/Settings.cpp b/Source/RMG-Core/Settings/Settings.cpp index 38961839..a4f12036 100644 --- a/Source/RMG-Core/Settings/Settings.cpp +++ b/Source/RMG-Core/Settings/Settings.cpp @@ -173,7 +173,7 @@ static l_Setting get_setting(SettingsID settingId) #ifdef _WIN32 CoreGetPluginDirectory() + "\\Input\\libmupen64plus-input-qt.dll", #else - CoreGetPluginDirectory() + "/Input/libmupen64plus-input-qt.so", + CoreGetPluginDirectory() + "/Input/RMG-Input.so", #endif // _WIN32 }; break; @@ -391,6 +391,247 @@ static l_Setting get_setting(SettingsID settingId) case SettingsID::Audio_Muted: setting = {SETTING_SECTION_AUDIO, "Muted", false}; break; + + case SettingsID::Input_PluggedIn: + setting = {"", "PluggedIn"}; + break; + case SettingsID::Input_DeviceType: + setting = {"", "DeviceType"}; + break; + case SettingsID::Input_DeviceName: + setting = {"", "DeviceName"}; + break; + case SettingsID::Input_DeviceNum: + setting = {"", "DeviceNum"}; + break; + case SettingsID::Input_Range: + setting = {"", "Range"}; + break; + case SettingsID::Input_Deadzone: + setting = {"", "Deadzone"}; + break; + case SettingsID::Input_Pak: + setting = {"", "Pak"}; + break; + case SettingsID::Input_RemoveDuplicateMappings: + setting = {"", "RemoveDuplicateMappings"}; + break; + case SettingsID::Input_A_InputType: + setting = {"", "A_InputType"}; + break; + case SettingsID::Input_A_Name: + setting = {"", "A_Name"}; + break; + case SettingsID::Input_A_Data: + setting = {"", "A_Data"}; + break; + case SettingsID::Input_A_ExtraData: + setting = {"", "A_ExtraData"}; + break; + case SettingsID::Input_B_InputType: + setting = {"", "B_InputType"}; + break; + case SettingsID::Input_B_Name: + setting = {"", "B_Name"}; + break; + case SettingsID::Input_B_Data: + setting = {"", "B_Data"}; + break; + case SettingsID::Input_B_ExtraData: + setting = {"", "B_ExtraData"}; + break; + case SettingsID::Input_Start_InputType: + setting = {"", "Start_InputType"}; + break; + case SettingsID::Input_Start_Name: + setting = {"", "Start_Name"}; + break; + case SettingsID::Input_Start_Data: + setting = {"", "Start_Data"}; + break; + case SettingsID::Input_Start_ExtraData: + setting = {"", "Start_ExtraData"}; + break; + case SettingsID::Input_DpadUp_InputType: + setting = {"", "DpadUp_InputType"}; + break; + case SettingsID::Input_DpadUp_Name: + setting = {"", "DpadUp_Name"}; + break; + case SettingsID::Input_DpadUp_Data: + setting = {"", "DpadUp_Data"}; + break; + case SettingsID::Input_DpadUp_ExtraData: + setting = {"", "DpadUp_ExtraData"}; + break; + case SettingsID::Input_DpadDown_InputType: + setting = {"", "DpadDown_InputType"}; + break; + case SettingsID::Input_DpadDown_Name: + setting = {"", "DpadDown_Name"}; + break; + case SettingsID::Input_DpadDown_Data: + setting = {"", "DpadDown_Data"}; + break; + case SettingsID::Input_DpadDown_ExtraData: + setting = {"", "DpadDown_ExtraData"}; + break; + case SettingsID::Input_DpadLeft_InputType: + setting = {"", "DpadLeft_InputType"}; + break; + case SettingsID::Input_DpadLeft_Name: + setting = {"", "DpadLeft_Name"}; + break; + case SettingsID::Input_DpadLeft_Data: + setting = {"", "DpadLeft_Data"}; + break; + case SettingsID::Input_DpadLeft_ExtraData: + setting = {"", "DpadLeft_ExtraData"}; + break; + case SettingsID::Input_DpadRight_InputType: + setting = {"", "DpadRight_InputType"}; + break; + case SettingsID::Input_DpadRight_Name: + setting = {"", "DpadRight_Name"}; + break; + case SettingsID::Input_DpadRight_Data: + setting = {"", "DpadRight_Data"}; + break; + case SettingsID::Input_DpadRight_ExtraData: + setting = {"", "DpadRight_ExtraData"}; + break; + case SettingsID::Input_CButtonUp_InputType: + setting = {"", "CButtonUp_InputType"}; + break; + case SettingsID::Input_CButtonUp_Name: + setting = {"", "CButtonUp_Name"}; + break; + case SettingsID::Input_CButtonUp_Data: + setting = {"", "CButtonUp_Data"}; + break; + case SettingsID::Input_CButtonUp_ExtraData: + setting = {"", "CButtonUp_ExtraData"}; + break; + case SettingsID::Input_CButtonDown_InputType: + setting = {"", "CButtonDown_InputType"}; + break; + case SettingsID::Input_CButtonDown_Name: + setting = {"", "CButtonDown_Name"}; + break; + case SettingsID::Input_CButtonDown_Data: + setting = {"", "CButtonDown_Data"}; + break; + case SettingsID::Input_CButtonDown_ExtraData: + setting = {"", "CButtonDown_ExtraData"}; + break; + case SettingsID::Input_CButtonLeft_InputType: + setting = {"", "CButtonLeft_InputType"}; + break; + case SettingsID::Input_CButtonLeft_Name: + setting = {"", "CButtonLeft_Name"}; + break; + case SettingsID::Input_CButtonLeft_Data: + setting = {"", "CButtonLeft_Data"}; + break; + case SettingsID::Input_CButtonLeft_ExtraData: + setting = {"", "CButtonLeft_ExtraData"}; + break; + case SettingsID::Input_CButtonRight_InputType: + setting = {"", "CButtonRight_InputType"}; + break; + case SettingsID::Input_CButtonRight_Name: + setting = {"", "CButtonRight_Name"}; + break; + case SettingsID::Input_CButtonRight_Data: + setting = {"", "CButtonRight_Data"}; + break; + case SettingsID::Input_CButtonRight_ExtraData: + setting = {"", "CButtonRight_ExtraData"}; + break; + case SettingsID::Input_LeftTrigger_InputType: + setting = {"", "LeftTrigger_InputType"}; + break; + case SettingsID::Input_LeftTrigger_Name: + setting = {"", "LeftTrigger_Name"}; + break; + case SettingsID::Input_LeftTrigger_Data: + setting = {"", "LeftTrigger_Data"}; + break; + case SettingsID::Input_LeftTrigger_ExtraData: + setting = {"", "LeftTrigger_ExtraData"}; + break; + case SettingsID::Input_RightTrigger_InputType: + setting = {"", "RightTrigger_InputType"}; + break; + case SettingsID::Input_RightTrigger_Name: + setting = {"", "RightTrigger_Name"}; + break; + case SettingsID::Input_RightTrigger_Data: + setting = {"", "RightTrigger_Data"}; + break; + case SettingsID::Input_RightTrigger_ExtraData: + setting = {"", "RightTrigger_ExtraData"}; + break; + case SettingsID::Input_ZTrigger_InputType: + setting = {"", "ZTrigger_InputType"}; + break; + case SettingsID::Input_ZTrigger_Name: + setting = {"", "ZTrigger_Name"}; + break; + case SettingsID::Input_ZTrigger_Data: + setting = {"", "ZTrigger_Data"}; + break; + case SettingsID::Input_ZTrigger_ExtraData: + setting = {"", "ZTrigger_ExtraData"}; + break; + case SettingsID::Input_AnalogStickUp_InputType: + setting = {"", "AnalogStickUp_InputType"}; + break; + case SettingsID::Input_AnalogStickUp_Name: + setting = {"", "AnalogStickUp_Name"}; + break; + case SettingsID::Input_AnalogStickUp_Data: + setting = {"", "AnalogStickUp_Data"}; + break; + case SettingsID::Input_AnalogStickUp_ExtraData: + setting = {"", "AnalogStickUp_ExtraData"}; + break; + case SettingsID::Input_AnalogStickDown_InputType: + setting = {"", "AnalogStickDown_InputType"}; + break; + case SettingsID::Input_AnalogStickDown_Name: + setting = {"", "AnalogStickDown_Name"}; + break; + case SettingsID::Input_AnalogStickDown_Data: + setting = {"", "AnalogStickDown_Data"}; + break; + case SettingsID::Input_AnalogStickDown_ExtraData: + setting = {"", "AnalogStickDown_ExtraData"}; + break; + case SettingsID::Input_AnalogStickLeft_InputType: + setting = {"", "AnalogStickLeft_InputType"}; + break; + case SettingsID::Input_AnalogStickLeft_Name: + setting = {"", "AnalogStickLeft_Name"}; + break; + case SettingsID::Input_AnalogStickLeft_Data: + setting = {"", "AnalogStickLeft_Data"}; + break; + case SettingsID::Input_AnalogStickLeft_ExtraData: + setting = {"", "AnalogStickLeft_ExtraData"}; + break; + case SettingsID::Input_AnalogStickRight_InputType: + setting = {"", "AnalogStickRight_InputType"}; + break; + case SettingsID::Input_AnalogStickRight_Name: + setting = {"", "AnalogStickRight_Name"}; + break; + case SettingsID::Input_AnalogStickRight_Data: + setting = {"", "AnalogStickRight_Data"}; + break; + case SettingsID::Input_AnalogStickRight_ExtraData: + setting = {"", "AnalogStickRight_ExtraData"}; + break; } return setting; diff --git a/Source/RMG-Core/Settings/SettingsID.hpp b/Source/RMG-Core/Settings/SettingsID.hpp index e25b70ba..bfadc456 100644 --- a/Source/RMG-Core/Settings/SettingsID.hpp +++ b/Source/RMG-Core/Settings/SettingsID.hpp @@ -107,6 +107,88 @@ enum class SettingsID Audio_Volume, Audio_Muted, + // Input Plugin Settings + Input_PluggedIn, + Input_DeviceType, + Input_DeviceName, + Input_DeviceNum, + Input_Range, + Input_Deadzone, + Input_Pak, + Input_RemoveDuplicateMappings, + Input_A_InputType, + Input_A_Name, + Input_A_Data, + Input_A_ExtraData, + Input_B_InputType, + Input_B_Name, + Input_B_Data, + Input_B_ExtraData, + Input_Start_InputType, + Input_Start_Name, + Input_Start_Data, + Input_Start_ExtraData, + Input_DpadUp_InputType, + Input_DpadUp_Name, + Input_DpadUp_Data, + Input_DpadUp_ExtraData, + Input_DpadDown_InputType, + Input_DpadDown_Name, + Input_DpadDown_Data, + Input_DpadDown_ExtraData, + Input_DpadLeft_InputType, + Input_DpadLeft_Name, + Input_DpadLeft_Data, + Input_DpadLeft_ExtraData, + Input_DpadRight_InputType, + Input_DpadRight_Name, + Input_DpadRight_Data, + Input_DpadRight_ExtraData, + Input_CButtonUp_InputType, + Input_CButtonUp_Name, + Input_CButtonUp_Data, + Input_CButtonUp_ExtraData, + Input_CButtonDown_InputType, + Input_CButtonDown_Name, + Input_CButtonDown_Data, + Input_CButtonDown_ExtraData, + Input_CButtonLeft_InputType, + Input_CButtonLeft_Name, + Input_CButtonLeft_Data, + Input_CButtonLeft_ExtraData, + Input_CButtonRight_InputType, + Input_CButtonRight_Name, + Input_CButtonRight_Data, + Input_CButtonRight_ExtraData, + Input_LeftTrigger_InputType, + Input_LeftTrigger_Name, + Input_LeftTrigger_Data, + Input_LeftTrigger_ExtraData, + Input_RightTrigger_InputType, + Input_RightTrigger_Name, + Input_RightTrigger_Data, + Input_RightTrigger_ExtraData, + Input_ZTrigger_InputType, + Input_ZTrigger_Name, + Input_ZTrigger_Data, + Input_ZTrigger_ExtraData, + Input_AnalogStickUp_InputType, + Input_AnalogStickUp_Name, + Input_AnalogStickUp_Data, + Input_AnalogStickUp_ExtraData, + Input_AnalogStickDown_InputType, + Input_AnalogStickDown_Name, + Input_AnalogStickDown_Data, + Input_AnalogStickDown_ExtraData, + Input_AnalogStickLeft_InputType, + Input_AnalogStickLeft_Name, + Input_AnalogStickLeft_Data, + Input_AnalogStickLeft_ExtraData, + Input_AnalogStickRight_InputType, + Input_AnalogStickRight_Name, + Input_AnalogStickRight_Data, + Input_AnalogStickRight_ExtraData, + Invalid }; diff --git a/Source/RMG-Input/CMakeLists.txt b/Source/RMG-Input/CMakeLists.txt new file mode 100644 index 00000000..849f2a63 --- /dev/null +++ b/Source/RMG-Input/CMakeLists.txt @@ -0,0 +1,44 @@ +# +# RMG-Input CMakeLists.txt +# +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# disable lib prefix +set(CMAKE_SHARED_LIBRARY_PREFIX "") + +find_package(Qt5 COMPONENTS Gui Widgets Core Svg REQUIRED) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(SDL2 REQUIRED sdl2) + +set(RMG_INPUT_SOURCES + UserInterface/Widget/ControllerWidget.ui + UserInterface/Widget/ControllerWidget.cpp + UserInterface/Widget/ControllerImageWidget.cpp + UserInterface/Widget/CustomButton.cpp + UserInterface/EventFilter.cpp + UserInterface/OptionsDialog.ui + UserInterface/OptionsDialog.cpp + UserInterface/MainDialog.ui + UserInterface/MainDialog.cpp + UserInterface/UIResources.qrc + Utilities/QtKeyToSdl2Key.cpp + Utilities/InputDevice.cpp + Thread/SDLThread.cpp + main.cpp +) + +add_library(RMG-Input SHARED ${RMG_INPUT_SOURCES}) + +target_link_libraries(RMG-Input RMG-Core ${SDL2_LIBRARIES}) + +target_include_directories(RMG-Input PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../ + ${SDL2_INCLUDE_DIRS} +) + +target_link_libraries(RMG-Input Qt5::Gui Qt5::Widgets Qt5::Svg) diff --git a/Source/RMG-Input/Thread/SDLThread.cpp b/Source/RMG-Input/Thread/SDLThread.cpp new file mode 100644 index 00000000..7b012dec --- /dev/null +++ b/Source/RMG-Input/Thread/SDLThread.cpp @@ -0,0 +1,92 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "SDLThread.hpp" + +#include + +using namespace Thread; + +SDLThread::SDLThread(QObject *parent) : QThread(parent) +{ + +} + +SDLThread::~SDLThread() +{ + if (this->isRunning()) + { + this->StopLoop(); + } +} + +void SDLThread::StopLoop(void) +{ + this->keepLoopRunning = false; + while (this->isRunning()) + { + // wait until we're not running anymore + } +} + +SDLThreadAction SDLThread::GetCurrentAction(void) +{ + return this->currentAction; +} + +void SDLThread::SetAction(SDLThreadAction action) +{ + this->currentAction = action; +} + +void SDLThread::run(void) +{ + while (this->keepLoopRunning) + { + switch (this->currentAction) + { + default: + case SDLThreadAction::None: + break; + case SDLThreadAction::SDLPumpEvents: + { + SDL_PumpEvents(); + } break; + case SDLThreadAction::GetInputDevices: + { + // force re-fresh joystick list + SDL_JoystickUpdate(); + + for (int i = 0; i < SDL_NumJoysticks(); i++) + { + const char* name; + + if (SDL_IsGameController(i)) + { + name = SDL_GameControllerNameForIndex(i); + } + else + { + name = SDL_JoystickNameForIndex(i); + } + + if (name != nullptr) + { + emit this->OnInputDeviceFound(QString(name), i); + } + } + this->currentAction = SDLThreadAction::None; + emit this->OnDeviceSearchFinished(); + } break; + } + + // sleep for 10ms + QThread::msleep(10); + } +} diff --git a/Source/RMG-Input/Thread/SDLThread.hpp b/Source/RMG-Input/Thread/SDLThread.hpp new file mode 100644 index 00000000..0a0a0e94 --- /dev/null +++ b/Source/RMG-Input/Thread/SDLThread.hpp @@ -0,0 +1,48 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef SDLTHREAD_HPP +#define SDLTHREAD_HPP + +#include + +enum class SDLThreadAction +{ + None = 0, + SDLPumpEvents, + GetInputDevices, +}; + +namespace Thread +{ +class SDLThread : public QThread +{ + Q_OBJECT +public: + SDLThread(QObject *); + ~SDLThread(void); + + void run(void) override; + + void StopLoop(void); + + SDLThreadAction GetCurrentAction(void); + void SetAction(SDLThreadAction action); + +private: + bool keepLoopRunning = true; + SDLThreadAction currentAction = SDLThreadAction::None; + +signals: + void OnInputDeviceFound(QString, int); + void OnDeviceSearchFinished(void); +}; +} // namespace Thread + +#endif // SDLTHREAD_HPP diff --git a/Source/RMG-Input/UserInterface/EventFilter.cpp b/Source/RMG-Input/UserInterface/EventFilter.cpp new file mode 100644 index 00000000..f0bf8956 --- /dev/null +++ b/Source/RMG-Input/UserInterface/EventFilter.cpp @@ -0,0 +1,37 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "EventFilter.hpp" + +using namespace UserInterface; + +EventFilter::EventFilter(QObject *parent) : QObject(parent) +{ +} + +EventFilter::~EventFilter(void) +{ +} + +bool EventFilter::eventFilter(QObject *object, QEvent *event) +{ + switch (event->type()) + { + case QEvent::Type::KeyPress: + emit this->on_EventFilter_KeyPressed((QKeyEvent *)event); + return true; + case QEvent::Type::KeyRelease: + emit this->on_EventFilter_KeyReleased((QKeyEvent *)event); + return true; + default: + break; + } + + return QObject::eventFilter(object, event); +} diff --git a/Source/RMG-Input/UserInterface/EventFilter.hpp b/Source/RMG-Input/UserInterface/EventFilter.hpp new file mode 100644 index 00000000..e6fb0ca0 --- /dev/null +++ b/Source/RMG-Input/UserInterface/EventFilter.hpp @@ -0,0 +1,37 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef EVENTFILTER_HPP +#define EVENTFILTER_HPP + +#include +#include +#include +#include + +namespace UserInterface +{ +class EventFilter : public QObject +{ + Q_OBJECT + + public: + EventFilter(QObject *); + ~EventFilter(void); + + protected: + bool eventFilter(QObject *, QEvent *); + + signals: + void on_EventFilter_KeyPressed(QKeyEvent *); + void on_EventFilter_KeyReleased(QKeyEvent *); +}; +} // namespace UserInterface + +#endif // EVENTFILTER_HPP \ No newline at end of file diff --git a/Source/RMG-Input/UserInterface/MainDialog.cpp b/Source/RMG-Input/UserInterface/MainDialog.cpp new file mode 100644 index 00000000..b783e63b --- /dev/null +++ b/Source/RMG-Input/UserInterface/MainDialog.cpp @@ -0,0 +1,327 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "MainDialog.hpp" +#include "Widget/ControllerWidget.hpp" +#include "Utilities/QtKeyToSdl2Key.hpp" + +#include + +#include +#include + +using namespace UserInterface; + +MainDialog::MainDialog(QWidget* parent, Thread::SDLThread* sdlThread) : QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint) +{ + this->setupUi(this); + this->setWindowIcon(QIcon(":Resource/RMG.png")); + + // setup SDL thread + this->sdlThread = sdlThread; + connect(this->sdlThread, &Thread::SDLThread::OnInputDeviceFound, this, + &MainDialog::on_SDLThread_DeviceFound); + connect(this->sdlThread, &Thread::SDLThread::OnDeviceSearchFinished, this, + &MainDialog::on_SDLThread_DeviceSearchFinished); + + // setup EventFilter + this->eventFilter = new EventFilter(this); + connect(this->eventFilter, &EventFilter::on_EventFilter_KeyPressed, this, + &MainDialog::on_EventFilter_KeyPressed); + connect(this->eventFilter, &EventFilter::on_EventFilter_KeyReleased, this, + &MainDialog::on_EventFilter_KeyReleased); + + // each tab needs its own ControllerWidget + for (int i = 0; i < this->tabWidget->count(); i++) + { + Widget::ControllerWidget* widget = new Widget::ControllerWidget(this, this->eventFilter); + widget->SetSettingsSection("Rosalie's Mupen GUI - Input Plugin Profile " + QString::number(i)); + widget->LoadSettings(); + this->tabWidget->widget(i)->layout()->addWidget(widget); + controllerWidgets.push_back(widget); + connect(widget, &Widget::ControllerWidget::CurrentInputDeviceChanged, this, + &MainDialog::on_ControllerWidget_CurrentInputDeviceChanged); + connect(widget, &Widget::ControllerWidget::RefreshInputDevicesButtonClicked, this, + &MainDialog::on_ControllerWidget_RefreshInputDevicesButtonClicked); + } + + // always add keyboard device + for (auto& controllerWidget : this->controllerWidgets) + { + controllerWidget->AddInputDevice("Keyboard", -1); + } + + // fill device list at least once + this->on_ControllerWidget_RefreshInputDevicesButtonClicked(); + + this->inputPollTimer = new QTimer(this); + connect(this->inputPollTimer, &QTimer::timeout, this, &MainDialog::on_InputPollTimer_triggered); + this->inputPollTimer->start(50); +} + +MainDialog::~MainDialog() +{ + this->closeInputDevice(); +} + +void MainDialog::addInputDevice(QString deviceName, int deviceNum) +{ + for (auto& controllerWidget : this->controllerWidgets) + { + controllerWidget->AddInputDevice(deviceName, deviceNum); + } +} + +void MainDialog::removeInputDevice(QString deviceName, int deviceNum) +{ + for (auto& controllerWidget : this->controllerWidgets) + { + controllerWidget->RemoveInputDevice(deviceName, deviceNum); + } +} + +void MainDialog::openInputDevice(QString deviceName, int deviceNum) +{ + SDL_JoystickID joystickId; + Widget::ControllerWidget* controllerWidget; + controllerWidget = controllerWidgets.at(this->tabWidget->currentIndex()); + + // we don't need to open a keyboard + if (deviceNum == -1) + { + currentDeviceName = ""; + currentDeviceNum = -1; + controllerWidget->SetCurrentJoystickID(-1); + return; + } + + if (SDL_IsGameController(deviceNum) == SDL_TRUE) + { + currentJoystick = nullptr; + currentController = SDL_GameControllerOpen(deviceNum); + } + else + { + currentJoystick = SDL_JoystickOpen(deviceNum); + currentController = nullptr; + } + currentDeviceNum = deviceNum; + joystickId = SDL_JoystickGetDeviceInstanceID(deviceNum); + controllerWidget->SetCurrentJoystickID(joystickId); +} + +void MainDialog::closeInputDevice() +{ + if (this->currentJoystick != nullptr) + { + SDL_JoystickClose(this->currentJoystick); + this->currentJoystick = nullptr; + } + + if (this->currentController != nullptr) + { + SDL_GameControllerClose(this->currentController); + this->currentController = nullptr; + } +} + +void MainDialog::on_InputPollTimer_triggered() +{ + Widget::ControllerWidget* controllerWidget; + controllerWidget = controllerWidgets.at(this->tabWidget->currentIndex()); + + // if the current controller widget + // is disabled (by the user), + // we don't need to do anything + if (!controllerWidget->IsPluggedIn()) + { + this->sdlThread->SetAction(SDLThreadAction::None); + return; + } + + // verify/update the SDL thread action state + switch (this->sdlThread->GetCurrentAction()) + { + case SDLThreadAction::GetInputDevices: + // don't do anything when we're querying the input devices + return; + case SDLThreadAction::None: + // when the SDL thread is doing nothing, pump the sdl events instead + sdlThread->SetAction(SDLThreadAction::SDLPumpEvents); + break; + default: + break; + } + + // check if controller has been disconnected, + // if so, keep trying to re-open it + if ((this->currentJoystick != nullptr && !SDL_JoystickGetAttached(this->currentJoystick)) || + (this->currentController != nullptr && !SDL_GameControllerGetAttached(this->currentController))) + { + this->closeInputDevice(); + this->openInputDevice(this->currentDeviceName, this->currentDeviceNum); + } + + // process SDL events + SDL_Event event; + while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, 0, SDL_LASTEVENT) == 1) + { + controllerWidget->on_MainDialog_SdlEvent(&event); + } + + controllerWidget->on_MainDialog_SdlEventPollFinished(); +} + +void MainDialog::on_ControllerWidget_CurrentInputDeviceChanged(ControllerWidget* widget, QString deviceName, int deviceNum) +{ + Widget::ControllerWidget* currentWidget; + currentWidget = controllerWidgets.at(this->tabWidget->currentIndex()); + + // ignore input device changes + // when it's not the current controllerwidget + if (widget != currentWidget) + { + return; + } + + this->closeInputDevice(); + + // only open device, + // when it's not a keyboard + if (deviceNum != -1) + { + this->openInputDevice(deviceName, deviceNum); + } +} + +void MainDialog::on_ControllerWidget_RefreshInputDevicesButtonClicked() +{ + if (this->updatingDeviceList) + { + return; + } + + this->updatingDeviceList = true; + this->inputDeviceList.clear(); + this->sdlThread->SetAction(SDLThreadAction::GetInputDevices); +} + +void MainDialog::on_tabWidget_currentChanged(int index) +{ + QString deviceName; + int deviceNum; + + Widget::ControllerWidget* controllerWidget = controllerWidgets.at(index); + controllerWidget->ClearControllerImage(); + + this->closeInputDevice(); + + controllerWidget->GetCurrentInputDevice(deviceName, deviceNum); + + // only open device, + // when it's not a keyboard + if (deviceNum != -1) + { + this->openInputDevice(deviceName, deviceNum); + } +} + +void MainDialog::on_SDLThread_DeviceFound(QString deviceName, int deviceNum) +{ + inputDevice_t inputDevice = {deviceName, deviceNum}; + this->inputDeviceList.append(inputDevice); +} + +void MainDialog::on_SDLThread_DeviceSearchFinished(void) +{ + // compare inputDeviceList with oldInputDeviceList + // and signal the changes + for (auto& inputDevice : this->inputDeviceList) + { + if (!this->oldInputDeviceList.contains(inputDevice)) + { + this->addInputDevice(inputDevice.deviceName, inputDevice.deviceNum); + } + } + for (auto& inputDevice : this->oldInputDeviceList) + { + if (!this->inputDeviceList.contains(inputDevice)) + { + this->removeInputDevice(inputDevice.deviceName, inputDevice.deviceNum); + } + } + + // update old device list + this->oldInputDeviceList = this->inputDeviceList; + + // we can refresh safely again + this->updatingDeviceList = false; + + // device search has been completed, + // signal that each widget can check device settings + for (auto& controllerWidget : this->controllerWidgets) + { + controllerWidget->CheckInputDeviceSettings(); + } +} + +void MainDialog::on_EventFilter_KeyPressed(QKeyEvent *event) +{ + int key = Utilities::QtKeyToSdl2Key(event->key()); + int mod = Utilities::QtModKeyToSdl2ModKey(event->modifiers()); + + SDL_KeyboardEvent keyboardEvent; + keyboardEvent.state = SDL_PRESSED; + keyboardEvent.type = SDL_KEYDOWN; + keyboardEvent.keysym.scancode = (SDL_Scancode)key; + keyboardEvent.keysym.sym = (SDL_Keycode)key; + keyboardEvent.keysym.mod = mod; + + SDL_Event sdlEvent; + sdlEvent.key = keyboardEvent; + sdlEvent.type = SDL_KEYDOWN; + + SDL_PushEvent(&sdlEvent); +} + +void MainDialog::on_EventFilter_KeyReleased(QKeyEvent *event) +{ + int key = Utilities::QtKeyToSdl2Key(event->key()); + int mod = Utilities::QtModKeyToSdl2ModKey(event->modifiers()); + + SDL_KeyboardEvent keyboardEvent; + keyboardEvent.state = SDL_RELEASED; + keyboardEvent.type = SDL_KEYUP; + keyboardEvent.keysym.scancode = (SDL_Scancode)key; + keyboardEvent.keysym.sym = (SDL_Keycode)key; + keyboardEvent.keysym.mod = mod; + + SDL_Event sdlEvent; + sdlEvent.key = keyboardEvent; + sdlEvent.type = SDL_KEYUP; + + SDL_PushEvent(&sdlEvent); +} + +void MainDialog::on_buttonBox_clicked(QAbstractButton *button) +{ + QPushButton *pushButton = (QPushButton *)button; + QPushButton *okButton = this->buttonBox->button(QDialogButtonBox::Ok); + + if (pushButton == okButton) + { + for (auto& controllerWidget : this->controllerWidgets) + { + controllerWidget->SaveSettings(); + } + + CoreSettingsSave(); + } +} + diff --git a/Source/RMG-Input/UserInterface/MainDialog.hpp b/Source/RMG-Input/UserInterface/MainDialog.hpp new file mode 100644 index 00000000..fd7bb724 --- /dev/null +++ b/Source/RMG-Input/UserInterface/MainDialog.hpp @@ -0,0 +1,82 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef MAINDIALOG_HPP +#define MAINDIALOG_HPP + +#include + +#include "ui_MainDialog.h" +#include "Widget/ControllerWidget.hpp" +#include "Thread/SDLThread.hpp" +#include "EventFilter.hpp" + +namespace UserInterface +{ +class MainDialog : public QDialog, private Ui::MainDialog +{ +Q_OBJECT +private: + struct inputDevice_t + { + QString deviceName; + int deviceNum; + + bool operator== (inputDevice_t other) + { + return other.deviceNum == deviceNum && + other.deviceName == deviceName; + } + }; + + QTimer* inputPollTimer; + Thread::SDLThread* sdlThread; + + QList oldInputDeviceList; + QList inputDeviceList; + bool updatingDeviceList = false; + + QList controllerWidgets; + SDL_Joystick* currentJoystick = nullptr; + SDL_GameController* currentController = nullptr; + QString currentDeviceName; + int currentDeviceNum = 0; + + EventFilter* eventFilter; + + void addInputDevice(QString, int); + void removeInputDevice(QString, int); + + void openInputDevice(QString, int); + void closeInputDevice(); + +public: + MainDialog(QWidget *parent, Thread::SDLThread*); + ~MainDialog(void); + +public slots: + void on_InputPollTimer_triggered(); + + void on_ControllerWidget_CurrentInputDeviceChanged(ControllerWidget*, QString, int); + void on_ControllerWidget_RefreshInputDevicesButtonClicked(); + + void on_tabWidget_currentChanged(int); + + void on_SDLThread_DeviceFound(QString, int); + void on_SDLThread_DeviceSearchFinished(void); + +private slots: + void on_EventFilter_KeyPressed(QKeyEvent *); + void on_EventFilter_KeyReleased(QKeyEvent *); + + void on_buttonBox_clicked(QAbstractButton *); +}; +} + +#endif // MAINDIALOG_HPP diff --git a/Source/RMG-Input/UserInterface/MainDialog.ui b/Source/RMG-Input/UserInterface/MainDialog.ui new file mode 100644 index 00000000..6aade56c --- /dev/null +++ b/Source/RMG-Input/UserInterface/MainDialog.ui @@ -0,0 +1,95 @@ + + + MainDialog + + + + 0 + 0 + 739 + 612 + + + + Rosalie's Mupen GUI - Input Plugin + + + + + + 0 + + + + Player 1 + + + + + + Player 2 + + + + + + Player 3 + + + + + + Player 4 + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + MainDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + MainDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/Source/RMG-Input/UserInterface/OptionsDialog.cpp b/Source/RMG-Input/UserInterface/OptionsDialog.cpp new file mode 100644 index 00000000..46fa4cab --- /dev/null +++ b/Source/RMG-Input/UserInterface/OptionsDialog.cpp @@ -0,0 +1,43 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "OptionsDialog.hpp" + +#include + +using namespace UserInterface; + +OptionsDialog::OptionsDialog(QWidget* parent, QString settingsSection) : QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint) +{ + this->setupUi(this); + + this->settingsSection = settingsSection; + + std::string section = settingsSection.toStdString(); + + this->removeDuplicateMappingsCheckbox->setChecked(CoreSettingsGetBoolValue(SettingsID::Input_RemoveDuplicateMappings, section)); + this->controllerPakComboBox->setCurrentIndex(CoreSettingsGetIntValue(SettingsID::Input_Pak, section)); +} + +void OptionsDialog::on_buttonBox_clicked(QAbstractButton *button) +{ + + QPushButton *pushButton = (QPushButton *)button; + QPushButton *okButton = this->buttonBox->button(QDialogButtonBox::Ok); + + if (pushButton == okButton) + { + std::string section = this->settingsSection.toStdString(); + + CoreSettingsSetValue(SettingsID::Input_RemoveDuplicateMappings, section, this->removeDuplicateMappingsCheckbox->isChecked()); + CoreSettingsSetValue(SettingsID::Input_Pak, section, this->controllerPakComboBox->currentIndex()); + } + + this->accept(); +} diff --git a/Source/RMG-Input/UserInterface/OptionsDialog.hpp b/Source/RMG-Input/UserInterface/OptionsDialog.hpp new file mode 100644 index 00000000..0e1bc8e3 --- /dev/null +++ b/Source/RMG-Input/UserInterface/OptionsDialog.hpp @@ -0,0 +1,34 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef OPTIONSDIALOG_HPP +#define OPTIONSDIALOG_HPP + +#include + +#include "ui_OptionsDialog.h" + +namespace UserInterface +{ +class OptionsDialog : public QDialog, private Ui::OptionsDialog +{ +Q_OBJECT + +public: + OptionsDialog(QWidget *parent, QString settingsSection); + +private: + QString settingsSection; + +private slots: + void on_buttonBox_clicked(QAbstractButton *); +}; +} + +#endif // OPTIONSDIALOG_HPP diff --git a/Source/RMG-Input/UserInterface/OptionsDialog.ui b/Source/RMG-Input/UserInterface/OptionsDialog.ui new file mode 100644 index 00000000..04019b7f --- /dev/null +++ b/Source/RMG-Input/UserInterface/OptionsDialog.ui @@ -0,0 +1,91 @@ + + + OptionsDialog + + + + 0 + 0 + 321 + 316 + + + + Options + + + + + + 0 + + + + General + + + + + + Remove Duplicate Mappings + + + + + + + Controller Pak + + + + + + + + Memory + + + + + Rumble + + + + + None + + + + + + + + Qt::Vertical + + + + 20 + 122 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller.svg b/Source/RMG-Input/UserInterface/Resource/Controller.svg new file mode 100644 index 00000000..785f735c --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_AnalogStick.svg b/Source/RMG-Input/UserInterface/Resource/Controller_AnalogStick.svg new file mode 100644 index 00000000..755113cf --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_AnalogStick.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_NoAnalogStick.svg b/Source/RMG-Input/UserInterface/Resource/Controller_NoAnalogStick.svg new file mode 100644 index 00000000..8db6cff9 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_NoAnalogStick.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_A.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_A.svg new file mode 100644 index 00000000..b17d0e0a --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_A.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_AnalogStick.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_AnalogStick.svg new file mode 100644 index 00000000..0be3fe67 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_AnalogStick.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_B.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_B.svg new file mode 100644 index 00000000..f18f4bd6 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_B.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonDown.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonDown.svg new file mode 100644 index 00000000..e8485a09 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonDown.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonLeft.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonLeft.svg new file mode 100644 index 00000000..480d0384 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonLeft.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonRight.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonRight.svg new file mode 100644 index 00000000..340908b7 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonRight.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonUp.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonUp.svg new file mode 100644 index 00000000..596d866a --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_CButtonUp.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadDown.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadDown.svg new file mode 100644 index 00000000..f80ccbf1 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadDown.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadLeft.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadLeft.svg new file mode 100644 index 00000000..633c4c99 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadLeft.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadRight.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadRight.svg new file mode 100644 index 00000000..ae3c2332 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadRight.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadUp.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadUp.svg new file mode 100644 index 00000000..fbb1da1c --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_DpadUp.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_LeftTrigger.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_LeftTrigger.svg new file mode 100644 index 00000000..876d21d0 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_LeftTrigger.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_RightTrigger.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_RightTrigger.svg new file mode 100644 index 00000000..7b643266 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_RightTrigger.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_Start.svg b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_Start.svg new file mode 100644 index 00000000..7bfeac93 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Resource/Controller_Pressed_Start.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/Source/RMG-Input/UserInterface/Resource/RMG.png b/Source/RMG-Input/UserInterface/Resource/RMG.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc34375cf580eac83f1bb09668ceee6c94e8382 GIT binary patch literal 49144 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 DL?&lh!_;Mpzi~y3xB63>Ns! zvy#=9@AjNGhJrJDB$OsP&c?;#*3IV)b-f&DSUHGNF#S49Jo?&omL98FeQ{>Q>p)-_T*rZX;R9I;|yG}_yvyla>Cbb4iK zuxf#r?ji=!hblg}nC_YB5x-om8tJK$`6|)n!2d`jrv2xQ%;!dCS0;+WjoWOO68jfd zPWn8kuA=xP{f;;ZafCNir252!u1PxU%!PjU z^8^QV4Ug-BjegSfiu4h?N9m^f1LqT?cE8p#Q&`-p+n;4#dDfLxoDWd0#qFClx_OG> znX@p=y0oj{^r zKDrc{Zx5*Oxb9(zlaw_!rUwGchfGYzi<#hjM#y=&-;gnfYgA8fs+=1mO~%A>motnn zx{22cB_Qv~`7RTK!KwbafxNh+$H3onjyP$0Jb8Hf?_Yq_DgqIlJ?aBsEKJIt;Nnko zsQ&o$pl7#O<|=|C$47A1I$0I23jTZKXaRX4aTc4IKd~+LKCZ0Ok;Rh_5MVoHC|xi=px}__AwY!B zpG4dc8mMDM1^7CdzPj+PG!#W!&HeoD+jarT{pFJ$eJ-gC2C?I9avPW0+GhkfiuZ@G z{p6D}WiAr+NE%6RNY8QMa!;9t$Em4bfsyN8C^lTD(G!@nMXwu(#uEW4hW|{= zZ^v#ZS6ahLknkN#;@`g{yoF*{Un!j;JEYomGBq`lrDmp*pULq>E6VuPIampC&(@pg zzFM6k`>08=xr`d>Obfe)?h5I=1f6ijSg3TMz?_}$QhqYq%>2OlZ$?+0fJo8^rhilF z#)?XMPO85%zYSp^xJclxV0C3WH{4Z^9);1tY((p%sn5&DCbC$a@r`AvG4G)0J1tiR ze}zdpr)|9i4?2}j!gXT?V#2=ha0|SZ;P@+vwMU^ z%<$h^sQ_IrqdF5YHqJbf3L*z0M1A;&2pX&#^a^G`7&GSp8Z~*wDAM|WXC|aA)tTg& zKm!`9#o_W?aC@9J*4SptpY){2Uvh$ZTRlfj-LcBiwe;>=2Nu`jNFxv|U77OLws$F{ z&pxHjWuHdACxR_4{T{4hEbJTpc=!{aw=GdRE9Zq;mk_J|Z9&^S+@m~+SIc+)DWwjF zRw9lx+pX;FiPAZ_I(oXcNqtTb3oWjT+$GE?Yf#$hDg) zQoxqH^Vb0u+aZFxM_hB++8OgrU6wUL?S)y4lAiGg=pjJoNgMX3oM8ogs!7CZZ~Y`So=U9$>Zgc{G{bRO_d z7-EU4)1Y)RuE4@=J_pnma?I)stUaV|5*p0Z&K-|=1{Y5bwwJR>>yNx}XdOn*!oNJb zd~5xHaISx$aeayBw+}#EU~E27zRLlTtygw0Bx0{R3@c_^2yN=ud0@f0<+=`sdjueQ zl-UvS3 zu0Eyimwbp;VR*YP?B?b+00iMmjJ`jb-~DAbw?O}p4j-oH-OrU84-MfB(eWb;`Syg@ zr~$66A=UFmIg#`6vqFeW{rE{m%Xb3va-~jk8lG^v!%z8$bKMu zb_CnTq@-QIa4>|Pb&iy@K<9kRkQZY6TRN>YJ>D;9v-(}edPjCKSNI|@6V!j@Cj%C$ z7)m`@7*<(?C6|d{X_Ph0dER%f?w^*4Vs-`nlzI8sar_tV|J$#CoNE!-PJ@5`l3i@~ z=znXo@LY4UzN-3cAai&$=VeGnDa1aExkQVFGX1!V&eF*RXgvYgw}a$M6`;vAdtF9E zerz6wJ>HMCP*?*a|4Abr4|c4++VKCfEWq0MOJ!v|p#A8nh`M;*y-sBX(>E=(|$!dT6(A3evludyx#WKJx)+O2ZR#KR>aKMOzdS-L9 zJ|Z{E(&&4usR5H8-UymP8VxuafAA4}cn zWbptNKOBk|d;m9vs8)(UXac<}w+TE`7C;$5%BUJGG>v*k8NI|dC7B`+DW4juyyQz+ zsPqt;Bh3?9I<@Z8GJbwy<8bxAJd-1s&KwPlvhO(K-3uYFmtR9rc{xIv@(4y~XCd3T zR9Llj%#1ZT9>59>)ICH3Z2z1Aj<>fUtqaHg&r#b*A*IAoeV?n5)Y&MT0e*$+j>&4z z1{VT(yzfShvVcUKo}Lc4XnP=IICXW0I>6aXu#(|aML}HNy*Hr$>W;y=4hVhbw%9&! zTeB$GzaEfi)zJe#y@k#}gcBgMWo$HJQ?@R!Zw{vi-07Us-Rv z)u00d`XB%JHikq#2UlZ=3j$3m^U=zbe#E?+bDqCGu5ppm`yZCO| za*A78=;He1^!4wX`kfV`e8lb~r7_PzjC4kRoL#TFCg9S8=n`&KJx9?@jSLy2ZvoCa zz15b#G4%wswG&NieXPXhzUmM9QdW0o#zCz6LS>)C@M$wOmcCQsEWHJm^YgQS91j~{ z@bdtZ=LVELAQ|&KE}o9iP)k5VC~*K<{|!Fsj9K*Zb0e5+=A0-vZ&e4XbV6*JQ-GJ4pTYZu5}-4CdWwb1OAH7zUHqdD@*^B!xPgFH z`~yJiP^` zllx$m`{heWRu?tfHc+V2!H!n1D$>n|S>BvM*_# zgyFJiY_cx+Uc_P$*RjN4{}q8m1gHz<P_wF_eHWjA=x!-S|dGSL0o4yIh{ilF};*<*0dCr~&= zroQ$ZdwWZLT)e#7YngR|29h0=Yq)d%rcb)=xV27tUR}O;GAorJ!E@Z+el%f-F}MVA zSf2sr?_vn4g&WZa7* zOvhQR$*D{{1;s=E%SU>yroZph|5}KCj^M`t?=WR>fLb^{j-s)w#$h(qhZh7Pn;#^4PP|d@mJN+f);7n5TN+YF6u?F_A48!N8EK~Q`hPR>%CF>< z$z0<6WcYrAHvWre7n)O>tT__K%c}%nFRw**gcq`=U8Re~hc$QmIuI^coML5b`vv&Q z0NeOUNdOR$-0LO>xbG=~;=wIbkiXXlMaR_J9;ftyT31R*g^0IH1$-l`?XxRBw^%qM ziLaVT`0g@|y1D$yB%_6?OFH*+TBeXN^FDt#{Nk!jU-$2MYDR`NnEq)vG_r+1=GWH# z`C3{k2(0`q2-dh+O@SvWV?fqa|B9tfQQ-( zCPI4@74KH-mjIpubg;H<}sI=TKY*h zwH1D%vikF!Pdt)ML7a2qH~ZC8k-`NE?ua04EH@V=Nx5aqh_u%F4>U zL;lw;1&xj3yl?%EH}x&H{?_LG`pW2b^g+bzq#xG>?nB + + Resource/Controller_NoAnalogStick.svg + Resource/Controller_AnalogStick.svg + Resource/Controller_Pressed_A.svg + Resource/Controller_Pressed_B.svg + Resource/Controller_Pressed_DpadUp.svg + Resource/Controller_Pressed_DpadDown.svg + Resource/Controller_Pressed_DpadLeft.svg + Resource/Controller_Pressed_DpadRight.svg + Resource/Controller_Pressed_CButtonUp.svg + Resource/Controller_Pressed_CButtonDown.svg + Resource/Controller_Pressed_CButtonLeft.svg + Resource/Controller_Pressed_CButtonRight.svg + Resource/Controller_Pressed_Start.svg + Resource/Controller_Pressed_LeftTrigger.svg + Resource/Controller_Pressed_RightTrigger.svg + Resource/RMG.png + + \ No newline at end of file diff --git a/Source/RMG-Input/UserInterface/Widget/ControllerImageWidget.cpp b/Source/RMG-Input/UserInterface/Widget/ControllerImageWidget.cpp new file mode 100644 index 00000000..841bc252 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Widget/ControllerImageWidget.cpp @@ -0,0 +1,229 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ControllerImageWidget.hpp" + +#include +#include +#include +#include +#include + +using namespace UserInterface::Widget; + +ControllerImageWidget::ControllerImageWidget(QWidget* parent) : QWidget(parent) +{ +} + +ControllerImageWidget::~ControllerImageWidget() +{ + +} + +void ControllerImageWidget::SetButtonState(enum N64ControllerButton button, bool state) +{ + if (this->buttonState[(int)button] != state) + { + this->buttonState[(int)button] = state; + this->needImageUpdate = true; + } +} + +void ControllerImageWidget::SetXAxisState(int xAxis) +{ + if (this->xAxisState != xAxis) + { + this->xAxisState = xAxis; + this->needImageUpdate = true; + } +} + +void ControllerImageWidget::SetYAxisState(int yAxis) +{ + if (this->yAxisState != yAxis) + { + this->yAxisState = yAxis; + this->needImageUpdate = true; + } +} + +void ControllerImageWidget::SetDeadzone(int value) +{ + if (this->deadzoneValue != value) + { + this->deadzoneValue = value; + this->needImageUpdate = true; + } +} + +void ControllerImageWidget::SetRange(int value) +{ + if (this->rangeValue != value) + { + this->rangeValue = value; + this->needImageUpdate = true; + } +} + +void ControllerImageWidget::ClearControllerState() +{ + // reset button state + for (int i = 0; i < (int)N64ControllerButton::Invalid; i++) + { + if (this->buttonState[i]) + { + this->buttonState[i] = false; + this->needImageUpdate = true; + } + } + + // reset x axis state + if (this->xAxisState != 0) + { + this->xAxisState = 0; + this->needImageUpdate = true; + } + + // reset y axis state + if (this->yAxisState != 0) + { + this->yAxisState = 0; + this->needImageUpdate = true; + } + + this->UpdateImage(); +} + +void ControllerImageWidget::UpdateImage() +{ + if (this->needImageUpdate) + { + this->update(); + this->needImageUpdate = false; + } +} + + +void ControllerImageWidget::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + QSvgRenderer renderer; + + static const struct + { + enum N64ControllerButton button; + QString imageUri; + } buttons[] = + { + { N64ControllerButton::A, ":Resource/Controller_Pressed_A.svg" }, + { N64ControllerButton::B, ":Resource/Controller_Pressed_B.svg" }, + { N64ControllerButton::Start, ":Resource/Controller_Pressed_Start.svg" }, + { N64ControllerButton::DpadUp, ":Resource/Controller_Pressed_DpadUp.svg" }, + { N64ControllerButton::DpadDown, ":Resource/Controller_Pressed_DpadDown.svg" }, + { N64ControllerButton::DpadLeft, ":Resource/Controller_Pressed_DpadLeft.svg" }, + { N64ControllerButton::DpadRight, ":Resource/Controller_Pressed_DpadRight.svg" }, + { N64ControllerButton::CButtonUp, ":Resource/Controller_Pressed_CButtonUp.svg" }, + { N64ControllerButton::CButtonDown, ":Resource/Controller_Pressed_CButtonDown.svg" }, + { N64ControllerButton::CButtonLeft, ":Resource/Controller_Pressed_CButtonLeft.svg" }, + { N64ControllerButton::CButtonRight, ":Resource/Controller_Pressed_CButtonRight.svg" }, + { N64ControllerButton::LeftTrigger, ":Resource/Controller_Pressed_LeftTrigger.svg" }, + { N64ControllerButton::RightTrigger, ":Resource/Controller_Pressed_RightTrigger.svg" }, + }; + + static const QString baseImageUri = ":Resource/Controller_NoAnalogStick.svg"; + static const QString analogStickImageUri = ":Resource/Controller_AnalogStick.svg"; + + // render base image first + renderer.load(baseImageUri); + renderer.render(&painter); + + // render button images on top + // when the button is pressed + for (auto& button : buttons) + { + if (this->buttonState[(int)button.button]) + { + renderer.load(button.imageUri); + renderer.render(&painter); + } + } + + // render analog stick + renderer.load(analogStickImageUri); + + QRectF rectF = renderer.viewBoxF(); + double offsetx = 0, offsety = 0; + const int width = rectF.width(); + const int height = rectF.height(); + // we'll move the analog stick by a percentage + // of the total width/height from the image + const double maxOffsety = ((double)(height * 0.12265f) / 2); + const double maxOffsetx = maxOffsety; + const double dist = sqrt(pow(this->xAxisState, 2) + pow(this->yAxisState, 2)); + + // take deadzone into account + if (dist > this->deadzoneValue) + { + offsetx = (maxOffsetx / 100 * this->xAxisState); + offsety = (maxOffsety / 100 * this->yAxisState); + } + + // take circle range into account + if (dist > 100) + { + offsetx = (maxOffsetx / 100 * (this->xAxisState / dist * 100)); + offsety = (maxOffsety / 100 * (this->yAxisState / dist * 100)); + } + + // take range into account + offsetx = offsetx / 100 * this->rangeValue; + offsety = offsety / 100 * this->rangeValue; + + // adjust rect with offset + rectF.adjust( + // dx1 + offsetx, + // dy1 + offsety, + // dx2 + 0, + // dy2 + 0 + ); + + // make sure we keep the same + // width and height + rectF.setWidth(width); + rectF.setHeight(height); + + // set viewbox and render + renderer.setViewBox(rectF); + renderer.render(&painter); +} + +void ControllerImageWidget::resizeEvent(QResizeEvent *event) +{ + const QSize& currentSize = event->size(); + + // calculate expected width & height + const int width = currentSize.width(); + const int height = std::ceil(currentSize.width() * 0.9759f); + const QSize expectedSize = QSize(width, height); + + // set widget's size as needed, + // the widget resize function doesn't work + // as expected, so set the minimum size + if (currentSize != expectedSize) + { + this->setMinimumSize(expectedSize); + this->update(); + } + + QWidget::resizeEvent(event); +} diff --git a/Source/RMG-Input/UserInterface/Widget/ControllerImageWidget.hpp b/Source/RMG-Input/UserInterface/Widget/ControllerImageWidget.hpp new file mode 100644 index 00000000..70373311 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Widget/ControllerImageWidget.hpp @@ -0,0 +1,57 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef CONTROLLERIMAGEWIDGET_HPP +#define CONTROLLERIMAGEWIDGET_HPP + +#include + +#include "common.hpp" + +namespace UserInterface +{ +namespace Widget +{ +class ControllerImageWidget : public QWidget +{ + Q_OBJECT + +private: + // button state + bool buttonState[(int)N64ControllerButton::Invalid] = {0}; + // x axis state, -100-100 + int xAxisState = 0; + // y axis state, -100-100 + int yAxisState = 0; + // deadzone value, 0-100 + int deadzoneValue = 0; + // range value, 0-100 + int rangeValue = 100; + + bool needImageUpdate = false; +public: + ControllerImageWidget(QWidget* parent); + ~ControllerImageWidget(); + + void SetButtonState(enum N64ControllerButton button, bool state); + void SetXAxisState(int xAxis); + void SetYAxisState(int yAxis); + void SetDeadzone(int value); + void SetRange(int value); + + void ClearControllerState(); + void UpdateImage(); + + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; +}; +} +} + +#endif // CONTROLLERIMAGEWIDGET_HPP \ No newline at end of file diff --git a/Source/RMG-Input/UserInterface/Widget/ControllerWidget.cpp b/Source/RMG-Input/UserInterface/Widget/ControllerWidget.cpp new file mode 100644 index 00000000..824c97d6 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Widget/ControllerWidget.cpp @@ -0,0 +1,874 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ControllerWidget.hpp" +#include "UserInterface/OptionsDialog.hpp" + +#include "common.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +using namespace UserInterface::Widget; + +ControllerWidget::ControllerWidget(QWidget* parent, EventFilter* eventFilter) : QWidget(parent) +{ + this->setupUi(this); + + // install eventFilter to all children and parents + this->installEventFilter(eventFilter); + for (auto& widget : this->children()) + { + widget->installEventFilter(eventFilter); + } + QObject* parentObject = this->parent(); + while (parentObject != nullptr) + { + parentObject->installEventFilter(eventFilter); + parentObject = parentObject->parent(); + } + + this->DrawControllerImage(); + + this->buttonWidgetMappings.append( + { + { N64ControllerButton::A, this->aButton, }, + { N64ControllerButton::B, this->bButton, }, + { N64ControllerButton::Start, this->startButton }, + { N64ControllerButton::DpadUp, this->dpadUpButton }, + { N64ControllerButton::DpadDown, this->dpadDownButton }, + { N64ControllerButton::DpadLeft, this->dpadLeftButton }, + { N64ControllerButton::DpadRight, this->dpadRightButton }, + { N64ControllerButton::CButtonUp, this->cbuttonUpButton }, + { N64ControllerButton::CButtonDown, this->cbuttonDownButton }, + { N64ControllerButton::CButtonLeft, this->cbuttonLeftButton }, + { N64ControllerButton::CButtonRight, this->cbuttonRightButton }, + { N64ControllerButton::LeftTrigger, this->leftTriggerButton }, + { N64ControllerButton::RightTrigger, this->rightTriggerButton }, + }); + + this->joystickWidgetMappings.append( + { + { InputAxisDirection::Up, this->analogStickUpButton }, + { InputAxisDirection::Down, this->analogStickDownButton }, + { InputAxisDirection::Left, this->analogStickLeftButton }, + { InputAxisDirection::Right, this->analogStickRightButton }, + }); + + this->buttonSettingMappings.append( + { + { this->aButton, SettingsID::Input_A_InputType, SettingsID::Input_A_Name, SettingsID::Input_A_Data, SettingsID::Input_A_ExtraData }, + { this->bButton, SettingsID::Input_B_InputType, SettingsID::Input_B_Name, SettingsID::Input_B_Data, SettingsID::Input_B_ExtraData }, + { this->startButton, SettingsID::Input_Start_InputType, SettingsID::Input_Start_Name, SettingsID::Input_Start_Data, SettingsID::Input_Start_ExtraData }, + { this->dpadUpButton, SettingsID::Input_DpadUp_InputType, SettingsID::Input_DpadUp_Name, SettingsID::Input_DpadUp_Data, SettingsID::Input_DpadUp_ExtraData }, + { this->dpadDownButton, SettingsID::Input_DpadDown_InputType, SettingsID::Input_DpadDown_Name, SettingsID::Input_DpadDown_Data, SettingsID::Input_DpadDown_ExtraData }, + { this->dpadLeftButton, SettingsID::Input_DpadLeft_InputType, SettingsID::Input_DpadLeft_Name, SettingsID::Input_DpadLeft_Data, SettingsID::Input_DpadLeft_ExtraData }, + { this->dpadRightButton, SettingsID::Input_DpadRight_InputType, SettingsID::Input_DpadRight_Name, SettingsID::Input_DpadRight_Data, SettingsID::Input_DpadRight_ExtraData }, + { this->cbuttonUpButton, SettingsID::Input_CButtonUp_InputType, SettingsID::Input_CButtonUp_Name, SettingsID::Input_CButtonUp_Data, SettingsID::Input_CButtonUp_ExtraData }, + { this->cbuttonDownButton, SettingsID::Input_CButtonDown_InputType, SettingsID::Input_CButtonDown_Name, SettingsID::Input_CButtonDown_Data, SettingsID::Input_CButtonDown_ExtraData }, + { this->cbuttonLeftButton, SettingsID::Input_CButtonLeft_InputType, SettingsID::Input_CButtonLeft_Name, SettingsID::Input_CButtonLeft_Data, SettingsID::Input_CButtonLeft_ExtraData }, + { this->cbuttonRightButton, SettingsID::Input_CButtonRight_InputType, SettingsID::Input_CButtonRight_Name, SettingsID::Input_CButtonRight_Data, SettingsID::Input_CButtonRight_ExtraData }, + { this->leftTriggerButton, SettingsID::Input_LeftTrigger_InputType, SettingsID::Input_LeftTrigger_Name, SettingsID::Input_LeftTrigger_Data, SettingsID::Input_LeftTrigger_ExtraData }, + { this->rightTriggerButton, SettingsID::Input_RightTrigger_InputType, SettingsID::Input_RightTrigger_Name, SettingsID::Input_RightTrigger_Data, SettingsID::Input_RightTrigger_ExtraData }, + { this->zTriggerButton, SettingsID::Input_ZTrigger_InputType, SettingsID::Input_ZTrigger_Name, SettingsID::Input_ZTrigger_Data, SettingsID::Input_ZTrigger_ExtraData }, + { this->analogStickUpButton, SettingsID::Input_AnalogStickUp_InputType, SettingsID::Input_AnalogStickUp_Name, SettingsID::Input_AnalogStickUp_Data, SettingsID::Input_AnalogStickUp_ExtraData }, + { this->analogStickDownButton, SettingsID::Input_AnalogStickDown_InputType, SettingsID::Input_AnalogStickDown_Name, SettingsID::Input_AnalogStickDown_Data, SettingsID::Input_AnalogStickDown_ExtraData }, + { this->analogStickLeftButton, SettingsID::Input_AnalogStickLeft_InputType, SettingsID::Input_AnalogStickLeft_Name, SettingsID::Input_AnalogStickLeft_Data, SettingsID::Input_AnalogStickLeft_ExtraData }, + { this->analogStickRightButton, SettingsID::Input_AnalogStickRight_InputType, SettingsID::Input_AnalogStickRight_Name, SettingsID::Input_AnalogStickRight_Data, SettingsID::Input_AnalogStickRight_ExtraData }, + }); + + CustomButton* buttonList[] = + { + // dpad + this->dpadUpButton, + this->dpadDownButton, + this->dpadLeftButton, + this->dpadRightButton, + // analog stick + this->analogStickUpButton, + this->analogStickDownButton, + this->analogStickLeftButton, + this->analogStickRightButton, + // cbuttons + this->cbuttonUpButton, + this->cbuttonDownButton, + this->cbuttonLeftButton, + this->cbuttonRightButton, + // triggers + this->leftTriggerButton, + this->rightTriggerButton, + this->zTriggerButton, + // buttons + this->aButton, + this->bButton, + this->startButton, + }; + for (auto& button : buttonList) + { + this->setupButtonWidgets.append(button); + } + + + this->initializeButtons(); +} + +ControllerWidget::~ControllerWidget() +{ + +} + +void ControllerWidget::initializeButtons() +{ + CustomButton* buttonList[] = + { + // dpad + this->dpadUpButton, + this->dpadDownButton, + this->dpadLeftButton, + this->dpadRightButton, + // analog stick + this->analogStickUpButton, + this->analogStickDownButton, + this->analogStickLeftButton, + this->analogStickRightButton, + // cbuttons + this->cbuttonUpButton, + this->cbuttonDownButton, + this->cbuttonLeftButton, + this->cbuttonRightButton, + // triggers + this->leftTriggerButton, + this->rightTriggerButton, + this->zTriggerButton, + // buttons + this->aButton, + this->bButton, + this->startButton, + }; + + for (auto& button : buttonList) + { + button->Initialize(this); + button->setText(" "); + } +} + +bool ControllerWidget::isCurrentDeviceKeyboard() +{ + return this->inputDeviceComboBox->currentIndex() == 0; +} + +bool ControllerWidget::isCurrentDeviceNotFound() +{ + QString title = this->inputDeviceComboBox->currentText(); + return title.endsWith("(not found)"); +} + +void ControllerWidget::disableAllChildren() +{ + for (auto& object : this->children()) + { + if (!object->inherits("QWidget")) + { + continue; + } + + QWidget* widget = (QWidget*)object; + if (widget->isEnabled()) + { + widget->setEnabled(false); + } + } +} + +void ControllerWidget::enableAllChildren() +{ + for (auto& object : this->children()) + { + if (!object->inherits("QWidget")) + { + continue; + } + + QWidget* widget = (QWidget*)object; + if (!widget->isEnabled()) + { + widget->setEnabled(true); + } + } +} + +void ControllerWidget::removeDuplicates(CustomButton* button) +{ + std::string section = this->settingsSection.toStdString(); + + if (!CoreSettingsGetBoolValue(SettingsID::Input_RemoveDuplicateMappings, section)) + { + return; + } + + for (auto& buttonWidget : this->setupButtonWidgets) + { + // skip ourselfes + if (buttonWidget == button) + { + continue; + } + + if ((buttonWidget->GetInputType() == button->GetInputType()) && + (buttonWidget->GetInputData() == button->GetInputData()) && + (buttonWidget->GetExtraInputData() == button->GetExtraInputData())) + { + buttonWidget->Clear(); + } + } +} + +void ControllerWidget::AddInputDevice(QString deviceName, int deviceNum) +{ + QString name = deviceName; + + if (deviceNum != -1) + { + name += " ("; + name += QString::number(deviceNum); + name += ")"; + } + + this->inputDeviceNameList.append(deviceName); + this->inputDeviceComboBox->addItem(name, deviceNum); +} + +void ControllerWidget::RemoveInputDevice(QString deviceName, int deviceNum) +{ + inputDeviceNameList.removeOne(deviceName); + + for (int i = 0; i < this->inputDeviceComboBox->count(); i++) + { + int tmpNum = this->inputDeviceComboBox->itemData(i).toInt(); + QString tmpName = this->inputDeviceComboBox->itemText(i); + + if (tmpName.contains(deviceName) && deviceNum == tmpNum) + { + this->inputDeviceComboBox->removeItem(i); + break; + } + } +} + +void ControllerWidget::CheckInputDeviceSettings() +{ + std::string section = this->settingsSection.toStdString(); + + if (!CoreSettingsSectionExists(section)) + { + return; + } + + std::string deviceName = CoreSettingsGetStringValue(SettingsID::Input_DeviceName, section); + int deviceNum = CoreSettingsGetIntValue(SettingsID::Input_DeviceNum, section); + + // clear (not found) devices first + int notFoundIndex = this->inputDeviceComboBox->findText("(not found)", Qt::MatchFlag::MatchEndsWith); + if (notFoundIndex != -1) + { + this->inputDeviceNameList.removeAt(notFoundIndex); + this->inputDeviceComboBox->removeItem(notFoundIndex); + } + + int deviceNameIndex = this->inputDeviceComboBox->findText(QString::fromStdString(deviceName), Qt::MatchFlag::MatchStartsWith); + int deviceNumIndex = this->inputDeviceComboBox->findData(deviceNum); + + if ((deviceNameIndex == deviceNumIndex) && + (deviceNameIndex != -1)) + { // full match + this->inputDeviceComboBox->setCurrentIndex(deviceNameIndex); + } + else if (deviceNameIndex != -1) + { // name only match + this->inputDeviceComboBox->setCurrentIndex(deviceNameIndex); + } + else + { // no match + QString title = QString::fromStdString(deviceName); + title += " (not found)"; + this->inputDeviceNameList.append(QString::fromStdString(deviceName)); + this->inputDeviceComboBox->addItem(title, deviceNum); + this->inputDeviceComboBox->setCurrentIndex(this->inputDeviceNameList.count() - 1); + } +} + +void ControllerWidget::DrawControllerImage() +{ + this->controllerImageWidget->UpdateImage(); +} + +void ControllerWidget::ClearControllerImage() +{ + this->controllerImageWidget->ClearControllerState(); +} + +void ControllerWidget::GetCurrentInputDevice(QString& deviceName, int& deviceNum, bool ignoreDeviceNotFound) +{ + int currentIndex = this->inputDeviceComboBox->currentIndex(); + + if (this->isCurrentDeviceNotFound() && !ignoreDeviceNotFound) + { + deviceName = ""; + deviceNum = -1; + } + else + { + deviceName = this->inputDeviceNameList.at(currentIndex); + deviceNum = this->inputDeviceComboBox->itemData(currentIndex).toInt(); + } +} + +void ControllerWidget::on_deadZoneSlider_valueChanged(int value) +{ + QString title = tr("Deadzone: "); + title += QString::number(value); + title += "%"; + + this->deadZoneGroupBox->setTitle(title); + this->controllerImageWidget->SetDeadzone(value); +} + +void ControllerWidget::on_analogStickRangeSlider_valueChanged(int value) +{ + QString text = QString::number(value); + text += "%"; + this->analogStickRangeLabel->setText(text); + this->controllerImageWidget->SetRange(value); +} + +void ControllerWidget::on_inputDeviceComboBox_currentIndexChanged(int value) +{ + QString deviceName = this->inputDeviceNameList.at(value); + int deviceNum = this->inputDeviceComboBox->itemData(value).toInt(); + + this->ClearControllerImage(); + + if (this->isCurrentDeviceNotFound()) + { + deviceName = ""; + deviceNum = -1; + } + + emit this->CurrentInputDeviceChanged(this, deviceName, deviceNum); +} + +void ControllerWidget::on_inputDeviceRefreshButton_clicked() +{ + emit this->RefreshInputDevicesButtonClicked(); +} + +void ControllerWidget::on_controllerPluggedCheckBox_toggled(bool value) +{ + QWidget* widgetList[] = + { + // dpad + this->dpadUpButton, + this->dpadDownButton, + this->dpadLeftButton, + this->dpadRightButton, + // analog stick + this->analogStickUpButton, + this->analogStickDownButton, + this->analogStickLeftButton, + this->analogStickRightButton, + this->analogStickRangeSlider, + // cbuttons + this->cbuttonUpButton, + this->cbuttonDownButton, + this->cbuttonLeftButton, + this->cbuttonRightButton, + // triggers + this->leftTriggerButton, + this->rightTriggerButton, + this->zTriggerButton, + // buttons + this->aButton, + this->bButton, + this->startButton, + // misc UI elements + this->deadZoneSlider, + this->optionsButton, + //this->setupButton, + this->resetButton, + this->inputDeviceComboBox, + this->inputDeviceRefreshButton + }; + + for (auto& widget : widgetList) + { + widget->setEnabled(value); + } + + this->ClearControllerImage(); +} + +void ControllerWidget::on_setupButton_clicked() +{ + this->currentInSetup = true; + this->currentSetupButtonWidgetIndex = 0; + + this->setupButtonWidgets.at(0)->setFocus(Qt::OtherFocusReason); + this->setupButtonWidgets.at(0)->click(); +} + +void ControllerWidget::on_resetButton_clicked() +{ + this->LoadSettings(); +} + +void ControllerWidget::on_optionsButton_clicked() +{ + OptionsDialog dialog(this, this->settingsSection); + dialog.exec(); +} + +void ControllerWidget::on_CustomButton_released(CustomButton* button) +{ + if (this->currentButton != nullptr) + { + this->currentButton->StopTimer(); + this->currentButton->RestoreState(); + } + + this->currentButton = button; + button->SaveState(); + button->StartTimer(); + + this->disableAllChildren(); +} + +void ControllerWidget::on_CustomButton_TimerFinished(CustomButton* button) +{ + if (this->currentButton == button) + { + this->currentButton = nullptr; + } + + button->RestoreState(); + + this->enableAllChildren(); +} + +void ControllerWidget::on_CustomButton_DataSet(CustomButton* button) +{ + this->enableAllChildren(); + this->removeDuplicates(button); + this->currentButton = nullptr; + + +/* + if (this->currentInSetup) + { + if (this->currentSetupButtonWidgetIndex + 1 >= this->setupButtonWidgets.count()) + { + this->currentInSetup = false; + return; + } + + this->currentSetupButtonWidgetIndex++; + this->setupButtonWidgets.at(currentSetupButtonWidgetIndex)->setFocus(Qt::OtherFocusReason); + this->setupButtonWidgets.at(currentSetupButtonWidgetIndex)->click(); + }*/ +} + +void ControllerWidget::on_MainDialog_SdlEvent(SDL_Event* event) +{ + switch (event->type) + { + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + { // gamepad & joystick button + SDL_JoystickID joystickId = -1; + InputType inputType = InputType::Invalid; + int sdlButton = 0; + bool sdlButtonPressed = false; + QString sdlButtonName; + + if ((event->type == SDL_CONTROLLERBUTTONDOWN) || + (event->type == SDL_CONTROLLERBUTTONUP)) + { // gamepad button + joystickId = event->cbutton.which; + inputType = InputType::GamepadButton; + sdlButton = event->cbutton.button; + sdlButtonPressed = (event->type == SDL_CONTROLLERBUTTONDOWN); + sdlButtonName = SDL_GameControllerGetStringForButton((SDL_GameControllerButton)sdlButton); + } + else + { // joystick button + joystickId = event->jbutton.which; + inputType = InputType::JoystickButton; + sdlButton = event->jbutton.button; + sdlButtonPressed = (event->type == SDL_JOYBUTTONDOWN); + sdlButtonName = "button " + QString::number(sdlButton); + } + + // make sure we have the right joystick + if (joystickId != this->currentJoystickId) + { + break; + } + + // handle button widget + if (this->currentButton != nullptr) + { + if (sdlButtonPressed) + { + this->currentButton->SetInputData( + inputType, + sdlButton, + 0, + sdlButtonName + ); + } + break; + } + + // update controller button state + for (auto& button : this->buttonWidgetMappings) + { + if (button.buttonWidget->GetInputType() == inputType && + button.buttonWidget->GetInputData() == sdlButton) + { + this->controllerImageWidget->SetButtonState(button.button, sdlButtonPressed); + } + } + + // update controller analog stick state + for (auto& joystick : this->joystickWidgetMappings) + { + if (joystick.buttonWidget->GetInputType() == inputType && + joystick.buttonWidget->GetInputData() == sdlButton) + { + switch (joystick.direction) + { + case InputAxisDirection::Up: + case InputAxisDirection::Down: + { + const int value = ( + joystick.direction == InputAxisDirection::Up ? + 100 : + -100 + ); + this->controllerImageWidget->SetYAxisState(sdlButtonPressed ? value : 0); + } break; + + case InputAxisDirection::Left: + case InputAxisDirection::Right: + { + const int value = ( + joystick.direction == InputAxisDirection::Left ? + 100 : + -100 + ); + this->controllerImageWidget->SetXAxisState(sdlButtonPressed ? value : 0); + } break; + + default: + break; + } + } + } + } break; + + case SDL_CONTROLLERAXISMOTION: + case SDL_JOYAXISMOTION: + { // gamepad & joystick axis + SDL_JoystickID joystickId = -1; + InputType inputType = InputType::Invalid; + int sdlAxis = 0; + int sdlAxisValue = 0; + QString sdlAxisName; + + if (event->type == SDL_CONTROLLERAXISMOTION) + { // gamepad axis + joystickId = event->caxis.which; + inputType = InputType::GamepadAxis; + sdlAxis = event->caxis.axis; + sdlAxisValue = event->caxis.value; + sdlAxisName = SDL_GameControllerGetStringForAxis((SDL_GameControllerAxis)sdlAxis); + sdlAxisName += sdlAxisValue > 0 ? "+" : "-"; + } + else + { // joystick axis + joystickId = event->jaxis.which; + inputType = InputType::JoystickAxis; + sdlAxis = event->jaxis.axis; + sdlAxisValue = event->jaxis.value; + sdlAxisName = "axis " + QString::number(sdlAxis); + sdlAxisName += sdlAxisValue > 0 ? "+" : "-"; + } + + // make sure the user presses the axis + // more than 50%, otherwise we might detect + // an accidental axis movement (due to i.e deadzone) + const bool sdlAxisButtonPressed = (abs(sdlAxisValue) >= (SDL_AXIS_PEAK / 2)); + const int sdlAxisDirection = (sdlAxisValue > 0 ? 1 : 0); + + // make sure we have the right joystick + if (joystickId != this->currentJoystickId) + { + break; + } + + // handle button widget + if (this->currentButton != nullptr) + { + if (sdlAxisButtonPressed) + { + this->currentButton->SetInputData( + inputType, + sdlAxis, + sdlAxisDirection, + sdlAxisName + ); + } + break; + } + + // update controller button state + for (auto& button : this->buttonWidgetMappings) + { + if (button.buttonWidget->GetInputType() == inputType && + button.buttonWidget->GetInputData() == sdlAxis && + button.buttonWidget->GetExtraInputData() == sdlAxisDirection) + { + this->controllerImageWidget->SetButtonState(button.button, sdlAxisButtonPressed); + } + } + + // update controller analog stick state + for (auto& joystick : this->joystickWidgetMappings) + { + if (joystick.buttonWidget->GetInputType() == inputType && + joystick.buttonWidget->GetInputData() == sdlAxis && + joystick.buttonWidget->GetExtraInputData() == sdlAxisDirection) + { + const int value = -(double)((double)sdlAxisValue / SDL_AXIS_PEAK * 100); + + switch (joystick.direction) + { + case InputAxisDirection::Up: + case InputAxisDirection::Down: + { + this->controllerImageWidget->SetYAxisState(value); + } break; + + case InputAxisDirection::Left: + case InputAxisDirection::Right: + { + this->controllerImageWidget->SetXAxisState(value); + } break; + + default: + break; + } + } + } + } break; + + case SDL_KEYDOWN: + case SDL_KEYUP: + { // keyboard button + + // make sure a keyboard is selected + if (!this->isCurrentDeviceKeyboard()) + { + break; + } + + const SDL_Scancode sdlButton = (SDL_Scancode)event->key.keysym.scancode; + const bool sdlButtonPressed = (event->type == SDL_KEYDOWN); + + // handle button widget + if (this->currentButton != nullptr) + { + if (sdlButtonPressed) + { + this->currentButton->SetInputData( + InputType::Keyboard, + sdlButton, + 0, + SDL_GetScancodeName(sdlButton) + ); + } + break; + } + + // update controller button state + for (auto& button : this->buttonWidgetMappings) + { + if (button.buttonWidget->GetInputType() == InputType::Keyboard && + button.buttonWidget->GetInputData() == sdlButton) + { + this->controllerImageWidget->SetButtonState(button.button, sdlButtonPressed); + } + } + + // update controller analog stick state + for (auto& joystick : this->joystickWidgetMappings) + { + if (joystick.buttonWidget->GetInputType() == InputType::Keyboard && + joystick.buttonWidget->GetInputData() == sdlButton) + { + switch (joystick.direction) + { + case InputAxisDirection::Up: + case InputAxisDirection::Down: + { + const int value = ( + joystick.direction == InputAxisDirection::Up ? + 100 : + -100 + ); + this->controllerImageWidget->SetYAxisState(sdlButtonPressed ? value : 0); + } break; + + case InputAxisDirection::Left: + case InputAxisDirection::Right: + { + const int value = ( + joystick.direction == InputAxisDirection::Left ? + 100 : + -100 + ); + this->controllerImageWidget->SetXAxisState(sdlButtonPressed ? value : 0); + } break; + + default: + break; + } + } + } + + break; + } + + default: + break; + } +} + +void ControllerWidget::on_MainDialog_SdlEventPollFinished() +{ + this->controllerImageWidget->UpdateImage(); +} + +bool ControllerWidget::IsPluggedIn() +{ + return this->controllerPluggedCheckBox->isChecked(); +} + +void ControllerWidget::SetSettingsSection(QString section) +{ + this->settingsSection = section; +} + +void ControllerWidget::LoadSettings() +{ + if (this->settingsSection.isEmpty()) + { + return; + } + + std::string section = this->settingsSection.toStdString(); + + if (!CoreSettingsSectionExists(section)) + { + this->SaveDefaultSettings(); + } + + this->controllerPluggedCheckBox->setChecked(CoreSettingsGetBoolValue(SettingsID::Input_PluggedIn, section)); + this->analogStickRangeSlider->setValue(CoreSettingsGetIntValue(SettingsID::Input_Range, section)); + this->deadZoneSlider->setValue(CoreSettingsGetIntValue(SettingsID::Input_Deadzone, section)); + + for (auto& buttonSetting : this->buttonSettingMappings) + { + enum InputType type = (InputType)CoreSettingsGetIntValue(buttonSetting.inputTypeSettingsId, section); + std::string name = CoreSettingsGetStringValue(buttonSetting.nameSettingsId, section); + int data = CoreSettingsGetIntValue(buttonSetting.dataSettingsId, section); + int extraData = CoreSettingsGetIntValue(buttonSetting.extraDataSettingsId, section); + + buttonSetting.button->SetInputData(type, data, extraData, QString::fromStdString(name)); + } + +} + +void ControllerWidget::SaveDefaultSettings() +{ + if (this->settingsSection.isEmpty()) + { + return; + } + + std::string section = this->settingsSection.toStdString(); + + CoreSettingsSetValue(SettingsID::Input_PluggedIn, section, false); + CoreSettingsSetValue(SettingsID::Input_DeviceName, section, std::string("Keyboard")); + CoreSettingsSetValue(SettingsID::Input_DeviceNum, section, -1); + CoreSettingsSetValue(SettingsID::Input_Range, section, 100); + CoreSettingsSetValue(SettingsID::Input_Deadzone, section, 15); + CoreSettingsSetValue(SettingsID::Input_Pak, section, 0); + CoreSettingsSetValue(SettingsID::Input_RemoveDuplicateMappings, section, true); + + for (auto& buttonSetting : this->buttonSettingMappings) + { + CoreSettingsSetValue(buttonSetting.inputTypeSettingsId, section, (int)InputType::Invalid); + CoreSettingsSetValue(buttonSetting.nameSettingsId, section, std::string(" ")); + CoreSettingsSetValue(buttonSetting.dataSettingsId, section, 0); + CoreSettingsSetValue(buttonSetting.extraDataSettingsId, section, 0); + } +} + +void ControllerWidget::SaveSettings() +{ + if (this->settingsSection.isEmpty()) + { + return; + } + + QString deviceName; + int deviceNum; + std::string section = this->settingsSection.toStdString(); + + this->GetCurrentInputDevice(deviceName, deviceNum, true); + + CoreSettingsSetValue(SettingsID::Input_PluggedIn, section, this->IsPluggedIn()); + CoreSettingsSetValue(SettingsID::Input_DeviceName, section, deviceName.toStdString()); + CoreSettingsSetValue(SettingsID::Input_DeviceNum, section, deviceNum); + CoreSettingsSetValue(SettingsID::Input_Range, section, this->analogStickRangeSlider->value()); + CoreSettingsSetValue(SettingsID::Input_Deadzone, section, this->deadZoneSlider->value()); + + for (auto& buttonSetting : this->buttonSettingMappings) + { + CoreSettingsSetValue(buttonSetting.inputTypeSettingsId, section, (int)buttonSetting.button->GetInputType()); + CoreSettingsSetValue(buttonSetting.nameSettingsId, section, buttonSetting.button->text().toStdString()); + CoreSettingsSetValue(buttonSetting.dataSettingsId, section, buttonSetting.button->GetInputData()); + CoreSettingsSetValue(buttonSetting.extraDataSettingsId, section, buttonSetting.button->GetExtraInputData()); + } +} + +void ControllerWidget::SetCurrentJoystickID(SDL_JoystickID joystickId) +{ + this->currentJoystickId = joystickId; +} + diff --git a/Source/RMG-Input/UserInterface/Widget/ControllerWidget.hpp b/Source/RMG-Input/UserInterface/Widget/ControllerWidget.hpp new file mode 100644 index 00000000..4d714da0 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Widget/ControllerWidget.hpp @@ -0,0 +1,130 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef CONTROLLERWIDGET_H +#define CONTROLLERWIDGET_H + +#include "CustomButton.hpp" +#include "ControllerImageWidget.hpp" + +#include "UserInterface/EventFilter.hpp" + +using namespace UserInterface::Widget; + +#include "ui_ControllerWidget.h" +#include "common.hpp" + +#include + +#include + +namespace UserInterface +{ +namespace Widget +{ +class ControllerWidget : public QWidget, Ui::ControllerWidget +{ + Q_OBJECT + +private: + QString settingsSection; + + QList inputDeviceNameList; + CustomButton* currentButton = nullptr; + + struct buttonWidgetMapping + { + enum N64ControllerButton button; + CustomButton* buttonWidget; + }; + + struct axisWidgetMapping + { + enum InputAxisDirection direction; + CustomButton* buttonWidget; + }; + + struct buttonSettingMapping + { + CustomButton* button; + SettingsID inputTypeSettingsId; + SettingsID nameSettingsId; + SettingsID dataSettingsId; + SettingsID extraDataSettingsId; + }; + + QList buttonWidgetMappings; + QList joystickWidgetMappings; + QList buttonSettingMappings; + QList setupButtonWidgets; + int currentSetupButtonWidgetIndex = 0; + bool currentInSetup = false; + + void initializeButtons(); + + bool isCurrentDeviceKeyboard(); + bool isCurrentDeviceNotFound(); + + void disableAllChildren(); + void enableAllChildren(); + + void removeDuplicates(CustomButton* button); + + SDL_JoystickID currentJoystickId = -1; + +public: + ControllerWidget(QWidget* parent, EventFilter* eventFilter); + ~ControllerWidget(); + + void AddInputDevice(QString deviceName, int deviceNum); + void RemoveInputDevice(QString deviceName, int deviceNum); + void CheckInputDeviceSettings(); + + void DrawControllerImage(); + void ClearControllerImage(); + + void GetCurrentInputDevice(QString& deviceName, int& deviceNum, bool ignoreDeviceNotFound = false); + bool IsPluggedIn(); + + void SetSettingsSection(QString section); + + void LoadSettings(); + void SaveDefaultSettings(); + void SaveSettings(); + + void SetCurrentJoystickID(SDL_JoystickID joystickId); + +private slots: + void on_deadZoneSlider_valueChanged(int value); + void on_analogStickRangeSlider_valueChanged(int value); + + void on_inputDeviceComboBox_currentIndexChanged(int value); + void on_inputDeviceRefreshButton_clicked(); + + void on_controllerPluggedCheckBox_toggled(bool value); + + void on_setupButton_clicked(); + void on_resetButton_clicked(); + void on_optionsButton_clicked(); + +public slots: + void on_CustomButton_released(CustomButton* button); + void on_CustomButton_TimerFinished(CustomButton* button); + void on_CustomButton_DataSet(CustomButton* button); + + void on_MainDialog_SdlEvent(SDL_Event* event); + void on_MainDialog_SdlEventPollFinished(); +signals: + void CurrentInputDeviceChanged(ControllerWidget* widget, QString deviceName, int deviceNum); + void RefreshInputDevicesButtonClicked(); +}; +} +} + +#endif // CONTROLLERWIDGET_H \ No newline at end of file diff --git a/Source/RMG-Input/UserInterface/Widget/ControllerWidget.ui b/Source/RMG-Input/UserInterface/Widget/ControllerWidget.ui new file mode 100644 index 00000000..8fe44493 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Widget/ControllerWidget.ui @@ -0,0 +1,951 @@ + + + ControllerWidget + + + + 0 + 0 + 811 + 554 + + + + + 0 + 0 + + + + ControllerWidget + + + + + + + + + 0 + 0 + + + + Input Device + + + + + + + 0 + 0 + + + + + + + + Refresh + + + + + + + + + + Deadzone: 25% + + + + + + + 0 + 0 + + + + 0 + + + 100 + + + Qt::Horizontal + + + + + + + + + + Controller is plugged in + + + true + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + Digital Pad + + + false + + + false + + + + + + + + + 0 + 0 + + + + Up: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + 0 + 0 + + + + Down: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + 0 + 0 + + + + left: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + 0 + 0 + + + + Right: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + 0 + 0 + + + + Analog Stick + + + false + + + false + + + + + + + + + 0 + 0 + + + + Up: + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Down: + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Left: + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Right: + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Range: + + + + + + + + 0 + 0 + + + + 1 + + + 100 + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + 100% + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + L-Trigger: + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + R-Trigger: + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + + 527 + 0 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Start: + + + + + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Z-Trigger: + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + C-Buttons + + + false + + + false + + + + + + + + + 0 + 0 + + + + Up: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + 0 + 0 + + + + Down: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + 0 + 0 + + + + Left: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + 0 + 0 + + + + Right: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + + + + B: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + 0 + 0 + + + + A: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Reset + + + + + + + Options + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + CustomButton + QPushButton +
UserInterface/Widget/CustomButton.hpp
+
+ + ControllerImageWidget + QWidget +
UserInterface/Widget/ControllerImageWidget.hpp
+ 1 +
+
+ + +
diff --git a/Source/RMG-Input/UserInterface/Widget/CustomButton.cpp b/Source/RMG-Input/UserInterface/Widget/CustomButton.cpp new file mode 100644 index 00000000..03fb0919 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Widget/CustomButton.cpp @@ -0,0 +1,113 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "CustomButton.hpp" +#include "ControllerWidget.hpp" + +using namespace UserInterface::Widget; + +CustomButton::CustomButton(QWidget* parent) : QPushButton(parent) +{ + this->countDownTimer = new QTimer(this); + connect(this->countDownTimer, &QTimer::timeout, this, &CustomButton::on_countDownTimer_triggered); +} + +CustomButton::~CustomButton() +{ + +} + +void CustomButton::Initialize(QWidget* widget) +{ + this->controllerWidget = (ControllerWidget*)widget; + + connect(this, &QPushButton::released, + [=] + { + emit this->controllerWidget->on_CustomButton_released(this); + }); +} + +void CustomButton::Clear() +{ + this->inputType = InputType::Invalid; + this->inputData = 0; + this->extraInputData = 0; + this->savedInputData = 0; + this->savedExtraInputData = 0; + this->setText(" "); +} + +void CustomButton::StartTimer() +{ + this->currentCountDownNum = 5; + this->on_countDownTimer_triggered(); + this->countDownTimer->start(1000); +} + +void CustomButton::StopTimer() +{ + this->countDownTimer->stop(); +} + +void CustomButton::SetInputData(enum InputType type, int value, int extraValue, QString name) +{ + this->inputType = type; + this->inputData = value; + this->extraInputData = extraValue; + this->setText(name); + this->StopTimer(); + emit this->controllerWidget->on_CustomButton_DataSet(this); +} + +enum InputType CustomButton::GetInputType() +{ + return this->inputType; +} + +int CustomButton::GetInputData() +{ + return this->inputData; +} + +int CustomButton::GetExtraInputData() +{ + return this->extraInputData; +} + +void CustomButton::SaveState() +{ + this->savedInputType = this->inputType; + this->savedInputData = this->inputData; + this->savedExtraInputData = this->extraInputData; + this->savedText = this->text(); +} + +void CustomButton::RestoreState() +{ + this->inputType = this->savedInputType; + this->inputData = this->savedInputData; + this->extraInputData = this->savedExtraInputData; + this->setText(this->savedText); +} + +void CustomButton::on_countDownTimer_triggered() +{ + this->setText(QString::number(this->currentCountDownNum)); + + if (this->currentCountDownNum == 0) + { + this->setText(""); + this->currentCountDownNum = 5; + this->countDownTimer->stop(); + emit this->controllerWidget->on_CustomButton_TimerFinished(this); + } + + this->currentCountDownNum--; +} diff --git a/Source/RMG-Input/UserInterface/Widget/CustomButton.hpp b/Source/RMG-Input/UserInterface/Widget/CustomButton.hpp new file mode 100644 index 00000000..9ad6cac4 --- /dev/null +++ b/Source/RMG-Input/UserInterface/Widget/CustomButton.hpp @@ -0,0 +1,67 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef CUSTOMBUTTON_HPP +#define CUSTOMBUTTON_HPP + +#include +#include + +#include "common.hpp" + +namespace UserInterface +{ +namespace Widget +{ + +// we can't include ControllerWidget.hpp +// because that header includes this header, +// so forward-declare it to prevent issues +class ControllerWidget; + +class CustomButton : public QPushButton +{ +private: + QTimer* countDownTimer = nullptr; + ControllerWidget* controllerWidget = nullptr; + int currentCountDownNum = 0; + enum InputType inputType = InputType::Invalid; + int inputData = 0; + int extraInputData = 0; + + enum InputType savedInputType = InputType::Invalid; + int savedInputData = 0; + int savedExtraInputData = 0; + QString savedText; +public: + CustomButton(QWidget* parent); + ~CustomButton(); + void Initialize(QWidget* widget); + void Clear(); + + void StartTimer(); + void StopTimer(); + + + void SetInputData(enum InputType type, int value, int extraValue, QString name); + enum InputType GetInputType(); + int GetInputData(); + int GetExtraInputData(); + + void SaveState(); + void RestoreState(); + +private slots: + void on_countDownTimer_triggered(); +}; +} +} + + +#endif // CUSTOMBUTTON_HPP diff --git a/Source/RMG-Input/Utilities/InputDevice.cpp b/Source/RMG-Input/Utilities/InputDevice.cpp new file mode 100644 index 00000000..70005b5a --- /dev/null +++ b/Source/RMG-Input/Utilities/InputDevice.cpp @@ -0,0 +1,175 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "InputDevice.hpp" + +using namespace Utilities; + +InputDevice::InputDevice() +{ + +} + +InputDevice::~InputDevice() +{ + this->CloseDevice(); +} + +void InputDevice::SetSDLThread(Thread::SDLThread* sdlThread) +{ + this->sdlThread = sdlThread; + connect(this->sdlThread, &Thread::SDLThread::OnInputDeviceFound, this, + &InputDevice::on_SDLThread_DeviceFound); + connect(this->sdlThread, &Thread::SDLThread::OnDeviceSearchFinished, this, + &InputDevice::on_SDLThread_DeviceSearchFinished); +} + +SDL_Joystick* InputDevice::GetJoystickHandle() +{ + return this->joystick; +} + +SDL_GameController* InputDevice::GetGameControllerHandle() +{ + return this->gameController; +} + +bool InputDevice::StartRumble(void) +{ + if (this->gameController != nullptr) + { + return SDL_GameControllerRumble(this->gameController, 0xFFFF, 0xFFFF, SDL_HAPTIC_INFINITY) == 0; + } + else if (this->joystick != nullptr) + { + return SDL_JoystickRumble(this->joystick, 0xFFFF, 0xFFFF, SDL_HAPTIC_INFINITY) == 0; + } + + return false; +} + +bool InputDevice::StopRumble(void) +{ + if (this->gameController != nullptr) + { + return SDL_GameControllerRumble(this->gameController, 0, 0, 0) == 0; + } + else if (this->joystick != nullptr) + { + return SDL_JoystickRumble(this->joystick, 0, 0, 0) == 0; + } + + return false; +} + +bool InputDevice::IsAttached(void) +{ + return SDL_JoystickGetAttached(this->joystick) == SDL_TRUE; +} + +bool InputDevice::HasOpenDevice() +{ + return this->hasOpenDevice; +} + +void InputDevice::OpenDevice(std::string name, int num) +{ + // wait until SDLThread is done first + while (this->sdlThread->GetCurrentAction() != SDLThreadAction::None) + { + QThread::msleep(50); + } + + this->foundDevicesWithNameMatch.clear(); + this->desiredDeviceName = name; + this->desiredDeviceNum = num; + this->isOpeningDevice = true; + + // tell SDLThread to query input devices + this->sdlThread->SetAction(SDLThreadAction::GetInputDevices); +} + +bool InputDevice::IsOpeningDevice(void) +{ + return this->isOpeningDevice; +} + +bool InputDevice::CloseDevice() +{ + if (this->joystick != nullptr) + { + SDL_JoystickClose(this->joystick); + this->joystick = nullptr; + } + + if (this->gameController != nullptr) + { + SDL_GameControllerClose(this->gameController); + this->gameController = nullptr; + } + + return true; +} + +void InputDevice::on_SDLThread_DeviceFound(QString name, int number) +{ + if ((!this->isOpeningDevice) || + (this->desiredDeviceName != name.toStdString())) + { + return; + } + + SDLDevice device; + device.name = name.toStdString(); + device.number = number; + + this->foundDevicesWithNameMatch.push_back(device); +} + +void InputDevice::on_SDLThread_DeviceSearchFinished(void) +{ + if (!this->isOpeningDevice) + { + return; + } + + this->CloseDevice(); + + if (this->foundDevicesWithNameMatch.empty()) + { + this->isOpeningDevice = false; + this->hasOpenDevice = false; + return; + } + + // try to find exact match + SDLDevice device; + device.name = this->desiredDeviceName; + device.number = this->desiredDeviceNum; + + auto iter = std::find(this->foundDevicesWithNameMatch.begin(), this->foundDevicesWithNameMatch.end(), device); + if (iter != this->foundDevicesWithNameMatch.end()) + { // use exact match + device.name = iter->name; + device.number = iter->number; + } + else + { // no exact match, use first with name match + device = this->foundDevicesWithNameMatch.at(0); + } + + this->joystick = SDL_JoystickOpen(device.number); + if (SDL_IsGameController(device.number)) + { + this->gameController = SDL_GameControllerOpen(device.number); + } + + this->isOpeningDevice = false; + this->hasOpenDevice = this->joystick != nullptr || this->gameController == nullptr; +} diff --git a/Source/RMG-Input/Utilities/InputDevice.hpp b/Source/RMG-Input/Utilities/InputDevice.hpp new file mode 100644 index 00000000..e4be10f2 --- /dev/null +++ b/Source/RMG-Input/Utilities/InputDevice.hpp @@ -0,0 +1,85 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef INPUTDEVICE_HPP +#define INPUTDEVICE_HPP + +#include "common.hpp" + +#include +#include +#include + +#include "Thread/SDLThread.hpp" + +namespace Utilities +{ +class InputDevice : public QObject +{ +Q_OBJECT +public: + InputDevice(); + ~InputDevice(); + + void SetSDLThread(Thread::SDLThread* sdlThread); + + SDL_Joystick* GetJoystickHandle(void); + SDL_GameController* GetGameControllerHandle(void); + + bool StartRumble(void); + bool StopRumble(void); + + // returns whether the device is attached + bool IsAttached(void); + + // returns whether a device has been opened + bool HasOpenDevice(void); + + // tries to open device with given name & num + void OpenDevice(std::string name, int num); + + // returns whether we're still trying to open the device + bool IsOpeningDevice(void); + + // tries to close opened device + bool CloseDevice(void); + +private: + struct SDLDevice + { + std::string name; + int number; + + bool operator== (SDLDevice other) + { + return other.name == name && + other.number == number; + } + }; + + SDL_Joystick* joystick = nullptr; + SDL_GameController* gameController = nullptr; + + bool hasOpenDevice = false; + bool isOpeningDevice = false; + + Thread::SDLThread* sdlThread = nullptr; + + std::string desiredDeviceName; + int desiredDeviceNum; + + std::vector foundDevicesWithNameMatch; + +private slots: + void on_SDLThread_DeviceFound(QString, int); + void on_SDLThread_DeviceSearchFinished(void); +}; +} // namespace Utilities + +#endif // INPUTDEVICE_HPP \ No newline at end of file diff --git a/Source/RMG-Input/Utilities/QtKeyToSdl2Key.cpp b/Source/RMG-Input/Utilities/QtKeyToSdl2Key.cpp new file mode 100644 index 00000000..0c3c0659 --- /dev/null +++ b/Source/RMG-Input/Utilities/QtKeyToSdl2Key.cpp @@ -0,0 +1,329 @@ +#include "QtKeyToSdl2Key.hpp" + +#include + +using namespace Utilities; + +int Utilities::QtKeyToSdl2Key(int key) +{ + SDL_Scancode returnValue; + switch (key) + { + case Qt::Key_Escape: + returnValue = SDL_SCANCODE_ESCAPE; + break; + case Qt::Key_Tab: + returnValue = SDL_SCANCODE_TAB; + break; + case Qt::Key_Backspace: + returnValue = SDL_SCANCODE_BACKSPACE; + break; + case Qt::Key_Return: + returnValue = SDL_SCANCODE_RETURN; + break; + case Qt::Key_Enter: + returnValue = SDL_SCANCODE_KP_ENTER; + break; + case Qt::Key_Insert: + returnValue = SDL_SCANCODE_INSERT; + break; + case Qt::Key_Delete: + returnValue = SDL_SCANCODE_DELETE; + break; + case Qt::Key_Pause: + returnValue = SDL_SCANCODE_PAUSE; + break; + case Qt::Key_Print: + returnValue = SDL_SCANCODE_PRINTSCREEN; + break; + case Qt::Key_SysReq: + returnValue = SDL_SCANCODE_SYSREQ; + break; + case Qt::Key_Clear: + returnValue = SDL_SCANCODE_CLEAR; + break; + case Qt::Key_Home: + returnValue = SDL_SCANCODE_HOME; + break; + case Qt::Key_End: + returnValue = SDL_SCANCODE_END; + break; + case Qt::Key_Left: + returnValue = SDL_SCANCODE_LEFT; + break; + case Qt::Key_Right: + returnValue = SDL_SCANCODE_RIGHT; + break; + case Qt::Key_Up: + returnValue = SDL_SCANCODE_UP; + break; + case Qt::Key_Down: + returnValue = SDL_SCANCODE_DOWN; + break; + case Qt::Key_PageUp: + returnValue = SDL_SCANCODE_PAGEUP; + break; + case Qt::Key_PageDown: + returnValue = SDL_SCANCODE_PAGEDOWN; + break; + case Qt::Key_Shift: + returnValue = SDL_SCANCODE_LSHIFT; + break; + case Qt::Key_Control: + returnValue = SDL_SCANCODE_LCTRL; + break; + case Qt::Key_Meta: + returnValue = SDL_SCANCODE_LGUI; + break; + case Qt::Key_Alt: + returnValue = SDL_SCANCODE_LALT; + break; + case Qt::Key_AltGr: + returnValue = SDL_SCANCODE_RALT; + break; + case Qt::Key_CapsLock: + returnValue = SDL_SCANCODE_CAPSLOCK; + break; + case Qt::Key_NumLock: + returnValue = SDL_SCANCODE_NUMLOCKCLEAR; + break; + case Qt::Key_ScrollLock: + returnValue = SDL_SCANCODE_SCROLLLOCK; + break; + case Qt::Key_F1: + returnValue = SDL_SCANCODE_F1; + break; + case Qt::Key_F2: + returnValue = SDL_SCANCODE_F2; + break; + case Qt::Key_F3: + returnValue = SDL_SCANCODE_F3; + break; + case Qt::Key_F4: + returnValue = SDL_SCANCODE_F4; + break; + case Qt::Key_F5: + returnValue = SDL_SCANCODE_F5; + break; + case Qt::Key_F6: + returnValue = SDL_SCANCODE_F6; + break; + case Qt::Key_F7: + returnValue = SDL_SCANCODE_F7; + break; + case Qt::Key_F8: + returnValue = SDL_SCANCODE_F8; + break; + case Qt::Key_F9: + returnValue = SDL_SCANCODE_F9; + break; + case Qt::Key_F10: + returnValue = SDL_SCANCODE_F10; + break; + case Qt::Key_F11: + returnValue = SDL_SCANCODE_F11; + break; + case Qt::Key_F12: + returnValue = SDL_SCANCODE_F12; + break; + case Qt::Key_F13: + returnValue = SDL_SCANCODE_F13; + break; + case Qt::Key_F14: + returnValue = SDL_SCANCODE_F14; + break; + case Qt::Key_F15: + returnValue = SDL_SCANCODE_F15; + break; + case Qt::Key_F16: + returnValue = SDL_SCANCODE_F16; + break; + case Qt::Key_F17: + returnValue = SDL_SCANCODE_F17; + break; + case Qt::Key_F18: + returnValue = SDL_SCANCODE_F18; + break; + case Qt::Key_F19: + returnValue = SDL_SCANCODE_F19; + break; + case Qt::Key_F20: + returnValue = SDL_SCANCODE_F20; + break; + case Qt::Key_F21: + returnValue = SDL_SCANCODE_F21; + break; + case Qt::Key_F22: + returnValue = SDL_SCANCODE_F22; + break; + case Qt::Key_F23: + returnValue = SDL_SCANCODE_F23; + break; + case Qt::Key_F24: + returnValue = SDL_SCANCODE_F24; + break; + case Qt::Key_Space: + returnValue = SDL_SCANCODE_SPACE; + break; + case Qt::Key_0: + returnValue = SDL_SCANCODE_0; + break; + case Qt::Key_1: + returnValue = SDL_SCANCODE_1; + break; + case Qt::Key_2: + returnValue = SDL_SCANCODE_2; + break; + case Qt::Key_3: + returnValue = SDL_SCANCODE_3; + break; + case Qt::Key_4: + returnValue = SDL_SCANCODE_4; + break; + case Qt::Key_5: + returnValue = SDL_SCANCODE_5; + break; + case Qt::Key_6: + returnValue = SDL_SCANCODE_6; + break; + case Qt::Key_7: + returnValue = SDL_SCANCODE_7; + break; + case Qt::Key_8: + returnValue = SDL_SCANCODE_8; + break; + case Qt::Key_9: + returnValue = SDL_SCANCODE_9; + break; + case Qt::Key_A: + returnValue = SDL_SCANCODE_A; + break; + case Qt::Key_B: + returnValue = SDL_SCANCODE_B; + break; + case Qt::Key_C: + returnValue = SDL_SCANCODE_C; + break; + case Qt::Key_D: + returnValue = SDL_SCANCODE_D; + break; + case Qt::Key_E: + returnValue = SDL_SCANCODE_E; + break; + case Qt::Key_F: + returnValue = SDL_SCANCODE_F; + break; + case Qt::Key_G: + returnValue = SDL_SCANCODE_G; + break; + case Qt::Key_H: + returnValue = SDL_SCANCODE_H; + break; + case Qt::Key_I: + returnValue = SDL_SCANCODE_I; + break; + case Qt::Key_J: + returnValue = SDL_SCANCODE_J; + break; + case Qt::Key_K: + returnValue = SDL_SCANCODE_K; + break; + case Qt::Key_L: + returnValue = SDL_SCANCODE_L; + break; + case Qt::Key_M: + returnValue = SDL_SCANCODE_M; + break; + case Qt::Key_N: + returnValue = SDL_SCANCODE_N; + break; + case Qt::Key_O: + returnValue = SDL_SCANCODE_O; + break; + case Qt::Key_P: + returnValue = SDL_SCANCODE_P; + break; + case Qt::Key_Q: + returnValue = SDL_SCANCODE_Q; + break; + case Qt::Key_R: + returnValue = SDL_SCANCODE_R; + break; + case Qt::Key_S: + returnValue = SDL_SCANCODE_S; + break; + case Qt::Key_T: + returnValue = SDL_SCANCODE_T; + break; + case Qt::Key_U: + returnValue = SDL_SCANCODE_U; + break; + case Qt::Key_V: + returnValue = SDL_SCANCODE_V; + break; + case Qt::Key_W: + returnValue = SDL_SCANCODE_W; + break; + case Qt::Key_X: + returnValue = SDL_SCANCODE_X; + break; + case Qt::Key_Y: + returnValue = SDL_SCANCODE_Y; + break; + case Qt::Key_Z: + returnValue = SDL_SCANCODE_Z; + break; + case Qt::Key_BracketLeft: + returnValue = SDL_SCANCODE_LEFTBRACKET; + break; + case Qt::Key_BracketRight: + returnValue = SDL_SCANCODE_RIGHTBRACKET; + break; + case Qt::Key_Minus: + returnValue = SDL_SCANCODE_MINUS; + break; + case Qt::Key_Semicolon: + returnValue = SDL_SCANCODE_SEMICOLON; + break; + case Qt::Key_Slash: + returnValue = SDL_SCANCODE_SLASH; + break; + case Qt::Key_Backslash: + returnValue = SDL_SCANCODE_BACKSLASH; + break; + case Qt::Key_Apostrophe: + returnValue = SDL_SCANCODE_APOSTROPHE; + break; + case Qt::Key_Comma: + returnValue = SDL_SCANCODE_COMMA; + break; + case Qt::Key_Period: + returnValue = SDL_SCANCODE_PERIOD; + break; + case Qt::Key_Equal: + returnValue = SDL_SCANCODE_EQUALS; + break; + case Qt::Key_QuoteLeft: + returnValue = SDL_SCANCODE_GRAVE; + break; + default: + returnValue = SDL_SCANCODE_UNKNOWN; + break; + } + + return returnValue; +} + +int Utilities::QtModKeyToSdl2ModKey(Qt::KeyboardModifiers modifiers) +{ + int value = 0; + if (modifiers & Qt::ShiftModifier) + value |= KMOD_SHIFT; + if (modifiers & Qt::ControlModifier) + value |= KMOD_CTRL; + if (modifiers & Qt::AltModifier) + value |= KMOD_ALT; + if (modifiers & Qt::MetaModifier) + value |= KMOD_GUI; + return value; +} diff --git a/Source/RMG-Input/Utilities/QtKeyToSdl2Key.hpp b/Source/RMG-Input/Utilities/QtKeyToSdl2Key.hpp new file mode 100644 index 00000000..1047885b --- /dev/null +++ b/Source/RMG-Input/Utilities/QtKeyToSdl2Key.hpp @@ -0,0 +1,12 @@ +#ifndef QTKEYTOSDL2KEY_HPP +#define QTKEYTOSDL2KEY_HPP + +#include + +namespace Utilities +{ +int QtKeyToSdl2Key(int); +int QtModKeyToSdl2ModKey(Qt::KeyboardModifiers); +} // namespace Utilities + +#endif // QTKEYTOSDL2KEY_HPP diff --git a/Source/RMG-Input/common.hpp b/Source/RMG-Input/common.hpp new file mode 100644 index 00000000..a809dc24 --- /dev/null +++ b/Source/RMG-Input/common.hpp @@ -0,0 +1,60 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef COMMON_HPP +#define COMMON_HPP + +#define SDL_AXIS_PEAK 32767 + +enum class N64ControllerButton +{ + A = 0, + B, + Start, + DpadUp, + DpadDown, + DpadLeft, + DpadRight, + CButtonUp, + CButtonDown, + CButtonLeft, + CButtonRight, + LeftTrigger, + RightTrigger, + ZTrigger, + Invalid +}; + +enum class InputType +{ + Keyboard = -1, + GamepadButton, + GamepadAxis, + JoystickButton, + JoystickAxis, + JoystickHat, + Invalid +}; + +enum class InputAxisDirection +{ + Up = 0, + Down, + Left, + Right +}; + +enum class N64ControllerPak +{ + MemoryPak = 0, + RumblePak, + None, +}; + +#endif // COMMON_HPP diff --git a/Source/RMG-Input/main.cpp b/Source/RMG-Input/main.cpp new file mode 100644 index 00000000..01c8d343 --- /dev/null +++ b/Source/RMG-Input/main.cpp @@ -0,0 +1,605 @@ +/* + * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG + * Copyright (C) 2020 Rosalie Wanders + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#define CORE_PLUGIN +#define M64P_PLUGIN_PROTOTYPES 1 +#define INPUT_PLUGIN_API_VERSION 0x020100 + +#include +#include "Thread/SDLThread.hpp" +#include "Utilities/InputDevice.hpp" +#include "common.hpp" + +#include + +#include +#include + +#include +#include + +// +// Local Defines +// + +#define NUM_CONTROLLERS 4 +#define MAX_AXIS_VALUE 85 + +#define RD_GETSTATUS 0x00 // get status +#define RD_READKEYS 0x01 // read button values +#define RD_READPAK 0x02 // read from controllerpack +#define RD_WRITEPAK 0x03 // write to controllerpack +#define RD_RESETCONTROLLER 0xff // reset controller +#define RD_READEEPROM 0x04 // read eeprom +#define RD_WRITEEPROM 0x05 // write eeprom + +#define PAK_IO_RUMBLE 0xC000 // the address where rumble-commands are sent to + +// +// Local Structures +// + +struct InputMapping +{ + InputType Type = InputType::Invalid; + int Data = 0; + int ExtraData = 0; +}; + +struct InputProfile +{ + bool PluggedIn = false; + int DeadzoneValue = 0; + int RangeValue = 100; + + N64ControllerPak ControllerPak = N64ControllerPak::None; + + // input device information + std::string DeviceName; + int DeviceNum = -1; + std::chrono::time_point LastDeviceCheckTime = std::chrono::high_resolution_clock::now(); + + // input device + Utilities::InputDevice InputDevice; + + // buttons + InputMapping Button_A; + InputMapping Button_B; + InputMapping Button_Start; + InputMapping Button_DpadUp; + InputMapping Button_DpadDown; + InputMapping Button_DpadLeft; + InputMapping Button_DpadRight; + InputMapping Button_CButtonUp; + InputMapping Button_CButtonDown; + InputMapping Button_CButtonLeft; + InputMapping Button_CButtonRight; + InputMapping Button_LeftTrigger; + InputMapping Button_RightTrigger; + InputMapping Button_ZTrigger; + + // analog stick + InputMapping AnalogStick_Up; + InputMapping AnalogStick_Down; + InputMapping AnalogStick_Left; + InputMapping AnalogStick_Right; +}; + +// +// Local variables +// + +// SDL thread +static Thread::SDLThread *l_SDLThread = nullptr; + +// input profiles +static InputProfile l_InputProfiles[NUM_CONTROLLERS]; + +// keyboard state +static bool l_KeyboardState[SDL_NUM_SCANCODES]; + +// +// Local Functions +// + +static void load_inputmapping_settings(InputMapping* mapping, std::string section, + SettingsID inputTypeSettingsId, SettingsID dataSettingsId, SettingsID extraDataSettingsId) +{ + mapping->Type = (InputType)CoreSettingsGetIntValue(inputTypeSettingsId, section); + mapping->Data = CoreSettingsGetIntValue(dataSettingsId, section); + mapping->ExtraData = CoreSettingsGetIntValue(extraDataSettingsId, section); +} + +static void load_settings(void) +{ + for (int i = 0; i < NUM_CONTROLLERS; i++) + { + InputProfile* profile = &l_InputProfiles[i]; + std::string section = "Rosalie's Mupen GUI - Input Plugin Profile " + std::to_string(i); + + // when the settings section doesn't exist, + // disable profile + if (!CoreSettingsSectionExists(section)) + { + profile->PluggedIn = false; + continue; + } + + profile->PluggedIn = CoreSettingsGetBoolValue(SettingsID::Input_PluggedIn, section); + profile->DeadzoneValue = CoreSettingsGetIntValue(SettingsID::Input_Deadzone, section); + profile->RangeValue = CoreSettingsGetIntValue(SettingsID::Input_Range, section); + profile->ControllerPak = (N64ControllerPak)CoreSettingsGetIntValue(SettingsID::Input_Pak, section); + profile->DeviceName = CoreSettingsGetStringValue(SettingsID::Input_DeviceName, section); + profile->DeviceNum = CoreSettingsGetIntValue(SettingsID::Input_DeviceNum, section); + + // load inputmapping settings + load_inputmapping_settings(&profile->Button_A, section, SettingsID::Input_A_InputType, SettingsID::Input_A_Data, SettingsID::Input_A_ExtraData); + load_inputmapping_settings(&profile->Button_B, section, SettingsID::Input_B_InputType, SettingsID::Input_B_Data, SettingsID::Input_B_ExtraData); + load_inputmapping_settings(&profile->Button_Start, section, SettingsID::Input_Start_InputType, SettingsID::Input_Start_Data, SettingsID::Input_Start_ExtraData); + load_inputmapping_settings(&profile->Button_DpadUp, section, SettingsID::Input_DpadUp_InputType, SettingsID::Input_DpadUp_Data, SettingsID::Input_DpadUp_ExtraData); + load_inputmapping_settings(&profile->Button_DpadDown, section, SettingsID::Input_DpadDown_InputType, SettingsID::Input_DpadDown_Data, SettingsID::Input_DpadDown_ExtraData); + load_inputmapping_settings(&profile->Button_DpadLeft, section, SettingsID::Input_DpadLeft_InputType, SettingsID::Input_DpadLeft_Data, SettingsID::Input_DpadLeft_ExtraData); + load_inputmapping_settings(&profile->Button_DpadRight, section, SettingsID::Input_DpadRight_InputType, SettingsID::Input_DpadRight_Data, SettingsID::Input_DpadRight_ExtraData); + load_inputmapping_settings(&profile->Button_CButtonUp, section, SettingsID::Input_CButtonUp_InputType, SettingsID::Input_CButtonUp_Data, SettingsID::Input_CButtonUp_ExtraData); + load_inputmapping_settings(&profile->Button_CButtonDown, section, SettingsID::Input_CButtonDown_InputType, SettingsID::Input_CButtonDown_Data, SettingsID::Input_CButtonDown_ExtraData); + load_inputmapping_settings(&profile->Button_CButtonLeft, section, SettingsID::Input_CButtonLeft_InputType, SettingsID::Input_CButtonLeft_Data, SettingsID::Input_CButtonLeft_ExtraData); + load_inputmapping_settings(&profile->Button_CButtonRight, section, SettingsID::Input_CButtonRight_InputType, SettingsID::Input_CButtonRight_Data, SettingsID::Input_CButtonRight_ExtraData); + load_inputmapping_settings(&profile->Button_LeftTrigger, section, SettingsID::Input_LeftTrigger_InputType, SettingsID::Input_LeftTrigger_Data, SettingsID::Input_LeftTrigger_ExtraData); + load_inputmapping_settings(&profile->Button_RightTrigger, section, SettingsID::Input_RightTrigger_InputType, SettingsID::Input_RightTrigger_Data, SettingsID::Input_RightTrigger_ExtraData); + load_inputmapping_settings(&profile->Button_ZTrigger, section, SettingsID::Input_ZTrigger_InputType, SettingsID::Input_ZTrigger_Data, SettingsID::Input_ZTrigger_ExtraData); + load_inputmapping_settings(&profile->AnalogStick_Up, section, SettingsID::Input_AnalogStickUp_InputType, SettingsID::Input_AnalogStickUp_Data, SettingsID::Input_AnalogStickUp_ExtraData); + load_inputmapping_settings(&profile->AnalogStick_Down, section, SettingsID::Input_AnalogStickDown_InputType, SettingsID::Input_AnalogStickDown_Data, SettingsID::Input_AnalogStickDown_ExtraData); + load_inputmapping_settings(&profile->AnalogStick_Left, section, SettingsID::Input_AnalogStickLeft_InputType, SettingsID::Input_AnalogStickLeft_Data, SettingsID::Input_AnalogStickLeft_ExtraData); + load_inputmapping_settings(&profile->AnalogStick_Right, section, SettingsID::Input_AnalogStickRight_InputType, SettingsID::Input_AnalogStickRight_Data, SettingsID::Input_AnalogStickRight_ExtraData); + } +} + +static void open_controllers(void) +{ + for (int i = 0; i < NUM_CONTROLLERS; i++) + { + InputProfile* profile = &l_InputProfiles[i]; + + profile->InputDevice.CloseDevice(); + + if (profile->DeviceNum != -1) + { + profile->InputDevice.OpenDevice(profile->DeviceName, profile->DeviceNum); + } + } +} + +static void close_controllers(void) +{ + for (int i = 0; i < NUM_CONTROLLERS; i++) + { + InputProfile* profile = &l_InputProfiles[i]; + + profile->InputDevice.CloseDevice(); + } +} + +static int get_button_state(InputProfile* profile, InputMapping* inputMapping) +{ + switch (inputMapping->Type) + { + case InputType::GamepadButton: + { + return SDL_GameControllerGetButton(profile->InputDevice.GetGameControllerHandle(), (SDL_GameControllerButton)inputMapping->Data); + }; + case InputType::GamepadAxis: + { + int axis_value = SDL_GameControllerGetAxis(profile->InputDevice.GetGameControllerHandle(), (SDL_GameControllerAxis)inputMapping->Data); + return (abs(axis_value) >= (SDL_AXIS_PEAK / 2) && (inputMapping->ExtraData ? axis_value > 0 : axis_value < 0)) ? 1 : 0; + }; + case InputType::JoystickButton: + { + return SDL_JoystickGetButton(profile->InputDevice.GetJoystickHandle(), inputMapping->Data); + }; + case InputType::JoystickAxis: + { + int axis_value = SDL_JoystickGetAxis(profile->InputDevice.GetJoystickHandle(), inputMapping->Data); + return (abs(axis_value) >= (SDL_AXIS_PEAK / 2) && (inputMapping->ExtraData ? axis_value > 0 : axis_value < 0)) ? 1 : 0; + } + case InputType::Keyboard: + { + return l_KeyboardState[inputMapping->Data] ? 1 : 0; + }; + default: + break; + } + + return 0; +} + +static int get_axis_state(InputProfile* profile, InputMapping* inputMapping, int direction, int value) +{ + switch (inputMapping->Type) + { + case InputType::GamepadButton: + { + int buttonState = SDL_GameControllerGetButton(profile->InputDevice.GetGameControllerHandle(), (SDL_GameControllerButton)inputMapping->Data); + return buttonState ? MAX_AXIS_VALUE * direction : value; + } break; + case InputType::GamepadAxis: + { + int axis_value = SDL_GameControllerGetAxis(profile->InputDevice.GetGameControllerHandle(), (SDL_GameControllerAxis)inputMapping->Data); + if (inputMapping->ExtraData ? axis_value > 0 : axis_value < 0) + { + axis_value = ((float)((float)axis_value / SDL_AXIS_PEAK * 100) * ((float)MAX_AXIS_VALUE / 100 * profile->RangeValue / 100)); + axis_value = abs(axis_value) * direction; + return axis_value; + } + } break; + case InputType::JoystickButton: + { + int buttonState = SDL_JoystickGetButton(profile->InputDevice.GetJoystickHandle(), inputMapping->Data); + return buttonState ? MAX_AXIS_VALUE * direction : value; + } break; + case InputType::JoystickAxis: + { + int axis_value = SDL_JoystickGetAxis(profile->InputDevice.GetJoystickHandle(), inputMapping->Data); + if (inputMapping->ExtraData ? axis_value > 0 : axis_value < 0) + { + axis_value = ((float)((float)axis_value / SDL_AXIS_PEAK * 100) * ((float)MAX_AXIS_VALUE / 100 * profile->RangeValue / 100)); + axis_value = abs(axis_value) * direction; + return axis_value; + } + } break; + case InputType::Keyboard: + { + return l_KeyboardState[inputMapping->Data] ? MAX_AXIS_VALUE * direction : value; + } break; + default: + break; + } + + return value; +} + +static bool is_deadzone(int x, int y, int deadzoneValue) +{ + return sqrt(pow(x, 2) + pow(y, 2)) <= deadzoneValue; +} + +static unsigned char data_crc(unsigned char *data, int length) +{ + unsigned char remainder = data[0]; + + int byte = 1; + unsigned char bit = 0; + + while (byte <= length) + { + int highBit = ((remainder & 0x80) != 0); + remainder = remainder << 1; + + remainder += (byte < length && data[byte] & (0x80 >> bit )) ? 1 : 0; + + remainder ^= (highBit) ? 0x85 : 0; + + bit++; + byte += bit/8; + bit %= 8; + } + + return remainder; +} + +void sdl_init() +{ + for (int subsystem : {SDL_INIT_GAMECONTROLLER, SDL_INIT_VIDEO, SDL_INIT_HAPTIC}) + { + if (!SDL_WasInit(subsystem)) + { + SDL_InitSubSystem(subsystem); + } + } +} + +void sdl_quit() +{ + for (int subsystem : {SDL_INIT_GAMECONTROLLER, SDL_INIT_HAPTIC}) + { + if (SDL_WasInit(subsystem)) + { + SDL_QuitSubSystem(subsystem); + } + } +} + +// +// Basic Plugin Functions +// + +EXPORT m64p_error CALL PluginStartup(m64p_dynlib_handle CoreLibHandle, void *Context, void (*DebugCallback)(void *, int, const char *)) +{ + if (l_SDLThread != nullptr) + { + return M64ERR_ALREADY_INIT; + } + + if (!CoreInit(CoreLibHandle)) + { + return M64ERR_SYSTEM_FAIL; + } + + sdl_init(); + + l_SDLThread = new Thread::SDLThread(nullptr); + l_SDLThread->start(); + + for (int i = 0; i < NUM_CONTROLLERS; i++) + { + l_InputProfiles[i].InputDevice.SetSDLThread(l_SDLThread); + } + + load_settings(); + + return M64ERR_SUCCESS; +} + +EXPORT m64p_error CALL PluginShutdown(void) +{ + if (l_SDLThread == nullptr) + { + return M64ERR_NOT_INIT; + } + + close_controllers(); + + l_SDLThread->StopLoop(); + l_SDLThread->deleteLater(); + l_SDLThread = nullptr; + + sdl_quit(); + + return M64ERR_SUCCESS; +} + +EXPORT m64p_error CALL PluginGetVersion(m64p_plugin_type *pluginType, int *pluginVersion, + int *apiVersion, const char **pluginNamePtr, int *capabilities) +{ + if (pluginType != nullptr) + { + *pluginType = M64PLUGIN_INPUT; + } + + if (pluginVersion != nullptr) + { + *pluginVersion = 0x010000; + } + + if (apiVersion != nullptr) + { + *apiVersion = INPUT_PLUGIN_API_VERSION; + } + + if (pluginNamePtr != nullptr) + { + *pluginNamePtr = "Rosalie's Mupen GUI - Input Plugin"; + } + + if (capabilities != nullptr) + { + *capabilities = 0; + } + + return M64ERR_SUCCESS; +} + +// +// Custom Plugin Functions +// + +EXPORT m64p_error CALL PluginConfig() +{ + if (l_SDLThread == nullptr) + { + return M64ERR_NOT_INIT; + } + + // close controllers + close_controllers(); + + l_SDLThread->SetAction(SDLThreadAction::SDLPumpEvents); + + UserInterface::MainDialog dialog(nullptr, l_SDLThread); + dialog.exec(); + + l_SDLThread->SetAction(SDLThreadAction::None); + + // reload settings + load_settings(); + + // open controllers + open_controllers(); + + return M64ERR_SUCCESS; +} + +// +// Input Plugin Functions +// + +EXPORT void CALL ControllerCommand(int Control, unsigned char* Command) +{ + unsigned char* data = &Command[5]; + + if (Control == -1) + { + return; + } + + InputProfile* profile = &l_InputProfiles[Control]; + + switch (Command[2]) + { + case RD_GETSTATUS: + case RD_READKEYS: + break; + case RD_READPAK: + if (profile->ControllerPak == N64ControllerPak::RumblePak) + { + unsigned int dwAddress = (Command[3] << 8) + (Command[4] & 0xE0); + + if ((dwAddress >= 0x8000 ) && (dwAddress < 0x9000 )) + { + memset(data, 0x80, 32); + } + else + { + memset(data, 0x00, 32); + } + + data[32] = data_crc(data, 32); + } + break; + case RD_WRITEPAK: + if (profile->ControllerPak == N64ControllerPak::RumblePak) + { + unsigned int dwAddress = (Command[3] << 8) + (Command[4] & 0xE0); + if (dwAddress == PAK_IO_RUMBLE) + { + if (*data) + { + profile->InputDevice.StartRumble(); + } + else + { + profile->InputDevice.StopRumble(); + } + } + data[32] = data_crc( data, 32 ); + } + break; + case RD_RESETCONTROLLER: + case RD_READEEPROM: + case RD_WRITEEPROM: + break; + } +} + +EXPORT void CALL GetKeys(int Control, BUTTONS* Keys) +{ + InputProfile* profile = &l_InputProfiles[Control]; + + if (!profile->PluggedIn) + { + return; + } + + // check if device has been disconnected, + // if it has, try to open it again, + // only do this every 2 seconds to prevent lag + const auto currentTime = std::chrono::high_resolution_clock::now(); + const int secondsPassed = std::chrono::duration_cast(currentTime - profile->LastDeviceCheckTime).count(); + if (secondsPassed >= 2) + { + profile->LastDeviceCheckTime = currentTime; + + if (profile->DeviceNum != -1) + { + if (profile->InputDevice.IsOpeningDevice()) + { + return; + } + + if (!profile->InputDevice.HasOpenDevice() || !profile->InputDevice.IsAttached()) + { + profile->InputDevice.OpenDevice(profile->DeviceName, profile->DeviceNum); + return; + } + } + } + + Keys->A_BUTTON = get_button_state(profile, &profile->Button_A); + Keys->B_BUTTON = get_button_state(profile, &profile->Button_B); + Keys->START_BUTTON = get_button_state(profile, &profile->Button_Start); + Keys->U_DPAD = get_button_state(profile, &profile->Button_DpadUp); + Keys->D_DPAD = get_button_state(profile, &profile->Button_DpadDown); + Keys->L_DPAD = get_button_state(profile, &profile->Button_DpadLeft); + Keys->R_DPAD = get_button_state(profile, &profile->Button_DpadRight); + Keys->U_CBUTTON = get_button_state(profile, &profile->Button_CButtonUp); + Keys->D_CBUTTON = get_button_state(profile, &profile->Button_CButtonDown); + Keys->L_CBUTTON = get_button_state(profile, &profile->Button_CButtonLeft); + Keys->R_CBUTTON = get_button_state(profile, &profile->Button_CButtonRight); + Keys->L_TRIG = get_button_state(profile, &profile->Button_LeftTrigger); + Keys->R_TRIG = get_button_state(profile, &profile->Button_RightTrigger); + Keys->Z_TRIG = get_button_state(profile, &profile->Button_ZTrigger); + + Keys->Y_AXIS = get_axis_state(profile, &profile->AnalogStick_Up, 1, Keys->Y_AXIS); + Keys->Y_AXIS = get_axis_state(profile, &profile->AnalogStick_Down, -1, Keys->Y_AXIS); + Keys->X_AXIS = get_axis_state(profile, &profile->AnalogStick_Left, -1, Keys->X_AXIS); + Keys->X_AXIS = get_axis_state(profile, &profile->AnalogStick_Right, 1, Keys->X_AXIS); + + // take deadzone into account + if (is_deadzone(Keys->X_AXIS, Keys->Y_AXIS, ((float)MAX_AXIS_VALUE / 100 * profile->DeadzoneValue))) + { + Keys->Y_AXIS = 0; + Keys->X_AXIS = 0; + } +} + +EXPORT void CALL InitiateControllers(CONTROL_INFO ControlInfo) +{ + for (int i = 0; i < NUM_CONTROLLERS; i++) + { + InputProfile* profile = &l_InputProfiles[i]; + int plugin = PLUGIN_NONE; + + switch (profile->ControllerPak) + { + case N64ControllerPak::MemoryPak: + plugin = PLUGIN_MEMPAK; + break; + case N64ControllerPak::RumblePak: + plugin = PLUGIN_RAW; + break; + default: + plugin = PLUGIN_NONE; + break; + } + + ControlInfo.Controls[i].Present = profile->PluggedIn ? 1 : 0; + ControlInfo.Controls[i].Plugin = plugin; + ControlInfo.Controls[i].RawData = 0; + } + + for (int i = 0; i < SDL_NUM_SCANCODES; i++) + { + l_KeyboardState[i] = 0; + } + + open_controllers(); +} + +EXPORT void CALL ReadController(int Control, unsigned char *Command) +{ +} + +EXPORT int CALL RomOpen(void) +{ + return 1; +} + +EXPORT void CALL RomClosed(void) +{ + close_controllers(); +} + +EXPORT void CALL SDL_KeyDown(int keymod, int keysym) +{ + l_KeyboardState[keysym] = true; +} + +EXPORT void CALL SDL_KeyUp(int keymod, int keysym) +{ + l_KeyboardState[keysym] = false; +}