├── 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