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 00000000..7fc34375 Binary files /dev/null and b/Source/RMG-Input/UserInterface/Resource/RMG.png differ diff --git a/Source/RMG-Input/UserInterface/UIResources.qrc b/Source/RMG-Input/UserInterface/UIResources.qrc new file mode 100644 index 00000000..1b5c1f8c --- /dev/null +++ b/Source/RMG-Input/UserInterface/UIResources.qrc @@ -0,0 +1,20 @@ + + + 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; +}