diff --git a/DS4Windows/App.xaml.cs b/DS4Windows/App.xaml.cs
index 93bdbdd06d..2bd8ce495b 100644
--- a/DS4Windows/App.xaml.cs
+++ b/DS4Windows/App.xaml.cs
@@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+using DS4Windows;
using NLog;
using System;
using System.Collections.Generic;
@@ -255,6 +256,22 @@ private void Application_Startup(object sender, StartupEventArgs e)
rootHub.CheckHidHidePresence();
}
+ // Try loading custom devices from disk
+ CustomDeviceInfo[] customDevs = null;
+ if (File.Exists(DS4Devices.CustomDevicesJsonFilePath)) {
+ rootHub.LogDebug(DS4WinWPF.Translations.Strings.CustomDevices_Log_LoadingFile);
+ try {
+ customDevs = DS4Windows.DS4Devices.LoadCustomDevicesListFromDisk();
+ }
+ catch {
+ rootHub.LogDebug(DS4WinWPF.Translations.Strings.CustomDevices_Log_LoadFail);
+ }
+ }
+ if(customDevs != null) {
+ DS4Devices.SetCustomDevices(customDevs);
+ }
+
+
rootHub.LoadPermanentSlotsConfig();
window.LateChecks(parser);
}
diff --git a/DS4Windows/DS4Forms/ControllerRegisterOptionsWindow.xaml b/DS4Windows/DS4Forms/ControllerRegisterOptionsWindow.xaml
index 062d5c33cd..aa0b188227 100644
--- a/DS4Windows/DS4Forms/ControllerRegisterOptionsWindow.xaml
+++ b/DS4Windows/DS4Forms/ControllerRegisterOptionsWindow.xaml
@@ -14,6 +14,8 @@
+
+
@@ -120,4 +122,11 @@
+
+
+
+
+
+
+
diff --git a/DS4Windows/DS4Forms/Converters/HexToIntConverter.cs b/DS4Windows/DS4Forms/Converters/HexToIntConverter.cs
new file mode 100644
index 0000000000..de3fdbc722
--- /dev/null
+++ b/DS4Windows/DS4Forms/Converters/HexToIntConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Data;
+
+namespace DS4WinWPF.DS4Forms.Converters
+{
+ internal class HexToIntConverter : IValueConverter
+ {
+ // Convert int -> hex string
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is int intValue)
+ return intValue.ToString("X"); // uppercase hex (no "0x")
+ return "0";
+ }
+
+ // Convert hex string -> int
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string s && int.TryParse(s, NumberStyles.HexNumber, culture, out int result))
+ return result;
+
+ // Optional: fallback to 0 or throw an exception
+ return 0;
+ }
+ }
+}
diff --git a/DS4Windows/DS4Forms/CustomDevicesEditor.xaml b/DS4Windows/DS4Forms/CustomDevicesEditor.xaml
new file mode 100644
index 0000000000..c3e12f0dec
--- /dev/null
+++ b/DS4Windows/DS4Forms/CustomDevicesEditor.xaml
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DS4Windows/DS4Forms/CustomDevicesEditor.xaml.cs b/DS4Windows/DS4Forms/CustomDevicesEditor.xaml.cs
new file mode 100644
index 0000000000..856a5d7bfc
--- /dev/null
+++ b/DS4Windows/DS4Forms/CustomDevicesEditor.xaml.cs
@@ -0,0 +1,189 @@
+using DS4WinWPF.DS4Forms.ViewModels;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace DS4WinWPF.DS4Forms
+{
+ ///
+ /// Interaction logic for CustomDevicesEditor.xaml
+ ///
+ public partial class CustomDevicesEditor : UserControl
+ {
+ CustomDevicesEditorViewModel ViewModel;
+
+ public CustomDevicesEditor()
+ {
+ ViewModel = new CustomDevicesEditorViewModel();
+ DataContext = ViewModel;
+ InitializeComponent();
+ ResertInfoDescriptionTextToDefault();
+ }
+
+ private void AddBtn_Click(object sender, RoutedEventArgs e)
+ {
+ ViewModel.Add();
+ }
+
+ private void EditBtn_Click(object sender, RoutedEventArgs e)
+ {
+ ViewModel.EditSelectedDevice();
+ }
+
+ private void RemoveBtn_Click(object sender, RoutedEventArgs e)
+ {
+ ViewModel.RemoveSelectedDevice();
+ }
+
+ private void SaveBtn_Click(object sender, RoutedEventArgs e)
+ {
+ ViewModel.SaveChanges();
+ }
+
+ private void CancelChangesBtn_Click(object sender, RoutedEventArgs e)
+ {
+ ViewModel.DiscardChanges();
+ }
+
+ private void OptionsBtn_Click(object sender, RoutedEventArgs e)
+ {
+ var button = sender as Button;
+ if (button?.ContextMenu != null) {
+ // Position the context menu at the bottom-left of the button
+ button.ContextMenu.PlacementTarget = button;
+ button.ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
+ button.ContextMenu.IsOpen = true;
+ }
+ }
+
+ private void ResetListMenuItemBtn_Click(object sender, RoutedEventArgs e)
+ {
+ ViewModel.ResetCustomDevicesList();
+ }
+
+ private void Flag0_ChkBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_Flag0_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void Flag1_ChkBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_Flag1_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void Flag2_ChkBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_Flag2_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void Flag3_ChkBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_Flag3_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void Flag4_ChkBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_Flag4_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void Flag5_ChkBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_Flag5_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void Flag6_ChkBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_Flag6_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void EnableDetection_ChxBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_EnableDetectionDescription;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void HexTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
+ {
+ e.Handled = !System.Text.RegularExpressions.Regex.IsMatch(e.Text, "^[0-9A-Fa-f]+$");
+ }
+
+ private void HexTextBox_Pasting(object sender, DataObjectPastingEventArgs e)
+ {
+ if (e.DataObject.GetDataPresent(typeof(string))) {
+ var text = (string)e.DataObject.GetData(typeof(string));
+ if (!System.Text.RegularExpressions.Regex.IsMatch(text, "^[0-9A-Fa-f]+$")) {
+ e.CancelCommand();
+ }
+ }
+ else {
+ e.CancelCommand();
+ }
+ }
+
+ private void SaveBtn_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_SaveChangesBtn_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void UpdateInfoDescriptionText(string description)
+ {
+ InfoDescription_TextBox.Text = description;
+ }
+
+ private void Name_TextBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_NameTextbox_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void Vid_TextBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ ShowVidPidInformationDescription();
+ }
+
+ private void ShowVidPidInformationDescription()
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_VidPid_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void Pid_TextBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ ShowVidPidInformationDescription();
+ }
+
+ private void InputDevType_Combobox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_InputDeviceType_Info;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void ConTypeDeterminer_ComboBox_MouseEnter(object sender, MouseEventArgs e)
+ {
+ var nD = DS4WinWPF.Translations.Strings.CustomDevices_ConnectionType_Description;
+ UpdateInfoDescriptionText(nD);
+ }
+
+ private void EditorArea_Border_MouseLeave(object sender, MouseEventArgs e)
+ {
+ ResertInfoDescriptionTextToDefault();
+ }
+
+ private void ResertInfoDescriptionTextToDefault()
+ {
+ var nT = "";
+ UpdateInfoDescriptionText(nT);
+
+ }
+ }
+
+
+}
diff --git a/DS4Windows/DS4Forms/MainWindow.xaml.cs b/DS4Windows/DS4Forms/MainWindow.xaml.cs
index b83f59f35e..355c90418a 100644
--- a/DS4Windows/DS4Forms/MainWindow.xaml.cs
+++ b/DS4Windows/DS4Forms/MainWindow.xaml.cs
@@ -1798,6 +1798,23 @@ private void ChecklogViewBtn_Click(object sender, RoutedEventArgs e)
private void DeviceOptionSettingsBtn_Click(object sender, RoutedEventArgs e)
{
+ // Check if a ControllerRegisterOptionsWindow is already open
+ foreach (Window window in Application.Current.Windows) {
+ if (window is ControllerRegisterOptionsWindow existingWindow) {
+ if (existingWindow.WindowState == WindowState.Minimized) {
+ existingWindow.WindowState = WindowState.Normal;
+ }
+
+ // Temporarily set TopMost to true to force it to front
+ existingWindow.Topmost = true;
+ existingWindow.Topmost = false;
+
+ existingWindow.Activate(); // Set focus
+ return;
+ }
+ }
+
+ // If not found, create and show a new one
ControllerRegisterOptionsWindow optsWindow =
new ControllerRegisterOptionsWindow(Program.rootHub.DeviceOptions, Program.rootHub);
diff --git a/DS4Windows/DS4Forms/ViewModels/CustomDevicesEditorViewModel.cs b/DS4Windows/DS4Forms/ViewModels/CustomDevicesEditorViewModel.cs
new file mode 100644
index 0000000000..e910dd29d9
--- /dev/null
+++ b/DS4Windows/DS4Forms/ViewModels/CustomDevicesEditorViewModel.cs
@@ -0,0 +1,372 @@
+using DS4Windows;
+using DS4Windows.InputDevices;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.ComponentModel.Design.Serialization;
+using System.Diagnostics.Eventing.Reader;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using WPFLocalizeExtension.Engine;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace DS4WinWPF.DS4Forms.ViewModels
+{
+ internal class CustomDevicesEditorViewModel : INotifyPropertyChanged
+ {
+ private ObservableCollection customDevicesVM;
+ private CustomDeviceVM selectedDeviceVM;
+ private string statusMessage = "";
+ private bool isReadWriteLockActive = true;
+ private bool isInEditMode;
+
+ private bool isReadyToUse = false;
+ private CustomDeviceVM editorVM;
+
+ string PathToCustomDevsJson => DS4Devices.CustomDevicesJsonFilePath;
+
+ ///
+ /// View model for the custom devices types editor.
+ ///
+ public CustomDevicesEditorViewModel()
+ {
+ Initialization();
+ }
+
+ public ObservableCollection CustomDevicesVM
+ {
+ get => customDevicesVM;
+ set {
+ customDevicesVM = value;
+ OnPropertyChanged(nameof(CustomDevicesVM));
+ }
+ }
+
+ public CustomDeviceVM SelectedDeviceVM
+ {
+ get => selectedDeviceVM;
+ set {
+ selectedDeviceVM = value;
+ EditorVM = value == null ? null : new(value.GetDeviceInfo());
+ OnPropertyChanged(nameof(SelectedDeviceVM));
+ }
+ }
+
+ public CustomDeviceVM EditorVM
+ {
+ get => editorVM;
+ private set {
+ editorVM = value;
+ OnPropertyChanged(nameof(EditorVM));
+ }
+ }
+
+ public bool IsInEditMode
+ {
+ get => isInEditMode;
+ set {
+ isInEditMode = value;
+ OnPropertyChanged(nameof(IsInEditMode));
+ }
+ }
+
+ public bool IsReadyToUse
+ {
+ get => isReadyToUse;
+ private set {
+
+ isReadyToUse = value;
+ OnPropertyChanged(nameof(IsReadyToUse));
+ }
+ }
+
+ public string StatusMessage
+ {
+ get => statusMessage;
+ private set {
+ statusMessage = $"[{DateTime.Now.ToString("HH:mm:ss")}] {value}";
+ OnPropertyChanged(nameof(StatusMessage));
+ }
+ }
+
+ public bool IsReadWriteLockActive
+ {
+ get => isReadWriteLockActive;
+ set {
+ isReadWriteLockActive = value;
+ OnPropertyChanged(nameof(IsReadWriteLockActive));
+ }
+ }
+
+ public async Task Initialization()
+ {
+ if (await ReadCustomDevicesListFromDisk()) {
+ IsReadyToUse = true;
+ return true;
+ }
+ return false;
+ }
+
+ public async Task ReadCustomDevicesListFromDisk()
+ {
+ IsReadWriteLockActive = true;
+ if (File.Exists(PathToCustomDevsJson)) {
+ try {
+
+ List customDeviceInfos = new List();
+
+ var wot = await Task.Run(() => DS4Devices.LoadCustomDevicesListFromDisk());
+ var wotwat = new ObservableCollection();
+ foreach (var devInfo in wot) {
+ wotwat.Add(new(devInfo));
+ }
+ CustomDevicesVM = new ObservableCollection(wotwat);
+ IsReadWriteLockActive = false;
+ StatusMessage = Translations.Strings.CustomDevices_Status_LoadSuccess;
+ return true;
+ }
+ catch {
+ StatusMessage = Translations.Strings.CustomDevices_StatusLoad_Failed;
+ }
+ }
+ else {
+
+ StatusMessage = Translations.Strings.CustomDevices_Status_ListNotFound;
+ }
+
+ return false;
+ }
+
+ public bool EditSelectedDevice()
+ {
+ if (EditorVM != null) {
+ IsInEditMode = true;
+ return true;
+ }
+ return false;
+ }
+
+ public async Task SaveCustomDevicesListToDisk()
+ {
+ bool status = true;
+ IsReadWriteLockActive = true;
+ StatusMessage = Translations.Strings.CustomDevices_Status_WritingToDisk;
+ try {
+ var listOfCustomDevs = CustomDevicesVM.ToList();
+ await Task.Run(() => DS4Devices.SaveCustomDevicesListToDisk(GetCustomDevicesInfo()));
+ StatusMessage = Translations.Strings.CustomDevices_Status_SaveSuccess;
+ }
+ catch {
+ StatusMessage = Translations.Strings.CustomDevices_Status_SaveFailed;
+ status = false;
+ }
+ IsReadWriteLockActive = false;
+ return status;
+ }
+
+ public async Task SaveChanges()
+ {
+ IsReadWriteLockActive = true;
+ if ( EditorVM == null) {
+ return false;
+ }
+
+ CustomDeviceVM newVM = new(EditorVM.GetDeviceInfo());
+ if(SelectedDeviceVM != null) {
+ int index = CustomDevicesVM.IndexOf(SelectedDeviceVM);
+ if (index >= 0) {
+ CustomDevicesVM[index] = newVM;
+ }
+ else {
+ return false;
+ }
+ }
+ else{
+ CustomDevicesVM.Add(newVM);
+ }
+ SelectedDeviceVM = newVM;
+ IsInEditMode = false;
+ MakeChangesEffective();
+ await SaveCustomDevicesListToDisk();
+ return true;
+ }
+
+ private CustomDeviceInfo[] GetCustomDevicesInfo()
+ {
+ var customDevsInfo = new CustomDeviceInfo[CustomDevicesVM.Count];
+ for ( int i = 0; i < CustomDevicesVM.Count; i++ ) {
+ customDevsInfo[i] = CustomDevicesVM[i].GetDeviceInfo();
+ }
+ return customDevsInfo;
+ }
+
+ public bool DiscardChanges()
+ {
+ SelectedDeviceVM = null;
+ IsInEditMode = false;
+ return true;
+ }
+
+ public bool Add()
+ {
+ SelectedDeviceVM = null;
+ var newCustomDev = new CustomDeviceInfo()
+ {
+ Name = "Custom Device Type Name",
+ Vid = 0,
+ Pid = 0,
+ ConnectionTypeDeterminer = null,
+ FeatureSet = VidPidFeatureSet.DefaultDS4,
+ };
+ EditorVM = new(newCustomDev);
+ EditSelectedDevice();
+ return true;
+ }
+
+ public async Task RemoveSelectedDevice() {
+ if(SelectedDeviceVM == null) {
+ return false;
+ }
+
+ int index = CustomDevicesVM.IndexOf(SelectedDeviceVM);
+ if (index >= 0) {
+ CustomDevicesVM.RemoveAt(index);
+ await SaveCustomDevicesListToDisk();
+ MakeChangesEffective();
+ }
+ SelectedDeviceVM = null;
+ return true;;
+ }
+
+ public async void ResetCustomDevicesList()
+ {
+ IsReadWriteLockActive = false;
+ IsInEditMode = false;
+ SelectedDeviceVM = null;
+ CustomDevicesVM = new();
+ await SaveCustomDevicesListToDisk();
+ IsReadyToUse = true;
+ MakeChangesEffective();
+ }
+
+ public void MakeChangesEffective()
+ {
+ DS4Devices.SetCustomDevices(GetCustomDevicesInfo());
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ internal class CustomDeviceVM : INotifyPropertyChanged
+ {
+ public CustomDeviceVM(CustomDeviceInfo cDev)
+ {
+ EnableDetection = cDev.EnableDetection;
+ Name = cDev.Name;
+ Vid = cDev.Vid;
+ Pid = cDev.Pid;
+ var connectionTypeDeterminer = cDev.ConnectionTypeDeterminer;
+ ConnectionType = connectionTypeDeterminer == null ? ConnectionTypeDeterminer.Undefined : cDev.ConnectionTypeDeterminer;
+ InputDeviceType = cDev.InputDevType;
+ Flags = (int)cDev.FeatureSet;
+ FromFlags(cDev.FeatureSet);
+ }
+
+ public bool EnableDetection { get; set; }
+ public string Name { get; set; }
+ public int Vid { get; set; }
+ public int Pid { get; set; }
+ public string VidPidInHexString => $"{Vid:X4}/{Pid:X4}";
+
+ public string FlagsAsBinaryString => Convert.ToString(Flags, 2).PadLeft(7, '0');
+ public InputDeviceType InputDeviceType { get; set; }
+ public ConnectionTypeDeterminer? ConnectionType { get; set; }
+
+ public List TypesOfConnectionDeterminers => new List(){
+ (int) ConnectionTypeDeterminer.Undefined,
+ ConnectionTypeDeterminer.DS4,
+ ConnectionTypeDeterminer.DualSense,
+ ConnectionTypeDeterminer.SwitchPro,
+ ConnectionTypeDeterminer.JoyCon,
+ ConnectionTypeDeterminer.DS3,
+ };
+
+ public List TypesOfInputDevice => new List(){
+ InputDeviceType.DS3,
+ InputDeviceType.DS4,
+ InputDeviceType.DualSense,
+ InputDeviceType.SwitchPro,
+ InputDeviceType.JoyConL,
+ InputDeviceType.JoyConR,
+ InputDeviceType.JoyConGrip,
+ };
+
+ public string ConnectionTypeAsString => ConnectionType?.ToString();
+ public int Flags { get; }
+ public bool IsDefaultDs4FlagEnabled { get; set; }
+ public bool IsOnlyInputData0x01FlagEnabled { get; set; }
+ public bool IsOnlyOutputData0x05FlagEnabled { get; set; }
+ public bool IsNoOutputDataFlagEnabled { get; set; }
+ public bool IsNoBatteryReadingFlagEnabled { get; set; }
+ public bool IsNoGyroCalibtureSetFlagEnabled { get; set; }
+ public bool IsMonitorAudioFlagEnabled { get; set; }
+ public bool IsVendorDefinedDeviceFlagEnabled { get; set; }
+
+ public bool IsInEditMode { get; set; }
+
+ public bool ReadCustomDevicesListFromDisk()
+ {
+ return true;
+ }
+
+ public CustomDeviceInfo GetDeviceInfo()
+ {
+ return new CustomDeviceInfo()
+ {
+ EnableDetection = EnableDetection,
+ Name = this.Name,
+ Vid = this.Vid,
+ Pid = this.Pid,
+ InputDevType = this.InputDeviceType,
+ ConnectionTypeDeterminer = this.ConnectionType,
+ FeatureSet = (VidPidFeatureSet)ToUShortFlags()
+ };
+ }
+
+ public ushort ToUShortFlags() =>
+ (ushort)(
+ (IsOnlyInputData0x01FlagEnabled ? 1 << 0 : 0) |
+ (IsOnlyOutputData0x05FlagEnabled ? 1 << 1 : 0) |
+ (IsNoOutputDataFlagEnabled ? 1 << 2 : 0) |
+ (IsNoBatteryReadingFlagEnabled ? 1 << 3 : 0) |
+ (IsNoGyroCalibtureSetFlagEnabled ? 1 << 4 : 0) |
+ (IsMonitorAudioFlagEnabled ? 1 << 5 : 0) |
+ (IsVendorDefinedDeviceFlagEnabled ? 1 << 6 : 0)
+ );
+
+ public void FromFlags(VidPidFeatureSet flags)
+ {
+ IsOnlyInputData0x01FlagEnabled = flags.HasFlag(VidPidFeatureSet.OnlyInputData0x01);
+ IsOnlyOutputData0x05FlagEnabled = flags.HasFlag(VidPidFeatureSet.OnlyOutputData0x05);
+ IsNoOutputDataFlagEnabled = flags.HasFlag(VidPidFeatureSet.NoOutputData);
+ IsNoBatteryReadingFlagEnabled = flags.HasFlag(VidPidFeatureSet.NoBatteryReading);
+ IsNoGyroCalibtureSetFlagEnabled = flags.HasFlag(VidPidFeatureSet.NoGyroCalib);
+ IsMonitorAudioFlagEnabled = flags.HasFlag(VidPidFeatureSet.MonitorAudio);
+ IsVendorDefinedDeviceFlagEnabled = flags.HasFlag(VidPidFeatureSet.VendorDefinedDevice);
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/DS4Windows/DS4Library/DS4Devices.cs b/DS4Windows/DS4Library/DS4Devices.cs
index 2f16fccd18..294f0780af 100644
--- a/DS4Windows/DS4Library/DS4Devices.cs
+++ b/DS4Windows/DS4Library/DS4Devices.cs
@@ -19,9 +19,11 @@ You should have received a copy of the GNU General Public License
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Principal;
+using System.Text.Json;
using DS4Windows.InputDevices;
namespace DS4Windows
@@ -68,6 +70,60 @@ internal VidPidInfo(int vid, int pid, string name = "Generic DS4", InputDeviceTy
}
}
+ public enum ConnectionTypeDeterminer
+ {
+ Undefined,
+ DS4,
+ DualSense,
+ SwitchPro,
+ JoyCon,
+ DS3,
+ }
+
+ public class CustomDeviceInfo
+ {
+ public string Name { get; init; }
+ public int Vid { get; init; }
+ public int Pid { get; init; }
+ public InputDeviceType InputDevType { get; init; }
+ public VidPidFeatureSet FeatureSet { get; init; }
+ public bool EnableDetection { get; init; }
+ public ConnectionTypeDeterminer? ConnectionTypeDeterminer { get; init; }
+
+ public CustomDeviceInfo() {}
+
+ public VidPidInfo GetVidPidInfo()
+ {
+ CheckConnectionDelegate connectionTypeDeterminer = null;
+ switch (this.ConnectionTypeDeterminer) {
+
+ case DS4Windows.ConnectionTypeDeterminer.DS4:
+ connectionTypeDeterminer = DS4Device.HidConnectionType;
+ break;
+ case DS4Windows.ConnectionTypeDeterminer.DualSense:
+ connectionTypeDeterminer = DualSenseDevice.HidConnectionType;
+ break;
+ case DS4Windows.ConnectionTypeDeterminer.SwitchPro:
+ connectionTypeDeterminer = SwitchProDevice.HidConnectionType;
+ break;
+ case DS4Windows.ConnectionTypeDeterminer.JoyCon:
+ connectionTypeDeterminer = JoyConDevice.HidConnectionType;
+ break;
+ case DS4Windows.ConnectionTypeDeterminer.DS3:
+ connectionTypeDeterminer = DS3Device.HidConnectionType;
+ break;
+ case DS4Windows.ConnectionTypeDeterminer.Undefined:
+ connectionTypeDeterminer = null;
+ break;
+ case null:
+ break;
+ default:
+ break;
+ }
+ return new VidPidInfo(Vid,Pid,Name,InputDevType,FeatureSet,connectionTypeDeterminer);
+ }
+ }
+
public class RequestElevationArgs : EventArgs
{
public const int STATUS_SUCCESS = 0;
@@ -137,6 +193,10 @@ public class DS4Devices
internal const int JOYCON_R_PRODUCT_ID = 0x2007;
internal const int JOYCON_CHARGING_GRIP_PRODUCT_ID = 0x200E;
+
+ public const string customDevicesJsonFileName = "CustomDevices.json";
+ public static string CustomDevicesJsonFilePath => Path.Combine(DS4Windows.Global.appdatapath, customDevicesJsonFileName);
+
// https://support.steampowered.com/kb_article.php?ref=5199-TOKV-4426&l=english web site has a list of other PS4 compatible device VID/PID values and brand names.
// However, not all those are guaranteed to work with DS4Windows app so support is added case by case when users of DS4Windows app tests non-official DS4 gamepads.
@@ -186,6 +246,8 @@ public class DS4Devices
new VidPidInfo(0x0C12, 0x0E15, "Playmax Wired Controller (PS4)", InputDeviceType.DS4, VidPidFeatureSet.NoBatteryReading | VidPidFeatureSet.NoGyroCalib), // Generic PS4 Controller by Playmax (brand primarily in New Zealand). Standard Wired PS4 controller, no Gyro, no Lightbar, no Battery. There is a newer model but I'm not sure if it uses a different Vid or Pid yet.
};
+ private static CustomDeviceInfo[] customDevices = null;
+ private static VidPidInfo[] supportedDevices = knownDevices;
private static bool detectNewControllers = true;
private static long timestamp;
@@ -236,10 +298,10 @@ public static void findControllers()
{
lock (Devices)
{
- IEnumerable hDevices = HidDevices.EnumerateDS4(knownDevices);
+ IEnumerable hDevices = HidDevices.EnumerateDS4(supportedDevices);
hDevices = hDevices.Where(d =>
{
- VidPidInfo metainfo = knownDevices.Single(x => x.vid == d.Attributes.VendorId &&
+ VidPidInfo metainfo = supportedDevices.Single(x => x.vid == d.Attributes.VendorId &&
x.pid == d.Attributes.ProductId);
return PreparePendingDevice(d, metainfo);
});
@@ -252,7 +314,7 @@ public static void findControllers()
{
// Need VidPidInfo instance to get CheckConnectionDelegate and
// check the connection type
- VidPidInfo metainfo = knownDevices.Single(x => x.vid == d.Attributes.VendorId &&
+ VidPidInfo metainfo = supportedDevices.Single(x => x.vid == d.Attributes.VendorId &&
x.pid == d.Attributes.ProductId);
//return DS4Device.HidConnectionType(d);
@@ -270,7 +332,7 @@ public static void findControllers()
//foreach (HidDevice hDevice in hDevices)
{
HidDevice hDevice = tempList[i];
- VidPidInfo metainfo = knownDevices.Single(x => x.vid == hDevice.Attributes.VendorId &&
+ VidPidInfo metainfo = supportedDevices.Single(x => x.vid == hDevice.Attributes.VendorId &&
x.pid == hDevice.Attributes.ProductId);
if (!metainfo.featureSet.HasFlag(VidPidFeatureSet.VendorDefinedDevice) &&
@@ -411,6 +473,70 @@ public static IEnumerable getDS4Controllers()
}
}
+ public static CustomDeviceInfo[] LoadCustomDevicesListFromDisk()
+ {
+ var options = new JsonSerializerOptions { WriteIndented = true };
+ string jsonString = File.ReadAllText(CustomDevicesJsonFilePath);
+ return JsonSerializer.Deserialize(jsonString);
+ }
+
+ public static void SaveCustomDevicesListToDisk(CustomDeviceInfo[] cDevsList)
+ {
+ var options = new JsonSerializerOptions { WriteIndented = true };
+ string jsonString = JsonSerializer.Serialize(cDevsList, options);
+ File.WriteAllText(CustomDevicesJsonFilePath, jsonString);
+ }
+
+ private static VidPidInfo[] GetSupportedDevices()
+ {
+ var supportedDevicesList = DS4Devices.knownDevices.ToList();
+
+ if (customDevices != null) {
+ foreach (var cDev in customDevices) {
+ AppLogger.LogToGui($"[Custom device] {cDev.Vid:X4}/{cDev.Pid:X4} - {cDev.Name} - [ {(cDev.EnableDetection ? "Detection: ON" : " Detection: OFF")} ][ Type: {(InputDeviceType)cDev.InputDevType} ][ FeatureSet: {Convert.ToString((int)cDev.FeatureSet, 2).PadLeft(7,'0')} ][ ConnectionTypeDeterminer: {((cDev.ConnectionTypeDeterminer != null) ? (ConnectionTypeDeterminer)cDev.ConnectionTypeDeterminer : null )} ].", false);
+ int index = supportedDevicesList.FindIndex(device => (device.vid, device.pid) == (cDev.Vid, cDev.Pid));
+ if(index >= 0) {
+ if (cDev.EnableDetection) {
+ supportedDevicesList[index] = cDev.GetVidPidInfo();
+ }
+ else {
+ supportedDevicesList.RemoveAt(index);
+ }
+ }
+ else {
+ if(cDev.EnableDetection) {
+ supportedDevicesList.Add(cDev.GetVidPidInfo());
+ }
+ else {
+ //
+ }
+
+ }
+
+ }
+ }
+
+ return supportedDevicesList.ToArray();
+ }
+
+ ///
+ /// Sets a new array of Custom Devices to be used in the detection logic, while also refreshing the SupportedDevices array
+ ///
+ ///
+ public static void SetCustomDevices(CustomDeviceInfo[] customDevices)
+ {
+ DS4Devices.customDevices = customDevices.ToArray();
+ supportedDevices = GetSupportedDevices();
+ }
+
+ ///
+ /// Returns a copy of the current array of Custom Devices
+ ///
+ ///
+ public static CustomDeviceInfo[] GetCustomDevices() {
+ return customDevices.ToArray();
+ }
+
public static void stopControllers()
{
lock (Devices)
diff --git a/DS4Windows/Translations/Strings.Designer.cs b/DS4Windows/Translations/Strings.Designer.cs
index c5bd10ca58..cb8cac62b4 100644
--- a/DS4Windows/Translations/Strings.Designer.cs
+++ b/DS4Windows/Translations/Strings.Designer.cs
@@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -887,6 +888,283 @@ public static string Custom {
}
}
+ ///
+ /// Looks up a localized string similar to The type of logic to be used to determine the connection type of your device.
+ ///Must match your real device type.
+ ///.
+ ///
+ public static string CustomDevices_ConnectionType_Description {
+ get {
+ return ResourceManager.GetString("CustomDevices_ConnectionType_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Connection Type Determiner.
+ ///
+ public static string CustomDevices_ConnectionTypeDeterminer {
+ get {
+ return ResourceManager.GetString("CustomDevices_ConnectionTypeDeterminer", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Enable detection.
+ ///
+ public static string CustomDevices_EnableDetection {
+ get {
+ return ResourceManager.GetString("CustomDevices_EnableDetection", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to If enabled then devices with the VID/PID defined here will become detectable.
+ ///
+ ///Keep in mind that for the device to work properly it has to "communicate" the same way as the chosen controller type (DS4, DualSense, Switch Pro, Joy-Con etc).
+ ///
+ ///If the VID/PID already exists in the natively supported devices list then the custom one will have priority (native one is replaced)..
+ ///
+ public static string CustomDevices_EnableDetectionDescription {
+ get {
+ return ResourceManager.GetString("CustomDevices_EnableDetectionDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag 0
+ ///Always read input reports in the "wired" format (valid only for PS4 controllers).
+ ///
+ ///For fake/third-party PS4 controllers that always send their input report in the "wired" format even when connected wirelessly..
+ ///
+ public static string CustomDevices_Flag0_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_Flag0_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag 1
+ ///Always send output reports in the "wired" format (valid only for PS4 controllers).
+ ///
+ ///For fake/third-party PS4 gamepads that only understand output reports (lightbar/rumble instructions) in the "wired" format even when connected wirelessly..
+ ///
+ public static string CustomDevices_Flag1_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_Flag1_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag 2
+ ///Fully disable output reports.
+ ///
+ ///For fake/third-party PS4 devices that don't support lightbar/rumble instructions at all.
+ ///.
+ ///
+ public static string CustomDevices_Flag2_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_Flag2_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag 3
+ ///Consider gamepad's battery as always at 99%.
+ ///
+ ///For devices that don't properly report their correct battery..
+ ///
+ public static string CustomDevices_Flag3_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_Flag3_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag 4
+ ///Disable gyro calibration.
+ ///
+ ///For devices that don't have gyro, or do have gyro but don't support the gyro calibration proccess..
+ ///
+ public static string CustomDevices_Flag4_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_Flag4_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag 5
+ ///Monitor audio (For PS4 gamepads)
+ ///
+ ///Your guess on what this flag do is as good as mine :D..
+ ///
+ public static string CustomDevices_Flag5_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_Flag5_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flag 6
+ ///Detect device even if its Vendor Defined.
+ ///
+ ///This app is coded to only search for devices classified as gamepad or joystick. This flag will make this device detectable even if its classified as "vendor-defined" (necessary for DS3 controllers running under DsHidMini's DS4W mode)..
+ ///
+ public static string CustomDevices_Flag6_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_Flag6_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Flags.
+ ///
+ public static string CustomDevices_Flags {
+ get {
+ return ResourceManager.GetString("CustomDevices_Flags", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Information - hover mouse over options to check.
+ ///
+ public static string CustomDevices_InfoHeader {
+ get {
+ return ResourceManager.GetString("CustomDevices_InfoHeader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Input device type.
+ ///
+ public static string CustomDevices_InputDeviceType {
+ get {
+ return ResourceManager.GetString("CustomDevices_InputDeviceType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to What your device is in reality, or what it tries to copy/mimic.
+ ///
+ ///This MUST match your device type (or how it presents itself). If you choose the wrong type or if your device doesn't properly communicate properly as this app expects then it won't function correctly..
+ ///
+ public static string CustomDevices_InputDeviceType_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_InputDeviceType_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to teste.
+ ///
+ public static string CustomDevices_Log_LoadFail {
+ get {
+ return ResourceManager.GetString("CustomDevices_Log_LoadFail", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Loading list of custom devices type from disk..
+ ///
+ public static string CustomDevices_Log_LoadingFile {
+ get {
+ return ResourceManager.GetString("CustomDevices_Log_LoadingFile", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Friendly name to facilitate identification.
+ ///
+ ///Can be the official name or a custom one, doesn't affect detection..
+ ///
+ public static string CustomDevices_NameTextbox_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_NameTextbox_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to (Re-)create list of custom devices (current list will be lost)..
+ ///
+ public static string CustomDevices_ResetOption {
+ get {
+ return ResourceManager.GetString("CustomDevices_ResetOption", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to After changes are made it may be necessary to reconnected gamepads for them to become detectable..
+ ///
+ public static string CustomDevices_SaveChangesBtn_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_SaveChangesBtn_Info", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to List of custom devices types not found on disk..
+ ///
+ public static string CustomDevices_Status_ListNotFound {
+ get {
+ return ResourceManager.GetString("CustomDevices_Status_ListNotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Successfully loaded from disk the list of custom devices types.
+ ///
+ public static string CustomDevices_Status_LoadSuccess {
+ get {
+ return ResourceManager.GetString("CustomDevices_Status_LoadSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to save changes to disk!.
+ ///
+ public static string CustomDevices_Status_SaveFailed {
+ get {
+ return ResourceManager.GetString("CustomDevices_Status_SaveFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Successfully saved changes to disk.
+ ///
+ public static string CustomDevices_Status_SaveSuccess {
+ get {
+ return ResourceManager.GetString("CustomDevices_Status_SaveSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Writing changes to disk.
+ ///
+ public static string CustomDevices_Status_WritingToDisk {
+ get {
+ return ResourceManager.GetString("CustomDevices_Status_WritingToDisk", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to load from disk the list of custom devices types. Try restarting the app or rebuilding the list from zero (see options).
+ ///
+ public static string CustomDevices_StatusLoad_Failed {
+ get {
+ return ResourceManager.GetString("CustomDevices_StatusLoad_Failed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The VID (Vendor ID) and PID (Product ID) of the device.
+ ///
+ ///This app looks for compatible devices by directly looking for their VID/PID, so this must absolutely be the same as your device's for it to be detectable..
+ ///
+ public static string CustomDevices_VidPid_Info {
+ get {
+ return ResourceManager.GetString("CustomDevices_VidPid_Info", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Custom Exe Name.
///
@@ -2550,6 +2828,15 @@ public static string ProfileEditor_VirtualDS4TrigOutput {
}
}
+ ///
+ /// Looks up a localized string similar to .
+ ///
+ public static string ProfileEditor_VirtualTrigButtonOutput {
+ get {
+ return ResourceManager.GetString("ProfileEditor.VirtualTrigButtonOutput", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Profile Folder.
///
diff --git a/DS4Windows/Translations/Strings.resx b/DS4Windows/Translations/Strings.resx
index 61010b4632..f60e605c6d 100644
--- a/DS4Windows/Translations/Strings.resx
+++ b/DS4Windows/Translations/Strings.resx
@@ -1478,4 +1478,122 @@ Please click OK to confirm that you have read the message. Press Cancel to be re
Inverse motors
+
+
+
+
+ (Re-)create list of custom devices (current list will be lost).
+
+
+ Enable detection
+
+
+ Connection Type Determiner
+
+
+ Flags
+
+
+ Information - hover mouse over options to check
+
+
+ Flag 0
+Always read input reports in the "wired" format (valid only for PS4 controllers).
+
+For fake/third-party PS4 controllers that always send their input report in the "wired" format even when connected wirelessly.
+
+
+ Flag 1
+Always send output reports in the "wired" format (valid only for PS4 controllers).
+
+For fake/third-party PS4 gamepads that only understand output reports (lightbar/rumble instructions) in the "wired" format even when connected wirelessly.
+
+
+ Flag 2
+Fully disable output reports.
+
+For fake/third-party PS4 devices that don't support lightbar/rumble instructions at all.
+
+
+
+ Flag 3
+Consider gamepad's battery as always at 99%.
+
+For devices that don't properly report their correct battery.
+
+
+ Flag 4
+Disable gyro calibration.
+
+For devices that don't have gyro, or do have gyro but don't support the gyro calibration proccess.
+
+
+ Flag 5
+Monitor audio (For PS4 gamepads)
+
+Your guess on what this flag do is as good as mine :D.
+
+
+ Flag 6
+Detect device even if its Vendor Defined.
+
+This app is coded to only search for devices classified as gamepad or joystick. This flag will make this device detectable even if its classified as "vendor-defined" (necessary for DS3 controllers running under DsHidMini's DS4W mode).
+
+
+ If enabled then devices with the VID/PID defined here will become detectable.
+
+Keep in mind that for the device to work properly it has to "communicate" the same way as the chosen controller type (DS4, DualSense, Switch Pro, Joy-Con etc).
+
+If the VID/PID already exists in the natively supported devices list then the custom one will have priority (native one is replaced).
+
+
+ After changes are made it may be necessary to reconnected gamepads for them to become detectable.
+
+
+ Friendly name to facilitate identification.
+
+Can be the official name or a custom one, doesn't affect detection.
+
+
+ The VID (Vendor ID) and PID (Product ID) of the device.
+
+This app looks for compatible devices by directly looking for their VID/PID, so this must absolutely be the same as your device's for it to be detectable.
+
+
+ What your device is in reality, or what it tries to copy/mimic.
+
+This MUST match your device type (or how it presents itself). If you choose the wrong type or if your device doesn't properly communicate properly as this app expects then it won't function correctly.
+
+
+ The type of logic to be used to determine the connection type of your device.
+Must match your real device type.
+
+
+
+ Successfully loaded from disk the list of custom devices types
+
+
+ Failed to load from disk the list of custom devices types. Try restarting the app or rebuilding the list from zero (see options)
+
+
+ List of custom devices types not found on disk.
+
+
+ Writing changes to disk
+
+
+ Successfully saved changes to disk
+
+
+ Failed to save changes to disk!
+
+
+ Input device type
+
+
+ Loading list of custom devices type from disk.
+
+
+ teste
+
\ No newline at end of file