diff --git a/Plugins/FabulousDualSense/BuildPlugin.bat b/Plugins/FabulousDualSense/BuildPlugin.bat new file mode 100644 index 0000000..8435774 --- /dev/null +++ b/Plugins/FabulousDualSense/BuildPlugin.bat @@ -0,0 +1,18 @@ +@echo off + +set EngineVesion=5.2 + +for /f "skip=2 tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\EpicGames\Unreal Engine\%EngineVesion%" /v "InstalledDirectory"') do set "EngineDirectory=%%b" + +set AutomationToolPath="%EngineDirectory%\Engine\Build\BatchFiles\RunUAT.bat" +set PluginPath="%cd%\FabulousDualSense.uplugin" +set OutputPath="%cd%\Build" + +title Build Plugin +echo Automation Tool Path: %AutomationToolPath% +echo: + +call %AutomationToolPath% BuildPlugin -Plugin=%PluginPath% -Package=%OutputPath% -Rocket -TargetPlatforms=Win64 +echo: +pause +exit 0 diff --git a/Plugins/FabulousDualSense/Config/Input.ini b/Plugins/FabulousDualSense/Config/Input.ini new file mode 100644 index 0000000..2a93d99 --- /dev/null +++ b/Plugins/FabulousDualSense/Config/Input.ini @@ -0,0 +1,2 @@ +[InputPlatformSettings_Windows InputPlatformSettings] ++HardwareDevices=(InputClassName="DsInputDevice",HardwareDeviceIdentifier="DualSense") diff --git a/Plugins/FabulousDualSense/FabulousDualSense.uplugin b/Plugins/FabulousDualSense/FabulousDualSense.uplugin new file mode 100644 index 0000000..fc387e3 --- /dev/null +++ b/Plugins/FabulousDualSense/FabulousDualSense.uplugin @@ -0,0 +1,32 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Fabulous DualSense", + "Description": "Minimal input device plugin for DualSense controller in Windows.", + "Category": "Input Devices", + "CreatedBy": "Sixze", + "CreatedByURL": "https://github.com/Sixze", + "DocsURL": "https://github.com/Sixze/FabulousDualSense", + "MarketplaceURL": "", + "SupportURL": "https://github.com/Sixze/FabulousDualSense/discussions", + "EngineVersion": "5.2.0", + "EnabledByDefault": true, + "CanContainContent": false, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "SupportedTargetPlatforms": [ + "Win64" + ], + "Modules": [ + { + "Name": "FabulousDualSense", + "Type": "Runtime", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64" + ] + } + ] +} \ No newline at end of file diff --git a/Plugins/FabulousDualSense/LICENSE.md b/Plugins/FabulousDualSense/LICENSE.md new file mode 100644 index 0000000..abf7488 --- /dev/null +++ b/Plugins/FabulousDualSense/LICENSE.md @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 Sixze + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Plugins/FabulousDualSense/README.md b/Plugins/FabulousDualSense/README.md new file mode 100644 index 0000000..eeb6799 --- /dev/null +++ b/Plugins/FabulousDualSense/README.md @@ -0,0 +1,20 @@ +# Fabulous DualSense + +Input device plugin for **DualSense** controller in **Windows** for **Unreal Engine**. + +This plugin includes generic gamepad and touchpad functionality, as well as partial support for the [Device Properties](https://docs.unrealengine.com/en-US/device-properties-in-unreal-engine/) feature (supported device properties are **Device Color**, **Trigger Feedback**, and **Trigger Resistance**). + +**This plugin is intended to be used during development only.** + +## Quick Start + +1. Clone the repository to your project's `Plugins` folder. +2. Recompile your project. + +## Credits + +- [DualSense Windows API](https://github.com/mattdevv/DualSense-Windows) by Matthew Hall + +## License & Contribution + +Fabulous DualSense is licensed under the MIT License, see [LICENSE.md](LICENSE.md) for more information. Other developers are encouraged to fork the repository, open issues & pull requests to help the development. \ No newline at end of file diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/FabulousDualSense.Build.cs b/Plugins/FabulousDualSense/Source/FabulousDualSense/FabulousDualSense.Build.cs new file mode 100644 index 0000000..ec27dbf --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/FabulousDualSense.Build.cs @@ -0,0 +1,18 @@ +using UnrealBuildTool; + +public class FabulousDualSense : ModuleRules +{ + public FabulousDualSense(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_2; + + bEnableNonInlinedGenCppWarnings = true; + + PrivateDependencyModuleNames.AddRange(new[] + { + "Core", "CoreUObject", "Engine", "ApplicationCore", "InputCore", + "InputDevice", "SlateCore", "Slate", "DeveloperSettings", "DualSenseWindows" + }); + } +} \ No newline at end of file diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsConstants.cpp b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsConstants.cpp new file mode 100644 index 0000000..6c0a86b --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsConstants.cpp @@ -0,0 +1,53 @@ +#include "DsConstants.h" + +#include + +const FName DsConstants::InputDeviceName{TEXTVIEW("DsInputDevice")}; +const FString DsConstants::HardwareDeviceIdentifier{TEXTVIEW("DualSense")}; + +const FKey DsConstants::TouchpadKey{FName{TEXTVIEW("DsTouchpad")}}; +const FKey DsConstants::LogoKey{FName{TEXTVIEW("DsLogo")}}; +const FKey DsConstants::MuteKey{FName{TEXTVIEW("DsMute")}}; + +const FKey DsConstants::Touch1Key{FName{TEXTVIEW("DsTouch1")}}; +const FKey DsConstants::Touch1AxisXKey{FName{TEXTVIEW("DsTouch1AxisX")}}; +const FKey DsConstants::Touch1AxisYKey{FName{TEXTVIEW("DsTouch1AxisY")}}; +const FKey DsConstants::Touch1AxisXYKey{FName{TEXTVIEW("DsTouch1AxisXY")}}; + +const FKey DsConstants::Touch2Key{FName{TEXTVIEW("DsTouch2")}}; +const FKey DsConstants::Touch2AxisXKey{FName{TEXTVIEW("DsTouch2AxisX")}}; +const FKey DsConstants::Touch2AxisYKey{FName{TEXTVIEW("DsTouch2AxisY")}}; +const FKey DsConstants::Touch2AxisXYKey{FName{TEXTVIEW("DsTouch2AxisXY")}}; + +const TMap& DsConstants::GetRegularButtons() +{ + static const TMap Buttons{ + {FGamepadKeyNames::DPadUp, DS5W_ISTATE_BTN_DPAD_UP}, + {FGamepadKeyNames::DPadDown, DS5W_ISTATE_BTN_DPAD_DOWN}, + {FGamepadKeyNames::DPadLeft, DS5W_ISTATE_BTN_DPAD_LEFT}, + {FGamepadKeyNames::DPadRight, DS5W_ISTATE_BTN_DPAD_RIGHT}, + + {FGamepadKeyNames::FaceButtonTop, DS5W_ISTATE_BTN_TRIANGLE}, + {FGamepadKeyNames::FaceButtonBottom, DS5W_ISTATE_BTN_CROSS}, + {FGamepadKeyNames::FaceButtonLeft, DS5W_ISTATE_BTN_SQUARE}, + {FGamepadKeyNames::FaceButtonRight, DS5W_ISTATE_BTN_CIRCLE}, + + {FGamepadKeyNames::LeftTriggerThreshold, DS5W_ISTATE_BTN_TRIGGER_LEFT}, + {FGamepadKeyNames::RightTriggerThreshold, DS5W_ISTATE_BTN_TRIGGER_RIGHT}, + + {FGamepadKeyNames::LeftShoulder, DS5W_ISTATE_BTN_BUMPER_LEFT}, + {FGamepadKeyNames::RightShoulder, DS5W_ISTATE_BTN_BUMPER_RIGHT}, + + {FGamepadKeyNames::LeftThumb, DS5W_ISTATE_BTN_STICK_LEFT}, + {FGamepadKeyNames::RightThumb, DS5W_ISTATE_BTN_STICK_RIGHT}, + + {FGamepadKeyNames::SpecialLeft, DS5W_ISTATE_BTN_SELECT}, + {FGamepadKeyNames::SpecialRight, DS5W_ISTATE_BTN_MENU}, + + {TouchpadKey.GetFName(), DS5W_ISTATE_BTN_PAD_BUTTON}, + {LogoKey.GetFName(), DS5W_ISTATE_BTN_PLAYSTATION_LOGO}, + {MuteKey.GetFName(), DS5W_ISTATE_BTN_MIC_BUTTON}, + }; + + return Buttons; +} diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsInputDevice.cpp b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsInputDevice.cpp new file mode 100644 index 0000000..5d62bdf --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsInputDevice.cpp @@ -0,0 +1,760 @@ +#include "DsInputDevice.h" + +#include "DsSettings.h" +#include "DsUtility.h" +#include "Containers/StaticBitArray.h" +#include "Framework/Application/SlateApplication.h" +#include "GameFramework/InputSettings.h" +#include "GenericPlatform/IInputInterface.h" +#include "Misc/ConfigCacheIni.h" + +FDsInputDevice::FDsInputDevice(const TSharedRef& MessageHandler) : MessageHandler{MessageHandler} +{ + GConfig->GetFloat(TEXT("/Script/Engine.InputSettings"), TEXT("InitialButtonRepeatDelay"), InitialButtonRepeatDelay, GInputIni); + GConfig->GetFloat(TEXT("/Script/Engine.InputSettings"), TEXT("ButtonRepeatDelay"), ButtonRepeatDelay, GInputIni); + + RefreshDevices(); +} + +FDsInputDevice::~FDsInputDevice() +{ + auto& InputDeviceMapper{IPlatformInputDeviceMapper::Get()}; + + for (auto ControllerId{0}; ControllerId < DsConstants::MaxDevicesCount; ControllerId++) + { + if (DeviceContexts[ControllerId]._internal.connected) + { + auto PlatformUserId{PLATFORMUSERID_NONE}; + auto InputDeviceId{INPUTDEVICEID_NONE}; + InputDeviceMapper.RemapControllerIdToPlatformUserAndDevice(ControllerId, PlatformUserId, InputDeviceId); + + DisconnectDevice(InputDeviceMapper, ControllerId, PlatformUserId, InputDeviceId); + } + } +} + +void FDsInputDevice::Tick(float DeltaTime) +{ + RefreshDevices(); +} + +void FDsInputDevice::SendControllerEvents() +{ + const auto Time{FPlatformTime::Seconds()}; + + auto& InputDeviceMapper{IPlatformInputDeviceMapper::Get()}; + + for (auto ControllerId{0}; ControllerId < DsConstants::MaxDevicesCount; ControllerId++) + { + if (!DeviceContexts[ControllerId]._internal.connected) + { + continue; + } + + FInputDeviceScope InputDeviceScope{this, DsConstants::InputDeviceName, ControllerId, DsConstants::HardwareDeviceIdentifier}; + + auto PlatformUserId{PLATFORMUSERID_NONE}; + auto InputDeviceId{INPUTDEVICEID_NONE}; + InputDeviceMapper.RemapControllerIdToPlatformUserAndDevice(ControllerId, PlatformUserId, InputDeviceId); + + const auto PreviousInput{InputStates[ControllerId]}; + + auto& Context{DeviceContexts[ControllerId]}; + auto& Input{InputStates[ControllerId]}; + auto& Output{OutputStates[ControllerId]}; + + const auto ReadInputResult{getDeviceInputState(&Context, &Input)}; + if (DS5W_FAILED(ReadInputResult)) + { + UE_LOG(LogFabulousDualSense, Warning, TEXT("Failed to read device input state: %s, Device: %s."), + DsUtility::ReturnValueToString(ReadInputResult).GetData(), Context._internal.devicePath); + + DisconnectDevice(InputDeviceMapper, ControllerId, PlatformUserId, InputDeviceId); + continue; + } + + // Sticks. + + ProcessStick(PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftAnalogX, PreviousInput.leftStick.x, Input.leftStick.x); + ProcessStick(PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftAnalogY, PreviousInput.leftStick.y, Input.leftStick.y); + + ProcessStick(PlatformUserId, InputDeviceId, FGamepadKeyNames::RightAnalogX, PreviousInput.rightStick.x, Input.rightStick.x); + ProcessStick(PlatformUserId, InputDeviceId, FGamepadKeyNames::RightAnalogY, PreviousInput.rightStick.y, Input.rightStick.y); + + // Triggers. + + if (PreviousInput.leftTrigger != Input.leftTrigger || Input.leftTrigger > DsConstants::TriggerDeadZone) + { + MessageHandler->OnControllerAnalog(FGamepadKeyNames::LeftTriggerAnalog, PlatformUserId, InputDeviceId, + Input.leftTrigger / static_cast(TNumericLimits::Max())); + } + + if (PreviousInput.rightTrigger != Input.rightTrigger || Input.rightTrigger > DsConstants::TriggerDeadZone) + { + MessageHandler->OnControllerAnalog(FGamepadKeyNames::RightTriggerAnalog, PlatformUserId, InputDeviceId, + Input.rightTrigger / static_cast(TNumericLimits::Max())); + } + + // Regular buttons. + + auto ButtonIndex{0}; + + for (const auto& [ButtonName, ButtonFlag] : DsConstants::GetRegularButtons()) + { + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, ButtonName, ButtonIndex, + (PreviousInput.buttonMap & ButtonFlag) > 0, + (Input.buttonMap & ButtonFlag) > 0, Time); + ButtonIndex += 1; + } + + // Virtual buttons. + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftStickUp, ButtonIndex, + PreviousInput.leftStick.y > DsConstants::StickDeadZone, Input.leftStick.y > DsConstants::StickDeadZone, Time); + ButtonIndex += 1; + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftStickDown, ButtonIndex, + PreviousInput.leftStick.y < -DsConstants::StickDeadZone, Input.leftStick.y < -DsConstants::StickDeadZone, Time); + ButtonIndex += 1; + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftStickLeft, ButtonIndex, + PreviousInput.leftStick.x < -DsConstants::StickDeadZone, Input.leftStick.x < -DsConstants::StickDeadZone, Time); + ButtonIndex += 1; + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftStickRight, ButtonIndex, + PreviousInput.leftStick.x > DsConstants::StickDeadZone, Input.leftStick.x > DsConstants::StickDeadZone, Time); + ButtonIndex += 1; + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, FGamepadKeyNames::RightStickUp, ButtonIndex, + PreviousInput.rightStick.y > DsConstants::StickDeadZone, Input.rightStick.y > DsConstants::StickDeadZone, Time); + ButtonIndex += 1; + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, FGamepadKeyNames::RightStickDown, ButtonIndex, + PreviousInput.rightStick.y < -DsConstants::StickDeadZone, Input.rightStick.y < -DsConstants::StickDeadZone, Time); + ButtonIndex += 1; + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, FGamepadKeyNames::RightStickLeft, ButtonIndex, + PreviousInput.rightStick.x < -DsConstants::StickDeadZone, Input.rightStick.x < -DsConstants::StickDeadZone, Time); + ButtonIndex += 1; + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, FGamepadKeyNames::RightStickRight, ButtonIndex, + PreviousInput.rightStick.x > DsConstants::StickDeadZone, Input.rightStick.x > DsConstants::StickDeadZone, Time); + ButtonIndex += 1; + + // Touch pad. + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, DsConstants::Touch1Key.GetFName(), ButtonIndex, + PreviousInput.touchPoint1.down, Input.touchPoint1.down, Time); + ButtonIndex += 1; + + ProcessButton(ControllerId, PlatformUserId, InputDeviceId, DsConstants::Touch2Key.GetFName(), ButtonIndex, + PreviousInput.touchPoint2.down, Input.touchPoint2.down, Time); + ButtonIndex += 1; + + ProcessTouch(PlatformUserId, InputDeviceId, DsConstants::Touch1AxisXKey.GetFName(), + DsConstants::Touch1AxisYKey.GetFName(), PreviousInput.touchPoint1, Input.touchPoint1); + + ProcessTouch(PlatformUserId, InputDeviceId, DsConstants::Touch2AxisXKey.GetFName(), + DsConstants::Touch2AxisYKey.GetFName(), PreviousInput.touchPoint2, Input.touchPoint2); + + if (PreviousInput.touchPoint1.down && Input.touchPoint1.down) + { + const auto TouchAxisX{static_cast(Input.touchPoint1.x - PreviousInput.touchPoint1.x)}; + const auto TouchAxisY{static_cast(Input.touchPoint1.y - PreviousInput.touchPoint1.y)}; + + if (GetDefault()->bEmitMouseEventsFromTouchpad) + { + MessageHandler->OnRawMouseMove(TouchAxisX, TouchAxisY); + } + } + + if (ExtraStates[ControllerId].bOutputChanged) + { + const auto WriteOutputResult{setDeviceOutputState(&Context, &Output)}; + if (DS5W_FAILED(WriteOutputResult)) + { + UE_LOG(LogFabulousDualSense, Warning, TEXT("Failed to write device output state: %s, Device: %s."), + DsUtility::ReturnValueToString(ReadInputResult).GetData(), Context._internal.devicePath); + + DisconnectDevice(InputDeviceMapper, ControllerId, PlatformUserId, InputDeviceId); + } + } + } +} + +void FDsInputDevice::SetMessageHandler(const TSharedRef& NewMessageHandler) +{ + MessageHandler = NewMessageHandler; +} + +bool FDsInputDevice::Exec(UWorld* World, const TCHAR* Command, FOutputDevice& Archive) +{ + return false; +} + +void FDsInputDevice::SetChannelValue(const int32 ControllerId, const FForceFeedbackChannelType ChannelType, const float Value) +{ + if (ControllerId < 0 || ControllerId >= DsConstants::MaxDevicesCount || !DeviceContexts[ControllerId]._internal.connected) + { + return; + } + + auto& Extra{ExtraStates[ControllerId]}; + + switch (ChannelType) + { + case FForceFeedbackChannelType::LEFT_LARGE: + Extra.ForceFeedbackLeftLarge = static_cast(Value * TNumericLimits::Max()); + break; + + case FForceFeedbackChannelType::LEFT_SMALL: + Extra.ForceFeedbackLeftSmall = static_cast(Value * TNumericLimits::Max()); + break; + + case FForceFeedbackChannelType::RIGHT_LARGE: + Extra.ForceFeedbackRightLarge = static_cast(Value * TNumericLimits::Max()); + break; + + case FForceFeedbackChannelType::RIGHT_SMALL: + Extra.ForceFeedbackRightSmall = static_cast(Value * TNumericLimits::Max()); + break; + } + + const auto NewForceFeedbackLeft{static_cast(FMath::Max(Extra.ForceFeedbackLeftLarge, Extra.ForceFeedbackLeftSmall))}; + const auto NewForceFeedbackRight{static_cast(FMath::Max(Extra.ForceFeedbackRightLarge, Extra.ForceFeedbackRightSmall))}; + + auto& Output{OutputStates[ControllerId]}; + + Extra.bOutputChanged |= Output.leftRumble != NewForceFeedbackLeft || Output.rightRumble != NewForceFeedbackRight; + + Output.leftRumble = NewForceFeedbackLeft; + Output.rightRumble = NewForceFeedbackRight; +} + +void FDsInputDevice::SetChannelValues(const int32 ControllerId, const FForceFeedbackValues& Values) +{ + if (ControllerId < 0 || ControllerId >= DsConstants::MaxDevicesCount || !DeviceContexts[ControllerId]._internal.connected) + { + return; + } + + auto& Extra{ExtraStates[ControllerId]}; + + Extra.ForceFeedbackLeftLarge = static_cast(Values.LeftLarge * TNumericLimits::Max()); + Extra.ForceFeedbackLeftSmall = static_cast(Values.LeftSmall * TNumericLimits::Max()); + Extra.ForceFeedbackRightLarge = static_cast(Values.RightLarge * TNumericLimits::Max()); + Extra.ForceFeedbackRightSmall = static_cast(Values.RightSmall * TNumericLimits::Max()); + + const auto NewForceFeedbackLeft{static_cast(FMath::Max(Extra.ForceFeedbackLeftLarge, Extra.ForceFeedbackLeftSmall))}; + const auto NewForceFeedbackRight{static_cast(FMath::Max(Extra.ForceFeedbackRightLarge, Extra.ForceFeedbackRightSmall))}; + + auto& Output{OutputStates[ControllerId]}; + + Extra.bOutputChanged |= Output.leftRumble != NewForceFeedbackLeft || Output.rightRumble != NewForceFeedbackRight; + + Output.leftRumble = NewForceFeedbackLeft; + Output.rightRumble = NewForceFeedbackRight; +} + +void FDsInputDevice::SetDeviceProperty(const int32 ControllerId, const FInputDeviceProperty* Property) +{ + if (ControllerId < 0 || ControllerId >= DsConstants::MaxDevicesCount || !DeviceContexts[ControllerId]._internal.connected) + { + return; + } + + auto& Output{OutputStates[ControllerId]}; + auto& Extra{ExtraStates[ControllerId]}; + + if (Property->Name == FInputDeviceLightColorProperty::PropertyName()) + { + const auto& LightColorProperty{static_cast(*Property)}; + + Extra.bOutputChanged |= ProcessLightColorProperty(Output, LightColorProperty); + return; + } + + if (Property->Name == FInputDeviceTriggerResetProperty::PropertyName()) + { + const auto& TriggerResetProperty{static_cast(*Property)}; + + Extra.bOutputChanged |= ProcessTriggerResetProperty(Output.leftTriggerEffect, TriggerResetProperty, + EInputDeviceTriggerMask::Left); + + Extra.bOutputChanged |= ProcessTriggerResetProperty(Output.rightTriggerEffect, TriggerResetProperty, + EInputDeviceTriggerMask::Right); + return; + } + + if (Property->Name == FInputDeviceTriggerFeedbackProperty::PropertyName()) + { + const auto& TriggerFeedbackProperty{static_cast(*Property)}; + + Extra.bOutputChanged |= ProcessTriggerFeedbackProperty(Output.leftTriggerEffect, TriggerFeedbackProperty, + EInputDeviceTriggerMask::Left); + + Extra.bOutputChanged |= ProcessTriggerFeedbackProperty(Output.rightTriggerEffect, TriggerFeedbackProperty, + EInputDeviceTriggerMask::Right); + return; + } + + if (Property->Name == FInputDeviceTriggerResistanceProperty::PropertyName()) + { + const auto& TriggerResistanceProperty{static_cast(*Property)}; + + Extra.bOutputChanged |= ProcessTriggerResistanceProperty(Output.leftTriggerEffect, TriggerResistanceProperty, + EInputDeviceTriggerMask::Left); + + Extra.bOutputChanged |= ProcessTriggerResistanceProperty(Output.rightTriggerEffect, TriggerResistanceProperty, + EInputDeviceTriggerMask::Right); + return; + } + + if (Property->Name == FInputDeviceTriggerVibrationProperty::PropertyName()) + { + const auto& TriggerVibrationProperty{static_cast(*Property)}; + + Extra.bOutputChanged |= ProcessTriggerVibrationProperty(Output.leftTriggerEffect, TriggerVibrationProperty, + EInputDeviceTriggerMask::Left); + + Extra.bOutputChanged |= ProcessTriggerVibrationProperty(Output.rightTriggerEffect, TriggerVibrationProperty, + EInputDeviceTriggerMask::Right); + } +} + +bool FDsInputDevice::IsGamepadAttached() const +{ + auto bResult{false}; + + for (auto i{0}; i < DsConstants::MaxDevicesCount; i++) + { + bResult |= DeviceContexts[i]._internal.connected; + } + + return bResult; +} + +void FDsInputDevice::RefreshDevices() +{ + static unsigned int KnownDeviceIds[DsConstants::MaxDevicesCount]; + unsigned int KnowDevicesCount{0}; + + for (auto i{0}; i < DsConstants::MaxDevicesCount; i++) + { + if (DeviceContexts[i]._internal.connected) + { + KnownDeviceIds[i] = DeviceContexts[i]._internal.uniqueID; + KnowDevicesCount += 1; + } + } + + static DS5W::DeviceEnumInfo DeviceInfos[DsConstants::MaxDevicesCount]; + unsigned int DevicesCount{0}; + + const auto EnumDevicesResult{ + enumUnknownDevices(DeviceInfos, DsConstants::MaxDevicesCount, KnownDeviceIds, KnowDevicesCount, &DevicesCount) + }; + + switch (EnumDevicesResult) + { + case DS5W_OK: + break; + + case DS5W_E_INSUFFICIENT_BUFFER: + DevicesCount = DsConstants::MaxDevicesCount; + break; + + default: + UE_LOG(LogFabulousDualSense, Warning, TEXT("Failed to enumerate devices: %s."), + DsUtility::ReturnValueToString(EnumDevicesResult).GetData()); + return; + } + + auto& InputDeviceMapper{IPlatformInputDeviceMapper::Get()}; + TStaticBitArray ProcessedDeviceIndexes; + + // First iteration: process devices reconnection and already connected devices. + + for (unsigned int DeviceIndex{0}; DeviceIndex < DevicesCount; DeviceIndex++) + { + for (auto ControllerId{0}; ControllerId < DsConstants::MaxDevicesCount; ControllerId++) + { + if (DeviceContexts[ControllerId]._internal.uniqueID == DeviceInfos[DeviceIndex]._internal.uniqueID) + { + ProcessedDeviceIndexes[DeviceIndex] = true; + + if (!DeviceContexts[ControllerId]._internal.connected) + { + ConnectDevice(InputDeviceMapper, DeviceInfos[DeviceIndex], ControllerId); + } + + break; + } + } + } + + // Second iteration: process the connection of new devices (without reusing the + // IDs of disconnected devices to give them the opportunity to reconnect later). + + for (unsigned int DeviceIndex{0}; DeviceIndex < DevicesCount; DeviceIndex++) + { + if (ProcessedDeviceIndexes[DeviceIndex]) + { + continue; + } + + for (auto ControllerId{0}; ControllerId < DsConstants::MaxDevicesCount; ControllerId++) + { + if (DeviceContexts[ControllerId]._internal.uniqueID == 0) + { + ProcessedDeviceIndexes[DeviceIndex] = true; + + ConnectDevice(InputDeviceMapper, DeviceInfos[DeviceIndex], ControllerId); + break; + } + } + } + + // Third iteration: process the connection of new devices (reusing the IDs of + // disconnected devices, because there are not enough unused IDs for new devices). + + for (unsigned int DeviceIndex{0}; DeviceIndex < DevicesCount; DeviceIndex++) + { + if (ProcessedDeviceIndexes[DeviceIndex]) + { + continue; + } + + for (auto ControllerId{0}; ControllerId < DsConstants::MaxDevicesCount; ControllerId++) + { + if (!DeviceContexts[ControllerId]._internal.connected) + { + ProcessedDeviceIndexes[DeviceIndex] = true; + + ConnectDevice(InputDeviceMapper, DeviceInfos[DeviceIndex], ControllerId); + break; + } + } + } +} + +void FDsInputDevice::ConnectDevice(IPlatformInputDeviceMapper& InputDeviceMapper, + DS5W::DeviceEnumInfo& DeviceInfo, const int32 ControllerId) +{ + UE_LOG(LogFabulousDualSense, Log, TEXT("New device found: %s, Connection: %s."), + DeviceInfo._internal.path, DsUtility::DeviceConnectionToString(DeviceInfo._internal.connection).GetData()); + + const auto InitializeDeviceContextResult{initDeviceContext(&DeviceInfo, &DeviceContexts[ControllerId])}; + if (DS5W_SUCCESS(InitializeDeviceContextResult)) + { + UE_LOG(LogFabulousDualSense, Log, TEXT("Device connected: %s."), DeviceInfo._internal.path); + + FMemory::Memzero(InputStates[ControllerId]); + FMemory::Memzero(OutputStates[ControllerId]); + FMemory::Memzero(ExtraStates[ControllerId]); + + auto PlatformUserId{PLATFORMUSERID_NONE}; + auto InputDeviceId{INPUTDEVICEID_NONE}; + InputDeviceMapper.RemapControllerIdToPlatformUserAndDevice(ControllerId, PlatformUserId, InputDeviceId); + + InputDeviceMapper.Internal_MapInputDeviceToUser(InputDeviceId, PlatformUserId, EInputDeviceConnectionState::Connected); + } + else + { + UE_LOG(LogFabulousDualSense, Warning, TEXT("Failed to initialize device context: %s, Device: %s."), + DsUtility::ReturnValueToString(InitializeDeviceContextResult).GetData(), DeviceInfo._internal.path); + + FMemory::Memzero(DeviceContexts[ControllerId]); + } +} + +void FDsInputDevice::DisconnectDevice(IPlatformInputDeviceMapper& InputDeviceMapper, const int32 ControllerId, + const FPlatformUserId PlatformUserId, const FInputDeviceId InputDeviceId) +{ + auto& Context{DeviceContexts[ControllerId]}; + + UE_LOG(LogFabulousDualSense, Log, TEXT("Device disconnected: %s."), Context._internal.devicePath); + + freeDeviceContext(&Context); + + if (FSlateApplication::Get().GetPlatformApplication().IsValid()) + { + const auto& Input{InputStates[ControllerId]}; + + // Release sticks. + + ReleaseStick(PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftAnalogX, Input.leftStick.x); + ReleaseStick(PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftAnalogY, Input.leftStick.y); + + ReleaseStick(PlatformUserId, InputDeviceId, FGamepadKeyNames::RightAnalogX, Input.rightStick.x); + ReleaseStick(PlatformUserId, InputDeviceId, FGamepadKeyNames::RightAnalogY, Input.rightStick.y); + + // Release triggers. + + if (Input.leftTrigger != 0) + { + MessageHandler->OnControllerAnalog(FGamepadKeyNames::LeftTriggerAnalog, PlatformUserId, InputDeviceId, 0.0f); + } + + if (Input.rightTrigger != 0) + { + MessageHandler->OnControllerAnalog(FGamepadKeyNames::RightTriggerAnalog, PlatformUserId, InputDeviceId, 0.0f); + } + + // Release regular buttons. + + for (const auto& [ButtonName, ButtonFlag] : DsConstants::GetRegularButtons()) + { + ReleaseButton(PlatformUserId, InputDeviceId, ButtonName, (Input.buttonMap & ButtonFlag) > 0); + } + + // Release virtual buttons. + + ReleaseButton(PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftStickUp, Input.leftStick.y > DsConstants::StickDeadZone); + ReleaseButton(PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftStickDown, Input.leftStick.y < -DsConstants::StickDeadZone); + ReleaseButton(PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftStickLeft, Input.leftStick.x < -DsConstants::StickDeadZone); + ReleaseButton(PlatformUserId, InputDeviceId, FGamepadKeyNames::LeftStickRight, Input.leftStick.x > DsConstants::StickDeadZone); + ReleaseButton(PlatformUserId, InputDeviceId, FGamepadKeyNames::RightStickUp, Input.rightStick.y > DsConstants::StickDeadZone); + ReleaseButton(PlatformUserId, InputDeviceId, FGamepadKeyNames::RightStickDown, Input.rightStick.y < -DsConstants::StickDeadZone); + ReleaseButton(PlatformUserId, InputDeviceId, FGamepadKeyNames::RightStickLeft, Input.rightStick.x < -DsConstants::StickDeadZone); + ReleaseButton(PlatformUserId, InputDeviceId, FGamepadKeyNames::RightStickRight, Input.rightStick.x > DsConstants::StickDeadZone); + + // Release touch pad. + + ReleaseButton(PlatformUserId, InputDeviceId, DsConstants::Touch1Key.GetFName(), Input.touchPoint1.down); + ReleaseButton(PlatformUserId, InputDeviceId, DsConstants::Touch2Key.GetFName(), Input.touchPoint2.down); + } + + InputDeviceMapper.Internal_MapInputDeviceToUser(InputDeviceId, PlatformUserId, EInputDeviceConnectionState::Disconnected); +} + +void FDsInputDevice::ProcessStick(const FPlatformUserId PlatformUserId, const FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& KeyName, const int8 PreviousValue, const int8 NewValue) const +{ + if (PreviousValue != NewValue || FMath::Abs(NewValue) > DsConstants::StickDeadZone) + { + const auto Scale{ + NewValue <= 0 + ? 1.0f / -static_cast(TNumericLimits::Min()) + : 1.0f / static_cast(TNumericLimits::Max()) + }; + + MessageHandler->OnControllerAnalog(KeyName, PlatformUserId, InputDeviceId, NewValue * Scale); + } +} + +void FDsInputDevice::ProcessButton(const int32 ControllerId, const FPlatformUserId PlatformUserId, const FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& KeyName, const int32 ButtonIndex, + const bool bPreviousKeyDown, const bool bNewKeyDown, const double Time) +{ + if (bPreviousKeyDown != bNewKeyDown) + { + if (bNewKeyDown) + { + MessageHandler->OnControllerButtonPressed(KeyName, PlatformUserId, InputDeviceId, false); + + ExtraStates[ControllerId].ButtonsNextRepeatTime[ButtonIndex] = Time + InitialButtonRepeatDelay; + } + else + { + MessageHandler->OnControllerButtonReleased(KeyName, PlatformUserId, InputDeviceId, false); + } + + return; + } + + if (bNewKeyDown && ExtraStates[ControllerId].ButtonsNextRepeatTime[ButtonIndex] <= Time) + { + MessageHandler->OnControllerButtonPressed(KeyName, PlatformUserId, InputDeviceId, true); + + ExtraStates[ControllerId].ButtonsNextRepeatTime[ButtonIndex] = Time + ButtonRepeatDelay; + } +} + +void FDsInputDevice::ProcessTouch(const FPlatformUserId PlatformUserId, const FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& AxisXKeyName, const FGamepadKeyNames::Type& AxisYKeyName, + const DS5W::Touch& PreviousTouch, const DS5W::Touch& NewTouch) const +{ + if (!PreviousTouch.down || !NewTouch.down) + { + return; + } + + const auto TouchAxisX{static_cast(NewTouch.x - PreviousTouch.x)}; + if (TouchAxisX != 0) + { + MessageHandler->OnControllerAnalog(AxisXKeyName, PlatformUserId, InputDeviceId, TouchAxisX); + } + + const auto TouchAxisY{static_cast(NewTouch.y - PreviousTouch.y)}; + if (TouchAxisY != 0) + { + MessageHandler->OnControllerAnalog(AxisYKeyName, PlatformUserId, InputDeviceId, TouchAxisY); + } + + if (GetDefault()->bEmitMouseEventsFromTouchpad) + { + MessageHandler->OnRawMouseMove(TouchAxisX, TouchAxisY); + } +} + +void FDsInputDevice::ReleaseStick(const FPlatformUserId PlatformUserId, const FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& KeyName, const int8 CurrentValue) const +{ + if (CurrentValue != 0) + { + MessageHandler->OnControllerAnalog(KeyName, PlatformUserId, InputDeviceId, 0.0f); + } +} + +void FDsInputDevice::ReleaseButton(const FPlatformUserId PlatformUserId, const FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& KeyName, const bool bPressed) const +{ + if (bPressed) + { + MessageHandler->OnControllerButtonReleased(KeyName, PlatformUserId, InputDeviceId, false); + } +} + +bool FDsInputDevice::ProcessLightColorProperty(DS5W::DS5OutputState& Output, const FInputDeviceLightColorProperty& ColorProperty) +{ + const auto PreviousColor{Output.lightbar}; + + if (ColorProperty.bEnable) + { + Output.lightbar.r = static_cast(ColorProperty.Color.R); + Output.lightbar.g = static_cast(ColorProperty.Color.G); + Output.lightbar.b = static_cast(ColorProperty.Color.B); + } + else + { + Output.lightbar = {}; + } + + return Output.lightbar.r != PreviousColor.r || Output.lightbar.g != PreviousColor.g || Output.lightbar.b != PreviousColor.b; +} + +bool FDsInputDevice::ProcessTriggerResetProperty(DS5W::TriggerEffect& TriggerEffect, + const FInputDeviceTriggerResetProperty& TriggerProperty, + const EInputDeviceTriggerMask TriggerMask) +{ + if (!EnumHasAnyFlags(TriggerProperty.AffectedTriggers, TriggerMask)) + { + return false; + } + + TriggerEffect.effectType = DS5W::TriggerEffectType::ReleaseAll; + return true; +} + +bool FDsInputDevice::ProcessTriggerFeedbackProperty(DS5W::TriggerEffect& TriggerEffect, + const FInputDeviceTriggerFeedbackProperty& TriggerProperty, + const EInputDeviceTriggerMask TriggerMask) +{ + if (!EnumHasAnyFlags(TriggerProperty.AffectedTriggers, TriggerMask)) + { + return false; + } + + const auto PreviousEffectType{TriggerEffect.effectType}; + const auto PreviousPosition{TriggerEffect.Continuous.startPosition}; + const auto PreviousForce{TriggerEffect.Continuous.force}; + + TriggerEffect.effectType = DS5W::TriggerEffectType::ContinuousResitance; + + const auto* InputSettings{UInputPlatformSettings::Get()}; + + const auto MaxPosition{static_cast(InputSettings->MaxTriggerFeedbackPosition)}; + + TriggerEffect.Continuous.startPosition = + MaxPosition > 0.0f + ? static_cast(TriggerProperty.Position / MaxPosition * TNumericLimits::Max()) + : 0; + + const auto MaxStrength{static_cast(InputSettings->MaxTriggerFeedbackStrength)}; + + TriggerEffect.Continuous.force = + MaxStrength > 0.0f + ? static_cast(TriggerProperty.Strengh / MaxStrength * TNumericLimits::Max()) + : 0; + + return TriggerEffect.effectType != PreviousEffectType || + TriggerEffect.Continuous.startPosition != PreviousPosition || + TriggerEffect.Continuous.force != PreviousForce; +} + +bool FDsInputDevice::ProcessTriggerResistanceProperty(DS5W::TriggerEffect& TriggerEffect, + const FInputDeviceTriggerResistanceProperty& TriggerProperty, + const EInputDeviceTriggerMask TriggerMask) +{ + if (!EnumHasAnyFlags(TriggerProperty.AffectedTriggers, TriggerMask)) + { + return false; + } + + // Partially supported. FInputDeviceTriggerResistanceProperty::StartStrength + // and FInputDeviceTriggerResistanceProperty::EndStrength are ignored. + + const auto PreviousEffectType{TriggerEffect.effectType}; + const auto PreviousStartPosition{TriggerEffect.Section.startPosition}; + const auto PreviousEndPosition{TriggerEffect.Section.endPosition}; + + TriggerEffect.effectType = DS5W::TriggerEffectType::SectionResitance; + + static constexpr auto MaxPositionInverse{1.0f / 9.0f}; + + TriggerEffect.Section.startPosition = static_cast( + TriggerProperty.StartPosition * MaxPositionInverse * TNumericLimits::Max()); + + TriggerEffect.Section.endPosition = static_cast( + TriggerProperty.EndPosition * MaxPositionInverse * TNumericLimits::Max()); + + return TriggerEffect.effectType != PreviousEffectType || + TriggerEffect.Section.startPosition != PreviousStartPosition || + TriggerEffect.Section.endPosition != PreviousEndPosition; +} + +bool FDsInputDevice::ProcessTriggerVibrationProperty(DS5W::TriggerEffect& TriggerEffect, + const FInputDeviceTriggerVibrationProperty& TriggerProperty, + const EInputDeviceTriggerMask TriggerMask) +{ + if (!EnumHasAnyFlags(TriggerProperty.AffectedTriggers, TriggerMask)) + { + return false; + } + + // Not supported. At the moment, it is not possible to make this work as expected. + + // const auto PreviousEffectType{TriggerEffect.effectType}; + // const auto PreviousPosition{TriggerEffect.EffectEx.startPosition}; + // const auto PreviousFrequency{TriggerEffect.EffectEx.frequency}; + // + // TriggerEffect.effectType = DS5W::TriggerEffectType::EffectEx; + // + // const auto* InputSettings{UInputPlatformSettings::Get()}; + // + // const auto MaxPosition{static_cast(InputSettings->MaxTriggerVibrationTriggerPosition)}; + // + // TriggerEffect.EffectEx.startPosition = + // MaxPosition > 0.0f + // ? static_cast(TriggerProperty.TriggerPosition / MaxPosition * TNumericLimits::Max()) + // : 0; + // + // TriggerEffect.EffectEx.keepEffect = true; + // TriggerEffect.EffectEx.beginForce = 0; + // TriggerEffect.EffectEx.middleForce = 0; + // TriggerEffect.EffectEx.endForce = 0; + // + // const auto MaxFrequency{static_cast(InputSettings->MaxTriggerVibrationFrequency)}; + // + // TriggerEffect.EffectEx.frequency = + // MaxFrequency > 0.0f + // ? static_cast(TriggerProperty.VibrationFrequency / MaxFrequency * TNumericLimits::Max()) + // : 0; + // + // return TriggerEffect.effectType != PreviousEffectType || + // TriggerEffect.EffectEx.startPosition != PreviousPosition || + // TriggerEffect.EffectEx.frequency != PreviousFrequency; + + return false; +} diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsInputDevice.h b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsInputDevice.h new file mode 100644 index 0000000..e4a0f0a --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsInputDevice.h @@ -0,0 +1,106 @@ +#pragma once + +#include + +#include "DsConstants.h" +#include "IInputDevice.h" + +enum class EInputDeviceTriggerMask : uint8; +struct FInputDeviceLightColorProperty; +struct FInputDeviceTriggerResetProperty; +struct FInputDeviceTriggerFeedbackProperty; +struct FInputDeviceTriggerResistanceProperty; +struct FInputDeviceTriggerVibrationProperty; + +struct FABULOUSDUALSENSE_API FDsExtraState +{ + double ButtonsNextRepeatTime[DsConstants::ButtonsCount]{}; + + uint8 ForceFeedbackLeftLarge{0}; + uint8 ForceFeedbackLeftSmall{0}; + + uint8 ForceFeedbackRightLarge{0}; + uint8 ForceFeedbackRightSmall{0}; + + bool bOutputChanged{true}; +}; + +class FABULOUSDUALSENSE_API FDsInputDevice : public IInputDevice +{ +private: + TSharedPtr MessageHandler; + + float InitialButtonRepeatDelay{0.2f}; + + float ButtonRepeatDelay{0.1f}; + + DS5W::DeviceContext DeviceContexts[DsConstants::MaxDevicesCount]{}; + + DS5W::DS5InputState InputStates[DsConstants::MaxDevicesCount]{}; + + DS5W::DS5OutputState OutputStates[DsConstants::MaxDevicesCount]{}; + + FDsExtraState ExtraStates[DsConstants::MaxDevicesCount]{}; + +public: + explicit FDsInputDevice(const TSharedRef& MessageHandler); + + virtual ~FDsInputDevice() override; + + virtual void Tick(float DeltaTime) override; + + virtual void SendControllerEvents() override; + + virtual void SetMessageHandler(const TSharedRef& NewMessageHandler) override; + + virtual bool Exec(UWorld* World, const TCHAR* Command, FOutputDevice& Archive) override; + + virtual void SetChannelValue(int32 ControllerId, FForceFeedbackChannelType ChannelType, float Value) override; + + virtual void SetChannelValues(int32 ControllerId, const FForceFeedbackValues& Values) override; + + virtual void SetDeviceProperty(int32 ControllerId, const FInputDeviceProperty* Property) override; + + virtual bool IsGamepadAttached() const override; + +private: + void RefreshDevices(); + + void ConnectDevice(IPlatformInputDeviceMapper& InputDeviceMapper, DS5W::DeviceEnumInfo& DeviceInfo, int32 ControllerId); + + void DisconnectDevice(IPlatformInputDeviceMapper& InputDeviceMapper, int32 ControllerId, + FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId); + + void ProcessStick(FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& KeyName, int8 PreviousValue, int8 NewValue) const; + + void ProcessButton(int32 ControllerId, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& KeyName, int32 ButtonIndex, bool bPreviousKeyDown, bool bNewKeyDown, double Time); + + void ProcessTouch(FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId, const FGamepadKeyNames::Type& AxisXKeyName, + const FGamepadKeyNames::Type& AxisYKeyName, const DS5W::Touch& PreviousTouch, const DS5W::Touch& NewTouch) const; + + void ReleaseStick(FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& KeyName, int8 CurrentValue) const; + + void ReleaseButton(FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId, + const FGamepadKeyNames::Type& KeyName, bool bPressed) const; + + static bool ProcessLightColorProperty(DS5W::DS5OutputState& Output, const FInputDeviceLightColorProperty& ColorProperty); + + static bool ProcessTriggerResetProperty(DS5W::TriggerEffect& TriggerEffect, + const FInputDeviceTriggerResetProperty& TriggerProperty, + EInputDeviceTriggerMask TriggerMask); + + static bool ProcessTriggerFeedbackProperty(DS5W::TriggerEffect& TriggerEffect, + const FInputDeviceTriggerFeedbackProperty& TriggerProperty, + EInputDeviceTriggerMask TriggerMask); + + static bool ProcessTriggerResistanceProperty(DS5W::TriggerEffect& TriggerEffect, + const FInputDeviceTriggerResistanceProperty& TriggerProperty, + EInputDeviceTriggerMask TriggerMask); + + static bool ProcessTriggerVibrationProperty(DS5W::TriggerEffect& TriggerEffect, + const FInputDeviceTriggerVibrationProperty& TriggerProperty, + EInputDeviceTriggerMask TriggerMask); +}; diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsSettings.cpp b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsSettings.cpp new file mode 100644 index 0000000..85dbed6 --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsSettings.cpp @@ -0,0 +1,24 @@ +#include "DsSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(DsSettings) + +#define LOCTEXT_NAMESPACE "DsSettings" + +UDsSettings::UDsSettings() +{ + CategoryName = FName{TEXTVIEW("Plugins")}; +} + +#if WITH_EDITOR +FText UDsSettings::GetSectionText() const +{ + return LOCTEXT("Section", "Fabulous DualSense"); +} + +FText UDsSettings::GetSectionDescription() const +{ + return LOCTEXT("SectionDescription", "Fabulous DualSense Settings"); +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsUtility.cpp b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsUtility.cpp new file mode 100644 index 0000000..35a8edb --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsUtility.cpp @@ -0,0 +1,46 @@ +#include "DsUtility.h" + +DEFINE_LOG_CATEGORY(LogFabulousDualSense) + +constexpr FStringView DsUtility::ReturnValueToString(const DS5W_ReturnValue ReturnValue) +{ + if (ReturnValue < DS5W_ReturnValue::OK || ReturnValue > DS5W_ReturnValue::E_IO_PENDING) + { + return TEXTVIEW("Unknown return value"); + } + + static constexpr FStringView Strings[] + { + TEXTVIEW("Operation completed without an error"), + TEXTVIEW("Operation encountered an unknown error"), + TEXTVIEW("The user supplied buffer is too small"), + TEXTVIEW("External unexpected WinAPI error"), + TEXTVIEW("Not enough memory on the stack"), + TEXTVIEW("Invalid arguments"), + TEXTVIEW("This feature is currently not supported"), + TEXTVIEW("Device was disconnected"), + TEXTVIEW("Bluetooth communication error"), + TEXTVIEW("IO timeout"), + TEXTVIEW("IO failed"), + TEXTVIEW("Overlapped IO request was not found"), + TEXTVIEW("IO did not complete because it is running in the background"), + }; + + return Strings[static_cast(ReturnValue)]; +} + +constexpr FStringView DsUtility::DeviceConnectionToString(const DS5W::DeviceConnection DeviceConnection) +{ + if (DeviceConnection < DS5W::DeviceConnection::USB || DeviceConnection > DS5W::DeviceConnection::BT) + { + return TEXTVIEW("Unknown device connection"); + } + + static constexpr FStringView Strings[] + { + TEXTVIEW("USB"), + TEXTVIEW("Bluetooth") + }; + + return Strings[static_cast(DeviceConnection)]; +} diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsUtility.h b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsUtility.h new file mode 100644 index 0000000..210f400 --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/DsUtility.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include "Logging/LogMacros.h" + +FABULOUSDUALSENSE_API DECLARE_LOG_CATEGORY_EXTERN(LogFabulousDualSense, Log, All) + +namespace DsUtility +{ + FABULOUSDUALSENSE_API constexpr FStringView ReturnValueToString(DS5W_ReturnValue ReturnValue); + + FABULOUSDUALSENSE_API constexpr FStringView DeviceConnectionToString(DS5W::DeviceConnection DeviceConnection); +} diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/FabulousDualSenseModule.cpp b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/FabulousDualSenseModule.cpp new file mode 100644 index 0000000..97f2d67 --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/FabulousDualSenseModule.cpp @@ -0,0 +1,71 @@ +#include "FabulousDualSenseModule.h" + +#include "DsInputDevice.h" + +IMPLEMENT_MODULE(FFabulousDualSenseModule, FabulousDualSense) + +#define LOCTEXT_NAMESPACE "FabulousDualSenseModule" + +void FFabulousDualSenseModule::StartupModule() +{ + IInputDeviceModule::StartupModule(); + + static const FName CategoryName{TEXTVIEW("DualSense")}; + + EKeys::AddMenuCategoryDisplayInfo(CategoryName, LOCTEXT("Category", "DualSense"), FName{TEXTVIEW("GraphEditor.PadEvent_16x")}); + + EKeys::AddKey({DsConstants::TouchpadKey, LOCTEXT("TouchpadKey", "DualSense Touchpad"), FKeyDetails::GamepadKey, CategoryName}); + EKeys::AddKey({DsConstants::LogoKey, LOCTEXT("LogoKey", "DualSense Logo"), FKeyDetails::GamepadKey, CategoryName}); + EKeys::AddKey({DsConstants::MuteKey, LOCTEXT("MuteKey", "DualSense Mute"), FKeyDetails::GamepadKey, CategoryName}); + + // Touch 1. + + EKeys::AddKey({ + DsConstants::Touch1Key, LOCTEXT("Touch1Key", "DualSense Touch 1"), FKeyDetails::GamepadKey | FKeyDetails::Touch, CategoryName + }); + + EKeys::AddKey({ + DsConstants::Touch1AxisXKey, LOCTEXT("Touch1AxisXKey", "DualSense Touch 1 X-Axis"), + FKeyDetails::GamepadKey | FKeyDetails::Touch | FKeyDetails::Axis1D | FKeyDetails::UpdateAxisWithoutSamples, CategoryName + }); + + EKeys::AddKey({ + DsConstants::Touch1AxisYKey, LOCTEXT("Touch1AxisYKey", "DualSense Touch 1 Y-Axis"), + FKeyDetails::GamepadKey | FKeyDetails::Touch | FKeyDetails::Axis1D | FKeyDetails::UpdateAxisWithoutSamples, CategoryName + }); + + EKeys::AddPairedKey({ + DsConstants::Touch1AxisXYKey, LOCTEXT("Touch1AxisXYKey", "DualSense Touch 1 XY-Axis"), + FKeyDetails::GamepadKey | FKeyDetails::Touch | FKeyDetails::Axis2D | FKeyDetails::UpdateAxisWithoutSamples, + CategoryName + }, DsConstants::Touch1AxisXKey, DsConstants::Touch1AxisYKey); + + // Touch 2. + + EKeys::AddKey({ + DsConstants::Touch2Key, LOCTEXT("Touch2Key", "DualSense Touch 2"), FKeyDetails::GamepadKey | FKeyDetails::Touch, CategoryName + }); + + EKeys::AddKey({ + DsConstants::Touch2AxisXKey, LOCTEXT("Touch2AxisXKey", "DualSense Touch 2 X-Axis"), + FKeyDetails::GamepadKey | FKeyDetails::Touch | FKeyDetails::Axis1D | FKeyDetails::UpdateAxisWithoutSamples, CategoryName + }); + + EKeys::AddKey({ + DsConstants::Touch2AxisYKey, LOCTEXT("Touch2AxisYKey", "DualSense Touch 2 Y-Axis"), + FKeyDetails::GamepadKey | FKeyDetails::Touch | FKeyDetails::Axis1D | FKeyDetails::UpdateAxisWithoutSamples, CategoryName + }); + + EKeys::AddPairedKey({ + DsConstants::Touch2AxisXYKey, LOCTEXT("Touch2AxisXYKey", "DualSense Touch 2 XY-Axis"), + FKeyDetails::GamepadKey | FKeyDetails::Touch | FKeyDetails::Axis2D | FKeyDetails::UpdateAxisWithoutSamples, + CategoryName + }, DsConstants::Touch2AxisXKey, DsConstants::Touch2AxisYKey); +} + +TSharedPtr FFabulousDualSenseModule::CreateInputDevice(const TSharedRef& MessageHandler) +{ + return MakeShared(MessageHandler); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/FabulousDualSenseModule.h b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/FabulousDualSenseModule.h new file mode 100644 index 0000000..bd8d3a2 --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Private/FabulousDualSenseModule.h @@ -0,0 +1,11 @@ +#pragma once + +#include "IInputDeviceModule.h" + +class FABULOUSDUALSENSE_API FFabulousDualSenseModule : public IInputDeviceModule +{ +public: + virtual void StartupModule() override; + + virtual TSharedPtr CreateInputDevice(const TSharedRef& MessageHandler) override; +}; diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Public/DsConstants.h b/Plugins/FabulousDualSense/Source/FabulousDualSense/Public/DsConstants.h new file mode 100644 index 0000000..3f4ed1d --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Public/DsConstants.h @@ -0,0 +1,31 @@ +#pragma once + +#include "InputCoreTypes.h" +#include "GenericPlatform/GenericApplicationMessageHandler.h" + +namespace DsConstants +{ + FABULOUSDUALSENSE_API extern const FName InputDeviceName; + FABULOUSDUALSENSE_API extern const FString HardwareDeviceIdentifier; + + inline constexpr auto MaxDevicesCount{4}; + inline constexpr auto ButtonsCount{29}; + inline constexpr auto StickDeadZone{30}; + inline constexpr auto TriggerDeadZone{30}; + + FABULOUSDUALSENSE_API extern const FKey TouchpadKey; + FABULOUSDUALSENSE_API extern const FKey LogoKey; + FABULOUSDUALSENSE_API extern const FKey MuteKey; + + FABULOUSDUALSENSE_API extern const FKey Touch1Key; + FABULOUSDUALSENSE_API extern const FKey Touch1AxisXKey; + FABULOUSDUALSENSE_API extern const FKey Touch1AxisYKey; + FABULOUSDUALSENSE_API extern const FKey Touch1AxisXYKey; + + FABULOUSDUALSENSE_API extern const FKey Touch2Key; + FABULOUSDUALSENSE_API extern const FKey Touch2AxisXKey; + FABULOUSDUALSENSE_API extern const FKey Touch2AxisYKey; + FABULOUSDUALSENSE_API extern const FKey Touch2AxisXYKey; + + FABULOUSDUALSENSE_API const TMap& GetRegularButtons(); +} diff --git a/Plugins/FabulousDualSense/Source/FabulousDualSense/Public/DsSettings.h b/Plugins/FabulousDualSense/Source/FabulousDualSense/Public/DsSettings.h new file mode 100644 index 0000000..2fae89c --- /dev/null +++ b/Plugins/FabulousDualSense/Source/FabulousDualSense/Public/DsSettings.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Engine/DeveloperSettings.h" +#include "DsSettings.generated.h" + +UCLASS(Config = "Engine", DefaultConfig) +class FABULOUSDUALSENSE_API UDsSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DualSense", Config) + bool bEmitMouseEventsFromTouchpad; + +public: + UDsSettings(); + +#if WITH_EDITOR + virtual FText GetSectionText() const override; + + virtual FText GetSectionDescription() const override; +#endif +}; diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/DualSenseWindows.Build.cs b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/DualSenseWindows.Build.cs new file mode 100644 index 0000000..5db3d2e --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/DualSenseWindows.Build.cs @@ -0,0 +1,24 @@ +using System.IO; +using UnrealBuildTool; + +public class DualSenseWindows : ModuleRules +{ + public DualSenseWindows(ReadOnlyTargetRules Target) : base(Target) + { + Type = ModuleType.External; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_2; + + bEnableNonInlinedGenCppWarnings = true; + + if (Target.Platform == UnrealTargetPlatform.Win64) + { + PublicDefinitions.Add("DS5W_USE_LIB"); + + PublicSystemIncludePaths.Add(Path.Combine(ModuleDirectory, "include")); + + PublicSystemLibraries.Add("hid.lib"); + + PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "lib", "ds5w_x64.lib")); + } + } +} \ No newline at end of file diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/LICENSE b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/LICENSE new file mode 100644 index 0000000..746bbf5 --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/LICENSE @@ -0,0 +1,32 @@ +MIT License + +----------------------------------- Info ----------------------------------- +This project is a fork of the "DualSense Windows API" project by Ludwig +Füchsl. As such portions of the project are held by the original copyright +Ludwig Füchsl (2020). + +Only the implementation is licensed under the MIT license. The protocol of the +DualSense 5 controller has been discovered independently and collectively by +several people. You can find my sources in the GitHub repository. +---------------------------------------------------------------------------- + +Copyright (c) 2022 Matthew Hall +Copyright (c) 2020 Ludwig Füchsl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DS5State.h b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DS5State.h new file mode 100644 index 0000000..794c043 --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DS5State.h @@ -0,0 +1,493 @@ +/* + DS5State.h is part of DualSenseWindows + https://github.com/mattdevv/DualSense-Windows + + Contributors of this file: + 11.2021 mattdevv + 11.2020 Ludwig Füchsl + + Licensed under the MIT License (To be found in repository root directory) +*/ +#pragma once + +// DPAD buttons +#define DS5W_ISTATE_BTN_DPAD_LEFT 0x01 +#define DS5W_ISTATE_BTN_DPAD_DOWN 0x02 +#define DS5W_ISTATE_BTN_DPAD_RIGHT 0x04 +#define DS5W_ISTATE_BTN_DPAD_UP 0x08 + +// Face buttons +#define DS5W_ISTATE_BTN_SQUARE 0x10 +#define DS5W_ISTATE_BTN_CROSS 0x20 +#define DS5W_ISTATE_BTN_CIRCLE 0x40 +#define DS5W_ISTATE_BTN_TRIANGLE 0x80 + +// Shoulder buttons +#define DS5W_ISTATE_BTN_BUMPER_LEFT 0x0100 +#define DS5W_ISTATE_BTN_BUMPER_RIGHT 0x0200 +#define DS5W_ISTATE_BTN_TRIGGER_LEFT 0x0400 +#define DS5W_ISTATE_BTN_TRIGGER_RIGHT 0x0800 + +// Menu buttons +#define DS5W_ISTATE_BTN_SELECT 0x1000 +#define DS5W_ISTATE_BTN_MENU 0x2000 + +// Stick buttons +#define DS5W_ISTATE_BTN_STICK_LEFT 0x4000 +#define DS5W_ISTATE_BTN_STICK_RIGHT 0x8000 + +// Extra buttons +#define DS5W_ISTATE_BTN_PLAYSTATION_LOGO 0x010000 +#define DS5W_ISTATE_BTN_PAD_BUTTON 0x020000 +#define DS5W_ISTATE_BTN_MIC_BUTTON 0x040000 + +#define DS5W_OSTATE_PLAYER_LED_LEFT 0x01 +#define DS5W_OSTATE_PLAYER_LED_MIDDLE_LEFT 0x02 +#define DS5W_OSTATE_PLAYER_LED_MIDDLE 0x04 +#define DS5W_OSTATE_PLAYER_LED_MIDDLE_RIGHT 0x08 +#define DS5W_OSTATE_PLAYER_LED_RIGHT 0x10 + +namespace DS5W { + + /// + /// Analog stick + /// + typedef struct _AnalogStick { + /// + /// X Position of stick (0 = Center) + /// + char x; + + /// + /// Y Posistion of stick (0 = Center) + /// + char y; + } AnalogStick; + + /// + /// 3 Component vector + /// + typedef struct _Vec3 { + int x; + int y; + int z; + } Vector3, Vec3; + + /// + /// RGB Color + /// + typedef struct _Color { + unsigned char r; + unsigned char g; + unsigned char b; + } Color; + + /// + /// Touchpad state + /// + typedef struct _Touch { + /// + /// X positon of finger (0 - 1920) + /// + unsigned int x; + + /// + /// Y position of finger (0 - 1080) + /// + unsigned int y; + + /// + /// Touch is down + /// + bool down; + + /// + /// 7-bit ID for touch + /// + unsigned char id; + } Touch; + + typedef struct _Battery { + /// + /// Charching state of the battery + /// + bool charging; + + /// + /// Indicates that the battery is fully charged + /// + bool fullyCharged; + + /// + /// Battery charge level 0x0 to + /// + unsigned char level; + } Battery; + + /// + /// State of the mic led + /// + typedef enum class _MicLed : unsigned char{ + /// + /// Lef is off + /// + OFF = 0x00, + + /// + /// Led is on + /// + ON = 0x01, + + /// + /// Led is pulsing + /// + PULSE = 0x02, + } MicLed; + + /// + /// Type of trigger effect + /// + typedef enum class _TriggerEffectType : unsigned char { + /// + /// Disable all effects (after trigger is released) + /// + NoResitance = 0x00, + + /// + /// Continuous Resitance is applied + /// + ContinuousResitance = 0x01, + + /// + /// Seciton resistance is applied + /// + SectionResitance = 0x02, + + /// + /// Disable all effects and release any active tension + /// + ReleaseAll = 0x05, + + /// + /// Extended trigger effect + /// + EffectEx = 0x26, + + /// + /// Calibrate triggers + /// + Calibrate = 0xFC, + } TriggerEffectType; + + /// + /// Trigger effect + /// + typedef struct _TriggerEffect { + /// + /// Trigger effect type + /// + TriggerEffectType effectType; + + /// + /// Union for effect parameters + /// + union { + /// + /// Union one raw data + /// + unsigned char _u1_raw[10]; + + /// + /// For type == ContinuousResitance + /// + struct { + /// + /// Start position of resistance + /// + unsigned char startPosition; + + /// + /// Force of resistance + /// + unsigned char force; + + /// + /// PAD / UNUSED + /// + unsigned char _pad[8]; + } Continuous; + + /// + /// For type == SectionResitance + /// + struct { + /// + /// Start position of resistance + /// + unsigned char startPosition; + + /// + /// End position of resistance (>= start) + /// + unsigned char endPosition; + + /// + /// PAD / UNUSED + /// + unsigned char _pad[8]; + } Section; + + /// + /// For type == EffectEx + /// + struct { + /// + /// Position at witch the effect starts + /// + unsigned char startPosition; + + /// + /// Wher the effect should keep playing when trigger goes beyond 255 + /// + bool keepEffect; + + /// + /// Force applied when trigger >= (255 / 2) + /// + unsigned char beginForce; + + /// + /// Force applied when trigger <= (255 / 2) + /// + unsigned char middleForce; + + /// + /// Force applied when trigger is beyond 255 + /// + unsigned char endForce; + + /// + /// Vibration frequency of the trigger + /// + unsigned char frequency; + + /// + /// PAD / UNUSED + /// + unsigned char _pad[4]; + } EffectEx; + }; + } TriggerEffect; + + /// + /// Led brightness + /// + typedef enum _LedBrightness : unsigned char { + /// + /// Low led brightness + /// + LOW = 0x02, + + /// + /// Medium led brightness + /// + MEDIUM = 0x01, + + /// + /// High led brightness + /// + HIGH = 0x00, + } LedBrightness; + + /// + /// Player leds values + /// + typedef struct _PlayerLeds { + /// + /// Player indication leds bitflag (You may used them for other features) DS5W_OSTATE_PLAYER_LED_??? + /// + unsigned char bitmask; + + /// + /// Indicates weather the player leds should fade in + /// + bool playerLedFade; + + /// + /// Brightness of the player leds + /// + LedBrightness brightness; + } PlayerLeds; + + /// + /// Flags used by DualSense controller to identify changes output report will perform + /// + typedef enum class _OutputFlags : unsigned short + { + SetMainMotorsA = 1 << 0, // Allow changing controller haptics. Also requires SetMainMotorsB flag + SetMainMotorsB = 1 << 1, // Allow changing controller haptics. Also requires SetMainMotorsA flag + SetTriggerMotorsA = 1 << 2, // Allow changing trigger haptics. Also requires SetTriggerMotorsB flag + SetTriggerMotorsB = 1 << 3, // Allow changing trigger haptics. Also requires SetTriggerMotorsA flag + SetAudioVolume = 1 << 4, // Enable modification of audio volume + EnableAudio = 1 << 5, // Enable internal speaker (even while headset is connected) + SetMicrophoneVolume = 1 << 6, // Enable modification of microphone volume + EnableMicrophone = 1 << 7, // Enable internal mic (even while headset is connected) + + SetMicrophoneLED = 1 << 8, // Allow changing microphone LED state + SetAudioMicMute = 1 << 9, // Set microphone to mute when flag is on? + SetColorLED = 1 << 10, // Allow changing lightbar RGB value + DisableAllLED = 1 << 11, // Turn off all lights while flag is set + SetPlayerLED = 1 << 12, // Allow changing which player LEDs are enabled + UnknownFlag1 = 1 << 13, // ? + SetMotorStrength = 1 << 14, // Allow changing rumble strength + UnknownFlag2 = 1 << 15, // ? + + } OutputFlags; + + /// + /// Default output flags to allow changing all settings other than audio/microphone + /// + const unsigned short DefaultOutputFlags = + (unsigned short)OutputFlags::SetMainMotorsA | + (unsigned short)OutputFlags::SetMainMotorsB | + (unsigned short)OutputFlags::SetTriggerMotorsA | + (unsigned short)OutputFlags::SetTriggerMotorsB | + (unsigned short)OutputFlags::SetMicrophoneLED | + (unsigned short)OutputFlags::SetAudioMicMute | + (unsigned short)OutputFlags::SetColorLED | + (unsigned short)OutputFlags::SetPlayerLED | + (unsigned short)OutputFlags::UnknownFlag1 | + (unsigned short)OutputFlags::SetMotorStrength | + (unsigned short)OutputFlags::UnknownFlag2; + + /// + /// Input state of the controler + /// + typedef struct _DS5InputState { + /// + /// Position of left stick + /// + AnalogStick leftStick; + + /// + /// Posisiton of right stick + /// + AnalogStick rightStick; + + /// + /// bitflags of buttons, (face | btnsA | btnsB), final 13 bits are empty + /// + unsigned int buttonMap; + + /// + /// Left trigger position + /// + unsigned char leftTrigger; + + /// + /// Right trigger position + /// + unsigned char rightTrigger; + + /// + /// Accelerometer + /// + Vector3 accelerometer; + + /// + /// Gyroscope (Currently only raw values will be dispayed! Probably needs calibration (Will be done within the lib in the future)) + /// + Vector3 gyroscope; + + /// + /// First touch point + /// + Touch touchPoint1; + + /// + /// Second touch point + /// + Touch touchPoint2; + + /// + /// Sensor timestamp in 0.33 microseconds + /// + unsigned int currentTime; + + /// + /// Time since last input report. Measured in 0.33 microseconds + /// + unsigned int deltaTime; + + /// + /// Battery information + /// + Battery battery; + + /// + /// Indicates the connection of headphone + /// + bool headPhoneConnected; + + /// + /// EXPERIMAENTAL: Feedback of the left adaptive trigger (only when trigger effect is active) + /// + unsigned char leftTriggerFeedback; + + /// + /// EXPERIMAENTAL: Feedback of the right adaptive trigger (only when trigger effect is active) + /// + unsigned char rightTriggerFeedback; + } DS5InputState; + + typedef struct _DS5OutputState { + + /// + /// Left / Hard rumbel motor + /// + unsigned char leftRumble; + + /// + /// Right / Soft rumbel motor + /// + unsigned char rightRumble; + + /// + /// strength of rumble motors in 12.5% steps + /// lower nibble (bits 0-3) main rumbles + /// uppper nibble (bits 4-7) trigger rumbles + /// + unsigned char rumbleStrength; + + /// + /// State of the microphone led + /// + MicLed microphoneLed; + + /// + /// Diables all leds + /// + bool disableLeds; + + /// + /// Player leds + /// + PlayerLeds playerLeds; + + /// + /// Color of the lightbar + /// + Color lightbar; + + /// + /// Effect of left trigger + /// + TriggerEffect leftTriggerEffect; + + /// + /// Effect of right trigger + /// + TriggerEffect rightTriggerEffect; + + } DS5OutputState; +} \ No newline at end of file diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DSW_Api.h b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DSW_Api.h new file mode 100644 index 0000000..bb3f204 --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DSW_Api.h @@ -0,0 +1,112 @@ +/* + DSW_Api.h is part of DualSenseWindows + https://github.com/mattdevv/DualSense-Windows + + Contributors of this file: + 12.2021 Matthew Hall + 11.2020 Ludwig Füchsl + + Licensed under the MIT License (To be found in repository root directory) +*/ +#pragma once + +#if defined(DS5W_BUILD_DLL) +#define DS5W_API __declspec(dllexport) +#elif defined(DS5W_BUILD_LIB) +#define DS5W_API +#elif defined(DS5W_USE_LIB) +#define DS5W_API +#else +#define DS5W_API __declspec(dllimport) +#endif + +#define IO_TIMEOUT_MILLISECONDS 100 /* How long to wait for IO requests before assuming device disconnect */ +#define ID_HASH_SEED 0xAABB /* Seed used when hashing device path */ + +#define DS5W_SUCCESS(expr) ((expr) == _DS5W_ReturnValue::OK) +#define DS5W_FAILED(expr) ((expr) != _DS5W_ReturnValue::OK) + +#define DS5W_OK _DS5W_ReturnValue::OK +#define DS5W_E_UNKNOWN _DS5W_ReturnValue::E_UNKNOWN +#define DS5W_E_INSUFFICIENT_BUFFER _DS5W_ReturnValue::E_INSUFFICIENT_BUFFER +#define DS5W_E_EXTERNAL_WINAPI _DS5W_ReturnValue::E_EXTERNAL_WINAPI +#define DS5W_E_STACK_OVERFLOW _DS5W_ReturnValue::E_STACK_OVERFLOW +#define DS5W_E_INVALID_ARGS _DS5W_ReturnValue::E_INVALID_ARGS +#define DS5W_E_CURRENTLY_NOT_SUPPORTED _DS5W_ReturnValue::E_CURRENTLY_NOT_SUPPORTED +#define DS5W_E_DEVICE_REMOVED _DS5W_ReturnValue::E_DEVICE_REMOVED +#define DS5W_E_BT_COM _DS5W_ReturnValue::E_BT_COM +#define DS5W_E_IO_TIMEDOUT _DS5W_ReturnValue::E_IO_TIMEDOUT +#define DS5W_E_IO_FAILED _DS5W_ReturnValue::E_IO_FAILED +#define DS5W_E_IO_NOT_FOUND _DS5W_ReturnValue::E_IO_NOT_FOUND +#define DS5W_E_IO_PENDING _DS5W_ReturnValue::E_IO_PENDING + +/// +/// Enum for return values +/// +typedef enum class _DS5W_ReturnValue : unsigned int { + /// + /// Operation completed without an error + /// + OK = 0, + + /// + /// Operation encountered an unknown error + /// + E_UNKNOWN = 1, + + /// + /// The user supplied buffer is to small + /// + E_INSUFFICIENT_BUFFER = 2, + + /// + /// External unexpected winapi error (please report as issue if you get this error!) + /// + E_EXTERNAL_WINAPI = 3, + + /// + /// Not enought memory on the stack + /// + E_STACK_OVERFLOW = 4, + + /// + /// Invalid arguments + /// + E_INVALID_ARGS = 5, + + /// + /// This feature is currently not supported + /// + E_CURRENTLY_NOT_SUPPORTED = 6, + + /// + /// Device was disconnected + /// + E_DEVICE_REMOVED = 7, + + /// + /// Bluetooth communication error + /// + E_BT_COM = 8, + + /// + /// IO timeout + /// + E_IO_TIMEDOUT = 9, + + /// + /// IO failed + /// + E_IO_FAILED = 10, + + /// + /// Overlapped IO request was not found + /// + E_IO_NOT_FOUND = 11, + + /// + /// IO did not complete because it is running in the background + /// + E_IO_PENDING = 12 + +} DS5W_ReturnValue, DS5W_RV; \ No newline at end of file diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/Device.h b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/Device.h new file mode 100644 index 0000000..9bc462f --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/Device.h @@ -0,0 +1,157 @@ +/* + Device.h is part of DualSenseWindows + https://github.com/mattdevv/DualSense-Windows + + Contributors of this file: + 11.2021 Matthew Hall + 11.2020 Ludwig Füchsl + + Licensed under the MIT License (To be found in repository root directory) +*/ +#pragma once + +#include +#include + +// more accurate integer multiplication by a fraction +constexpr int mult_frac(int x, int numer, int denom) +{ + int quot = x / denom; + int rem = x % denom; + return quot * numer + (rem * numer) / denom; +} + +namespace DS5W { + /// + /// Storage for calibration values used to parse raw motion data + /// + typedef struct _AxisCalibrationData { + short bias; + int sens_numer; + int sens_denom; + + int calibrate(int rawValue) + { + return mult_frac(sens_numer, rawValue - bias, sens_denom); + } + } AxisCalibrationData; + + typedef struct _DeviceCalibrationData { + /// + /// Values to calibrate controller's accelerometer and gyroscope + /// + AxisCalibrationData accelerometer[3]; + + /// + /// Values to calibrate controller's gyroscope + /// + AxisCalibrationData gyroscope[3]; + } DeviceCalibrationData; + + /// + /// Enum for device connection type + /// + typedef enum class _DeviceConnection : unsigned char { + /// + /// Controler is connected via USB + /// + USB = 0, + + /// + /// Controler is connected via bluetooth + /// + BT = 1, + } DeviceConnection; + + /// + /// Struckt for storing device enum info while device discovery + /// + typedef struct _DeviceEnumInfo { + /// + /// Encapsulate data in struct to (at least try) prevent user from modifing the context + /// + struct { + /// + /// Path to the discovered device + /// + wchar_t path[260]; + + /// + /// Connection type of the discoverd device + /// + DeviceConnection connection; + + /// + /// Unique device identifier + /// 32-bit hash of device interface's path + /// + UINT32 uniqueID; + } _internal; + } DeviceEnumInfo; + + /// + /// Device context + /// + typedef struct _DeviceContext { + /// + /// Encapsulate data in struct to (at least try) prevent user from modifing the context + /// + struct { + /// + /// Path to the device + /// + wchar_t devicePath[260]; + + /// + /// Unique device identifier + /// 32-bit hash of device interface's path + /// + UINT32 uniqueID; + + /// + /// Handle to the open device + /// + HANDLE deviceHandle; + + /// + /// Synchronization struct for async input + /// + OVERLAPPED olRead; + + /// + /// Synchronization struct for async output + /// + OVERLAPPED olWrite; + + /// + /// Connection of the device + /// + DeviceConnection connectionType; + + /// + /// Collection of values required to parse controller's motion data + /// + DeviceCalibrationData calibrationData; + + /// + /// Time when last input report was received, measured in 0.33 microseconds + /// + unsigned int timestamp; + + /// + /// Current state of connection + /// + bool connected; + + /// + /// HID Input buffer + /// + unsigned char hidInBuffer[DS_MAX_INPUT_REPORT_SIZE]; + + /// + /// HID Output buffer + /// + unsigned char hidOutBuffer[DS_MAX_OUTPUT_REPORT_SIZE]; + }_internal; + } DeviceContext; +} \ No newline at end of file diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DeviceSpecs.h b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DeviceSpecs.h new file mode 100644 index 0000000..e99d89d --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DeviceSpecs.h @@ -0,0 +1,120 @@ +/* + DS5Specs.h is part of DualSenseWindows + https://github.com/mattdevv/DualSense-Windows + + Contributors of this file: + 11.2021 Matthew Hall + + Licensed under the MIT License (To be found in repository root directory) +*/ +#pragma once + +#define SONY_CORP_VENDOR_ID 0x054C +#define DUALSENSE_CONTROLLER_PROD_ID 0x0CE6 + +#define DS_INPUT_REPORT_USB 0x01 +#define DS_INPUT_REPORT_USB_SIZE 64 +#define DS_INPUT_REPORT_BT 0x31 +#define DS_INPUT_REPORT_BT_SIZE 78 + +#define DS_OUTPUT_REPORT_USB 0x02 +#define DS_OUTPUT_REPORT_USB_SIZE 63 +#define DS_OUTPUT_REPORT_BT 0x31 +#define DS_OUTPUT_REPORT_BT_SIZE 78 + +#define DS_FEATURE_REPORT_CALIBRATION 0x05 +#define DS_FEATURE_REPORT_CALIBRATION_SIZE 41 +#define DS_FEATURE_REPORT_PAIRING_INFO 0x09 +#define DS_FEATURE_REPORT_PAIRING_INFO_SIZE 20 +#define DS_FEATURE_REPORT_FIRMWARE_INFO 0x20 +#define DS_FEATURE_REPORT_FIRMWARE_INFO_SIZE 64 + +#define DS_MAX_INPUT_REPORT_SIZE 78 /* DS_INPUT_REPORT_BT_SIZE = 78 */ +#define DS_MAX_OUTPUT_REPORT_SIZE 78 /* DS_OUTPUT_REPORT_BT_SIZE = 78 */ + +#define DS_ACC_RES_PER_G 8192 +#define DS_ACC_RANGE (4*DS_ACC_RES_PER_G) +#define DS_GYRO_RES_PER_DEG_S 1024 +#define DS_GYRO_RANGE (2048*DS_GYRO_RES_PER_DEG_S) +#define DS_TOUCHPAD_WIDTH 1920 +#define DS_TOUCHPAD_HEIGHT 1080 + +/* + // body of input report + // starts at byte index 1 (USB), 2 (BT) + + 0x00 uint8_t left_stick_x + 0x01 uint8_t left_stick_y + 0x02 uint8_t right_stick_x + 0x03 uint8_t right_stick_y + 0x04 uint8_t left_trigger + 0x05 uint8_t right_trigger + + 0x06 uint8_t seq_number; // unknown use + + 0x07 uint8_t buttons[4]; + 0x0B uint8_t reserved[4]; + + 0x0F uint16_t gyro[3]; // needs calibration + 0x15 uint16_t accel[3]; // needs calibration + 0x1B uint32_t sensor_timestamp; // in units of 0.333_ microseconds + 0x1F uint8_t reserved2; + + 0x20 struct touch_point points[2]; // 4 bytes each + + 0x28 uint8_t reserved3[12]; + 0x34 uint8_t status; + 0x35 uint8_t reserved4[10]; +*/ + +/* + // from https://gist.github.com/stealth-alex/10a8e7cc6027b78fa18a7f48a0d3d1e4 + // body of output report + // starts at byte index 1 (USB), 2 (BT) + + uint8_t feature_flags_1; + uint8_t feature_flags_2; + uint8_t motor_strength_right; + uint8_t motor_strength_left; + + // audio settings requiring volume control flags + outputReport[5] = 0xff; // audio volume of connected headphones (maxes out at about 0x7f) + outputReport[6] = 0xff; // volume of internal speaker (0-255) (ties in with index 38?!? PS5 appears to only use the range 0x3d-0x64) + outputReport[7] = 0xff; // internal microphone volume (not at all linear; 0-255, maxes out at 0x40, all values above are treated like 0x40; 0 is not fully muted, use audio mute flag instead!) + outputReport[8] = 0x0c; // audio flags (switching between mic settings causes up to 1s of silence) + // 0x01 = force use of internal controller mic (if neither 0x01 and 0x02 are set, an attached headset will take precedence) + // 0x02 = force use of mic attached to the controller (headset) + // 0x04 = pads left channel of external mic (~1/3rd of the volume? maybe the amount can be controlled?) + // 0x08 = pads left channel of internal mic (~1/3rd of the volume? maybe the amount can be controlled?) + // 0x10 = disable attached headphones (only if 0x20 to enable internal speakers is provided as well) + // 0x20 = enable audio on internal speaker (in addition to a connected headset; headset will use a stereo upmix of the left channel, internal speaker will play the right channel) + + // audio related LEDs requiring according LED toggle flags + outputReport[9] = 0x01; // microphone LED (1 = on, 2 = pulsating / neither does affect the mic) + + // audio settings requiring mute toggling flags + outputReport[10] = 0x00; // 0x10 microphone mute, 0x40 audio mute + + // trigger motors (see below for details) + outputReport[11..21] right trigger effect (mode byte + up to 10 parameters) + outputReport[22..32] left trigger effect (mode byte + up to 10 parameters) + + outputReport[33] = 0x00; // value is copied to input report at offset 43 + outputReport[34] = 0x00; // value is copied to input report at offset 44 + outputReport[35] = 0x00; // value is copied to input report at offset 45 + outputReport[36] = 0x00; // value is copied to input report at offset 46 + + outputReport[37] = 0x00; // (lower nibble: main motor; upper nibble trigger effects) 0x00 to 0x07 - reduce overall power of the respective motors/effects by 12.5% per increment (this does not affect the regular trigger motor settings, just the automatically repeating trigger effects) + outputReport[38] = 0x07; // volume of internal speaker (0-7; ties in with index 6 - the PS5 default for this appears to be 4) + + // LED section (requires LED setting flag) + outputReport[39] = 2; // flags 0x01 = set player led brightness (value in index 43), 0x02 = uninterruptable blue LED pulse (action in index 42) + outputReport[42] = 2; // pulse option + 1 = slowly (2s) fade to blue (scheduled to when the regular LED settings are active) + 2 = slowly (2s) fade out (scheduled after fade-in completion) with eventual switch back to configured LED color; only a fade-out can cancel the pulse (neither index 2, 0x08, nor turning this off will cancel it!) + outputReport[43] = 0x02; // 0x00 = high brightness, 0x01 = medium brightness, 0x02 = low brightness (requires flag from index 39) + outputReport[44] = 0x04; // 5 white player indicator LEDs below the touchpad (bitmask 00-1f from left to right with 0x04 being the center LED; bit 0x20 to set the brightness immediately and not fade in;) + outputReport[45] = 0x1f; // Red value of light bars left and right from touchpad + outputReport[46] = 0xff; // Green value of light bars left and right from touchpad + outputReport[47] = 0x1f; // Blue value of light bars left and right from touchpad +*/ \ No newline at end of file diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DualSenseWindows.h b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DualSenseWindows.h new file mode 100644 index 0000000..ccd7f3f --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/DualSenseWindows.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Windows/PreWindowsApi.h" + +#include +#include +#include +#include +#include +#include + +#include "Windows/PostWindowsApi.h" + +#ifdef max +#undef max +#endif diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/Helpers.h b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/Helpers.h new file mode 100644 index 0000000..cec291a --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/Helpers.h @@ -0,0 +1,54 @@ +/* + Helpers.h is part of DualSenseWindows + https://github.com/mattdevv/DualSense-Windows + + Contributors of this file: + 11.2020 Ludwig Füchsl + + Licensed under the MIT License (To be found in repository root directory) +*/ +#pragma once + +#include +#include + +namespace DS5W { + /// + /// Convert from 3-Color RGB normalized float to DS5W::Color + /// + /// Red channel + /// Green channel + /// Blue channel + /// DS5W::Color + DS5W_API DS5W::Color color_R32G32B32_FLOAT(float r, float g, float b); + + /// + /// Convert from 4-Color RGBA normalized float to DS5W::Color + /// + /// Red channel + /// Green channel + /// Blue channel + /// Alpha channel + /// DS5W::Color + DS5W_API DS5W::Color color_R32G32B32A32_FLOAT(float r, float g, float b, float a); + + /// + /// Convert from 4-Color RGBA byte / UChar to DS5W::Color + /// + /// Red channel + /// Green channel + /// Blue channel + /// Alpha channel + /// DS5W::Color + DS5W_API DS5W::Color color_R8G8B8A8_UCHAR(unsigned char r, unsigned char g, unsigned char b, unsigned char a); + + /// + /// Convert from 4-Color RGB byte / UChar to DS5W::Color while using a normalized float for alpha chanel + /// + /// Red channel + /// Green channel + /// Blue channel + /// Alpha channel + /// DS5W::Color + DS5W_API DS5W::Color color_R8G8B8_UCHAR_A32_FLOAT(unsigned char r, unsigned char g, unsigned char b, float a); +} diff --git a/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/IO.h b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/IO.h new file mode 100644 index 0000000..7867406 --- /dev/null +++ b/Plugins/FabulousDualSense/Source/ThirdParty/DualSenseWindows/include/IO.h @@ -0,0 +1,101 @@ +/* + DualSenseWindows API + https://github.com/mattdevv/DualSense-Windows + + Licensed under the MIT License (To be found in repository root directory) +*/ +#pragma once + +#include +#include +#include +#include + +namespace DS5W { + /// + /// Enumerate all ds5 deviced connected to the computer + /// + /// Pointer to begin of array of DeviceEnumInfo objects / DeviceEnumInfo pointers + /// Length of imput array + /// pointer to uint witch recives the required total length + /// DeviceEnumInfo pointer is the pointer to an array of DeviceEnumInfo objects. false: DeviceEnumInfo pointer is a pointer to DeviceEnumInfo pointers to DeviceEnumInfo objects + /// DS5W Return value + extern "C" DS5W_API DS5W_ReturnValue enumDevices(void* ptrBuffer, unsigned int inArrLength, unsigned int* requiredLength, bool pointerToArray = true); + + /// + /// Enumerate all ds5 deviced that are not in the list of known devices + /// Devices are 'known' if their unique ID is in the array passed to this function + /// + /// Pointer to begin of array of unused DeviceEnumInfo objects / DeviceEnumInfo pointers + /// Length of input array + /// pointer to array of known device IDs + /// length of knownDeviceIDs array + /// pointer to uint witch recives the required total length + /// DeviceEnumInfo pointer is the pointer to an array of DeviceEnumInfo objects. false: DeviceEnumInfo pointer is a pointer to DeviceEnumInfo pointers to DeviceEnumInfo objects + /// DS5W Return value + extern "C" DS5W_API DS5W_ReturnValue enumUnknownDevices(void* ptrBuffer, unsigned int inArrLength, unsigned int* knownDeviceIDs, unsigned int numKnownDevices, unsigned int* requiredLength, bool pointerToArray = true); + + /// + /// Initializes a DeviceContext from its enum infos + /// + /// Pointer to enum object to create device from + /// Pointer to context to create to + /// If creation was successfull + extern "C" DS5W_API DS5W_ReturnValue initDeviceContext(DS5W::DeviceEnumInfo* ptrEnumInfo, DS5W::DeviceContext* ptrContext); + + /// + /// Stop device functions and free all links in Windows + /// This context will not be able to be reconnected + /// + /// Pointer to context + extern "C" DS5W_API void freeDeviceContext(DS5W::DeviceContext* ptrContext); + + /// + /// Stop device functions and disconnect device from windows + /// This context is able to be reconnected + /// + /// Context to shutdown + extern "C" DS5W_API void shutdownDevice(DS5W::DeviceContext * ptrContext); + + /// + /// Try to reconnect a disconnected device + /// + /// Context to reconnect on + /// Result + extern "C" DS5W_API DS5W_ReturnValue reconnectDevice(DS5W::DeviceContext* ptrContext); + + /// + /// Get device input state + /// Blocks thread until state is read or an error occurs + /// + /// Pointer to context + /// Pointer to input state + /// Result of call + extern "C" DS5W_API DS5W_ReturnValue getDeviceInputState(DS5W::DeviceContext* ptrContext, DS5W::DS5InputState* ptrInputState); + + /// + /// Set the device output state + /// Blocks thread until state is read or an error occurs + /// + /// Pointer to context + /// Pointer to output state to be set + /// Result of call + extern "C" DS5W_API DS5W_ReturnValue setDeviceOutputState(DS5W::DeviceContext* ptrContext, DS5W::DS5OutputState* ptrOutputState); + + /// + /// Starts an overlapped IO call to get device input report + /// + extern "C" DS5W_API DS5W_ReturnValue startInputRequest(DS5W::DeviceContext* ptrContext); + + /// + /// Waits until overlapped call finishes + /// Only call this if startInputRequest() returned DS5W_E_IO_PENDING + /// + extern "C" DS5W_API DS5W_ReturnValue awaitInputRequest(DS5W::DeviceContext* ptrContext); + + /// + /// Parses and copies the last input report read into an InputState struct + /// Intended to be used with startInputRequest() after the request is completed + /// + extern "C" DS5W_API void getHeldInputState(DS5W::DeviceContext * ptrContext, DS5W::DS5InputState * ptrInputState); +}