├── dist ├── ! Install the drivers in the Drivers folder └── Drivers │ └── README.txt ├── title.png ├── BetterJoy ├── Icons │ ├── n64.png │ ├── nes.png │ ├── pro.png │ ├── cross.png │ ├── snes.png │ ├── jc_left.png │ ├── jc_right.png │ ├── famicom_i.png │ ├── famicom_ii.png │ ├── jc_left_s.png │ ├── jc_right_s.png │ ├── n64_charging.png │ ├── nes_charging.png │ ├── pro_charging.png │ ├── snes_charging.png │ ├── src │ │ ├── charging.png │ │ └── controllers.psd │ ├── betterjoy_icon.ico │ ├── jc_left_charging.png │ ├── famicom_i_charging.png │ ├── jc_left_s_charging.png │ ├── jc_right_charging.png │ ├── famicom_ii_charging.png │ └── jc_right_s_charging.png ├── Logging │ ├── LogLevel.cs │ ├── ILogger.cs │ └── Logger.cs ├── Hardware │ ├── Data │ │ ├── TwoAxisUShort.cs │ │ ├── MotionShort.cs │ │ ├── ThreeAxisShort.cs │ │ └── BitWrangler.cs │ ├── SubCommand │ │ ├── BatteryLevel.cs │ │ ├── InputReportMode.cs │ │ ├── SPIPage.cs │ │ ├── SubCommandOperation.cs │ │ ├── IncomingPacket.cs │ │ ├── SubCommandReturnPacket.cs │ │ └── SubCommandPacket.cs │ └── Calibration │ │ ├── StickRangeCalibration.cs │ │ ├── StickDeadZoneCalibration.cs │ │ ├── StickLimitsCalibration.cs │ │ └── MotionCalibration.cs ├── Controller │ ├── Motion.cs │ ├── Stick.cs │ └── Mapping │ │ ├── OutputControllerXbox360.cs │ │ └── OutputControllerDualShock4.cs ├── Exceptions │ ├── DeviceComFailedException.cs │ ├── DeviceNullHandleException.cs │ ├── DeviceQueryFailedException.cs │ └── ExceptionExtensions.cs ├── HIDApi │ ├── Exceptions │ │ ├── HIDApiInitFailedException.cs │ │ └── HIDApiCallbackFailedException.cs │ ├── DeviceNotificationEventArgs.cs │ ├── Native │ │ ├── Hotplug.cs │ │ ├── DeviceInfo.cs │ │ └── NativeMethods.cs │ ├── StringUtils.cs │ ├── Manager.cs │ └── Device.cs ├── Config │ ├── MainFormConfig.cs │ ├── ProgramConfig.cs │ ├── Config.cs │ └── ControllerConfig.cs ├── Network │ ├── Server │ │ ├── ControllerEnums.cs │ │ ├── UdpUtils.cs │ │ └── UdpControllerReport.cs │ └── MacAddress.cs ├── Properties │ ├── PublishProfiles │ │ └── FolderProfile.pubxml │ ├── app.manifest │ └── Resources.resx ├── NativeMethods.cs ├── Memory │ ├── ArrayPoolHelper.cs │ └── ArrayOwner.cs ├── Collections │ ├── ConcurrentSpinQueue.cs │ └── ConcurrentList.cs ├── InputCapture.cs ├── BetterJoy.csproj ├── MadgwickAHRS.cs ├── Forms │ ├── Reassign.cs │ ├── ThirdpartyControllers.cs │ └── ThirdpartyControllers.Designer.cs ├── Settings.cs └── App.config ├── FormatCode.bat ├── nuget.config ├── .gitmodules ├── .github └── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report---problems.md ├── PUBLISH.bat ├── .gitattributes ├── LICENSE ├── .gitignore ├── README.md └── BetterJoy.sln /dist/! Install the drivers in the Drivers folder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/title.png -------------------------------------------------------------------------------- /BetterJoy/Icons/n64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/n64.png -------------------------------------------------------------------------------- /BetterJoy/Icons/nes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/nes.png -------------------------------------------------------------------------------- /BetterJoy/Icons/pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/pro.png -------------------------------------------------------------------------------- /BetterJoy/Icons/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/cross.png -------------------------------------------------------------------------------- /BetterJoy/Icons/snes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/snes.png -------------------------------------------------------------------------------- /BetterJoy/Icons/jc_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/jc_left.png -------------------------------------------------------------------------------- /BetterJoy/Icons/jc_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/jc_right.png -------------------------------------------------------------------------------- /BetterJoy/Icons/famicom_i.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/famicom_i.png -------------------------------------------------------------------------------- /BetterJoy/Icons/famicom_ii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/famicom_ii.png -------------------------------------------------------------------------------- /BetterJoy/Icons/jc_left_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/jc_left_s.png -------------------------------------------------------------------------------- /BetterJoy/Icons/jc_right_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/jc_right_s.png -------------------------------------------------------------------------------- /BetterJoy/Icons/n64_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/n64_charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/nes_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/nes_charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/pro_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/pro_charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/snes_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/snes_charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/src/charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/src/charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/betterjoy_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/betterjoy_icon.ico -------------------------------------------------------------------------------- /BetterJoy/Icons/jc_left_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/jc_left_charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/src/controllers.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/src/controllers.psd -------------------------------------------------------------------------------- /BetterJoy/Icons/famicom_i_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/famicom_i_charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/jc_left_s_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/jc_left_s_charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/jc_right_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/jc_right_charging.png -------------------------------------------------------------------------------- /FormatCode.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Formatting the code of the BetterJoy project... 3 | dotnet format BetterJoy --no-restore 4 | pause -------------------------------------------------------------------------------- /BetterJoy/Icons/famicom_ii_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/famicom_ii_charging.png -------------------------------------------------------------------------------- /BetterJoy/Icons/jc_right_s_charging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3xMachina/BetterJoy/HEAD/BetterJoy/Icons/jc_right_s_charging.png -------------------------------------------------------------------------------- /BetterJoy/Logging/LogLevel.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Logging; 2 | 3 | public enum LogLevel 4 | { 5 | Info, 6 | Warning, 7 | Error, 8 | Debug 9 | } 10 | -------------------------------------------------------------------------------- /dist/Drivers/README.txt: -------------------------------------------------------------------------------- 1 | Latest ViGEmBus releases are here: https://github.com/nefarius/ViGEmBus/releases 2 | If you're on Win7, please read the instructions on the page. 3 | 4 | Latest HidHide releases are here: https://github.com/nefarius/HidHide/releases 5 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/Data/TwoAxisUShort.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Hardware.Data; 2 | 3 | public record struct TwoAxisUShort(ushort X, ushort Y) 4 | { 5 | public override readonly string ToString() 6 | { 7 | return $"X: {X}, Y: {Y}"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BetterJoy/Controller/Motion.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace BetterJoy.Controller; 4 | 5 | public struct Motion(Vector3 gyroscope, Vector3 accelerometer) 6 | { 7 | public Vector3 Gyroscope = gyroscope; 8 | public Vector3 Accelerometer = accelerometer; 9 | } 10 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/SubCommand/BatteryLevel.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Hardware.SubCommand; 2 | 3 | public enum BatteryLevel : byte 4 | { 5 | Empty = 0x00, 6 | Critical = 0x02, 7 | Low = 0x04, 8 | Medium = 0x06, 9 | Full = 0x08, 10 | Unknown = 0xFF, 11 | } 12 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/Data/MotionShort.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Hardware.Data; 2 | 3 | public struct MotionShort(ThreeAxisShort gyroscope, ThreeAxisShort accelerometer) 4 | { 5 | public ThreeAxisShort Gyroscope = gyroscope; 6 | public ThreeAxisShort Accelerometer = accelerometer; 7 | } 8 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /BetterJoy/Controller/Stick.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Controller; 2 | 3 | public record struct Stick(float X, float Y) 4 | { 5 | public static readonly Stick Zero = new(0, 0); 6 | 7 | public override readonly string ToString() 8 | { 9 | return $"X: {X}, Y: {Y}"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BetterJoy/Logging/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BetterJoy.Logging; 4 | 5 | public interface ILogger : IDisposable 6 | { 7 | void Log(string message, LogLevel level = LogLevel.Info); 8 | void Log(string message, Exception exception, LogLevel level = LogLevel.Error); 9 | 10 | event Action? OnMessageLogged; 11 | } 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdparty/hidapi"] 2 | path = thirdparty/hidapi 3 | url = https://github.com/d3xMachina/hidapi.git 4 | [submodule "thirdparty/ViGEm.NET"] 5 | path = thirdparty/ViGEm.NET 6 | url = https://github.com/d3xMachina/ViGEm.NET.git 7 | [submodule "thirdparty/WindowsInput"] 8 | path = thirdparty/WindowsInput 9 | url = https://github.com/d3xMachina/WindowsInput.git 10 | -------------------------------------------------------------------------------- /BetterJoy/Exceptions/DeviceComFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BetterJoy.Exceptions; 4 | 5 | public class DeviceComFailedException : Exception 6 | { 7 | public DeviceComFailedException() { } 8 | 9 | public DeviceComFailedException(string message) : base(message) { } 10 | 11 | public DeviceComFailedException(string message, Exception innerException) : base(message, innerException) { } 12 | } 13 | -------------------------------------------------------------------------------- /BetterJoy/Exceptions/DeviceNullHandleException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BetterJoy.Exceptions; 4 | 5 | public class DeviceNullHandleException : Exception 6 | { 7 | public DeviceNullHandleException() { } 8 | 9 | public DeviceNullHandleException(string message) : base(message) { } 10 | 11 | public DeviceNullHandleException(string message, Exception innerException) : base(message, innerException) { } 12 | } 13 | -------------------------------------------------------------------------------- /BetterJoy/Exceptions/DeviceQueryFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BetterJoy.Exceptions; 4 | 5 | public class DeviceQueryFailedException : Exception 6 | { 7 | public DeviceQueryFailedException() { } 8 | 9 | public DeviceQueryFailedException(string message) : base(message) { } 10 | 11 | public DeviceQueryFailedException(string message, Exception innerException) : base(message, innerException) { } 12 | } 13 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/Exceptions/HIDApiInitFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BetterJoy.HIDApi.Exceptions; 4 | 5 | public class HIDApiInitFailedException : Exception 6 | { 7 | public HIDApiInitFailedException() { } 8 | 9 | public HIDApiInitFailedException(string message) : base(message) { } 10 | 11 | public HIDApiInitFailedException(string message, Exception innerException) : base(message, innerException) { } 12 | } 13 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/Exceptions/HIDApiCallbackFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BetterJoy.HIDApi.Exceptions; 4 | 5 | public class HIDApiCallbackFailedException : Exception 6 | { 7 | public HIDApiCallbackFailedException() { } 8 | 9 | public HIDApiCallbackFailedException(string message) : base(message) { } 10 | 11 | public HIDApiCallbackFailedException(string message, Exception innerException) : base(message, innerException) { } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "[ENHANCEMENT]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/DeviceNotificationEventArgs.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.HIDApi.Native; 2 | using System; 3 | 4 | namespace BetterJoy.HIDApi; 5 | 6 | public sealed class DeviceNotificationEventArgs : EventArgs 7 | { 8 | public DeviceNotificationEventArgs(DeviceInfo deviceInfo, HotplugEvent ev) 9 | { 10 | DeviceInfo = deviceInfo; 11 | DeviceEvent = ev; 12 | } 13 | 14 | public DeviceInfo DeviceInfo { get; } 15 | 16 | public HotplugEvent DeviceEvent { get; } 17 | } 18 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/Native/Hotplug.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace BetterJoy.HIDApi.Native; 5 | 6 | [Flags] 7 | public enum HotplugEvent 8 | { 9 | DeviceArrived = 1 << 0, 10 | DeviceLeft = 1 << 1 11 | } 12 | 13 | [Flags] 14 | public enum HotplugFlag 15 | { 16 | None = 0, 17 | Enumerate = 1 << 0 18 | } 19 | 20 | public delegate int HotplugCallback( 21 | int callbackHandle, 22 | [MarshalAs(UnmanagedType.Struct)] DeviceInfo deviceInfo, 23 | int events, 24 | IntPtr userData 25 | ); 26 | -------------------------------------------------------------------------------- /BetterJoy/Config/MainFormConfig.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Logging; 2 | 3 | namespace BetterJoy.Config; 4 | 5 | public class MainFormConfig : Config 6 | { 7 | public bool AllowCalibration = true; 8 | 9 | public MainFormConfig(ILogger? logger) : base(logger) { } 10 | 11 | public MainFormConfig(MainFormConfig config) : base(config._logger) 12 | { 13 | AllowCalibration = config.AllowCalibration; 14 | } 15 | 16 | public override void Update() 17 | { 18 | TryUpdateSetting("AllowCalibration", ref AllowCalibration); 19 | } 20 | 21 | public override MainFormConfig Clone() 22 | { 23 | return new MainFormConfig(this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/SubCommand/InputReportMode.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Hardware.SubCommand; 2 | 3 | public enum InputReportMode : byte 4 | { 5 | #pragma warning disable IDE0055 // Disable formatting 6 | ActiveNFCIRData = 0x00, 7 | ActiveNFCIRConfig = 0x01, 8 | ActiveNFCIRDataAndConfig = 0x02, 9 | ActiveIRData = 0x03, 10 | MCUUpdateState = 0x23, 11 | StandardFull = 0x30, 12 | NFCIRPush = 0x31, 13 | Unknown33 = 0x33, 14 | Unknown35 = 0x35, 15 | SimpleHID = 0x3F, 16 | USBHID = 0x81 17 | #pragma warning restore IDE0055 18 | } 19 | -------------------------------------------------------------------------------- /BetterJoy/Network/Server/ControllerEnums.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Network.Server; 2 | 3 | public enum ControllerState : byte 4 | { 5 | Disconnected = 0x00, 6 | Connected = 0x02 7 | }; 8 | 9 | public enum ControllerConnection : byte 10 | { 11 | None = 0x00, 12 | USB = 0x01, 13 | Bluetooth = 0x02 14 | }; 15 | 16 | public enum ControllerModel : byte 17 | { 18 | None = 0x00, 19 | DS3 = 0x01, 20 | DS4 = 0x02, 21 | Generic = 0x03 22 | } 23 | 24 | public enum ControllerBattery : byte 25 | { 26 | Empty = 0x00, 27 | Critical = 0x01, 28 | Low = 0x02, 29 | Medium = 0x03, 30 | High = 0x04, 31 | Full = 0x05, 32 | Charging = 0xEE, 33 | Charged = 0xEF 34 | }; 35 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/Data/ThreeAxisShort.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Hardware.Data; 2 | 3 | public record struct ThreeAxisShort(short X, short Y, short Z) 4 | { 5 | public static readonly ThreeAxisShort Zero = new(0, 0, 0); 6 | public readonly bool Invalid => X == -1 || Y == -1 || Z == -1; 7 | 8 | public static ThreeAxisShort operator -(ThreeAxisShort left, ThreeAxisShort right) 9 | { 10 | return new ThreeAxisShort( 11 | (short)(left.X - right.X), 12 | (short)(left.Y - right.Y), 13 | (short)(left.Z - right.Z) 14 | ); 15 | } 16 | 17 | public override readonly string ToString() 18 | { 19 | return $"X: {X}, Y: {Y}, Z: {Z}"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BetterJoy/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | x64 9 | ..\build\ 10 | FileSystem 11 | net9.0-windows 12 | win-x64 13 | false 14 | true 15 | false 16 | true 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report---problems.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report / Problems 3 | about: PLEASE USE THE ISSUE SEARCH FUNCTION FIRST BEFORE MAKING A NEW ISSUE 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Controller (please complete the following information):** 17 | - Type: [e.g. Pro/Split Joycons] 18 | - Connection: [e.g. USB/BT] 19 | - Thirdparty : [e.g. Nintendo/Custom model] 20 | 21 | **Logs** 22 | Join your LogDebug.txt file here. It is located in the BetterJoy directory. It is cleared each time you launch BetterJoy. 23 | To have more informations, please set "DebugType" to all in the config. -------------------------------------------------------------------------------- /BetterJoy/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace BetterJoy; 5 | 6 | internal static partial class NativeMethods 7 | { 8 | // SetDefaultDllDirectories flag parameter 9 | public const uint LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200; 10 | public const uint LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400; 11 | public const uint LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800; 12 | public const uint LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000; 13 | 14 | [LibraryImport("kernel32.dll", SetLastError = true)] 15 | [return: MarshalAs(UnmanagedType.Bool)] 16 | public static partial bool SetDefaultDllDirectories(uint directoryFlags); 17 | 18 | [LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] 19 | public static partial IntPtr AddDllDirectory(string directory); 20 | } 21 | -------------------------------------------------------------------------------- /BetterJoy/Memory/ArrayPoolHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BetterJoy.Memory; 4 | 5 | public sealed partial class ArrayPoolHelper 6 | { 7 | private ArrayPoolHelper() { } 8 | 9 | public static ArrayPoolHelper Shared { get; } = new(); 10 | public static int MaxBufferSize => Array.MaxLength; 11 | 12 | public IArrayOwner Rent(int length) 13 | { 14 | return RentImpl(length); 15 | } 16 | 17 | public IArrayOwner RentCleared(int length) 18 | { 19 | var buffer = RentImpl(length); 20 | Array.Clear(buffer.Array); 21 | 22 | return buffer; 23 | } 24 | 25 | private static ArrayOwner RentImpl(int length) 26 | { 27 | if (length > MaxBufferSize) 28 | { 29 | throw new ArgumentOutOfRangeException(nameof(length), length, null); 30 | } 31 | 32 | return new ArrayOwner(length); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BetterJoy/Network/MacAddress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace BetterJoy.Network; 5 | 6 | [InlineArray(6)] 7 | public struct MacAddress 8 | { 9 | private byte _firstElement; 10 | 11 | public override readonly string ToString() 12 | { 13 | ReadOnlySpan macBytes = this; 14 | 15 | // Each byte is represented by 2 hex characters 16 | Span hexChars = stackalloc char[macBytes.Length * 2]; 17 | 18 | for (int i = 0; i < macBytes.Length; i++) 19 | { 20 | byte b = macBytes[i]; 21 | hexChars[i * 2] = ToHexChar((byte)(b >> 4)); // High nibble 22 | hexChars[i * 2 + 1] = ToHexChar((byte)(b & 0xF)); // Low nibble 23 | } 24 | 25 | return new string(hexChars); 26 | } 27 | 28 | private static char ToHexChar(byte value) 29 | { 30 | return (char)(value < 10 ? '0' + value : 'a' + value - 10); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/SubCommand/SPIPage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BetterJoy.Hardware.SubCommand; 4 | 5 | public class SPIPage 6 | { 7 | private const byte HighAddressIndex = 1; 8 | private const byte LowAddressIndex = 0; 9 | private const byte PageSizeIndex = 4; 10 | private readonly byte[] _raw; 11 | 12 | public byte HighAddress => _raw[HighAddressIndex]; 13 | public byte LowAddress => _raw[LowAddressIndex]; 14 | public byte PageSize => _raw[PageSizeIndex]; 15 | 16 | private SPIPage(byte high, byte low, byte len) 17 | { 18 | _raw = [low, high, 0x00, 0x00, len]; 19 | } 20 | 21 | public static implicit operator ReadOnlySpan(SPIPage page) => page._raw; 22 | 23 | // Calibration pages 24 | public static readonly SPIPage UserStickCalibration = new(0x80, 0x10, 0x16); 25 | public static readonly SPIPage FactoryStickCalibration = new(0x60, 0x3D, 0x12); 26 | public static readonly SPIPage StickDeadZone = new(0x60, 0x89, 0x15); 27 | public static readonly SPIPage UserMotionCalibration = new(0x80, 0x26, 0x1A); 28 | public static readonly SPIPage FactoryMotionCalibration = new(0x60, 0x20, 0x18); 29 | } 30 | -------------------------------------------------------------------------------- /BetterJoy/Network/Server/UdpUtils.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Controller; 2 | using BetterJoy.Hardware.SubCommand; 3 | using System; 4 | using System.IO.Hashing; 5 | 6 | namespace BetterJoy.Network.Server; 7 | public static class UdpUtils 8 | { 9 | public static ControllerBattery GetBattery(Joycon controller) 10 | { 11 | if (controller.Charging) 12 | { 13 | return ControllerBattery.Charging; 14 | } 15 | 16 | return controller.Battery switch 17 | { 18 | BatteryLevel.Critical => ControllerBattery.Critical, 19 | BatteryLevel.Low => ControllerBattery.Low, 20 | BatteryLevel.Medium => ControllerBattery.Medium, 21 | BatteryLevel.Full => ControllerBattery.Full, 22 | _ => ControllerBattery.Empty, 23 | }; 24 | } 25 | 26 | public static int CalculateCrc32(ReadOnlySpan data, Span crc) 27 | { 28 | return Crc32.Hash(data, crc); 29 | } 30 | 31 | public static uint CalculateCrc32(ReadOnlySpan data) 32 | { 33 | Span crc = stackalloc byte[4]; 34 | Crc32.Hash(data, crc); 35 | return BitConverter.ToUInt32(crc); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/Native/DeviceInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace BetterJoy.HIDApi.Native; 5 | 6 | public enum BusType 7 | { 8 | Unknown = 0x00, 9 | USB = 0x01, 10 | Bluetooth = 0x02, 11 | I2C = 0x03, 12 | SPI = 0x04 13 | } 14 | 15 | [StructLayout(LayoutKind.Sequential)] 16 | public struct DeviceInfo 17 | { 18 | [MarshalAs(UnmanagedType.LPStr)] public string Path; 19 | public ushort VendorId; 20 | public ushort ProductId; 21 | [MarshalAs(UnmanagedType.LPWStr)] public string SerialNumber; 22 | public ushort ReleaseNumber; 23 | [MarshalAs(UnmanagedType.LPWStr)] public string ManufacturerString; 24 | [MarshalAs(UnmanagedType.LPWStr)] public string ProductString; 25 | public ushort UsagePage; 26 | public ushort Usage; 27 | public int InterfaceNumber; 28 | private readonly IntPtr _next; 29 | public BusType BusType; // >= 0.13.0 30 | 31 | public readonly DeviceInfo? Next 32 | { 33 | get 34 | { 35 | if (_next == IntPtr.Zero) 36 | { 37 | return null; 38 | } 39 | 40 | return Marshal.PtrToStructure(_next); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/StringUtils.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Memory; 2 | using System; 3 | using System.Text; 4 | 5 | namespace BetterJoy.HIDApi; 6 | 7 | internal static class StringUtils 8 | { 9 | private const int MaxStringLength = 200; // Value from MAX_DEVICE_ID_LEN in cfgmgr32.h, it includes the null character 10 | private const int UnicodeBufferSize = MaxStringLength * sizeof(ushort); 11 | 12 | public delegate int GetStringDelegate(Span b, nuint length); 13 | 14 | public static string GetUnicodeString(GetStringDelegate getStringFunc) 15 | { 16 | using var bufferOwner = ArrayPoolHelper.Shared.Rent(UnicodeBufferSize); 17 | var buffer = bufferOwner.Span; 18 | 19 | var ret = getStringFunc(buffer, MaxStringLength); 20 | if (ret < 0) 21 | { 22 | return string.Empty; 23 | } 24 | 25 | var nullTerminatorPosition = FindNullTerminator(buffer); 26 | return Encoding.Unicode.GetString(buffer[..nullTerminatorPosition]); 27 | } 28 | 29 | private static int FindNullTerminator(ReadOnlySpan buffer) 30 | { 31 | for (int i = 0; i < buffer.Length - 1; i += 2) 32 | { 33 | if (buffer[i] == 0 && buffer[i + 1] == 0) 34 | { 35 | return i; 36 | } 37 | } 38 | 39 | return buffer.Length; // No terminator found 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/Calibration/StickRangeCalibration.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Config; 2 | using BetterJoy.Hardware.Data; 3 | using System; 4 | 5 | namespace BetterJoy.Hardware.Calibration; 6 | 7 | public readonly struct StickRangeCalibration 8 | { 9 | private readonly float _value; 10 | 11 | public StickRangeCalibration() 12 | { 13 | _value = 0; 14 | } 15 | 16 | public StickRangeCalibration(float value) 17 | { 18 | _value = value; 19 | } 20 | 21 | public StickRangeCalibration(Span raw) 22 | { 23 | if (raw.Length != 2) 24 | { 25 | throw new ArgumentException($"{nameof(StickRangeCalibration)} expects 2 bytes, got {raw.Length}."); 26 | } 27 | 28 | _value = CalculateRange(BitWrangler.Upper3NibblesLittleEndian(raw[0], raw[1])); 29 | } 30 | 31 | public static StickRangeCalibration FromConfigRight(ControllerConfig config) 32 | { 33 | return new StickRangeCalibration(config.StickRightRange); 34 | } 35 | 36 | public static StickRangeCalibration FromConfigLeft(ControllerConfig config) 37 | { 38 | return new StickRangeCalibration(config.StickLeftRange); 39 | } 40 | 41 | public static implicit operator float(StickRangeCalibration range) => range._value; 42 | 43 | private static float CalculateRange(ushort value) 44 | { 45 | return (float)value / 0xFFF; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/SubCommand/SubCommandOperation.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Hardware.SubCommand; 2 | 3 | public enum SubCommandOperation : byte 4 | { 5 | #pragma warning disable IDE0055 // Disable formatting 6 | GetControllerState = 0x00, 7 | ManualBluetoothPairing = 0x01, 8 | RequestDeviceInfo = 0x02, 9 | SetReportMode = 0x03, 10 | GetTriggerButtonsElapsedTime = 0x04, 11 | GetPageListState = 0x05, 12 | SetHCIState = 0x06, 13 | ErasePairingInfo = 0x07, 14 | EnableLowPowerMode = 0x08, 15 | 16 | SPIFlashRead = 0x10, 17 | SPIFlashWrite = 0x11, 18 | 19 | ResetMCU = 0x20, 20 | SetMCUConfig = 0x21, 21 | SetMCUState = 0x22, 22 | 23 | SetPlayerLights = 0x30, 24 | GetPlayerLights = 0x31, 25 | SetHomeLight = 0x38, 26 | 27 | EnableIMU = 0x40, 28 | SetIMUSensitivity = 0x41, 29 | WriteIMURegister = 0x42, 30 | ReadIMURegister = 0x43, 31 | 32 | EnableVibration = 0x48, 33 | 34 | GetRegulatedVoltage = 0x50, 35 | Unknown 36 | #pragma warning restore IDE0055 37 | } 38 | -------------------------------------------------------------------------------- /BetterJoy/Memory/ArrayOwner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Threading; 4 | 5 | namespace BetterJoy.Memory; 6 | 7 | public sealed partial class ArrayPoolHelper 8 | { 9 | public interface IArrayOwner : IDisposable 10 | { 11 | int Length { get; } 12 | U[] Array { get; } 13 | Span Span { get; } 14 | ReadOnlyMemory ReadOnlyMemory { get; } 15 | } 16 | 17 | private sealed class ArrayOwner : IArrayOwner 18 | { 19 | private readonly int _length; 20 | private U[]? _array; 21 | 22 | public ArrayOwner(int length) 23 | { 24 | _array = ArrayPool.Shared.Rent(length); 25 | _length = length; 26 | } 27 | 28 | public int Length => _length; 29 | public U[] Array => _array ?? throw new ObjectDisposedException(nameof(ArrayOwner)); // carefull, length allocated to the array might be bigger than demanded, prefer to use Span instead 30 | public Span Span => _array.AsSpan(0, _length); 31 | public ReadOnlyMemory ReadOnlyMemory => new(_array, 0, Length); 32 | 33 | public void Dispose() 34 | { 35 | var array = Interlocked.Exchange(ref _array, null); 36 | 37 | if (array != null) 38 | { 39 | ArrayPool.Shared.Return(array); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /BetterJoy/Config/ProgramConfig.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Logging; 2 | using System.Net; 3 | 4 | namespace BetterJoy.Config; 5 | 6 | public class ProgramConfig : Config 7 | { 8 | public bool UseHIDHide = true; 9 | public bool HIDHideAlwaysOn = false; 10 | public bool PurgeWhitelist = false; 11 | public bool PurgeAffectedDevices = false; 12 | public bool MotionServer = true; 13 | public IPAddress IP = IPAddress.Loopback; 14 | public int Port = 26760; 15 | 16 | public ProgramConfig(ILogger logger) : base(logger) { } 17 | 18 | public ProgramConfig(ProgramConfig config) : base(config._logger) 19 | { 20 | UseHIDHide = config.UseHIDHide; 21 | HIDHideAlwaysOn = config.HIDHideAlwaysOn; 22 | PurgeWhitelist = config.PurgeWhitelist; 23 | PurgeAffectedDevices = config.PurgeAffectedDevices; 24 | MotionServer = config.MotionServer; 25 | IP = config.IP; 26 | Port = config.Port; 27 | } 28 | 29 | public override void Update() 30 | { 31 | TryUpdateSetting("UseHidHide", ref UseHIDHide); 32 | TryUpdateSetting("HIDHideAlwaysOn", ref HIDHideAlwaysOn); 33 | TryUpdateSetting("PurgeWhitelist", ref PurgeWhitelist); 34 | TryUpdateSetting("PurgeAffectedDevices", ref PurgeAffectedDevices); 35 | TryUpdateSetting("MotionServer", ref MotionServer); 36 | TryUpdateSetting("IP", ref IP); 37 | TryUpdateSetting("Port", ref Port); 38 | } 39 | 40 | public override ProgramConfig Clone() 41 | { 42 | return new ProgramConfig(this); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BetterJoy/Exceptions/ExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Nefarius.ViGEm.Client.Exceptions; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Configuration; 5 | 6 | namespace BetterJoy.Exceptions; 7 | 8 | public static class ExceptionExtensions 9 | { 10 | public static string Display(this Exception e, bool stackTrace = false) 11 | { 12 | var message = "("; 13 | 14 | #pragma warning disable IDE0066 // Convert switch statement to expression 15 | switch (e) 16 | { 17 | case Win32Exception win32Ex: 18 | message += $"0x{win32Ex.NativeErrorCode:X} - {win32Ex.Message}"; 19 | break; 20 | case BadImageFormatException: 21 | case ConfigurationErrorsException: 22 | case DeviceNullHandleException: 23 | case DeviceComFailedException: 24 | case DeviceQueryFailedException: 25 | case VigemBusNotFoundException: 26 | case VigemBusAccessFailedException: 27 | case VigemBusVersionMismatchException: 28 | case VigemAllocFailedException: 29 | case VigemAlreadyConnectedException: 30 | message += $"{e.Message}"; 31 | break; 32 | default: 33 | message += $"{e.GetType()} - {e.Message}"; 34 | break; 35 | } 36 | #pragma warning restore IDE0066 37 | 38 | message += ")"; 39 | 40 | if (stackTrace) 41 | { 42 | message += $"{Environment.NewLine}{e.StackTrace}"; 43 | } 44 | 45 | return message; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/Calibration/StickDeadZoneCalibration.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Config; 2 | using BetterJoy.Hardware.Data; 3 | using System; 4 | 5 | namespace BetterJoy.Hardware.Calibration; 6 | 7 | public readonly struct StickDeadZoneCalibration 8 | { 9 | private readonly float _value; 10 | 11 | public StickDeadZoneCalibration() 12 | { 13 | _value = 0; 14 | } 15 | 16 | public StickDeadZoneCalibration(float value) 17 | { 18 | _value = value; 19 | } 20 | 21 | public StickDeadZoneCalibration(StickLimitsCalibration stickLimitsCalibration, Span raw) 22 | { 23 | if (raw.Length != 2) 24 | { 25 | throw new ArgumentException($"{nameof(StickDeadZoneCalibration)} expects 2 bytes, got {raw.Length}."); 26 | } 27 | 28 | _value = CalculateDeadZone(stickLimitsCalibration, BitWrangler.Lower3NibblesLittleEndian(raw[0], raw[1])); 29 | } 30 | 31 | public static StickDeadZoneCalibration FromConfigRight(ControllerConfig config) 32 | { 33 | return new StickDeadZoneCalibration(config.StickRightDeadzone); 34 | } 35 | 36 | public static StickDeadZoneCalibration FromConfigLeft(ControllerConfig config) 37 | { 38 | return new StickDeadZoneCalibration(config.StickLeftDeadzone); 39 | } 40 | 41 | public static implicit operator float(StickDeadZoneCalibration deadZone) => deadZone._value; 42 | 43 | private static float CalculateDeadZone(StickLimitsCalibration stickLimitsCalibration, ushort deadZone) 44 | { 45 | return 2.0f * deadZone / Math.Max(stickLimitsCalibration.XMax + stickLimitsCalibration.XMin, stickLimitsCalibration.YMax + stickLimitsCalibration.YMin); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/SubCommand/IncomingPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Text; 4 | 5 | namespace BetterJoy.Hardware.SubCommand; 6 | 7 | public abstract class IncomingPacket 8 | { 9 | protected const int ResponseCodeIndex = 0; 10 | protected const int TimerIndex = 1; 11 | protected const int BatteryAndConnectionIndex = 2; 12 | protected const int ButtonStateStartIndex = 3; 13 | protected const int StickStateStartIndex = 6; 14 | protected const int RumbleStateIndex = 12; 15 | 16 | private const int USBPacketSize = 64; 17 | 18 | [InlineArray(USBPacketSize)] 19 | protected struct ResponseBuffer 20 | { 21 | private byte _firstElement; 22 | } 23 | 24 | private readonly ResponseBuffer _raw; 25 | private readonly int _length; 26 | protected ReadOnlySpan Raw => _raw[.._length]; 27 | 28 | protected IncomingPacket(ReadOnlySpan buffer) 29 | { 30 | if (buffer.Length < RumbleStateIndex) 31 | { 32 | throw new ArgumentException($"Provided length cannot be less than {RumbleStateIndex}."); 33 | } 34 | 35 | _length = buffer.Length; 36 | 37 | buffer.CopyTo(_raw); 38 | } 39 | 40 | public byte MessageCode => Raw[ResponseCodeIndex]; 41 | 42 | public int Length => Raw.Length; 43 | 44 | public override string ToString() 45 | { 46 | var output = new StringBuilder(); 47 | 48 | output.Append($" Message Code: {MessageCode:X2}"); 49 | output.Append($" Data: "); 50 | 51 | foreach (var dataByte in Raw[TimerIndex..]) 52 | { 53 | output.Append($" {dataByte:X2}"); 54 | } 55 | 56 | return output.ToString(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /BetterJoy/Network/Server/UdpControllerReport.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Controller; 2 | using BetterJoy.Controller.Mapping; 3 | using System; 4 | 5 | namespace BetterJoy.Network.Server; 6 | 7 | public class UdpControllerReport 8 | { 9 | public const ulong DefaultDeltaPackets = 0; 10 | public ulong Timestamp; 11 | public int PacketCounter; 12 | public ulong DeltaPackets; 13 | public int PadId; 14 | public MacAddress MacAddress; 15 | public ControllerConnection ConnectionType; 16 | public ControllerBattery Battery; 17 | 18 | public OutputControllerDualShock4InputState Input; 19 | 20 | public readonly Motion[] Motion = new Motion[3]; 21 | 22 | public UdpControllerReport(Joycon controller, ulong deltaPackets = DefaultDeltaPackets) 23 | { 24 | Timestamp = controller.Timestamp; 25 | PacketCounter = controller.PacketCounter; 26 | DeltaPackets = deltaPackets; 27 | PadId = controller.PadId; 28 | MacAddress = controller.MacAddress; 29 | ConnectionType = controller.IsUSB ? ControllerConnection.USB : ControllerConnection.Bluetooth; 30 | Battery = UdpUtils.GetBattery(controller); 31 | } 32 | 33 | public void AddInput(Joycon controller) 34 | { 35 | Input = controller.MapToDualShock4Input(); 36 | 37 | // Invert Y axis 38 | Input.ThumbLeftY = (byte)(byte.MaxValue - Input.ThumbLeftY); 39 | Input.ThumbRightY = (byte)(byte.MaxValue - Input.ThumbRightY); 40 | } 41 | 42 | public void AddMotion(Joycon controller, int packetNumber) 43 | { 44 | Motion[packetNumber] = controller.GetMotion(); 45 | } 46 | 47 | public void ClearMotionAndDeltaPackets() 48 | { 49 | Array.Clear(Motion); 50 | DeltaPackets = DefaultDeltaPackets; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PUBLISH.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set "MSBUILD_PATH=%ProgramFiles%\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" 3 | set "DOTNET_CLI_TELEMETRY_OPTOUT=1" 4 | set "options=--nologo -p:PublishProfile=Properties\PublishProfiles\FolderProfile.pubxml" 5 | 6 | echo Restore nuget packages... 7 | nuget restore 8 | IF %ERRORLEVEL% NEQ 0 goto :ERROR 9 | 10 | echo. 11 | echo Build hidapi 32 bits... 12 | "%MSBUILD_PATH%" "thirdparty\hidapi\windows\hidapi.vcxproj" -p:Configuration=Release -p:Platform=Win32 -t:Build 13 | if %ERRORLEVEL% NEQ 0 goto :ERROR 14 | 15 | echo. 16 | echo Build hidapi 64 bits... 17 | "%MSBUILD_PATH%" "thirdparty\hidapi\windows\hidapi.vcxproj" -p:Configuration=Release -p:Platform=x64 -t:Build 18 | if %ERRORLEVEL% NEQ 0 goto :ERROR 19 | 20 | echo. 21 | echo Build ViGEmClient 32 bits... 22 | "%MSBUILD_PATH%" "thirdparty\ViGEm.NET\ViGEmClientNative\src\ViGEmClient.vcxproj" -p:Configuration=Release_DLL -p:Platform=Win32 -t:Build 23 | if %ERRORLEVEL% NEQ 0 goto :ERROR 24 | 25 | echo. 26 | echo Build ViGEmClient 64 bits... 27 | "%MSBUILD_PATH%" "thirdparty\ViGEm.NET\ViGEmClientNative\src\ViGEmClient.vcxproj" -p:Configuration=Release_DLL -p:Platform=x64 -t:Build 28 | if %ERRORLEVEL% NEQ 0 goto :ERROR 29 | 30 | echo. 31 | echo Build ViGEm.NET... 32 | "%MSBUILD_PATH%" "thirdparty\ViGEm.NET\ViGEmClient\ViGEmClient.NET.csproj" -p:Configuration=Release -p:Platform=x64 -t:Build 33 | if %ERRORLEVEL% NEQ 0 goto :ERROR 34 | 35 | echo. 36 | echo Build WindowsInput... 37 | "%MSBUILD_PATH%" "thirdparty\WindowsInput\WindowsInput\WindowsInput.csproj" -p:Configuration=Release -p:Platform=x64 -t:Build 38 | if %ERRORLEVEL% NEQ 0 goto :ERROR 39 | 40 | echo. 41 | echo Publish BetterJoy... 42 | dotnet publish BetterJoy %options% 43 | if %ERRORLEVEL% NEQ 0 goto :ERROR 44 | 45 | echo. 46 | echo Build succeeded! 47 | pause 48 | goto :eof 49 | 50 | :ERROR 51 | echo. 52 | echo Build failed! 53 | pause 54 | 55 | -------------------------------------------------------------------------------- /BetterJoy/Collections/ConcurrentSpinQueue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Threading; 5 | 6 | namespace BetterJoy.Collections; 7 | 8 | public class ConcurrentSpinQueue 9 | { 10 | private readonly int _maxItems; 11 | private readonly Queue _internalQueue; 12 | private SpinLock _lock; 13 | 14 | public ConcurrentSpinQueue(int maxItems) 15 | { 16 | _maxItems = maxItems; 17 | _internalQueue = new Queue(); 18 | _lock = new SpinLock(); 19 | } 20 | 21 | public void Enqueue(T item) 22 | { 23 | LockInternalQueueAndCommand( 24 | queue => 25 | { 26 | if (_internalQueue.Count >= _maxItems) 27 | { 28 | _internalQueue.Dequeue(); 29 | } 30 | _internalQueue.Enqueue(item); 31 | } 32 | ); 33 | } 34 | 35 | public bool TryDequeue([MaybeNullWhen(false)] out T result) 36 | { 37 | var lockTaken = false; 38 | try 39 | { 40 | _lock.Enter(ref lockTaken); 41 | return _internalQueue.TryDequeue(out result); 42 | } 43 | finally 44 | { 45 | if (lockTaken) 46 | { 47 | _lock.Exit(); 48 | } 49 | } 50 | } 51 | 52 | public void Clear() 53 | { 54 | LockInternalQueueAndCommand(queue => queue.Clear()); 55 | } 56 | 57 | private void LockInternalQueueAndCommand(Action> action) 58 | { 59 | var lockTaken = false; 60 | try 61 | { 62 | _lock.Enter(ref lockTaken); 63 | action(_internalQueue); 64 | } 65 | finally 66 | { 67 | if (lockTaken) 68 | { 69 | _lock.Exit(); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/SubCommand/SubCommandReturnPacket.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Hardware.Data; 2 | using System; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Text; 5 | 6 | namespace BetterJoy.Hardware.SubCommand; 7 | 8 | public class SubCommandReturnPacket : IncomingPacket 9 | { 10 | protected const int SubCommandOperationIndex = 14; 11 | protected const int PayloadStartIndex = 15; 12 | public const int MinimumSubcommandReplySize = 20; 13 | 14 | protected const int SubCommandReturnPacketResponseCode = 0x21; 15 | 16 | public static bool TryConstruct( 17 | SubCommandOperation operation, 18 | ReadOnlySpan buffer, 19 | [NotNullWhen(true)] 20 | out SubCommandReturnPacket? packet) 21 | { 22 | try 23 | { 24 | packet = new SubCommandReturnPacket(operation, buffer); 25 | 26 | return true; 27 | } 28 | catch (ArgumentException) 29 | { 30 | packet = null; 31 | 32 | return false; 33 | } 34 | } 35 | 36 | protected SubCommandReturnPacket(SubCommandOperation operation, ReadOnlySpan buffer) : base(buffer) 37 | { 38 | if (!IsValidSubCommandReturnPacket(operation, buffer)) 39 | { 40 | throw new ArgumentException("Provided array is not valid subcommand response for given operation."); 41 | } 42 | } 43 | 44 | private static bool IsValidSubCommandReturnPacket(SubCommandOperation operation, ReadOnlySpan buffer) 45 | { 46 | return buffer.Length >= MinimumSubcommandReplySize && 47 | buffer[ResponseCodeIndex] == SubCommandReturnPacketResponseCode && 48 | buffer[SubCommandOperationIndex] == (byte)operation; 49 | } 50 | 51 | public bool IsSubCommandReply => Raw[ResponseCodeIndex] == SubCommandReturnPacketResponseCode; 52 | 53 | 54 | public SubCommandOperation SubCommandOperation => 55 | BitWrangler.ByteToEnumOrDefault( 56 | BitWrangler.UpperNibble(Raw[SubCommandOperationIndex]), SubCommandOperation.Unknown); 57 | 58 | public ReadOnlySpan Payload => Raw[PayloadStartIndex..]; 59 | 60 | 61 | public override string ToString() 62 | { 63 | var output = new StringBuilder(); 64 | 65 | output.Append($"Subcommand Echo: {(byte)SubCommandOperation:X2} "); 66 | 67 | if (!Payload.IsEmpty) 68 | { 69 | output.Append(" Payload:"); 70 | 71 | foreach (var dataByte in Payload) 72 | { 73 | output.Append($" {dataByte:X2}"); 74 | } 75 | } 76 | 77 | return output.ToString(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /BetterJoy/InputCapture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using WindowsInput.Events.Sources; 4 | 5 | namespace BetterJoy; 6 | 7 | public sealed class InputCapture : IDisposable 8 | { 9 | private static readonly Lazy _instance = new(() => new InputCapture()); 10 | public static InputCapture Global => _instance.Value; 11 | 12 | private readonly IKeyboardEventSource Keyboard; 13 | private readonly IMouseEventSource Mouse; 14 | 15 | private int _nbKeyboardEvents = 0; 16 | private int _nbMouseEvents = 0; 17 | 18 | private bool _disposed = false; 19 | 20 | private InputCapture() 21 | { 22 | Keyboard = WindowsInput.Capture.Global.KeyboardAsync(false); 23 | Mouse = WindowsInput.Capture.Global.MouseAsync(false, false); 24 | } 25 | 26 | public void RegisterEvent(EventHandler> ev) 27 | { 28 | Keyboard.KeyEvent += ev; 29 | KeyboardEventCountChange(true); 30 | } 31 | 32 | public void UnregisterEvent(EventHandler> ev) 33 | { 34 | Keyboard.KeyEvent -= ev; 35 | KeyboardEventCountChange(false); 36 | } 37 | 38 | public void RegisterEvent(EventHandler> ev) 39 | { 40 | Mouse.MouseEvent += ev; 41 | MouseEventCountChange(true); 42 | } 43 | 44 | public void UnregisterEvent(EventHandler> ev) 45 | { 46 | Mouse.MouseEvent -= ev; 47 | MouseEventCountChange(false); 48 | } 49 | 50 | private void KeyboardEventCountChange(bool newEvent) 51 | { 52 | int count = newEvent ? Interlocked.Increment(ref _nbKeyboardEvents) : Interlocked.Decrement(ref _nbKeyboardEvents); 53 | 54 | // The property calls invoke, so only do it if necessary 55 | if (count == 0) 56 | { 57 | Keyboard.Enabled = false; 58 | } 59 | else if (count == 1) 60 | { 61 | Keyboard.Enabled = true; 62 | } 63 | } 64 | 65 | private void MouseEventCountChange(bool newEvent) 66 | { 67 | int count = newEvent ? Interlocked.Increment(ref _nbMouseEvents) : Interlocked.Decrement(ref _nbMouseEvents); 68 | 69 | // The property calls invoke, so only do it if necessary 70 | if (count == 0) 71 | { 72 | Mouse.Enabled = false; 73 | } 74 | else if (count == 1) 75 | { 76 | Mouse.Enabled = true; 77 | } 78 | } 79 | 80 | public void Dispose() 81 | { 82 | if (_disposed) 83 | { 84 | return; 85 | } 86 | 87 | Keyboard.Dispose(); 88 | Mouse.Dispose(); 89 | _disposed = true; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /BetterJoy/Config/Config.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Logging; 2 | using System; 3 | using System.Configuration; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace BetterJoy.Config; 8 | 9 | public abstract class Config 10 | { 11 | protected readonly ILogger? _logger; 12 | public bool ShowErrors = true; 13 | 14 | protected Config(ILogger? logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | protected Config(Config config) : this(config._logger) { } 20 | public abstract void Update(); 21 | public abstract Config Clone(); 22 | 23 | private void ParseAs(string value, ref T setting) 24 | { 25 | switch (setting) 26 | { 27 | case Array: 28 | ParseArrayAs(value, (dynamic)setting); 29 | break; 30 | case Enum: 31 | setting = (T)Enum.Parse(typeof(T), value, true); 32 | break; 33 | case IConvertible: 34 | setting = (T)Convert.ChangeType(value, typeof(T)); 35 | break; 36 | default: 37 | { 38 | var method = typeof(T).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, [typeof(string)]); 39 | setting = (T)method!.Invoke(null, [value])!; 40 | break; 41 | } 42 | } 43 | } 44 | 45 | private void ParseArrayAs(string value, T[] settings) 46 | { 47 | var tokens = value.Split(',', StringSplitOptions.TrimEntries); 48 | 49 | for (int i = 0; i < settings.Length; i++) 50 | { 51 | var currentToken = i < tokens.Length ? tokens[i] : tokens[^1]; 52 | ParseAs(currentToken, ref settings[i]); 53 | } 54 | } 55 | 56 | protected void TryUpdateSetting(string key, ref T setting) 57 | { 58 | var defaultValue = setting; 59 | var value = ConfigurationManager.AppSettings[key]; 60 | 61 | if (value != null) 62 | { 63 | try 64 | { 65 | ParseAs(value, ref setting); 66 | 67 | return; 68 | } 69 | catch (FormatException) { } 70 | catch (InvalidCastException) { } 71 | catch (ArgumentException) { } 72 | } 73 | 74 | setting = defaultValue; 75 | 76 | if (ShowErrors) 77 | { 78 | string defaultValueTxt; 79 | if (defaultValue is Array array) 80 | { 81 | defaultValueTxt = $"{string.Join(",", array.Cast())}"; 82 | } 83 | else 84 | { 85 | defaultValueTxt = $"{defaultValue}"; 86 | } 87 | 88 | _logger?.Log($"Invalid value \"{value}\" for setting {key}! Using safe value \"{defaultValueTxt}\".", LogLevel.Warning); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/Data/BitWrangler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace BetterJoy.Hardware.Data; 5 | 6 | public static class BitWrangler 7 | { 8 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 9 | public static byte LowerNibble(byte b) => (byte)(b & 0x0F); 10 | 11 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 12 | public static byte UpperNibble(byte b) => (byte)(b >> 4); 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public static byte LowerToUpper(byte b) => (byte)(b << 4); 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public static byte EncodeNibblesAsByteLittleEndian(byte low, byte high) 19 | => (byte)(LowerToUpper(high) | LowerNibble(low)); 20 | 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | public static ushort EncodeBytesAsWordLittleEndian(byte low, byte high) 23 | => (ushort)((high << 8) | low); 24 | 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public static short EncodeBytesAsWordLittleEndianSigned(byte low, byte high) 27 | => (short)((high << 8) | low); 28 | 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public static ushort Lower3Nibbles(ushort word) 31 | => (ushort)(word & 0x0FFF); 32 | 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public static ushort Upper3Nibbles(ushort word) 35 | => (ushort)(word >> 4); 36 | 37 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 38 | public static ushort Lower3NibblesLittleEndian(byte low, byte high) 39 | => Lower3Nibbles(EncodeBytesAsWordLittleEndian(low, high)); 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public static ushort Upper3NibblesLittleEndian(byte low, byte high) 43 | => Upper3Nibbles(EncodeBytesAsWordLittleEndian(low, high)); 44 | 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | public static ushort InvertWord(ushort word) 47 | => (ushort)(ushort.MaxValue - word); 48 | 49 | //WARNING: Do not use with enums that are not backed by byte values 50 | public static TEnum ByteToEnumOrDefault(byte value, TEnum defaultValue, byte mask) 51 | where TEnum : struct, Enum 52 | { 53 | return ByteToEnumOrDefault((byte)(value & mask), defaultValue); 54 | } 55 | 56 | //WARNING: Do not use with enums that are not backed by byte values 57 | public static TEnum ByteToEnumOrDefault(byte value, TEnum defaultValue) 58 | where TEnum : struct, Enum 59 | { 60 | 61 | #if DEBUG 62 | if (typeof(byte) != Enum.GetUnderlyingType(typeof(TEnum))) 63 | { 64 | throw new ArgumentException("Invalid underlying enum type : byte expected"); 65 | } 66 | #endif 67 | 68 | return Enum.IsDefined(typeof(TEnum), value) 69 | ? Unsafe.As(ref value) 70 | : defaultValue; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /BetterJoy/Properties/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 55 | 56 | 70 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/Calibration/StickLimitsCalibration.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Hardware.Data; 2 | using System; 3 | 4 | namespace BetterJoy.Hardware.Calibration; 5 | 6 | public class StickLimitsCalibration 7 | { 8 | private static readonly ushort[] _defaultCalibration = [2048, 2048, 2048, 2048, 2048, 2048]; // Default stick calibration 9 | public ushort XMax { get; private set; } 10 | public ushort YMax { get; private set; } 11 | public ushort XCenter { get; private set; } 12 | public ushort YCenter { get; private set; } 13 | public ushort XMin { get; private set; } 14 | public ushort YMin { get; private set; } 15 | private bool? _isLeft; 16 | 17 | public StickLimitsCalibration(bool? isLeft = null) 18 | { 19 | InitFromValues(_defaultCalibration, isLeft); 20 | } 21 | 22 | public StickLimitsCalibration(ReadOnlySpan values, bool? isLeft = null) 23 | { 24 | InitFromValues(values, isLeft); 25 | } 26 | 27 | public static StickLimitsCalibration FromRightStickCalibrationBytes(ReadOnlySpan raw) 28 | { 29 | return new StickLimitsCalibration(raw, false); 30 | } 31 | 32 | public static StickLimitsCalibration FromLeftStickCalibrationBytes(ReadOnlySpan raw) 33 | { 34 | return new StickLimitsCalibration(raw, true); 35 | } 36 | 37 | private StickLimitsCalibration(ReadOnlySpan raw, bool isLeft) 38 | { 39 | InitFromBytes(raw, isLeft); 40 | } 41 | 42 | private void InitFromBytes(ReadOnlySpan raw, bool isLeft) 43 | { 44 | if (raw.Length != 9) 45 | { 46 | throw new ArgumentException($"{nameof(StickLimitsCalibration)} expects 9 bytes, got {raw.Length}."); 47 | } 48 | 49 | int offset = isLeft ? 0 : 6; 50 | 51 | InitFromValues([ 52 | BitWrangler.Lower3NibblesLittleEndian(raw[IndexOffsetter(0, offset)], raw[IndexOffsetter(1, offset)]), 53 | BitWrangler.Upper3NibblesLittleEndian(raw[IndexOffsetter(1, offset)], raw[IndexOffsetter(2, offset)]), 54 | BitWrangler.Lower3NibblesLittleEndian(raw[IndexOffsetter(3, offset)], raw[IndexOffsetter(4, offset)]), 55 | BitWrangler.Upper3NibblesLittleEndian(raw[IndexOffsetter(4, offset)], raw[IndexOffsetter(5, offset)]), 56 | BitWrangler.Lower3NibblesLittleEndian(raw[IndexOffsetter(6, offset)], raw[IndexOffsetter(7, offset)]), 57 | BitWrangler.Upper3NibblesLittleEndian(raw[IndexOffsetter(7, offset)], raw[IndexOffsetter(8, offset)]), 58 | ], isLeft); 59 | } 60 | 61 | private static int IndexOffsetter(int index, int offset) 62 | { 63 | return (index + offset) % 9; 64 | } 65 | 66 | private void InitFromValues(ReadOnlySpan values, bool? isLeft) 67 | { 68 | if (values.Length != 6) 69 | { 70 | throw new ArgumentException($"{nameof(StickLimitsCalibration)} expects 6 values, got {values.Length}."); 71 | } 72 | 73 | #pragma warning disable IDE0055 // Disable formatting 74 | _isLeft = isLeft; 75 | XMax = values[0]; 76 | YMax = values[1]; 77 | XCenter = values[2]; 78 | YCenter = values[3]; 79 | XMin = values[4]; 80 | YMin = values[5]; 81 | #pragma warning restore IDE0055 82 | } 83 | 84 | public override string ToString() 85 | { 86 | string name = _isLeft == null ? "S" : _isLeft.Value ? "Left s" : "Right s"; 87 | return $"{name}tick calibration data: (XMin: {XMin:D}, XCenter: {XCenter:D}, XMax: {XMax:D}, YMin: {YMin:D}, YCenter: {YCenter:D}, YMax: {YMax:D})"; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /BetterJoy/Logging/Logger.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Exceptions; 2 | using System; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Channels; 6 | using System.Threading.Tasks; 7 | 8 | namespace BetterJoy.Logging; 9 | 10 | public sealed class Logger : ILogger 11 | { 12 | private const int _logLevelPadding = 9; // length of the longest LogLevel + 2 13 | 14 | private readonly StreamWriter _logWriter; 15 | private bool _disposed = false; 16 | 17 | public event Action? OnMessageLogged; 18 | 19 | private readonly Task _logWriterTask; 20 | private readonly CancellationTokenSource _ctsLogs; 21 | private readonly Channel _logChannel; 22 | private readonly bool _isRunning = false; 23 | 24 | private record LogEntry(string Message, LogLevel Level); 25 | 26 | public Logger(string path) 27 | { 28 | _logWriter = new StreamWriter(path, append: false); 29 | 30 | _logChannel = Channel.CreateUnbounded( 31 | new UnboundedChannelOptions 32 | { 33 | SingleWriter = false, 34 | SingleReader = true, 35 | AllowSynchronousContinuations = false 36 | } 37 | ); 38 | 39 | _ctsLogs = new CancellationTokenSource(); 40 | _logWriterTask = Task.Run( 41 | async () => 42 | { 43 | try 44 | { 45 | await ProcessLogs(_ctsLogs.Token); 46 | } 47 | catch (OperationCanceledException) when (_ctsLogs.IsCancellationRequested) 48 | { 49 | // Nothing to do 50 | } 51 | } 52 | ); 53 | 54 | //Log("Task log writer started.", LogLevel.Debug); 55 | 56 | _isRunning = true; 57 | } 58 | 59 | private async Task ProcessLogs(CancellationToken token) 60 | { 61 | await foreach (var entry in _logChannel.Reader.ReadAllAsync(token)) 62 | { 63 | var levelPadded = $"[{entry.Level}]".ToUpper().PadRight(_logLevelPadding); 64 | var log = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {levelPadded} {entry.Message}"; 65 | await _logWriter.WriteLineAsync(log); 66 | await _logWriter.FlushAsync(CancellationToken.None); 67 | } 68 | } 69 | 70 | public void Log(string message, LogLevel level = LogLevel.Info) 71 | { 72 | ObjectDisposedException.ThrowIf(_disposed, this); 73 | 74 | OnMessageLogged?.Invoke(message, level, null); 75 | LogImpl(message, level); 76 | } 77 | 78 | public void Log(string message, Exception e, LogLevel level = LogLevel.Error) 79 | { 80 | ObjectDisposedException.ThrowIf(_disposed, this); 81 | 82 | OnMessageLogged?.Invoke(message, level, e); 83 | LogImpl($"{message} {e.Display(true)}", level); 84 | } 85 | 86 | private void LogImpl(string message, LogLevel level) 87 | { 88 | var log = new LogEntry(Message: message, Level: level); 89 | 90 | while (!_logChannel.Writer.TryWrite(log)) { } 91 | } 92 | 93 | private void Close() 94 | { 95 | if (!_isRunning) 96 | { 97 | return; 98 | } 99 | 100 | _ctsLogs.Cancel(); 101 | _logWriterTask.GetAwaiter().GetResult(); 102 | 103 | _logWriter.Close(); 104 | } 105 | 106 | public void Dispose() 107 | { 108 | if (_disposed) 109 | { 110 | return; 111 | } 112 | 113 | Close(); 114 | 115 | _logWriter.Dispose(); 116 | _disposed = true; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Khachaturov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --------- 24 | 25 | This software uses HIDAPI: 26 | 27 | HIDAPI - Multi-Platform library for 28 | communication with HID devices. 29 | 30 | Copyright 2009, Alan Ott, Signal 11 Software. 31 | All Rights Reserved. 32 | 33 | This software may be used by anyone for any reason so 34 | long as the copyright notice in the source files 35 | remains intact. 36 | 37 | --------- 38 | 39 | This software uses UDP Server: 40 | 41 | UDP Server - Rajkosto - WTFPL license 42 | 43 | --------- 44 | 45 | This software uses ViGEm: 46 | 47 | Copyright (c) 2018 Benjamin Hoglinger-Stelzer 48 | 49 | Permission is hereby granted, free of charge, to any person obtaining a copy 50 | of this software and associated documentation files (the "Software"), to deal 51 | in the Software without restriction, including without limitation the rights 52 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 53 | copies of the Software, and to permit persons to whom the Software is 54 | furnished to do so, subject to the following conditions: 55 | 56 | The above copyright notice and this permission notice shall be included in all 57 | copies or substantial portions of the Software. 58 | 59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 60 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 61 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 62 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 63 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 64 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 65 | SOFTWARE. 66 | 67 | --------- 68 | 69 | This software uses HidHide: 70 | 71 | Copyright (c) 2020 Eric Korff de Gidts 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy 74 | of this software and associated documentation files (the "Software"), to deal 75 | in the Software without restriction, including without limitation the rights 76 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 77 | copies of the Software, and to permit persons to whom the Software is 78 | furnished to do so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in all 81 | copies or substantial portions of the Software. 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 89 | SOFTWARE. 90 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/Manager.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.HIDApi.Exceptions; 2 | using BetterJoy.HIDApi.Native; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace BetterJoy.HIDApi; 8 | 9 | public delegate void DeviceNotificationReceivedEventHandler(object? sender, DeviceNotificationEventArgs e); 10 | 11 | public static class Manager 12 | { 13 | public static event DeviceNotificationReceivedEventHandler? DeviceNotificationReceived; 14 | 15 | private static int _deviceNotificationsHandle = 0; // A valid callback handle is a positive integer 16 | 17 | public static void Init() 18 | { 19 | int ret = Native.NativeMethods.Init(); 20 | if (ret != 0) 21 | { 22 | throw new HIDApiInitFailedException(GetError()); 23 | } 24 | } 25 | 26 | // It also deregister all callbacks 27 | public static void Exit() 28 | { 29 | // Ignore if there is a returned error, we can't do anything about it 30 | _ = Native.NativeMethods.Exit(); 31 | } 32 | 33 | public static string GetError() 34 | { 35 | var ptr = Native.NativeMethods.Error(IntPtr.Zero); 36 | return Marshal.PtrToStringUni(ptr)!; 37 | } 38 | 39 | public static IEnumerable EnumerateDevices(ushort vendorId, ushort productId) 40 | { 41 | var devicesPtr = Native.NativeMethods.Enumerate(vendorId, productId); 42 | if (devicesPtr == IntPtr.Zero) 43 | { 44 | yield break; 45 | } 46 | 47 | try 48 | { 49 | DeviceInfo? currentDevice = Marshal.PtrToStructure(devicesPtr); 50 | 51 | do 52 | { 53 | var deviceInfo = currentDevice.Value; 54 | SanitizeDeviceInfo(ref deviceInfo); 55 | 56 | yield return deviceInfo; 57 | currentDevice = deviceInfo.Next; 58 | } 59 | while (currentDevice != null); 60 | } 61 | finally 62 | { 63 | Native.NativeMethods.FreeEnumeration(devicesPtr); 64 | } 65 | } 66 | 67 | public static void StartDeviceNotifications() 68 | { 69 | if (_deviceNotificationsHandle != 0) 70 | { 71 | return; 72 | } 73 | 74 | static int notificationCallback(int callbackHandle, DeviceInfo deviceInfo, int events, IntPtr userData) 75 | { 76 | SanitizeDeviceInfo(ref deviceInfo); 77 | DeviceNotificationReceived?.Invoke(null, new DeviceNotificationEventArgs(deviceInfo, (HotplugEvent)events)); 78 | return 0; // keep the callback registered 79 | } 80 | 81 | int ret = Native.NativeMethods.HotplugRegisterCallback( 82 | 0x0, 83 | 0x0, 84 | (int)(HotplugEvent.DeviceArrived | HotplugEvent.DeviceLeft), 85 | (int)HotplugFlag.Enumerate, 86 | notificationCallback, 87 | IntPtr.Zero, 88 | out _deviceNotificationsHandle 89 | ); 90 | 91 | if (ret != 0) 92 | { 93 | throw new HIDApiCallbackFailedException(); 94 | } 95 | } 96 | 97 | public static void StopDeviceNotifications() 98 | { 99 | if (_deviceNotificationsHandle == 0) 100 | { 101 | return; 102 | } 103 | 104 | int ret = Native.NativeMethods.HotplugDeregisterCallback(_deviceNotificationsHandle); 105 | if (ret != 0) 106 | { 107 | throw new HIDApiCallbackFailedException(); 108 | } 109 | 110 | _deviceNotificationsHandle = 0; 111 | } 112 | 113 | private static void SanitizeDeviceInfo(ref DeviceInfo deviceInfo) 114 | { 115 | deviceInfo.ManufacturerString = deviceInfo.ManufacturerString.Trim(); 116 | deviceInfo.ProductString = deviceInfo.ProductString.Trim(); 117 | deviceInfo.SerialNumber = deviceInfo.SerialNumber.Trim(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/SubCommand/SubCommandPacket.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Hardware.Data; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Text; 5 | 6 | namespace BetterJoy.Hardware.SubCommand; 7 | 8 | public class SubCommandPacket 9 | { 10 | private static readonly byte[] _stopRumbleBuf = [0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40]; // Stop rumble 11 | private static readonly byte[] _ignoreRumbleBuf = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]; // Ignore rumble 12 | 13 | private const int PacketStartIndex = 0; 14 | private const int CommandCountIndex = 1; 15 | private const int RumbleContentsStartIndex = 2; 16 | private const int CommandIndex = 10; 17 | private const int ArgumentsStartIndex = 11; 18 | private const int BluetoothPacketSize = 49; 19 | private const int USBPacketSize = 64; 20 | private const int RumbleLength = CommandIndex - RumbleContentsStartIndex; 21 | 22 | private readonly int _packetSize; 23 | private readonly int _argsLength; 24 | private int MaxArgsLength => _packetSize - ArgumentsStartIndex; 25 | 26 | [InlineArray(USBPacketSize)] 27 | private struct CommandBuffer 28 | { 29 | private byte _firstElement; 30 | } 31 | 32 | private readonly CommandBuffer _raw; 33 | 34 | public SubCommandPacket( 35 | SubCommandOperation subCommandOperation, 36 | byte commandCount, 37 | ReadOnlySpan args = default, 38 | ReadOnlySpan rumble = default, 39 | bool useUSBPacketSize = false) 40 | { 41 | _packetSize = useUSBPacketSize ? USBPacketSize : BluetoothPacketSize; 42 | 43 | // Default to stopping the rumble 44 | if (rumble.IsEmpty) 45 | { 46 | rumble = _stopRumbleBuf; 47 | } 48 | else if (rumble.Length != RumbleLength) // Check the rumble length if user provided 49 | { 50 | throw new ArgumentException($@"Rumble span is not correct size. Expected: {RumbleLength} Received: {rumble.Length}", nameof(rumble)); 51 | } 52 | 53 | // Check the args length 54 | if (args.Length > MaxArgsLength) 55 | { 56 | throw new ArgumentException($@"Args span is too large. Expected at most: {MaxArgsLength} Received: {args.Length}", nameof(args)); 57 | } 58 | 59 | _argsLength = args.Length; 60 | _raw[PacketStartIndex] = 0x01; // Always 61 | _raw[CommandCountIndex] = BitWrangler.LowerNibble(commandCount); // Command index only uses 4 bits 62 | _raw[CommandIndex] = (byte)subCommandOperation; 63 | 64 | rumble.CopyTo(_raw[RumbleContentsStartIndex..]); 65 | args.CopyTo(_raw[ArgumentsStartIndex..]); 66 | } 67 | 68 | public static implicit operator ReadOnlySpan(SubCommandPacket subCommand) => subCommand._raw[..subCommand._packetSize]; 69 | 70 | public SubCommandOperation Operation => (SubCommandOperation)_raw[CommandIndex]; 71 | 72 | public ReadOnlySpan Arguments => ((ReadOnlySpan)_raw).Slice(ArgumentsStartIndex, _argsLength); 73 | 74 | public ReadOnlySpan Rumble => ((ReadOnlySpan)_raw).Slice(RumbleContentsStartIndex, RumbleLength); 75 | 76 | public override string ToString() 77 | { 78 | var output = new StringBuilder(); 79 | 80 | output.Append($"Subcommand {(byte)Operation:X2} sent."); 81 | 82 | if (_argsLength > 0) 83 | { 84 | output.Append(" Data:"); 85 | 86 | foreach (var arg in Arguments) 87 | { 88 | output.Append($" {arg:X2}"); 89 | } 90 | } 91 | 92 | if (!Rumble.SequenceEqual(_ignoreRumbleBuf.AsSpan())) 93 | { 94 | output.Append(" Rumble:"); 95 | 96 | if (Rumble.SequenceEqual(_stopRumbleBuf.AsSpan())) 97 | { 98 | output.Append(" "); 99 | } 100 | else 101 | { 102 | foreach (var rumbleVal in Rumble) 103 | { 104 | output.Append($" {rumbleVal:X2}"); 105 | } 106 | } 107 | } 108 | 109 | return output.ToString(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/Native/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace BetterJoy.HIDApi.Native; 5 | 6 | internal static partial class NativeMethods 7 | { 8 | private const string Dll = "hidapi.dll"; 9 | 10 | [LibraryImport(Dll, EntryPoint = "hid_init")] 11 | public static partial int Init(); 12 | 13 | [LibraryImport(Dll, EntryPoint = "hid_exit")] 14 | public static partial int Exit(); 15 | 16 | [LibraryImport(Dll, EntryPoint = "hid_enumerate")] 17 | public static partial IntPtr Enumerate(ushort vendorId, ushort productId); 18 | 19 | [LibraryImport(Dll, EntryPoint = "hid_free_enumeration")] 20 | public static partial void FreeEnumeration(IntPtr phidDeviceInfo); 21 | 22 | [LibraryImport(Dll, EntryPoint = "hid_open")] 23 | public static partial IntPtr Open(ushort vendorId, ushort productId, [MarshalAs(UnmanagedType.LPWStr)] string serialNumber); 24 | 25 | [LibraryImport(Dll, EntryPoint = "hid_open_path")] 26 | public static partial IntPtr OpenPath([MarshalAs(UnmanagedType.LPStr)] string path); 27 | 28 | [LibraryImport(Dll, EntryPoint = "hid_write")] 29 | public static partial int Write(IntPtr device, ref byte data, nuint length); 30 | 31 | [LibraryImport(Dll, EntryPoint = "hid_read_timeout")] 32 | public static partial int ReadTimeout(IntPtr dev, ref byte data, nuint length, int milliseconds); 33 | 34 | [LibraryImport(Dll, EntryPoint = "hid_read")] 35 | public static partial int Read(IntPtr device, ref byte data, nuint length); 36 | 37 | [LibraryImport(Dll, EntryPoint = "hid_set_nonblocking")] 38 | public static partial int SetNonBlocking(IntPtr device, int nonblock); 39 | 40 | [LibraryImport(Dll, EntryPoint = "hid_send_feature_report")] 41 | public static partial int SendFeatureReport(IntPtr device, ref byte data, nuint length); 42 | 43 | [LibraryImport(Dll, EntryPoint = "hid_get_feature_report")] 44 | public static partial int GetFeatureReport(IntPtr device, ref byte data, nuint length); 45 | 46 | [LibraryImport(Dll, EntryPoint = "hid_close")] 47 | public static partial void Close(IntPtr device); 48 | 49 | [LibraryImport(Dll, EntryPoint = "hid_get_manufacturer_string")] 50 | public static partial int GetManufacturerString(IntPtr device, ref byte str, nuint maxlen); 51 | 52 | [LibraryImport(Dll, EntryPoint = "hid_get_product_string")] 53 | public static partial int GetProductString(IntPtr device, ref byte str, nuint maxlen); 54 | 55 | [LibraryImport(Dll, EntryPoint = "hid_get_serial_number_string")] 56 | public static partial int GetSerialNumberString(IntPtr device, ref byte str, nuint maxlen); 57 | 58 | [LibraryImport(Dll, EntryPoint = "hid_get_indexed_string")] 59 | public static partial int GetIndexedString(IntPtr device, int stringIndex, ref byte str, nuint maxlen); 60 | 61 | [LibraryImport(Dll, EntryPoint = "hid_error")] 62 | public static partial IntPtr Error(IntPtr device); 63 | 64 | [LibraryImport(Dll, EntryPoint = "hid_error_code")] 65 | public static partial int ErrorCode(IntPtr device); 66 | 67 | [LibraryImport(Dll, EntryPoint = "hid_winapi_get_container_id")] 68 | public static partial int GetContainerId(IntPtr device, out Guid containerId); 69 | 70 | // Added in my fork of HIDapi at https://github.com/d3xMachina/hidapi (needed for HIDHide to work correctly) 71 | #region HIDAPI_MYFORK 72 | 73 | [LibraryImport(Dll, EntryPoint = "hid_winapi_get_instance_string")] 74 | public static partial int GetInstanceString(IntPtr device, ref byte str, nuint maxlen); 75 | 76 | [LibraryImport(Dll, EntryPoint = "hid_winapi_get_parent_instance_string")] 77 | public static partial int GetParentInstanceString(IntPtr device, ref byte str, nuint maxlen); 78 | 79 | #endregion 80 | 81 | #region HIDAPI_CALLBACK 82 | 83 | [LibraryImport(Dll, EntryPoint = "hid_hotplug_register_callback")] 84 | public static partial int HotplugRegisterCallback( 85 | ushort vendorId, 86 | ushort productId, 87 | int events, 88 | int flags, 89 | [MarshalAs(UnmanagedType.FunctionPtr)] HotplugCallback callback, 90 | IntPtr userData, 91 | out int callbackHandle 92 | ); 93 | 94 | [LibraryImport(Dll, EntryPoint = "hid_hotplug_deregister_callback")] 95 | public static partial int HotplugDeregisterCallback(int callbackHandle); 96 | 97 | #endregion 98 | } 99 | -------------------------------------------------------------------------------- /BetterJoy/Collections/ConcurrentList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | 6 | namespace BetterJoy.Collections; 7 | 8 | /// A thread-safe IEnumerator implementation. 9 | /// https://www.codeproject.com/Articles/56575/Thread-safe-enumeration-in-C 10 | public sealed class SafeEnumerator : IEnumerator 11 | { 12 | private readonly IEnumerator _inner; 13 | private readonly Lock _lock; 14 | 15 | public SafeEnumerator(IEnumerable inner, Lock @lock) 16 | { 17 | _lock = @lock; 18 | 19 | _lock.Enter(); 20 | _inner = inner.GetEnumerator(); 21 | } 22 | 23 | public void Dispose() 24 | { 25 | // called when foreach loop finishes 26 | _lock.Exit(); 27 | } 28 | 29 | public bool MoveNext() 30 | { 31 | return _inner.MoveNext(); 32 | } 33 | 34 | public void Reset() 35 | { 36 | _inner.Reset(); 37 | } 38 | 39 | public T Current => _inner.Current; 40 | 41 | object? IEnumerator.Current => Current; 42 | } 43 | 44 | // https://codereview.stackexchange.com/a/125341 45 | public sealed class ConcurrentList : IList 46 | { 47 | public T this[int index] 48 | { 49 | get { return LockInternalListAndGet(l => l[index]); } 50 | 51 | set { LockInternalListAndCommand(l => l[index] = value); } 52 | } 53 | 54 | public int Count 55 | { 56 | get { return LockInternalListAndQuery(l => l.Count); } 57 | } 58 | 59 | public bool IsReadOnly => false; 60 | 61 | public void Add(T item) 62 | { 63 | LockInternalListAndCommand(l => l.Add(item)); 64 | } 65 | 66 | public void Clear() 67 | { 68 | LockInternalListAndCommand(l => l.Clear()); 69 | } 70 | 71 | public bool Contains(T item) 72 | { 73 | return LockInternalListAndQuery(l => l.Contains(item)); 74 | } 75 | 76 | public void CopyTo(T[] array, int arrayIndex) 77 | { 78 | LockInternalListAndCommand(l => l.CopyTo(array, arrayIndex)); 79 | } 80 | 81 | public int IndexOf(T item) 82 | { 83 | return LockInternalListAndQuery(l => l.IndexOf(item)); 84 | } 85 | 86 | public void Insert(int index, T item) 87 | { 88 | LockInternalListAndCommand(l => l.Insert(index, item)); 89 | } 90 | 91 | public bool Remove(T item) 92 | { 93 | return LockInternalListAndQuery(l => l.Remove(item)); 94 | } 95 | 96 | public void RemoveAt(int index) 97 | { 98 | LockInternalListAndCommand(l => l.RemoveAt(index)); 99 | } 100 | 101 | IEnumerator IEnumerable.GetEnumerator() 102 | { 103 | return LockInternalAndEnumerate(); 104 | } 105 | 106 | IEnumerator IEnumerable.GetEnumerator() 107 | { 108 | return LockInternalAndEnumerate(); 109 | } 110 | 111 | public void Set(IList list) 112 | { 113 | lock (_lock) 114 | { 115 | _internalList.Clear(); 116 | foreach (var item in list) 117 | { 118 | _internalList.Add(item); 119 | } 120 | } 121 | } 122 | 123 | #region Fields 124 | 125 | private readonly List _internalList; 126 | private readonly Lock _lock = new(); 127 | 128 | #endregion 129 | 130 | #region ctor 131 | 132 | public ConcurrentList() 133 | { 134 | _internalList = []; 135 | } 136 | 137 | public ConcurrentList(int capacity) 138 | { 139 | _internalList = new List(capacity); 140 | } 141 | 142 | public ConcurrentList(IEnumerable list) 143 | { 144 | _internalList = [.. list]; 145 | } 146 | 147 | #endregion 148 | 149 | #region Utilities 150 | 151 | private void LockInternalListAndCommand(Action> action) 152 | { 153 | lock (_lock) 154 | { 155 | action(_internalList); 156 | } 157 | } 158 | 159 | private T LockInternalListAndGet(Func, T> func) 160 | { 161 | lock (_lock) 162 | { 163 | return func(_internalList); 164 | } 165 | } 166 | 167 | private TObject LockInternalListAndQuery(Func, TObject> query) 168 | { 169 | lock (_lock) 170 | { 171 | return query(_internalList); 172 | } 173 | } 174 | 175 | private SafeEnumerator LockInternalAndEnumerate() 176 | { 177 | return new SafeEnumerator(_internalList, _lock); 178 | } 179 | 180 | #endregion 181 | } 182 | -------------------------------------------------------------------------------- /BetterJoy/HIDApi/Device.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace BetterJoy.HIDApi; 5 | 6 | public sealed class Device : IDisposable 7 | { 8 | private IntPtr _deviceHandle; 9 | private bool _disposed; 10 | 11 | public bool IsValid => _deviceHandle != IntPtr.Zero; 12 | 13 | public enum ErrorCode 14 | { 15 | Success = 0, 16 | UnknownFailure = -1, 17 | DeviceNotConnected = 0x0000048F 18 | } 19 | 20 | private Device(IntPtr deviceHandle) 21 | { 22 | _deviceHandle = deviceHandle; 23 | } 24 | 25 | public static Device Open(ushort vendorId, ushort productId, string serialNumber) 26 | { 27 | return new Device(Native.NativeMethods.Open(vendorId, productId, serialNumber)); 28 | } 29 | 30 | public static Device OpenPath(string path) 31 | { 32 | return new Device(Native.NativeMethods.OpenPath(path)); 33 | } 34 | 35 | public int Write(ReadOnlySpan data, int length) 36 | { 37 | return Native.NativeMethods.Write(_deviceHandle, ref MemoryMarshal.GetReference(data), (nuint)length); 38 | } 39 | 40 | public int ReadTimeout(Span data, int length, int milliseconds) 41 | { 42 | return Native.NativeMethods.ReadTimeout(_deviceHandle, ref MemoryMarshal.GetReference(data), (nuint)length, milliseconds); 43 | } 44 | 45 | public int Read(Span data, int length) 46 | { 47 | return Native.NativeMethods.Read(_deviceHandle, ref MemoryMarshal.GetReference(data), (nuint)length); 48 | } 49 | 50 | public int SetNonBlocking(int nonblock) 51 | { 52 | return Native.NativeMethods.SetNonBlocking(_deviceHandle, nonblock); 53 | } 54 | 55 | public int SendFeatureReport(ReadOnlySpan data, int length) 56 | { 57 | return Native.NativeMethods.SendFeatureReport(_deviceHandle, ref MemoryMarshal.GetReference(data), (nuint)length); 58 | } 59 | 60 | public int GetFeatureReport(Span data, int length) 61 | { 62 | return Native.NativeMethods.GetFeatureReport(_deviceHandle, ref MemoryMarshal.GetReference(data), (nuint)length); 63 | } 64 | 65 | public string GetManufacturer() 66 | { 67 | return StringUtils.GetUnicodeString( 68 | (buffer, length) => Native.NativeMethods.GetManufacturerString(_deviceHandle, ref MemoryMarshal.GetReference(buffer), length) 69 | ); 70 | } 71 | 72 | public string GetProduct() 73 | { 74 | return StringUtils.GetUnicodeString( 75 | (buffer, length) => Native.NativeMethods.GetProductString(_deviceHandle, ref MemoryMarshal.GetReference(buffer), length) 76 | ); 77 | } 78 | 79 | public string GetSerialNumber() 80 | { 81 | return StringUtils.GetUnicodeString( 82 | (buffer, length) => Native.NativeMethods.GetSerialNumberString(_deviceHandle, ref MemoryMarshal.GetReference(buffer), length) 83 | ); 84 | } 85 | 86 | public string GetIndexed(int stringIndex) 87 | { 88 | return StringUtils.GetUnicodeString( 89 | (buffer, length) => Native.NativeMethods.GetIndexedString(_deviceHandle, stringIndex, ref MemoryMarshal.GetReference(buffer), length) 90 | ); 91 | } 92 | 93 | public string GetInstance() 94 | { 95 | return StringUtils.GetUnicodeString( 96 | (buffer, length) => Native.NativeMethods.GetInstanceString(_deviceHandle, ref MemoryMarshal.GetReference(buffer), length) 97 | ); 98 | } 99 | 100 | public string GetParentInstance() 101 | { 102 | return StringUtils.GetUnicodeString( 103 | (buffer, length) => Native.NativeMethods.GetParentInstanceString(_deviceHandle, ref MemoryMarshal.GetReference(buffer), length) 104 | ); 105 | } 106 | 107 | public int GetContainerId(out Guid containerId) 108 | { 109 | return Native.NativeMethods.GetContainerId(_deviceHandle, out containerId); 110 | } 111 | 112 | public string GetError() 113 | { 114 | var ptr = Native.NativeMethods.Error(_deviceHandle); 115 | return Marshal.PtrToStringUni(ptr)!; 116 | } 117 | 118 | public int GetErrorCode() 119 | { 120 | return Native.NativeMethods.ErrorCode(_deviceHandle); 121 | } 122 | 123 | public void Dispose() 124 | { 125 | if (_disposed) 126 | { 127 | return; 128 | } 129 | 130 | if (_deviceHandle != IntPtr.Zero) 131 | { 132 | Native.NativeMethods.Close(_deviceHandle); 133 | _deviceHandle = IntPtr.Zero; 134 | } 135 | 136 | _disposed = true; 137 | GC.SuppressFinalize(this); 138 | } 139 | 140 | ~Device() 141 | { 142 | Dispose(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /BetterJoy/Hardware/Calibration/MotionCalibration.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Hardware.Data; 2 | using System; 3 | 4 | namespace BetterJoy.Hardware.Calibration; 5 | 6 | public class MotionCalibration 7 | { 8 | // Default joycon values 9 | private readonly ThreeAxisShort _defaultAccelerometerNeutralConfig = new(0, 0, 0); 10 | private readonly ThreeAxisShort _defaultAccelerometerSensitivityConfig = new(16384, 16384, 16384); 11 | private readonly ThreeAxisShort _defaultGyroscopeNeutralConfig = new(0, 0, 0); 12 | private readonly ThreeAxisShort _defaultGyroscopeSensitivityConfig = new(13371, 13371, 13371); 13 | 14 | public ThreeAxisShort AccelerometerNeutral { get; private set; } 15 | public ThreeAxisShort AccelerometerSensitivity { get; private set; } 16 | public ThreeAxisShort GyroscopeNeutral { get; private set; } 17 | public ThreeAxisShort GyroscopeSensitivity { get; private set; } 18 | public bool UsedDefaultValues { get; private set; } 19 | 20 | public MotionCalibration() 21 | { 22 | AccelerometerNeutral = _defaultAccelerometerNeutralConfig; 23 | AccelerometerSensitivity = _defaultAccelerometerSensitivityConfig; 24 | GyroscopeNeutral = _defaultGyroscopeNeutralConfig; 25 | GyroscopeSensitivity = _defaultGyroscopeSensitivityConfig; 26 | UsedDefaultValues = true; 27 | } 28 | 29 | public MotionCalibration(ReadOnlySpan values) 30 | { 31 | InitFromValues(values); 32 | } 33 | 34 | public MotionCalibration(ReadOnlySpan raw) 35 | { 36 | InitFromBytes(raw); 37 | } 38 | 39 | private void InitFromBytes(ReadOnlySpan raw) 40 | { 41 | if (raw.Length != 24) 42 | { 43 | throw new ArgumentException($"{nameof(MotionCalibration)} expects 24 bytes, got {raw.Length}."); 44 | } 45 | 46 | InitFromValues([ 47 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[0], raw[1]), 48 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[2], raw[3]), 49 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[4], raw[5]), 50 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[6], raw[7]), 51 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[8], raw[9]), 52 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[10], raw[11]), 53 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[12], raw[13]), 54 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[14], raw[15]), 55 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[16], raw[17]), 56 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[18], raw[19]), 57 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[20], raw[21]), 58 | BitWrangler.EncodeBytesAsWordLittleEndianSigned(raw[22], raw[23]), 59 | ]); 60 | } 61 | 62 | private void InitFromValues(ReadOnlySpan values) 63 | { 64 | if (values.Length != 12) 65 | { 66 | throw new ArgumentException($"{nameof(MotionCalibration)} expects 12 values, got {values.Length}."); 67 | } 68 | 69 | var inputAccelerometerNeutral = new ThreeAxisShort(values[0], values[1], values[2]); 70 | var inputAccelerometerSensitivity = new ThreeAxisShort(values[3], values[4], values[5]); 71 | var inputGyroscopeNeutral = new ThreeAxisShort(values[6], values[7], values[8]); 72 | var inputGyroscopeSensitivity = new ThreeAxisShort(values[9], values[10], values[11]); 73 | 74 | AccelerometerNeutral = inputAccelerometerNeutral.Invalid 75 | ? _defaultAccelerometerNeutralConfig 76 | : inputAccelerometerNeutral; 77 | 78 | AccelerometerSensitivity = inputAccelerometerSensitivity.Invalid 79 | ? _defaultAccelerometerSensitivityConfig 80 | : inputAccelerometerSensitivity; 81 | 82 | GyroscopeNeutral = inputGyroscopeNeutral.Invalid 83 | ? _defaultGyroscopeNeutralConfig 84 | : inputGyroscopeNeutral; 85 | 86 | GyroscopeSensitivity = inputGyroscopeSensitivity.Invalid 87 | ? _defaultGyroscopeSensitivityConfig 88 | : inputGyroscopeSensitivity; 89 | 90 | UsedDefaultValues = 91 | inputAccelerometerNeutral.Invalid || 92 | inputAccelerometerSensitivity.Invalid || 93 | inputGyroscopeNeutral.Invalid || 94 | inputGyroscopeSensitivity.Invalid; 95 | } 96 | 97 | public override string ToString() 98 | { 99 | return $"Motion calibration data: " + 100 | $"{nameof(AccelerometerNeutral)}: {AccelerometerNeutral}, " + 101 | $"{nameof(AccelerometerSensitivity)}: {AccelerometerSensitivity}, " + 102 | $"{nameof(GyroscopeNeutral)}: {GyroscopeNeutral}, " + 103 | $"{nameof(GyroscopeSensitivity)} {GyroscopeSensitivity}"; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /BetterJoy/BetterJoy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net9.0-windows 5 | true 6 | true 7 | 8 | BetterJoy 9 | BetterJoy 10 | Icons\betterjoy_icon.ico 11 | BetterJoy.Program 12 | 8.4.9 13 | $(AssemblyVersion) 14 | $(AssemblyVersion) 15 | Copyright © 2024 16 | $(MSBuildProjectName).zip 17 | true 18 | en 19 | enable 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Designer 36 | 37 | 38 | 39 | Always 40 | false 41 | 42 | 43 | Drivers\%(Filename)%(Extension) 44 | Always 45 | false 46 | 47 | 48 | Drivers\%(Filename)%(Extension) 49 | Always 50 | false 51 | 52 | 53 | Drivers\%(Filename)%(Extension) 54 | Always 55 | false 56 | 57 | 58 | 59 | Always 60 | x86\%(Filename)%(Extension) 61 | 62 | 63 | Always 64 | x64\%(Filename)%(Extension) 65 | 66 | 67 | Always 68 | %(Filename)%(Extension) 69 | 70 | 71 | 72 | 73 | ThirdpartyControllers.cs 74 | 75 | 76 | MainForm.cs 77 | 78 | 79 | ResXFileCodeGenerator 80 | Resources.Designer.cs 81 | 82 | 83 | Reassign.cs 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 6.5.0 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | */x64/* 19 | */x86/* 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | # bat publish folder 264 | build/ 265 | 266 | # BetterJoy ressources 267 | /title.png 268 | /title.pdn 269 | /dist/Drivers/* 270 | 271 | !README.txt -------------------------------------------------------------------------------- /BetterJoy/MadgwickAHRS.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // source: https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MadgwickAHRS.cs 4 | 5 | namespace BetterJoy; 6 | 7 | /// 8 | /// MadgwickAHRS class. Implementation of Madgwick's IMU and AHRS algorithms. 9 | /// 10 | /// 11 | /// See: http://www.x-io.co.uk/node/8#open_source_ahrs_and_imu_algorithms 12 | /// 13 | public class MadgwickAHRS 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// 19 | /// Sample period. 20 | /// 21 | public MadgwickAHRS(float samplePeriod) 22 | : this(samplePeriod, 1f) { } 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// 28 | /// Sample period. 29 | /// 30 | /// 31 | /// Algorithm gain beta. 32 | /// 33 | public MadgwickAHRS(float samplePeriod, float beta) 34 | { 35 | SamplePeriod = samplePeriod; 36 | Beta = beta; 37 | Quaternion = [1f, 0f, 0f, 0f]; 38 | OldPitchYawRoll = [0f, 0f, 0f]; 39 | } 40 | 41 | /// 42 | /// Gets or sets the sample period. 43 | /// 44 | public float SamplePeriod { get; set; } 45 | 46 | /// 47 | /// Gets or sets the algorithm gain beta. 48 | /// 49 | public float Beta { get; set; } 50 | 51 | /// 52 | /// Gets or sets the Quaternion output. 53 | /// 54 | public float[] Quaternion { get; set; } 55 | 56 | public float[] OldPitchYawRoll { get; set; } 57 | 58 | /// 59 | /// Algorithm IMU update method. Requires only gyroscope and accelerometer data. 60 | /// 61 | /// 62 | /// Gyroscope x axis measurement in radians/s. 63 | /// 64 | /// 65 | /// Gyroscope y axis measurement in radians/s. 66 | /// 67 | /// 68 | /// Gyroscope z axis measurement in radians/s. 69 | /// 70 | /// 71 | /// Accelerometer x axis measurement in any calibrated units. 72 | /// 73 | /// 74 | /// Accelerometer y axis measurement in any calibrated units. 75 | /// 76 | /// 77 | /// Accelerometer z axis measurement in any calibrated units. 78 | /// 79 | /// 80 | /// Optimised for minimal arithmetic. 81 | /// Total ±: 45 82 | /// Total *: 85 83 | /// Total /: 3 84 | /// Total sqrt: 3 85 | /// 86 | public void Update(float gx, float gy, float gz, float ax, float ay, float az) 87 | { 88 | float q1 = Quaternion[0], 89 | q2 = Quaternion[1], 90 | q3 = Quaternion[2], 91 | q4 = Quaternion[3]; // short name local variable for readability 92 | float norm; 93 | float s1, s2, s3, s4; 94 | float qDot1, qDot2, qDot3, qDot4; 95 | 96 | // Auxiliary variables to avoid repeated arithmetic 97 | var _2q1 = 2f * q1; 98 | var _2q2 = 2f * q2; 99 | var _2q3 = 2f * q3; 100 | var _2q4 = 2f * q4; 101 | var _4q1 = 4f * q1; 102 | var _4q2 = 4f * q2; 103 | var _4q3 = 4f * q3; 104 | var _8q2 = 8f * q2; 105 | var _8q3 = 8f * q3; 106 | var q1Q1 = q1 * q1; 107 | var q2Q2 = q2 * q2; 108 | var q3Q3 = q3 * q3; 109 | var q4Q4 = q4 * q4; 110 | 111 | // Normalise accelerometer measurement 112 | norm = MathF.Sqrt(ax * ax + ay * ay + az * az); 113 | if (norm == 0f) 114 | { 115 | return; // handle NaN 116 | } 117 | 118 | norm = 1 / norm; // use reciprocal for division 119 | ax *= norm; 120 | ay *= norm; 121 | az *= norm; 122 | 123 | // Gradient decent algorithm corrective step 124 | s1 = _4q1 * q3Q3 + _2q3 * ax + _4q1 * q2Q2 - _2q2 * ay; 125 | s2 = _4q2 * q4Q4 - _2q4 * ax + 4f * q1Q1 * q2 - _2q1 * ay - _4q2 + _8q2 * q2Q2 + _8q2 * q3Q3 + _4q2 * az; 126 | s3 = 4f * q1Q1 * q3 + _2q1 * ax + _4q3 * q4Q4 - _2q4 * ay - _4q3 + _8q3 * q2Q2 + _8q3 * q3Q3 + _4q3 * az; 127 | s4 = 4f * q2Q2 * q4 - _2q2 * ax + 4f * q3Q3 * q4 - _2q3 * ay; 128 | norm = 1f / MathF.Sqrt(s1 * s1 + s2 * s2 + s3 * s3 + s4 * s4); // normalise step magnitude 129 | s1 *= norm; 130 | s2 *= norm; 131 | s3 *= norm; 132 | s4 *= norm; 133 | 134 | // Compute rate of change of quaternion 135 | qDot1 = 0.5f * (-q2 * gx - q3 * gy - q4 * gz) - Beta * s1; 136 | qDot2 = 0.5f * (q1 * gx + q3 * gz - q4 * gy) - Beta * s2; 137 | qDot3 = 0.5f * (q1 * gy - q2 * gz + q4 * gx) - Beta * s3; 138 | qDot4 = 0.5f * (q1 * gz + q2 * gy - q3 * gx) - Beta * s4; 139 | 140 | // Integrate to yield quaternion 141 | q1 += qDot1 * SamplePeriod; 142 | q2 += qDot2 * SamplePeriod; 143 | q3 += qDot3 * SamplePeriod; 144 | q4 += qDot4 * SamplePeriod; 145 | norm = 1f / MathF.Sqrt(q1 * q1 + q2 * q2 + q3 * q3 + q4 * q4); // normalise quaternion 146 | Quaternion[0] = q1 * norm; 147 | Quaternion[1] = q2 * norm; 148 | Quaternion[2] = q3 * norm; 149 | Quaternion[3] = q4 * norm; 150 | } 151 | 152 | public void GetEulerAngles(float[] angles) 153 | { 154 | OldPitchYawRoll.CopyTo(angles, 3); 155 | 156 | float q0 = Quaternion[0], q1 = Quaternion[1], q2 = Quaternion[2], q3 = Quaternion[3]; 157 | float sq1 = q1 * q1, sq2 = q2 * q2, sq3 = q3 * q3; 158 | angles[0] = MathF.Asin(2f * (q0 * q2 - q3 * q1)); // Pitch 159 | angles[1] = MathF.Atan2(2f * (q0 * q3 + q1 * q2), 1 - 2f * (sq2 + sq3)); // Yaw 160 | angles[2] = MathF.Atan2(2f * (q0 * q1 + q2 * q3), 1 - 2f * (sq1 + sq2)); // Roll 161 | 162 | Array.Copy(angles, OldPitchYawRoll, 3); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /BetterJoy/Config/ControllerConfig.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Logging; 2 | using System; 3 | using static BetterJoy.Controller.Joycon; 4 | 5 | namespace BetterJoy.Config; 6 | 7 | public class ControllerConfig : Config 8 | { 9 | public int LowFreq = 160; 10 | public int HighFreq = 320; 11 | public bool EnableRumble = true; 12 | public bool ShowAsXInput = true; 13 | public bool ShowAsDs4 = false; 14 | public float StickLeftDeadzone = 0.15f; 15 | public float StickRightDeadzone = 0.15f; 16 | public float StickLeftRange = 0.90f; 17 | public float StickRightRange = 0.90f; 18 | public bool SticksSquared = false; 19 | public float[] StickLeftAntiDeadzone = [0.0f, 0.0f]; 20 | public float[] StickRightAntiDeadzone = [0.0f, 0.0f]; 21 | public float AHRSBeta = 0.05f; 22 | public float ShakeDelay = 200; 23 | public bool ShakeInputEnabled = false; 24 | public float ShakeSensitivity = 10; 25 | public bool ChangeOrientationDoubleClick = true; 26 | public bool DragToggle = false; 27 | public string ExtraGyroFeature = "none"; 28 | public int GyroAnalogSensitivity = 400; 29 | public bool GyroAnalogSliders = false; 30 | public bool GyroHoldToggle = true; 31 | public bool GyroLeftHanded = false; 32 | public int[] GyroMouseSensitivity = [1200, 800]; 33 | public float GyroStickReduction = 1.5f; 34 | public float[] GyroStickSensitivity = [40.0f, 10.0f]; 35 | public bool HomeLongPowerOff = true; 36 | public bool HomeLEDOn = true; 37 | public long PowerOffInactivityMins = -1; 38 | public bool SwapAB = false; 39 | public bool SwapXY = false; 40 | public bool UseFilteredMotion = true; 41 | public DebugType DebugType = DebugType.None; 42 | public Orientation DoNotRejoin = Orientation.None; 43 | public bool AutoPowerOff = false; 44 | public bool AllowCalibration = true; 45 | 46 | public ControllerConfig(ILogger? logger) : base(logger) { } 47 | 48 | public ControllerConfig(ControllerConfig config) : base(config._logger) 49 | { 50 | LowFreq = config.LowFreq; 51 | HighFreq = config.HighFreq; 52 | EnableRumble = config.EnableRumble; 53 | ShowAsXInput = config.ShowAsXInput; 54 | ShowAsDs4 = config.ShowAsDs4; 55 | StickLeftDeadzone = config.StickLeftDeadzone; 56 | StickRightDeadzone = config.StickRightDeadzone; 57 | StickLeftRange = config.StickLeftRange; 58 | StickRightRange = config.StickRightRange; 59 | SticksSquared = config.SticksSquared; 60 | Array.Copy(config.StickLeftAntiDeadzone, StickLeftAntiDeadzone, StickLeftAntiDeadzone.Length); 61 | Array.Copy(config.StickRightAntiDeadzone, StickRightAntiDeadzone, StickRightAntiDeadzone.Length); 62 | AHRSBeta = config.AHRSBeta; 63 | ShakeDelay = config.ShakeDelay; 64 | ShakeInputEnabled = config.ShakeInputEnabled; 65 | ShakeSensitivity = config.ShakeSensitivity; 66 | ChangeOrientationDoubleClick = config.ChangeOrientationDoubleClick; 67 | DragToggle = config.DragToggle; 68 | ExtraGyroFeature = config.ExtraGyroFeature; 69 | GyroAnalogSensitivity = config.GyroAnalogSensitivity; 70 | GyroAnalogSliders = config.GyroAnalogSliders; 71 | GyroHoldToggle = config.GyroHoldToggle; 72 | GyroLeftHanded = config.GyroLeftHanded; 73 | Array.Copy(config.GyroMouseSensitivity, GyroMouseSensitivity, GyroMouseSensitivity.Length); 74 | GyroStickReduction = config.GyroStickReduction; 75 | Array.Copy(config.GyroStickSensitivity, GyroStickSensitivity, GyroStickSensitivity.Length); 76 | HomeLongPowerOff = config.HomeLongPowerOff; 77 | HomeLEDOn = config.HomeLEDOn; 78 | PowerOffInactivityMins = config.PowerOffInactivityMins; 79 | SwapAB = config.SwapAB; 80 | SwapXY = config.SwapXY; 81 | UseFilteredMotion = config.UseFilteredMotion; 82 | DebugType = config.DebugType; 83 | DoNotRejoin = config.DoNotRejoin; 84 | AutoPowerOff = config.AutoPowerOff; 85 | AllowCalibration = config.AllowCalibration; 86 | } 87 | 88 | public override void Update() 89 | { 90 | TryUpdateSetting("LowFreqRumble", ref LowFreq); 91 | TryUpdateSetting("HighFreqRumble", ref HighFreq); 92 | TryUpdateSetting("EnableRumble", ref EnableRumble); 93 | TryUpdateSetting("ShowAsXInput", ref ShowAsXInput); 94 | TryUpdateSetting("ShowAsDS4", ref ShowAsDs4); 95 | TryUpdateSetting("StickLeftDeadzone", ref StickLeftDeadzone); 96 | TryUpdateSetting("StickRightDeadzone", ref StickRightDeadzone); 97 | TryUpdateSetting("StickLeftRange", ref StickLeftRange); 98 | TryUpdateSetting("StickRightRange", ref StickRightRange); 99 | TryUpdateSetting("SticksSquared", ref SticksSquared); 100 | TryUpdateSetting("StickLeftAntiDeadzone", ref StickLeftAntiDeadzone); 101 | TryUpdateSetting("StickRightAntiDeadzone", ref StickRightAntiDeadzone); 102 | TryUpdateSetting("AHRS_beta", ref AHRSBeta); 103 | TryUpdateSetting("ShakeInputDelay", ref ShakeDelay); 104 | TryUpdateSetting("EnableShakeInput", ref ShakeInputEnabled); 105 | TryUpdateSetting("ShakeInputSensitivity", ref ShakeSensitivity); 106 | TryUpdateSetting("ChangeOrientationDoubleClick", ref ChangeOrientationDoubleClick); 107 | TryUpdateSetting("DragToggle", ref DragToggle); 108 | TryUpdateSetting("GyroToJoyOrMouse", ref ExtraGyroFeature); 109 | TryUpdateSetting("GyroAnalogSensitivity", ref GyroAnalogSensitivity); 110 | TryUpdateSetting("GyroAnalogSliders", ref GyroAnalogSliders); 111 | TryUpdateSetting("GyroHoldToggle", ref GyroHoldToggle); 112 | TryUpdateSetting("GyroLeftHanded", ref GyroLeftHanded); 113 | TryUpdateSetting("GyroMouseSensitivity", ref GyroMouseSensitivity); 114 | TryUpdateSetting("GyroStickReduction", ref GyroStickReduction); 115 | TryUpdateSetting("GyroStickSensitivity", ref GyroStickSensitivity); 116 | TryUpdateSetting("HomeLongPowerOff", ref HomeLongPowerOff); 117 | TryUpdateSetting("HomeLEDOn", ref HomeLEDOn); 118 | TryUpdateSetting("PowerOffInactivity", ref PowerOffInactivityMins); 119 | TryUpdateSetting("SwapAB", ref SwapAB); 120 | TryUpdateSetting("SwapXY", ref SwapXY); 121 | TryUpdateSetting("UseFilteredIMU", ref UseFilteredMotion); 122 | TryUpdateSetting("DebugType", ref DebugType); 123 | TryUpdateSetting("DoNotRejoinJoycons", ref DoNotRejoin); 124 | TryUpdateSetting("AutoPowerOff", ref AutoPowerOff); 125 | TryUpdateSetting("AllowCalibration", ref AllowCalibration); 126 | } 127 | 128 | 129 | 130 | public override ControllerConfig Clone() 131 | { 132 | return new ControllerConfig(this); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /BetterJoy/Controller/Mapping/OutputControllerXbox360.cs: -------------------------------------------------------------------------------- 1 | using Nefarius.ViGEm.Client.Targets; 2 | using Nefarius.ViGEm.Client.Targets.Xbox360; 3 | 4 | namespace BetterJoy.Controller.Mapping; 5 | 6 | public struct OutputControllerXbox360InputState 7 | { 8 | // buttons 9 | public bool ThumbStickLeft; 10 | public bool ThumbStickRight; 11 | 12 | public bool Y; 13 | public bool X; 14 | public bool B; 15 | public bool A; 16 | 17 | public bool Start; 18 | public bool Back; 19 | 20 | public bool Guide; 21 | 22 | public bool ShoulderLeft; 23 | public bool ShoulderRight; 24 | 25 | // dpad 26 | public bool DpadUp; 27 | public bool DpadRight; 28 | public bool DpadDown; 29 | public bool DpadLeft; 30 | 31 | // axis 32 | public short AxisLeftX; 33 | public short AxisLeftY; 34 | 35 | public short AxisRightX; 36 | public short AxisRightY; 37 | 38 | // triggers 39 | public byte TriggerLeft; 40 | public byte TriggerRight; 41 | 42 | public readonly bool IsEqual(OutputControllerXbox360InputState other) 43 | { 44 | var buttons = ThumbStickLeft == other.ThumbStickLeft 45 | && ThumbStickRight == other.ThumbStickRight 46 | && Y == other.Y 47 | && X == other.X 48 | && B == other.B 49 | && A == other.A 50 | && Start == other.Start 51 | && Back == other.Back 52 | && Guide == other.Guide 53 | && ShoulderLeft == other.ShoulderLeft 54 | && ShoulderRight == other.ShoulderRight; 55 | 56 | var dpad = DpadUp == other.DpadUp 57 | && DpadRight == other.DpadRight 58 | && DpadDown == other.DpadDown 59 | && DpadLeft == other.DpadLeft; 60 | 61 | var axis = AxisLeftX == other.AxisLeftX 62 | && AxisLeftY == other.AxisLeftY 63 | && AxisRightX == other.AxisRightX 64 | && AxisRightY == other.AxisRightY; 65 | 66 | var triggers = TriggerLeft == other.TriggerLeft 67 | && TriggerRight == other.TriggerRight; 68 | 69 | return buttons && dpad && axis && triggers; 70 | } 71 | } 72 | 73 | public class OutputControllerXbox360 74 | { 75 | public delegate void Xbox360FeedbackReceivedEventHandler(Xbox360FeedbackReceivedEventArgs e); 76 | 77 | private readonly IXbox360Controller? _xboxController; 78 | 79 | private OutputControllerXbox360InputState _currentState; 80 | 81 | private bool _connected = false; 82 | 83 | public OutputControllerXbox360() 84 | { 85 | if (Program.EmClient == null) 86 | { 87 | return; 88 | } 89 | 90 | _xboxController = Program.EmClient.CreateXbox360Controller(); 91 | Init(); 92 | } 93 | 94 | public OutputControllerXbox360(ushort vendorId, ushort productId) 95 | { 96 | if (Program.EmClient == null) 97 | { 98 | return; 99 | } 100 | 101 | _xboxController = Program.EmClient.CreateXbox360Controller(vendorId, productId); 102 | Init(); 103 | } 104 | 105 | public event Xbox360FeedbackReceivedEventHandler? FeedbackReceived; 106 | 107 | private void Init() 108 | { 109 | if (_xboxController == null) 110 | { 111 | return; 112 | } 113 | 114 | _xboxController.FeedbackReceived += FeedbackReceivedRcv; 115 | _xboxController.AutoSubmitReport = false; 116 | } 117 | 118 | private void FeedbackReceivedRcv(object sender, Xbox360FeedbackReceivedEventArgs e) 119 | { 120 | FeedbackReceived?.Invoke(e); 121 | } 122 | 123 | public bool UpdateInput(OutputControllerXbox360InputState newState) 124 | { 125 | if (!_connected || _currentState.IsEqual(newState)) 126 | { 127 | return false; 128 | } 129 | 130 | DoUpdateInput(newState); 131 | 132 | return true; 133 | } 134 | 135 | public bool IsConnected() 136 | { 137 | return _connected; 138 | } 139 | 140 | public void Connect() 141 | { 142 | if (_xboxController == null) 143 | { 144 | return; 145 | } 146 | 147 | _xboxController.Connect(); 148 | _connected = true; 149 | } 150 | 151 | public void Disconnect() 152 | { 153 | _connected = false; 154 | 155 | try 156 | { 157 | _xboxController?.Disconnect(); 158 | } 159 | catch { } // nothing we can do, might not be connected in the first place 160 | } 161 | 162 | private void DoUpdateInput(OutputControllerXbox360InputState newState) 163 | { 164 | if (_xboxController == null) 165 | { 166 | return; 167 | } 168 | 169 | _xboxController.SetButtonState(Xbox360Button.LeftThumb, newState.ThumbStickLeft); 170 | _xboxController.SetButtonState(Xbox360Button.RightThumb, newState.ThumbStickRight); 171 | 172 | _xboxController.SetButtonState(Xbox360Button.Y, newState.Y); 173 | _xboxController.SetButtonState(Xbox360Button.X, newState.X); 174 | _xboxController.SetButtonState(Xbox360Button.B, newState.B); 175 | _xboxController.SetButtonState(Xbox360Button.A, newState.A); 176 | 177 | _xboxController.SetButtonState(Xbox360Button.Start, newState.Start); 178 | _xboxController.SetButtonState(Xbox360Button.Back, newState.Back); 179 | _xboxController.SetButtonState(Xbox360Button.Guide, newState.Guide); 180 | 181 | _xboxController.SetButtonState(Xbox360Button.Up, newState.DpadUp); 182 | _xboxController.SetButtonState(Xbox360Button.Right, newState.DpadRight); 183 | _xboxController.SetButtonState(Xbox360Button.Down, newState.DpadDown); 184 | _xboxController.SetButtonState(Xbox360Button.Left, newState.DpadLeft); 185 | 186 | _xboxController.SetButtonState(Xbox360Button.LeftShoulder, newState.ShoulderLeft); 187 | _xboxController.SetButtonState(Xbox360Button.RightShoulder, newState.ShoulderRight); 188 | 189 | _xboxController.SetAxisValue(Xbox360Axis.LeftThumbX, newState.AxisLeftX); 190 | _xboxController.SetAxisValue(Xbox360Axis.LeftThumbY, newState.AxisLeftY); 191 | _xboxController.SetAxisValue(Xbox360Axis.RightThumbX, newState.AxisRightX); 192 | _xboxController.SetAxisValue(Xbox360Axis.RightThumbY, newState.AxisRightY); 193 | 194 | _xboxController.SetSliderValue(Xbox360Slider.LeftTrigger, newState.TriggerLeft); 195 | _xboxController.SetSliderValue(Xbox360Slider.RightTrigger, newState.TriggerRight); 196 | 197 | _xboxController.SubmitReport(); 198 | 199 | _currentState = newState; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /BetterJoy/Forms/Reassign.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Controller; 2 | using System; 3 | using System.Drawing; 4 | using System.Windows.Forms; 5 | using WindowsInput.Events; 6 | using WindowsInput.Events.Sources; 7 | 8 | namespace BetterJoy.Forms; 9 | 10 | public partial class Reassign : Form 11 | { 12 | public event EventHandler? ActionAssigned; 13 | 14 | private Control? _curAssignment; 15 | 16 | private enum ButtonAction 17 | { 18 | None = 0, 19 | Disabled = 1 20 | } 21 | 22 | public Reassign() 23 | { 24 | InitializeComponent(); 25 | 26 | var menuJoyButtons = CreateMenuJoyButtons(); 27 | 28 | var menuJoyButtonsNoDisable = CreateMenuJoyButtons(); 29 | var key = Enum.GetName(ButtonAction.Disabled); 30 | menuJoyButtonsNoDisable.Items.RemoveByKey(key); 31 | 32 | foreach (var c in new[] 33 | { 34 | btn_capture, btn_home, btn_sl_l, btn_sl_r, btn_sr_l, btn_sr_r, btn_shake, btn_reset_mouse, 35 | btn_active_gyro, btn_swap_ab, btn_swap_xy 36 | }) 37 | { 38 | c.Tag = c.Name[4..]; 39 | GetPrettyName(c); 40 | 41 | tip_reassign.SetToolTip( 42 | c, 43 | $"Left-click to detect input.{Environment.NewLine}Middle-click to clear to default.{Environment.NewLine}Right-click to see more options." 44 | ); 45 | c.MouseDown += Remap; 46 | c.Menu = (c.Parent == gb_inputs) ? menuJoyButtons : menuJoyButtonsNoDisable; 47 | c.TextAlign = ContentAlignment.MiddleLeft; 48 | } 49 | } 50 | 51 | private void Menu_joy_buttons_ItemClicked(object? sender, ToolStripItemClickedEventArgs e) 52 | { 53 | if (sender is not Control { Tag: SplitButton caller } || 54 | e.ClickedItem is not { Tag: object clickedItem }) 55 | { 56 | return; 57 | } 58 | 59 | string value = $"{(int)clickedItem}"; 60 | 61 | if (clickedItem is not ButtonAction action) 62 | { 63 | value = "joy_" + value; 64 | } 65 | else if (action != ButtonAction.None) 66 | { 67 | value = "act_" + value; 68 | } 69 | 70 | Assign(caller, value); 71 | } 72 | 73 | private void Remap(object? sender, MouseEventArgs e) 74 | { 75 | if (sender is not SplitButton { Tag: string key } control) 76 | { 77 | return; 78 | } 79 | 80 | switch (e.Button) 81 | { 82 | case MouseButtons.Left: 83 | control.Text = "..."; 84 | _curAssignment = control; 85 | break; 86 | case MouseButtons.Middle: 87 | Assign(control, key); 88 | break; 89 | case MouseButtons.Right: 90 | break; 91 | } 92 | } 93 | 94 | private void Reassign_Load(object sender, EventArgs e) 95 | { 96 | InputCapture.Global.RegisterEvent(GlobalKeyEvent); 97 | InputCapture.Global.RegisterEvent(GlobalMouseEvent); 98 | } 99 | 100 | private void GlobalMouseEvent(object? sender, EventSourceEventArgs e) 101 | { 102 | ButtonCode? button = e.Data.ButtonDown?.Button; 103 | 104 | if (_curAssignment != null && button != null) 105 | { 106 | Assign(_curAssignment, "mse_" + (int)button); 107 | 108 | _curAssignment = null; 109 | e.Next_Hook_Enabled = false; 110 | } 111 | } 112 | 113 | private void GlobalKeyEvent(object? sender, EventSourceEventArgs e) 114 | { 115 | KeyCode? key = e.Data.KeyDown?.Key; 116 | 117 | if (_curAssignment != null && key != null) 118 | { 119 | Assign(_curAssignment, "key_" + (int)key); 120 | 121 | _curAssignment = null; 122 | e.Next_Hook_Enabled = false; 123 | } 124 | } 125 | 126 | private void Reassign_FormClosing(object sender, FormClosingEventArgs e) 127 | { 128 | InputCapture.Global.UnregisterEvent(GlobalKeyEvent); 129 | InputCapture.Global.UnregisterEvent(GlobalMouseEvent); 130 | } 131 | 132 | private void GetPrettyName(Control control) 133 | { 134 | if (InvokeRequired) 135 | { 136 | Invoke(new Action(GetPrettyName), control); 137 | return; 138 | } 139 | 140 | if (control.Tag is not string key) 141 | { 142 | return; 143 | } 144 | 145 | string val = Settings.Value(key); 146 | 147 | if (val == "0") 148 | { 149 | control.Text = string.Empty; 150 | } 151 | else 152 | { 153 | var type = 154 | val.StartsWith("act_") ? typeof(ButtonAction) : 155 | val.StartsWith("joy_") ? typeof(Joycon.Button) : 156 | val.StartsWith("key_") ? typeof(KeyCode) : typeof(ButtonCode); 157 | 158 | control.Text = Enum.GetName(type, int.Parse(val.AsSpan(4))); 159 | } 160 | } 161 | 162 | private void btn_apply_Click(object sender, EventArgs e) 163 | { 164 | Settings.Save(); 165 | } 166 | 167 | private void btn_ok_Click(object sender, EventArgs e) 168 | { 169 | btn_apply_Click(sender, e); 170 | Close(); 171 | } 172 | 173 | private ContextMenuStrip CreateMenuJoyButtons() 174 | { 175 | var menuJoyButtons = new ContextMenuStrip(components); 176 | 177 | foreach (var action in Enum.GetValues()) 178 | { 179 | var name = action.ToString(); 180 | var temp = new ToolStripMenuItem(name) 181 | { 182 | Name = name, 183 | Tag = action 184 | }; 185 | menuJoyButtons.Items.Add(temp); 186 | } 187 | 188 | foreach (var button in Enum.GetValues()) 189 | { 190 | var name = button.ToString(); 191 | var temp = new ToolStripMenuItem(name) 192 | { 193 | Name = name, 194 | Tag = button 195 | }; 196 | menuJoyButtons.Items.Add(temp); 197 | } 198 | 199 | menuJoyButtons.ItemClicked += Menu_joy_buttons_ItemClicked; 200 | 201 | return menuJoyButtons; 202 | } 203 | 204 | private void Assign(Control control, string input) 205 | { 206 | if (control.Tag is not string key) 207 | { 208 | return; 209 | } 210 | 211 | Settings.SetValue(key, input); 212 | GetPrettyName(control); 213 | 214 | if (control.Parent == gb_actions) 215 | { 216 | ActionAssigned?.Invoke(this, EventArgs.Empty); 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /BetterJoy/Controller/Mapping/OutputControllerDualShock4.cs: -------------------------------------------------------------------------------- 1 | using Nefarius.ViGEm.Client.Targets; 2 | using Nefarius.ViGEm.Client.Targets.DualShock4; 3 | using System; 4 | 5 | namespace BetterJoy.Controller.Mapping; 6 | 7 | public enum DpadDirection 8 | { 9 | None, 10 | Northwest, 11 | West, 12 | Southwest, 13 | South, 14 | Southeast, 15 | East, 16 | Northeast, 17 | North 18 | } 19 | 20 | public struct OutputControllerDualShock4InputState 21 | { 22 | public bool Triangle; 23 | public bool Circle; 24 | public bool Cross; 25 | public bool Square; 26 | 27 | public bool TriggerLeft; 28 | public bool TriggerRight; 29 | 30 | public bool ShoulderLeft; 31 | public bool ShoulderRight; 32 | 33 | public bool Options; 34 | public bool Share; 35 | public bool Ps; 36 | public bool Touchpad; 37 | 38 | public bool ThumbLeft; 39 | public bool ThumbRight; 40 | 41 | public DpadDirection DPad; 42 | 43 | public byte ThumbLeftX; 44 | public byte ThumbLeftY; 45 | public byte ThumbRightX; 46 | public byte ThumbRightY; 47 | 48 | public byte TriggerLeftValue; 49 | public byte TriggerRightValue; 50 | 51 | public readonly bool IsEqual(OutputControllerDualShock4InputState other) 52 | { 53 | var buttons = Triangle == other.Triangle 54 | && Circle == other.Circle 55 | && Cross == other.Cross 56 | && Square == other.Square 57 | && TriggerLeft == other.TriggerLeft 58 | && TriggerRight == other.TriggerRight 59 | && ShoulderLeft == other.ShoulderLeft 60 | && ShoulderRight == other.ShoulderRight 61 | && Options == other.Options 62 | && Share == other.Share 63 | && Ps == other.Ps 64 | && Touchpad == other.Touchpad 65 | && ThumbLeft == other.ThumbLeft 66 | && ThumbRight == other.ThumbRight 67 | && DPad == other.DPad; 68 | 69 | var axis = ThumbLeftX == other.ThumbLeftX 70 | && ThumbLeftY == other.ThumbLeftY 71 | && ThumbRightX == other.ThumbRightX 72 | && ThumbRightY == other.ThumbRightY; 73 | 74 | var triggers = TriggerLeftValue == other.TriggerLeftValue 75 | && TriggerRightValue == other.TriggerRightValue; 76 | 77 | return buttons && axis && triggers; 78 | } 79 | } 80 | 81 | public class OutputControllerDualShock4 82 | { 83 | public delegate void DualShock4FeedbackReceivedEventHandler(DualShock4FeedbackReceivedEventArgs e); 84 | 85 | private readonly IDualShock4Controller? _controller; 86 | 87 | private OutputControllerDualShock4InputState _currentState; 88 | 89 | private bool _connected = false; 90 | 91 | public OutputControllerDualShock4() 92 | { 93 | if (Program.EmClient == null) 94 | { 95 | return; 96 | } 97 | 98 | _controller = Program.EmClient.CreateDualShock4Controller(); 99 | Init(); 100 | } 101 | 102 | public OutputControllerDualShock4(ushort vendorId, ushort productId) 103 | { 104 | if (Program.EmClient == null) 105 | { 106 | return; 107 | } 108 | 109 | _controller = Program.EmClient.CreateDualShock4Controller(vendorId, productId); 110 | Init(); 111 | } 112 | 113 | public event DualShock4FeedbackReceivedEventHandler? FeedbackReceived; 114 | 115 | private void Init() 116 | { 117 | if (_controller == null) 118 | { 119 | return; 120 | } 121 | 122 | _controller.AutoSubmitReport = false; 123 | #pragma warning disable CS0618 // Type or member is obsolete 124 | _controller.FeedbackReceived += FeedbackReceivedRcv; 125 | #pragma warning restore CS0618 126 | } 127 | 128 | private void FeedbackReceivedRcv(object sender, DualShock4FeedbackReceivedEventArgs e) 129 | { 130 | FeedbackReceived?.Invoke(e); 131 | } 132 | 133 | public bool IsConnected() 134 | { 135 | return _connected; 136 | } 137 | 138 | public void Connect() 139 | { 140 | if (_controller == null) 141 | { 142 | return; 143 | } 144 | 145 | _controller.Connect(); 146 | _connected = true; 147 | } 148 | 149 | public void Disconnect() 150 | { 151 | _connected = false; 152 | 153 | try 154 | { 155 | _controller?.Disconnect(); 156 | } 157 | catch { } // nothing we can do, might not be connected in the first place 158 | } 159 | 160 | public bool UpdateInput(OutputControllerDualShock4InputState newState) 161 | { 162 | if (!_connected || _currentState.IsEqual(newState)) 163 | { 164 | return false; 165 | } 166 | 167 | DoUpdateInput(newState); 168 | 169 | return true; 170 | } 171 | 172 | private void DoUpdateInput(OutputControllerDualShock4InputState newState) 173 | { 174 | if (_controller == null) 175 | { 176 | return; 177 | } 178 | 179 | _controller.SetButtonState(DualShock4Button.Triangle, newState.Triangle); 180 | _controller.SetButtonState(DualShock4Button.Circle, newState.Circle); 181 | _controller.SetButtonState(DualShock4Button.Cross, newState.Cross); 182 | _controller.SetButtonState(DualShock4Button.Square, newState.Square); 183 | 184 | _controller.SetButtonState(DualShock4Button.ShoulderLeft, newState.ShoulderLeft); 185 | _controller.SetButtonState(DualShock4Button.ShoulderRight, newState.ShoulderRight); 186 | 187 | _controller.SetButtonState(DualShock4Button.TriggerLeft, newState.TriggerLeft); 188 | _controller.SetButtonState(DualShock4Button.TriggerRight, newState.TriggerRight); 189 | 190 | _controller.SetButtonState(DualShock4Button.ThumbLeft, newState.ThumbLeft); 191 | _controller.SetButtonState(DualShock4Button.ThumbRight, newState.ThumbRight); 192 | 193 | _controller.SetButtonState(DualShock4Button.Share, newState.Share); 194 | _controller.SetButtonState(DualShock4Button.Options, newState.Options); 195 | _controller.SetButtonState(DualShock4SpecialButton.Ps, newState.Ps); 196 | _controller.SetButtonState(DualShock4SpecialButton.Touchpad, newState.Touchpad); 197 | 198 | _controller.SetDPadDirection(MapDPadDirection(newState.DPad)); 199 | 200 | _controller.SetAxisValue(DualShock4Axis.LeftThumbX, newState.ThumbLeftX); 201 | _controller.SetAxisValue(DualShock4Axis.LeftThumbY, newState.ThumbLeftY); 202 | _controller.SetAxisValue(DualShock4Axis.RightThumbX, newState.ThumbRightX); 203 | _controller.SetAxisValue(DualShock4Axis.RightThumbY, newState.ThumbRightY); 204 | 205 | _controller.SetSliderValue(DualShock4Slider.LeftTrigger, newState.TriggerLeftValue); 206 | _controller.SetSliderValue(DualShock4Slider.RightTrigger, newState.TriggerRightValue); 207 | 208 | _controller.SubmitReport(); 209 | 210 | _currentState = newState; 211 | } 212 | 213 | private static DualShock4DPadDirection MapDPadDirection(DpadDirection dPad) 214 | { 215 | return dPad switch 216 | { 217 | DpadDirection.None => DualShock4DPadDirection.None, 218 | DpadDirection.North => DualShock4DPadDirection.North, 219 | DpadDirection.Northeast => DualShock4DPadDirection.Northeast, 220 | DpadDirection.East => DualShock4DPadDirection.East, 221 | DpadDirection.Southeast => DualShock4DPadDirection.Southeast, 222 | DpadDirection.South => DualShock4DPadDirection.South, 223 | DpadDirection.Southwest => DualShock4DPadDirection.Southwest, 224 | DpadDirection.West => DualShock4DPadDirection.West, 225 | DpadDirection.Northwest => DualShock4DPadDirection.Northwest, 226 | _ => throw new NotImplementedException(), 227 | }; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /BetterJoy/Settings.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Controller; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using WindowsInput.Events; 6 | 7 | namespace BetterJoy; 8 | 9 | public static class Settings 10 | { 11 | private const int SettingsNum = 13; // currently - ProgressiveScan, StartInTray + special buttons 12 | 13 | // stores dynamic configuration, including 14 | private static readonly string _path; 15 | private static readonly Dictionary _variables = []; 16 | private static readonly string[] _actionKeys = ["reset_mouse", "active_gyro", "swap_ab", "swap_xy"]; 17 | 18 | static Settings() 19 | { 20 | _path = Path.GetDirectoryName(Program.ProgramLocation) + "\\settings"; 21 | } 22 | 23 | public static string GetDefaultValue(string key) 24 | { 25 | return key switch 26 | { 27 | "ProgressiveScan" => "1", 28 | "capture" => "key_" + (int)KeyCode.PrintScreen, 29 | "reset_mouse" => "joy_" + (int)Joycon.Button.Stick, 30 | _ => "0", 31 | }; 32 | } 33 | 34 | // Helper function to count how many lines are in a file 35 | // https://www.dotnetperls.com/line-count 36 | private static long CountLinesInFile(string f) 37 | { 38 | // Zero based count 39 | long count = -1; 40 | using (var r = new StreamReader(f)) 41 | { 42 | while (r.ReadLine() != null) 43 | { 44 | count++; 45 | } 46 | } 47 | 48 | return count; 49 | } 50 | 51 | public static void Init( 52 | List> calibrationMotionData, 53 | List> calibrationSticksData 54 | ) 55 | { 56 | foreach (var s in new[] 57 | { 58 | "ProgressiveScan", "StartInTray", "capture", "home", "sl_l", "sl_r", "sr_l", "sr_r", 59 | "shake", "reset_mouse", "active_gyro", "swap_ab", "swap_xy" 60 | }) 61 | { 62 | _variables[s] = GetDefaultValue(s); 63 | } 64 | 65 | if (File.Exists(_path)) 66 | { 67 | // Reset settings file if old settings 68 | if (CountLinesInFile(_path) < SettingsNum) 69 | { 70 | File.Delete(_path); 71 | Init(calibrationMotionData, calibrationSticksData); 72 | return; 73 | } 74 | 75 | using var file = new StreamReader(_path); 76 | var line = string.Empty; 77 | var lineNo = 0; 78 | while ((line = file.ReadLine()) != null) 79 | { 80 | var vs = line.Split(); 81 | try 82 | { 83 | if (lineNo < SettingsNum) 84 | { 85 | // load in basic settings 86 | _variables[vs[0]] = vs[1]; 87 | } 88 | else 89 | { 90 | // load in calibration presets 91 | if (lineNo == SettingsNum) 92 | { 93 | // Motion 94 | calibrationMotionData.Clear(); 95 | for (var i = 0; i < vs.Length; i++) 96 | { 97 | var caliArr = vs[i].Split(','); 98 | var newArr = new short[6]; 99 | for (var j = 1; j < caliArr.Length; j++) 100 | { 101 | newArr[j - 1] = short.Parse(caliArr[j]); 102 | } 103 | 104 | calibrationMotionData.Add( 105 | new KeyValuePair( 106 | caliArr[0], 107 | newArr 108 | ) 109 | ); 110 | } 111 | } 112 | else if (lineNo == SettingsNum + 1) 113 | { 114 | // Sticks 115 | calibrationSticksData.Clear(); 116 | for (var i = 0; i < vs.Length; i++) 117 | { 118 | var caliArr = vs[i].Split(','); 119 | var newArr = new ushort[12]; 120 | for (var j = 1; j < caliArr.Length; j++) 121 | { 122 | newArr[j - 1] = ushort.Parse(caliArr[j]); 123 | } 124 | 125 | calibrationSticksData.Add( 126 | new KeyValuePair( 127 | caliArr[0], 128 | newArr 129 | ) 130 | ); 131 | } 132 | } 133 | } 134 | } 135 | catch { } 136 | 137 | lineNo++; 138 | } 139 | } 140 | else 141 | { 142 | using var file = new StreamWriter(_path); 143 | foreach (var k in _variables.Keys) 144 | { 145 | file.WriteLine("{0} {1}", k, _variables[k]); 146 | } 147 | 148 | // Motion Calibration 149 | var caliStr = ""; 150 | for (var i = 0; i < calibrationMotionData.Count; i++) 151 | { 152 | var space = " "; 153 | if (i == 0) 154 | { 155 | space = ""; 156 | } 157 | 158 | caliStr += space + calibrationMotionData[i].Key + "," + string.Join(",", calibrationMotionData[i].Value); 159 | } 160 | 161 | file.WriteLine(caliStr); 162 | 163 | // Stick Calibration 164 | caliStr = ""; 165 | for (var i = 0; i < calibrationSticksData.Count; i++) 166 | { 167 | var space = " "; 168 | if (i == 0) 169 | { 170 | space = ""; 171 | } 172 | 173 | caliStr += space + calibrationSticksData[i].Key + "," + string.Join(",", calibrationSticksData[i].Value); 174 | } 175 | 176 | file.WriteLine(caliStr); 177 | } 178 | } 179 | 180 | public static int IntValue(string key) => _variables.TryGetValue(key, out string? value) ? int.Parse(value) : 0; 181 | 182 | public static string Value(string key) => _variables.GetValueOrDefault(key, string.Empty); 183 | 184 | public static bool SetValue(string key, string value) 185 | { 186 | if (!_variables.ContainsKey(key)) 187 | { 188 | return false; 189 | } 190 | 191 | _variables[key] = value; 192 | return true; 193 | } 194 | 195 | public static void SaveCalibrationMotionData(List> caliData) 196 | { 197 | var txt = File.ReadAllLines(_path); 198 | if (txt.Length < SettingsNum + 1) // no custom motion calibrations yet 199 | { 200 | Array.Resize(ref txt, txt.Length + 1); 201 | } 202 | 203 | var caliStr = ""; 204 | for (var i = 0; i < caliData.Count; i++) 205 | { 206 | var space = " "; 207 | if (i == 0) 208 | { 209 | space = ""; 210 | } 211 | 212 | caliStr += space + caliData[i].Key + "," + string.Join(",", caliData[i].Value); 213 | } 214 | 215 | txt[SettingsNum] = caliStr; 216 | File.WriteAllLines(_path, txt); 217 | } 218 | 219 | public static void SaveCaliSticksData(List> caliData) 220 | { 221 | var txt = File.ReadAllLines(_path); 222 | if (txt.Length < SettingsNum + 2) // no custom sticks calibrations yet 223 | { 224 | Array.Resize(ref txt, txt.Length + 1); 225 | } 226 | 227 | var caliStr = ""; 228 | for (var i = 0; i < caliData.Count; i++) 229 | { 230 | var space = " "; 231 | if (i == 0) 232 | { 233 | space = ""; 234 | } 235 | 236 | caliStr += space + caliData[i].Key + "," + string.Join(",", caliData[i].Value); 237 | } 238 | 239 | txt[SettingsNum + 1] = caliStr; 240 | File.WriteAllLines(_path, txt); 241 | } 242 | 243 | public static void Save() 244 | { 245 | var txt = File.ReadAllLines(_path); 246 | var no = 0; 247 | foreach (var k in _variables.Keys) 248 | { 249 | txt[no] = $"{k} {_variables[k]}"; 250 | no++; 251 | } 252 | 253 | File.WriteAllLines(_path, txt); 254 | } 255 | 256 | public static ReadOnlySpan GetActionsKeys() 257 | { 258 | return _actionKeys; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # BetterJoy v8.4 LTS 6 | #### Fork changes 7 | - fixed a good amount of bugs, crashes, controller connection/disconnection issues 8 | - added the calibration of the controller with the calibrate button 9 | - added deadzone, range and anti-deadzone settings 10 | - updated to .NET 9 11 | - updated hidapi and packages 12 | - use HidHide instead of the outdated HIDGuardian 13 | - instantly connect and disconnect 14 | - other minor improvements 15 | 16 | I only tested the changes with the official pro controller and joycons as I don't have the other controllers to test. 17 | 18 | #### Description 19 | 20 | Allows the Nintendo Switch Pro Controller, Joycons, and Switch SNES controller to be used with [Cemu](http://cemu.info/) using [Cemuhook](https://sshnuke.net/cemuhook/), [Citra](https://citra-emu.org/), [Dolphin](https://dolphin-emu.org/), [Yuzu](https://yuzu-emu.org/), and system-wide with generic XInput support. 21 | 22 | It also allows using the gyro to control your mouse and remap the special buttons (SL, SR, Capture) to key bindings of your choice. 23 | 24 | If anyone would like to donate to the original creator (for whatever reason), [you can do so here](https://www.paypal.me/DavidKhachaturov/5). 25 | 26 | #### Original creator note 27 | Thank you for using my software and all the constructive feedback I've been getting about it. I started writing this project a while back and have since then learnt a lot more about programming and software development in general. I don't have too much time to work on this project, but I will try to fix bugs when and if they arise. Thank you for your patience in that regard too! 28 | 29 | It's been quite a wild ride, with nearly **590k** (!!) official download on GitHub and probably many more through the nightlies. I think this project was responsible for both software jobs I landed so far, so I am quite proud of it. 30 | 31 | ### Screenshot 32 | ![Example](https://github.com/d3xMachina/BetterJoy/assets/16732772/2cb06404-5484-480b-a733-2db72fdf043d) 33 | 34 | # Downloads 35 | Go to the [Releases tab](https://github.com/d3xMachina/BetterJoy/releases/)! 36 | 37 | # How to use 38 | 1. Install drivers 39 | 1. Read the READMEs (they're there for a reason!) 40 | 2. Run *Drivers/ViGEmBus_1.22.0_x64_x86_arm64.exe* 41 | 3. Run *Drivers/HidHide_1.5.212_x64.exe* (optional but recommended to hide the default device) 42 | 4. Restart your computer 43 | 2. Run *BetterJoy.exe* 44 | 1. Run as Administrator if your keyboard/mouse button mappings don't work 45 | 3. Connect your controllers. 46 | 4. Start your game or emulator and configure your controller. 47 | 5. If you use Cemu, ensure CemuHook has the controller selected. 48 | 1. If using Joycons, CemuHook will detect two controllers - each will give all buttons, but choosing one over the other just chooses preference for which hand to use for gyro controls. 49 | 2. Go into *Input Settings*, choose XInput as a source and assign buttons normally. 50 | 1. If you don't want to do this for some reason, just have one input profile set up with *Wii U Gamepad* as the controller and enable "Also use for buttons/axes" under *GamePad motion source*. **This is no longer required as of version 3** 51 | 2. Turn rumble up to 70-80% if you want rumble. 52 | 53 | * As of version 3, you can use the pro controller and Joycons as normal xbox controllers on your PC - try it with Steam! 54 | 55 | # More Info 56 | Check out the [wiki](https://github.com/Davidobot/BetterJoy/wiki)! There, you'll find all sorts of goodness such as the changelog, description of app settings, the FAQ and Problems page, and info on how to make BetterJoy work with Steam *better*. 57 | 58 | # Connecting and Disconnecting the Controller 59 | ## Bluetooth Mode 60 | * Hold down the small button (sync) on the top of the controller for 5 seconds - this puts the controller into broadcasting mode. 61 | * Search for it in your bluetooth settings and pair normally. 62 | * To disconnect the controller - hold the home button (or capture button) down for 2 seconds (or press the sync button). To reconnect - press any button on your controller. 63 | 64 | ## USB Mode 65 | * Plug the controller into your computer. 66 | 67 | ## Disconnecting \[Windows 10] 68 | 1. Go into "Bluetooth and other devices settings" 69 | 1. Under the first category "Mouse, keyboard, & pen", there should be the pro controller. 70 | 1. Click on it and a "Remove" button will be revealed. 71 | 1. Press the "Remove" button 72 | 73 | # Building 74 | 75 | ## Visual Studio (IDE) 76 | 77 | 1. If you didn't already, install **Visual Studio Community 2022** via 78 | [the official guide](https://docs.microsoft.com/en-us/visualstudio/install/install-visual-studio?view=vs-2022). 79 | When asked about the workloads, select **.NET Desktop Development**. 80 | 2. Get the code project via Git (git clone --recurse-submodules https://github.com/d3xMachina/BetterJoy.git) 81 | 3. Open Visual Studio Community and open the solution file (*BetterJoy.sln*). 82 | 83 | ### For a production build : 84 | 85 | 4. Right click on "BetterJoy" in The Solution Explorer -> "Publish..." -> Click "Publish". 86 | 5. The build is in the build\ folder 87 | 88 | ### For a standard build : 89 | 90 | 4. Open the NuGet manager via *Tools > NuGet Package Manager > Package Manager Settings*. 91 | 5. You should have a warning mentioning *restoring your packages*. Click on the **Restore** button. 92 | 6. You can now run and build BetterJoy. 93 | 94 | ## Visual Studio Build Tools (CLI) 95 | 1. Download **Visual Studio Build Tools** via 96 | [the official link](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022). 97 | 2. Install **NuGet** by following 98 | [the official guide](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli). 99 | You should follow the section for ***nuget.exe***. 100 | Verify that you can run `nuget` from your favourite terminal. 101 | 3. Get the code project via Git (git clone --recurse-submodules https://github.com/d3xMachina/BetterJoy.git) 102 | 103 | ### For a production build : 104 | 105 | 4. Set the MSBUILD_PATH variable to the path of your MSBuild.exe in "Publish.bat" 106 | 5. Run "Publish.bat" 107 | 6. The build is in the build\ folder 108 | 109 | ### For a standard build : 110 | 111 | 4. Open a terminal (*cmd*, *PowerShell*, ...) and enter the folder with the source code. 112 | 5. Restore the NuGet dependencies by running: `nuget restore` 113 | 6. Now build the app with MSBuild: 114 | ``` 115 | msbuild .\BetterJoy.sln -p:Configuration=CONFIGURATION -p:Platform=PLATFORM -t:Rebuild 116 | ``` 117 | The available values for **CONFIGURATION** are *Release* and *Debug*. 118 | The available values for **PLATFORM** are *x86* and *x64* (you want the latter 99.99% of the time). 119 | 7. You have now built the app. See the next section for locating the binaries. 120 | 121 | ## Binaries location 122 | The built binaries are located under 123 | 124 | *BetterJoy\bin\PLATFORM\CONFIGURATION* 125 | 126 | where `PLATFORM` and `CONFIGURATION` are the one provided at build time. 127 | 128 | # Acknowledgements 129 | A massive thanks goes out to [rajkosto](https://github.com/rajkosto/) for putting up with 17 emails and replying very quickly to my silly queries. The UDP server is also mostly taken from his [ScpToolkit](https://github.com/rajkosto/ScpToolkit) repo. 130 | 131 | Also I am very grateful to [mfosse](https://github.com/mfosse/JoyCon-Driver) for pointing me in the right direction and to [Looking-Glass](https://github.com/Looking-Glass/JoyconLib) without whom I would not be able to figure anything out. (being honest here - the joycon code is his) 132 | 133 | Many thanks to [nefarius](https://github.com/ViGEm/ViGEmBus) for his ViGEm project! Apologies and appreciation go out to [epigramx](https://github.com/epigramx), creator of *WiimoteHook*, for giving me the driver idea and for letting me keep using his installation batch script even though I took it without permission. Thanks go out to [MTCKC](https://github.com/MTCKC/ProconXInput) for inspiration and batch files. 134 | 135 | A last thanks goes out to [dekuNukem](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering) for his documentation, especially on the SPI calibration data and the IMU sensor notes! 136 | 137 | Massive *thank you* to **all** code contributors! 138 | 139 | Icons (modified): "[Switch Pro Controller](https://thenounproject.com/term/nintendo-switch/930119/)", "[ 140 | Switch Detachable Controller Left](https://thenounproject.com/remsing/uploads/?i=930115)", "[Switch Detachable Controller Right](https://thenounproject.com/remsing/uploads/?i=930121)" icons by Chad Remsing from [the Noun Project](https://thenounproject.com/). [Super Nintendo Controller](https://thenounproject.com/themizarkshow/collection/vectogram/?i=193592) icon by Mark Davis from the [the Noun Project](https://thenounproject.com/); icon modified by [Amy Alexander](https://www.linkedin.com/in/-amy-alexander/). [Switch Nintendo 64 Controller](https://thenounproject.com/icon/game-controller-193588/) icon by Mark Davis from the [the Noun Project](https://thenounproject.com/); icon modified by [d3xMachina](https://www.github.com/d3xMachina). [NES Controller](https://thenounproject.com/icon/nes-gamepad-949405/) icon by Viktor Korobkov from [the Noun Project](http://thenounproject.com/); icon modified by anon. 141 | -------------------------------------------------------------------------------- /BetterJoy/Forms/ThirdpartyControllers.cs: -------------------------------------------------------------------------------- 1 | using BetterJoy.Controller; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Windows.Forms; 6 | 7 | namespace BetterJoy.Forms; 8 | 9 | public partial class _3rdPartyControllers : Form 10 | { 11 | private static readonly string _path; 12 | 13 | static _3rdPartyControllers() 14 | { 15 | _path = Path.GetDirectoryName(Program.ProgramLocation) 16 | + "\\3rdPartyControllers"; 17 | } 18 | 19 | public _3rdPartyControllers() 20 | { 21 | InitializeComponent(); 22 | list_allControllers.HorizontalScrollbar = true; 23 | list_customControllers.HorizontalScrollbar = true; 24 | 25 | list_allControllers.Sorted = true; 26 | list_customControllers.Sorted = true; 27 | 28 | foreach (Joycon.ControllerType type in Enum.GetValues()) 29 | { 30 | chooseType.Items.Add(Joycon.GetControllerName(type)); 31 | } 32 | 33 | chooseType.FormattingEnabled = true; 34 | group_props.Controls.Add(chooseType); 35 | group_props.Enabled = false; 36 | 37 | GetSavedThirdpartyControllers().ForEach(controller => list_customControllers.Items.Add(controller)); 38 | RefreshControllerList(); 39 | } 40 | 41 | public static List GetSavedThirdpartyControllers() 42 | { 43 | var controllers = new List(); 44 | 45 | if (File.Exists(_path)) 46 | { 47 | using var file = new StreamReader(_path); 48 | var line = string.Empty; 49 | while (!string.IsNullOrEmpty(line = file.ReadLine())) 50 | { 51 | var split = line.Split('|'); 52 | if (split.Length < 6) 53 | { 54 | continue; 55 | } 56 | 57 | controllers.Add( 58 | new SController( 59 | split[0], 60 | split[1], 61 | ushort.Parse(split[2]), 62 | ushort.Parse(split[3]), 63 | split[4], 64 | byte.Parse(split[5]) 65 | ) 66 | ); 67 | } 68 | } 69 | 70 | return controllers; 71 | } 72 | 73 | private List GetActiveThirdpartyControllers() 74 | { 75 | var controllers = new List(); 76 | 77 | foreach (SController controller in list_customControllers.Items) 78 | { 79 | controllers.Add(controller); 80 | } 81 | 82 | return controllers; 83 | } 84 | 85 | private void CopyCustomControllers() 86 | { 87 | var controllers = GetActiveThirdpartyControllers(); 88 | Program.UpdateThirdpartyControllers(controllers); 89 | } 90 | 91 | private static bool ContainsController(ListBox a, SController controller) 92 | { 93 | foreach (SController currentController in a.Items) 94 | { 95 | if (currentController.Equals(controller)) 96 | { 97 | return true; 98 | } 99 | } 100 | 101 | return false; 102 | } 103 | 104 | private void RefreshControllerList() 105 | { 106 | list_allControllers.Items.Clear(); 107 | var devices = HIDApi.Manager.EnumerateDevices(0x0, 0x0); 108 | 109 | // Add devices to the list 110 | foreach (var device in devices) 111 | { 112 | var controller = new SController( 113 | device.ManufacturerString, 114 | device.ProductString, 115 | device.VendorId, 116 | device.ProductId, 117 | device.SerialNumber, 118 | 0 // type not set 119 | ); 120 | 121 | if (!ContainsController(list_customControllers, controller) && !ContainsController(list_allControllers, controller)) 122 | { 123 | list_allControllers.Items.Add(controller); 124 | Console.WriteLine($"Found controller {controller}"); 125 | } 126 | } 127 | } 128 | 129 | private void btn_add_Click(object sender, EventArgs e) 130 | { 131 | if (list_allControllers.SelectedItem != null) 132 | { 133 | list_customControllers.Items.Add(list_allControllers.SelectedItem); 134 | list_allControllers.Items.Remove(list_allControllers.SelectedItem); 135 | 136 | list_allControllers.ClearSelected(); 137 | } 138 | } 139 | 140 | private void btn_remove_Click(object sender, EventArgs e) 141 | { 142 | if (list_customControllers.SelectedItem != null) 143 | { 144 | list_allControllers.Items.Add(list_customControllers.SelectedItem); 145 | list_customControllers.Items.Remove(list_customControllers.SelectedItem); 146 | 147 | list_customControllers.ClearSelected(); 148 | } 149 | } 150 | 151 | private void btn_apply_Click(object sender, EventArgs e) 152 | { 153 | var content = ""; 154 | foreach (SController controller in list_customControllers.Items) 155 | { 156 | content += controller.Serialise() + Environment.NewLine; 157 | } 158 | 159 | File.WriteAllText(_path, content); 160 | CopyCustomControllers(); 161 | } 162 | 163 | private void btn_applyAndClose_Click(object sender, EventArgs e) 164 | { 165 | btn_apply_Click(sender, e); 166 | Close(); 167 | } 168 | 169 | private void _3rdPartyControllers_FormClosing(object sender, FormClosingEventArgs e) 170 | { 171 | btn_apply_Click(sender, e); 172 | } 173 | 174 | private void btn_refresh_Click(object sender, EventArgs e) 175 | { 176 | RefreshControllerList(); 177 | } 178 | 179 | private void list_allControllers_SelectedValueChanged(object sender, EventArgs e) 180 | { 181 | if (list_allControllers.SelectedItem is SController controller) 182 | { 183 | tip_device.Show(controller.ToString(), list_allControllers); 184 | } 185 | } 186 | 187 | private void list_customControllers_SelectedValueChanged(object sender, EventArgs e) 188 | { 189 | if (list_customControllers.SelectedItem is SController controller) 190 | { 191 | tip_device.Show(controller.ToString(), list_customControllers); 192 | 193 | chooseType.SelectedIndex = controller.Type - 1; 194 | group_props.Enabled = true; 195 | } 196 | else 197 | { 198 | chooseType.SelectedIndex = -1; 199 | group_props.Enabled = false; 200 | } 201 | } 202 | 203 | private void list_customControllers_MouseDown(object sender, MouseEventArgs e) 204 | { 205 | if (e.Y > list_customControllers.ItemHeight * list_customControllers.Items.Count) 206 | { 207 | list_customControllers.SelectedItems.Clear(); 208 | } 209 | } 210 | 211 | private void list_allControllers_MouseDown(object sender, MouseEventArgs e) 212 | { 213 | if (e.Y > list_allControllers.ItemHeight * list_allControllers.Items.Count) 214 | { 215 | list_allControllers.SelectedItems.Clear(); 216 | } 217 | } 218 | 219 | private void chooseType_SelectedValueChanged(object sender, EventArgs e) 220 | { 221 | if (list_customControllers.SelectedItem is SController controller) 222 | { 223 | controller.Type = (byte)(chooseType.SelectedIndex + 1); 224 | } 225 | } 226 | 227 | public class SController 228 | { 229 | public readonly string Manufacturer; 230 | public readonly string Product; 231 | public readonly ushort VendorId; 232 | public readonly ushort ProductId; 233 | public readonly string SerialNumber; 234 | public byte Type; 235 | private string? _name; 236 | 237 | public SController(string manufacturer, string product, ushort vendorId, ushort productId, string serialNumber, byte type) 238 | { 239 | Manufacturer = manufacturer; 240 | Product = product; 241 | VendorId = vendorId; 242 | ProductId = productId; 243 | SerialNumber = serialNumber; 244 | Type = type; 245 | } 246 | 247 | public override bool Equals(object? obj) 248 | { 249 | if (obj is not SController controllerObj) 250 | { 251 | return false; 252 | } 253 | 254 | return controllerObj.Manufacturer == Manufacturer && 255 | controllerObj.Product == Product && 256 | controllerObj.VendorId == VendorId && 257 | controllerObj.ProductId == ProductId && 258 | controllerObj.SerialNumber == SerialNumber; 259 | } 260 | 261 | public override int GetHashCode() 262 | { 263 | return Tuple.Create(Manufacturer, Product, VendorId, ProductId, SerialNumber).GetHashCode(); 264 | } 265 | 266 | public override string ToString() 267 | { 268 | if (_name == null) 269 | { 270 | _name = Manufacturer; 271 | 272 | if (Product != "") 273 | { 274 | _name += $" {Product}"; 275 | } 276 | else 277 | { 278 | _name = "Unknown"; 279 | } 280 | 281 | _name += $" (V{VendorId:X2} P{ProductId:X2}"; 282 | 283 | if (SerialNumber != "") 284 | { 285 | _name += $" S{SerialNumber}"; 286 | } 287 | 288 | _name += ")"; 289 | } 290 | 291 | return _name; 292 | } 293 | 294 | public string Serialise() 295 | { 296 | return $"{Manufacturer}|{Product}|{VendorId}|{ProductId}|{SerialNumber}|{Type}"; 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /BetterJoy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34928.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterJoy", "BetterJoy\BetterJoy.csproj", "{1BF709E9-C133-41DF-933A-C9FF3F664C7B}" 7 | ProjectSection(ProjectDependencies) = postProject 8 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35} = {34810FEB-8BC0-ECDE-F6B4-3707E025DA35} 9 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8} = {3FBE01B2-3F36-419D-97D2-09DE503FEDA8} 10 | {A107C21C-418A-4697-BB10-20C3AA60E2E4} = {A107C21C-418A-4697-BB10-20C3AA60E2E4} 11 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5} = {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5} 12 | EndProjectSection 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Thirdparty", "Thirdparty", "{6F6C51C3-D4F2-4F57-9454-24BC9C83FA15}" 15 | EndProject 16 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hidapi", "thirdparty\hidapi\windows\hidapi.vcxproj", "{A107C21C-418A-4697-BB10-20C3AA60E2E4}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViGEmClient.NET", "thirdparty\ViGEm.NET\ViGEmClient\ViGEmClient.NET.csproj", "{AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}" 19 | ProjectSection(ProjectDependencies) = postProject 20 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC} = {7DB06674-1F4F-464B-8E1C-172E9587F9DC} 21 | {84C931CF-00BE-4337-96EB-241A38A43A71} = {84C931CF-00BE-4337-96EB-241A38A43A71} 22 | EndProjectSection 23 | EndProject 24 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hidapi64", "thirdparty\hidapi\windows\hidapi64.vcxproj", "{3FBE01B2-3F36-419D-97D2-09DE503FEDA8}" 25 | EndProject 26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInput", "thirdparty\WindowsInput\WindowsInput\WindowsInput.csproj", "{34810FEB-8BC0-ECDE-F6B4-3707E025DA35}" 27 | EndProject 28 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ViGEmClient", "thirdparty\ViGEm.NET\ViGEmClientNative\src\ViGEmClient.vcxproj", "{7DB06674-1F4F-464B-8E1C-172E9587F9DC}" 29 | EndProject 30 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ViGEmClient64", "thirdparty\ViGEm.NET\ViGEmClientNative\src\ViGEmClient64.vcxproj", "{84C931CF-00BE-4337-96EB-241A38A43A71}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Debug|x64 = Debug|x64 36 | Debug|x86 = Debug|x86 37 | Release|Any CPU = Release|Any CPU 38 | Release|x64 = Release|x64 39 | Release|x86 = Release|x86 40 | EndGlobalSection 41 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 42 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Debug|x64.ActiveCfg = Debug|Any CPU 45 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Debug|x64.Build.0 = Debug|Any CPU 46 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Debug|x86.ActiveCfg = Debug|Any CPU 47 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Debug|x86.Build.0 = Debug|Any CPU 48 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Release|x64.ActiveCfg = Release|Any CPU 51 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Release|x64.Build.0 = Release|Any CPU 52 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Release|x86.ActiveCfg = Release|Any CPU 53 | {1BF709E9-C133-41DF-933A-C9FF3F664C7B}.Release|x86.Build.0 = Release|Any CPU 54 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Debug|Any CPU.ActiveCfg = Release|Win32 55 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Debug|Any CPU.Build.0 = Release|Win32 56 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Debug|x64.ActiveCfg = Release|Win32 57 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Debug|x64.Build.0 = Release|Win32 58 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Debug|x86.ActiveCfg = Release|Win32 59 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Debug|x86.Build.0 = Release|Win32 60 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Release|Any CPU.ActiveCfg = Release|Win32 61 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Release|Any CPU.Build.0 = Release|Win32 62 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Release|x64.ActiveCfg = Release|Win32 63 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Release|x64.Build.0 = Release|Win32 64 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Release|x86.ActiveCfg = Release|Win32 65 | {A107C21C-418A-4697-BB10-20C3AA60E2E4}.Release|x86.Build.0 = Release|Win32 66 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Debug|Any CPU.ActiveCfg = Release|Any CPU 67 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Debug|Any CPU.Build.0 = Release|Any CPU 68 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Debug|x64.ActiveCfg = Release|Any CPU 69 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Debug|x64.Build.0 = Release|Any CPU 70 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Debug|x86.ActiveCfg = Release|Any CPU 71 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Debug|x86.Build.0 = Release|Any CPU 72 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Release|Any CPU.Build.0 = Release|Any CPU 74 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Release|x64.ActiveCfg = Release|Any CPU 75 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Release|x64.Build.0 = Release|Any CPU 76 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Release|x86.ActiveCfg = Release|Any CPU 77 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5}.Release|x86.Build.0 = Release|Any CPU 78 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Debug|Any CPU.ActiveCfg = Release|x64 79 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Debug|Any CPU.Build.0 = Release|x64 80 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Debug|x64.ActiveCfg = Release|x64 81 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Debug|x64.Build.0 = Release|x64 82 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Debug|x86.ActiveCfg = Release|x64 83 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Debug|x86.Build.0 = Release|x64 84 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Release|Any CPU.ActiveCfg = Release|x64 85 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Release|Any CPU.Build.0 = Release|x64 86 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Release|x64.ActiveCfg = Release|x64 87 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Release|x64.Build.0 = Release|x64 88 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Release|x86.ActiveCfg = Release|x64 89 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8}.Release|x86.Build.0 = Release|x64 90 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Debug|Any CPU.ActiveCfg = Release|Any CPU 91 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Debug|Any CPU.Build.0 = Release|Any CPU 92 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Debug|x64.ActiveCfg = Release|Any CPU 93 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Debug|x64.Build.0 = Release|Any CPU 94 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Debug|x86.ActiveCfg = Release|Any CPU 95 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Debug|x86.Build.0 = Release|Any CPU 96 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Release|Any CPU.ActiveCfg = Release|Any CPU 97 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Release|Any CPU.Build.0 = Release|Any CPU 98 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Release|x64.ActiveCfg = Release|Any CPU 99 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Release|x64.Build.0 = Release|Any CPU 100 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Release|x86.ActiveCfg = Release|Any CPU 101 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35}.Release|x86.Build.0 = Release|Any CPU 102 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug|Any CPU.ActiveCfg = Release_DLL|Win32 103 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug|Any CPU.Build.0 = Release_DLL|Win32 104 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug|x64.ActiveCfg = Release_DLL|Win32 105 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug|x64.Build.0 = Release_DLL|Win32 106 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug|x86.ActiveCfg = Release_DLL|Win32 107 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug|x86.Build.0 = Release_DLL|Win32 108 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release|Any CPU.ActiveCfg = Release_DLL|Win32 109 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release|Any CPU.Build.0 = Release_DLL|Win32 110 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release|x64.ActiveCfg = Release_DLL|Win32 111 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release|x64.Build.0 = Release_DLL|Win32 112 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release|x86.ActiveCfg = Release_DLL|Win32 113 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release|x86.Build.0 = Release_DLL|Win32 114 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Debug|Any CPU.ActiveCfg = Release_DLL|x64 115 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Debug|Any CPU.Build.0 = Release_DLL|x64 116 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Debug|x64.ActiveCfg = Release_DLL|x64 117 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Debug|x64.Build.0 = Release_DLL|x64 118 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Debug|x86.ActiveCfg = Release_DLL|x64 119 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Debug|x86.Build.0 = Release_DLL|x64 120 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Release|Any CPU.ActiveCfg = Release_DLL|x64 121 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Release|Any CPU.Build.0 = Release_DLL|x64 122 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Release|x64.ActiveCfg = Release_DLL|x64 123 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Release|x64.Build.0 = Release_DLL|x64 124 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Release|x86.ActiveCfg = Release_DLL|x64 125 | {84C931CF-00BE-4337-96EB-241A38A43A71}.Release|x86.Build.0 = Release_DLL|x64 126 | EndGlobalSection 127 | GlobalSection(SolutionProperties) = preSolution 128 | HideSolutionNode = FALSE 129 | EndGlobalSection 130 | GlobalSection(NestedProjects) = preSolution 131 | {A107C21C-418A-4697-BB10-20C3AA60E2E4} = {6F6C51C3-D4F2-4F57-9454-24BC9C83FA15} 132 | {AA18EBCF-7E9D-4BC5-8760-E8C6E9A773E5} = {6F6C51C3-D4F2-4F57-9454-24BC9C83FA15} 133 | {3FBE01B2-3F36-419D-97D2-09DE503FEDA8} = {6F6C51C3-D4F2-4F57-9454-24BC9C83FA15} 134 | {34810FEB-8BC0-ECDE-F6B4-3707E025DA35} = {6F6C51C3-D4F2-4F57-9454-24BC9C83FA15} 135 | {7DB06674-1F4F-464B-8E1C-172E9587F9DC} = {6F6C51C3-D4F2-4F57-9454-24BC9C83FA15} 136 | {84C931CF-00BE-4337-96EB-241A38A43A71} = {6F6C51C3-D4F2-4F57-9454-24BC9C83FA15} 137 | EndGlobalSection 138 | GlobalSection(ExtensibilityGlobals) = postSolution 139 | SolutionGuid = {F928F866-3D27-4531-970D-25ADE4DDD979} 140 | EndGlobalSection 141 | EndGlobal 142 | -------------------------------------------------------------------------------- /BetterJoy/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /BetterJoy/Forms/ThirdpartyControllers.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace BetterJoy.Forms 2 | { 3 | partial class _3rdPartyControllers 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing) 17 | { 18 | components?.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | components = new System.ComponentModel.Container(); 32 | var resources = new System.ComponentModel.ComponentResourceManager(typeof(_3rdPartyControllers)); 33 | list_allControllers = new System.Windows.Forms.ListBox(); 34 | list_customControllers = new System.Windows.Forms.ListBox(); 35 | btn_add = new System.Windows.Forms.Button(); 36 | btn_remove = new System.Windows.Forms.Button(); 37 | group_props = new System.Windows.Forms.GroupBox(); 38 | label2 = new System.Windows.Forms.Label(); 39 | chooseType = new System.Windows.Forms.ComboBox(); 40 | btn_applyAndClose = new System.Windows.Forms.Button(); 41 | btn_apply = new System.Windows.Forms.Button(); 42 | lbl_all = new System.Windows.Forms.Label(); 43 | label1 = new System.Windows.Forms.Label(); 44 | tip_device = new System.Windows.Forms.ToolTip(components); 45 | btn_refresh = new System.Windows.Forms.Button(); 46 | group_props.SuspendLayout(); 47 | SuspendLayout(); 48 | // 49 | // list_allControllers 50 | // 51 | list_allControllers.FormattingEnabled = true; 52 | list_allControllers.Location = new System.Drawing.Point(14, 31); 53 | list_allControllers.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 54 | list_allControllers.Name = "list_allControllers"; 55 | list_allControllers.Size = new System.Drawing.Size(320, 229); 56 | list_allControllers.TabIndex = 0; 57 | list_allControllers.SelectedValueChanged += list_allControllers_SelectedValueChanged; 58 | list_allControllers.MouseDown += list_allControllers_MouseDown; 59 | // 60 | // list_customControllers 61 | // 62 | list_customControllers.FormattingEnabled = true; 63 | list_customControllers.Location = new System.Drawing.Point(397, 31); 64 | list_customControllers.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 65 | list_customControllers.Name = "list_customControllers"; 66 | list_customControllers.Size = new System.Drawing.Size(320, 154); 67 | list_customControllers.TabIndex = 1; 68 | list_customControllers.SelectedValueChanged += list_customControllers_SelectedValueChanged; 69 | list_customControllers.MouseDown += list_customControllers_MouseDown; 70 | // 71 | // btn_add 72 | // 73 | btn_add.Font = new System.Drawing.Font("Segoe UI", 10F); 74 | btn_add.Location = new System.Drawing.Point(341, 43); 75 | btn_add.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 76 | btn_add.Name = "btn_add"; 77 | btn_add.Size = new System.Drawing.Size(49, 27); 78 | btn_add.TabIndex = 2; 79 | btn_add.Text = "🠊"; 80 | tip_device.SetToolTip(btn_add, "Add"); 81 | btn_add.UseVisualStyleBackColor = true; 82 | btn_add.Click += btn_add_Click; 83 | // 84 | // btn_remove 85 | // 86 | btn_remove.Font = new System.Drawing.Font("Segoe UI", 10F); 87 | btn_remove.Location = new System.Drawing.Point(341, 144); 88 | btn_remove.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 89 | btn_remove.Name = "btn_remove"; 90 | btn_remove.Size = new System.Drawing.Size(49, 27); 91 | btn_remove.TabIndex = 3; 92 | btn_remove.Text = "🠈"; 93 | tip_device.SetToolTip(btn_remove, "Remove"); 94 | btn_remove.UseVisualStyleBackColor = true; 95 | btn_remove.Click += btn_remove_Click; 96 | // 97 | // group_props 98 | // 99 | group_props.Controls.Add(label2); 100 | group_props.Controls.Add(chooseType); 101 | group_props.Location = new System.Drawing.Point(392, 194); 102 | group_props.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 103 | group_props.Name = "group_props"; 104 | group_props.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3); 105 | group_props.Size = new System.Drawing.Size(325, 66); 106 | group_props.TabIndex = 4; 107 | group_props.TabStop = false; 108 | group_props.Text = "Settings"; 109 | // 110 | // label2 111 | // 112 | label2.AutoSize = true; 113 | label2.Location = new System.Drawing.Point(8, 28); 114 | label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); 115 | label2.Name = "label2"; 116 | label2.Size = new System.Drawing.Size(32, 15); 117 | label2.TabIndex = 1; 118 | label2.Text = "Type"; 119 | // 120 | // chooseType 121 | // 122 | chooseType.FormattingEnabled = true; 123 | chooseType.Location = new System.Drawing.Point(55, 25); 124 | chooseType.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 125 | chooseType.Name = "chooseType"; 126 | chooseType.Size = new System.Drawing.Size(262, 23); 127 | chooseType.TabIndex = 0; 128 | chooseType.SelectedValueChanged += chooseType_SelectedValueChanged; 129 | // 130 | // btn_applyAndClose 131 | // 132 | btn_applyAndClose.Location = new System.Drawing.Point(372, 272); 133 | btn_applyAndClose.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 134 | btn_applyAndClose.Name = "btn_applyAndClose"; 135 | btn_applyAndClose.Size = new System.Drawing.Size(80, 27); 136 | btn_applyAndClose.TabIndex = 5; 137 | btn_applyAndClose.Text = "Close"; 138 | btn_applyAndClose.UseVisualStyleBackColor = true; 139 | btn_applyAndClose.Click += btn_applyAndClose_Click; 140 | // 141 | // btn_apply 142 | // 143 | btn_apply.Location = new System.Drawing.Point(280, 272); 144 | btn_apply.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 145 | btn_apply.Name = "btn_apply"; 146 | btn_apply.Size = new System.Drawing.Size(80, 27); 147 | btn_apply.TabIndex = 6; 148 | btn_apply.Text = "Apply"; 149 | btn_apply.UseVisualStyleBackColor = true; 150 | btn_apply.Click += btn_apply_Click; 151 | // 152 | // lbl_all 153 | // 154 | lbl_all.AutoSize = true; 155 | lbl_all.Location = new System.Drawing.Point(14, 10); 156 | lbl_all.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); 157 | lbl_all.Name = "lbl_all"; 158 | lbl_all.Size = new System.Drawing.Size(64, 15); 159 | lbl_all.TabIndex = 7; 160 | lbl_all.Text = "All Devices"; 161 | // 162 | // label1 163 | // 164 | label1.AutoSize = true; 165 | label1.Location = new System.Drawing.Point(397, 10); 166 | label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); 167 | label1.Name = "label1"; 168 | label1.Size = new System.Drawing.Size(103, 15); 169 | label1.TabIndex = 8; 170 | label1.Text = "Switch Controllers"; 171 | // 172 | // btn_refresh 173 | // 174 | btn_refresh.Font = new System.Drawing.Font("Segoe UI", 18F, System.Drawing.FontStyle.Bold); 175 | btn_refresh.Location = new System.Drawing.Point(341, 77); 176 | btn_refresh.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 177 | btn_refresh.Name = "btn_refresh"; 178 | btn_refresh.Padding = new System.Windows.Forms.Padding(3, 0, 0, 0); 179 | btn_refresh.Size = new System.Drawing.Size(49, 58); 180 | btn_refresh.TabIndex = 9; 181 | btn_refresh.Text = "⟳"; 182 | tip_device.SetToolTip(btn_refresh, "Refresh"); 183 | btn_refresh.UseVisualStyleBackColor = true; 184 | btn_refresh.Click += btn_refresh_Click; 185 | // 186 | // _3rdPartyControllers 187 | // 188 | AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); 189 | AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; 190 | ClientSize = new System.Drawing.Size(731, 308); 191 | Controls.Add(btn_refresh); 192 | Controls.Add(label1); 193 | Controls.Add(lbl_all); 194 | Controls.Add(btn_apply); 195 | Controls.Add(btn_applyAndClose); 196 | Controls.Add(group_props); 197 | Controls.Add(btn_remove); 198 | Controls.Add(btn_add); 199 | Controls.Add(list_customControllers); 200 | Controls.Add(list_allControllers); 201 | FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; 202 | Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); 203 | Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); 204 | MaximizeBox = false; 205 | MinimizeBox = false; 206 | Name = "_3rdPartyControllers"; 207 | StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; 208 | Text = "Add 3rd-Party Controllers"; 209 | FormClosing += _3rdPartyControllers_FormClosing; 210 | group_props.ResumeLayout(false); 211 | group_props.PerformLayout(); 212 | ResumeLayout(false); 213 | PerformLayout(); 214 | } 215 | 216 | #endregion 217 | 218 | private System.Windows.Forms.ListBox list_allControllers; 219 | private System.Windows.Forms.ListBox list_customControllers; 220 | private System.Windows.Forms.Button btn_add; 221 | private System.Windows.Forms.Button btn_remove; 222 | private System.Windows.Forms.GroupBox group_props; 223 | private System.Windows.Forms.Button btn_applyAndClose; 224 | private System.Windows.Forms.Button btn_apply; 225 | private System.Windows.Forms.Label lbl_all; 226 | private System.Windows.Forms.Label label1; 227 | private System.Windows.Forms.ToolTip tip_device; 228 | private System.Windows.Forms.Button btn_refresh; 229 | private System.Windows.Forms.Label label2; 230 | private System.Windows.Forms.ComboBox chooseType; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /BetterJoy/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\Icons\jc_left.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | 125 | ..\Icons\pro.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 126 | 127 | 128 | ..\Icons\snes.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 129 | 130 | 131 | ..\Icons\nes.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 132 | 133 | 134 | ..\Icons\famicom_i.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 135 | 136 | 137 | ..\Icons\famicom_ii.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 138 | 139 | 140 | ..\Icons\jc_left_s.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 141 | 142 | 143 | ..\Icons\jc_right_s.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 144 | 145 | 146 | ..\Icons\cross.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 147 | 148 | 149 | ..\Icons\jc_right.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 150 | 151 | 152 | ..\Icons\betterjoy_icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 153 | 154 | 155 | ..\Icons\jc_left_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 156 | 157 | 158 | ..\Icons\jc_left_s_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 159 | 160 | 161 | ..\Icons\jc_right_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 162 | 163 | 164 | ..\Icons\jc_right_s_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 165 | 166 | 167 | ..\Icons\pro_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 168 | 169 | 170 | ..\Icons\snes_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 171 | 172 | 173 | ..\Icons\nes_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 174 | 175 | 176 | ..\Icons\famicom_i_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 177 | 178 | 179 | ..\Icons\famicom_ii_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 180 | 181 | 182 | ..\Icons\n64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 183 | 184 | 185 | ..\Icons\n64_charging.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 186 | 187 | --------------------------------------------------------------------------------