diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/LedConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/LedConfigController.cs index 21727d62e..8ed68cd07 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/LedConfigController.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/LedConfigController.cs @@ -2,14 +2,19 @@ { public class LedConfigController { - /// - /// Packed RGB int of the color - /// - public uint LedColor { get; set; } - /// /// Enable LED color changing by the emulator /// public bool EnableLed { get; set; } + + /// + /// Ignores the color and disables the LED entirely. + /// + public bool TurnOffLed { get; set; } + + /// + /// Packed RGB int of the color + /// + public uint LedColor { get; set; } } } diff --git a/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs b/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs index ed22c3661..f64e1c479 100644 --- a/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs +++ b/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using SDL2; using System; using System.Collections.Generic; @@ -86,7 +87,7 @@ namespace Ryujinx.Input.SDL2 Id = driverId; Features = GetFeaturesFlag(); _triggerThreshold = 0.0f; - + // Enable motion tracking if (Features.HasFlag(GamepadFeaturesFlag.Motion)) { @@ -102,6 +103,18 @@ namespace Ryujinx.Input.SDL2 } } + public void SetLed(uint packedRgb) + { + if (!Features.HasFlag(GamepadFeaturesFlag.Led)) return; + + byte red = packedRgb > 0 ? (byte)(packedRgb >> 16) : (byte)0; + byte green = packedRgb > 0 ? (byte)(packedRgb >> 8) : (byte)0; + byte blue = packedRgb > 0 ? (byte)(packedRgb % 256) : (byte)0; + + if (SDL_GameControllerSetLED(_gamepadHandle, red, green, blue) != 0) + Logger.Error?.Print(LogClass.Hid, "LED is not supported on this game controller."); + } + private GamepadFeaturesFlag GetFeaturesFlag() { GamepadFeaturesFlag result = GamepadFeaturesFlag.None; @@ -112,9 +125,7 @@ namespace Ryujinx.Input.SDL2 result |= GamepadFeaturesFlag.Motion; } - int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100); - - if (error == 0) + if (SDL_GameControllerHasRumble(_gamepadHandle) == SDL_bool.SDL_TRUE) { result |= GamepadFeaturesFlag.Rumble; } @@ -220,6 +231,14 @@ namespace Ryujinx.Input.SDL2 { _configuration = (StandardControllerInputConfig)configuration; + if (Features.HasFlag(GamepadFeaturesFlag.Led) && _configuration.Led.EnableLed) + { + if (_configuration.Led.TurnOffLed) + (this as IGamepad).ClearLed(); + else + SetLed(_configuration.Led.LedColor); + } + _buttonsUserMapping.Clear(); // First update sticks diff --git a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs index c580e4e7d..251f53cba 100644 --- a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs +++ b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs @@ -173,5 +173,16 @@ namespace Ryujinx.Input.SDL2 return new SDL2Gamepad(gamepadHandle, id); } + + public IEnumerable GetGamepads() + { + lock (_gamepadsIds) + { + foreach (string gamepadId in _gamepadsIds) + { + yield return GetGamepad(gamepadId); + } + } + } } } diff --git a/src/Ryujinx.Input.SDL2/SDL2Keyboard.cs b/src/Ryujinx.Input.SDL2/SDL2Keyboard.cs index 8d6a30d11..270af5114 100644 --- a/src/Ryujinx.Input.SDL2/SDL2Keyboard.cs +++ b/src/Ryujinx.Input.SDL2/SDL2Keyboard.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Logging; using System; using System.Collections.Generic; using System.Numerics; @@ -385,6 +386,11 @@ namespace Ryujinx.Input.SDL2 } } + public void SetLed(uint packedRgb) + { + Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL2Keyboard"); + } + public void SetTriggerThreshold(float triggerThreshold) { // No operations diff --git a/src/Ryujinx.Input.SDL2/SDL2Mouse.cs b/src/Ryujinx.Input.SDL2/SDL2Mouse.cs index 37b356b76..eb86fa799 100644 --- a/src/Ryujinx.Input.SDL2/SDL2Mouse.cs +++ b/src/Ryujinx.Input.SDL2/SDL2Mouse.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Logging; using System; using System.Drawing; using System.Numerics; @@ -76,6 +77,11 @@ namespace Ryujinx.Input.SDL2 throw new NotImplementedException(); } + public void SetLed(uint packedRgb) + { + Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL2Mouse"); + } + public void SetTriggerThreshold(float triggerThreshold) { throw new NotImplementedException(); diff --git a/src/Ryujinx.Input.SDL2/SDL2MouseDriver.cs b/src/Ryujinx.Input.SDL2/SDL2MouseDriver.cs index 768ea8c62..7a9679901 100644 --- a/src/Ryujinx.Input.SDL2/SDL2MouseDriver.cs +++ b/src/Ryujinx.Input.SDL2/SDL2MouseDriver.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Numerics; @@ -164,6 +165,8 @@ namespace Ryujinx.Input.SDL2 return new SDL2Mouse(this); } + public IEnumerable GetGamepads() => [GetGamepad("0")]; + public void Dispose() { if (_isDisposed) diff --git a/src/Ryujinx.Input.SDL2/SDLKeyboardDriver.cs b/src/Ryujinx.Input.SDL2/SDLKeyboardDriver.cs index 965f7935a..69e12bda0 100644 --- a/src/Ryujinx.Input.SDL2/SDLKeyboardDriver.cs +++ b/src/Ryujinx.Input.SDL2/SDLKeyboardDriver.cs @@ -1,5 +1,6 @@ using Ryujinx.SDL2.Common; using System; +using System.Collections.Generic; namespace Ryujinx.Input.SDL2 { @@ -51,5 +52,13 @@ namespace Ryujinx.Input.SDL2 return new SDL2Keyboard(this, _keyboardIdentifers[0], "All keyboards"); } + + public IEnumerable GetGamepads() + { + foreach (var keyboardId in _keyboardIdentifers) + { + yield return GetGamepad(keyboardId); + } + } } } diff --git a/src/Ryujinx.Input/IGamepad.cs b/src/Ryujinx.Input/IGamepad.cs index 3853f2819..832950660 100644 --- a/src/Ryujinx.Input/IGamepad.cs +++ b/src/Ryujinx.Input/IGamepad.cs @@ -65,6 +65,15 @@ namespace Ryujinx.Input /// The configuration of the gamepad void SetConfiguration(InputConfig configuration); + /// + /// Set the LED on the gamepad to a given color. + /// + /// Does nothing on a controller without LED functionality. + /// The packed RGB integer. + void SetLed(uint packedRgb); + + public void ClearLed() => SetLed(0); + /// /// Starts a rumble effect on the gamepad. /// diff --git a/src/Ryujinx.Input/IGamepadDriver.cs b/src/Ryujinx.Input/IGamepadDriver.cs index 625c3e694..25b2295db 100644 --- a/src/Ryujinx.Input/IGamepadDriver.cs +++ b/src/Ryujinx.Input/IGamepadDriver.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Ryujinx.Input { @@ -33,6 +34,11 @@ namespace Ryujinx.Input /// The unique id of the gamepad /// An instance of associated to the gamepad id given or null if not found IGamepad GetGamepad(string id); + + /// + /// Returns an of the connected gamepads. + /// + IEnumerable GetGamepads(); /// /// Clear the internal state of the driver. diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 4df3eab0d..31f27a965 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -587,6 +587,11 @@ namespace Ryujinx.Ava return; } + foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads()) + { + gamepad?.ClearLed(); + } + _isStopped = true; Stop(); } diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 8ceef5f67..d4a52c003 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -7647,6 +7647,31 @@ "zh_TW": "" } }, + { + "ID": "ControllerSettingsLedColorDisable", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Disable", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "ControllerSettingsSave", "Translations": { diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs index 0b63af2d9..031d8b033 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboard.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Logging; using Ryujinx.Input; using System; using System.Collections.Generic; @@ -143,6 +144,11 @@ namespace Ryujinx.Ava.Input } } + public void SetLed(uint packedRgb) + { + Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); + } + public void SetTriggerThreshold(float triggerThreshold) { } public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { } diff --git a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs index 9f87e821a..214652265 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs @@ -59,6 +59,8 @@ namespace Ryujinx.Ava.Input return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]); } + public IEnumerable GetGamepads() => [GetGamepad("0")]; + protected virtual void Dispose(bool disposing) { if (disposing) diff --git a/src/Ryujinx/Input/AvaloniaMouse.cs b/src/Ryujinx/Input/AvaloniaMouse.cs index 1aa2d586a..52a341a01 100644 --- a/src/Ryujinx/Input/AvaloniaMouse.cs +++ b/src/Ryujinx/Input/AvaloniaMouse.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Logging; using Ryujinx.Input; using System; using System.Drawing; @@ -74,6 +75,11 @@ namespace Ryujinx.Ava.Input throw new NotImplementedException(); } + public void SetLed(uint packedRgb) + { + Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaMouse"); + } + public void SetTriggerThreshold(float triggerThreshold) { throw new NotImplementedException(); diff --git a/src/Ryujinx/Input/AvaloniaMouseDriver.cs b/src/Ryujinx/Input/AvaloniaMouseDriver.cs index e71bbf64a..be1441101 100644 --- a/src/Ryujinx/Input/AvaloniaMouseDriver.cs +++ b/src/Ryujinx/Input/AvaloniaMouseDriver.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Input; using Ryujinx.Input; using System; +using System.Collections.Generic; using System.Numerics; using MouseButton = Ryujinx.Input.MouseButton; using Size = System.Drawing.Size; @@ -134,6 +135,8 @@ namespace Ryujinx.Ava.Input return new AvaloniaMouse(this); } + public IEnumerable GetGamepads() => [GetGamepad("0")]; + public void Dispose() { if (_isDisposed) diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs index ea7dd34c3..ae3676853 100644 --- a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -400,6 +400,18 @@ namespace Ryujinx.Ava.UI.Models.Input } } + private bool _turnOffLed; + + public bool TurnOffLed + { + get => _turnOffLed; + set + { + _turnOffLed = value; + OnPropertyChanged(); + } + } + private Color _ledColor; public Color LedColor @@ -512,6 +524,7 @@ namespace Ryujinx.Ava.UI.Models.Input if (controllerInput.Led != null) { EnableLedChanging = controllerInput.Led.EnableLed; + TurnOffLed = controllerInput.Led.TurnOffLed; uint rawColor = controllerInput.Led.LedColor; byte alpha = (byte)(rawColor >> 24); byte red = (byte)(rawColor >> 16); @@ -579,6 +592,7 @@ namespace Ryujinx.Ava.UI.Models.Input Led = new LedConfigController { EnableLed = EnableLedChanging, + TurnOffLed = this.TurnOffLed, LedColor = LedColor.ToUInt32() }, Version = InputConfig.CurrentVersion, diff --git a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs index 0380ef598..d291f09a0 100644 --- a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs @@ -1,5 +1,8 @@ using Avalonia.Svg.Skia; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Views.Input; @@ -57,6 +60,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input await RumbleInputView.Show(this); } + public RelayCommand LedDisabledChanged => Commands.Create(() => + { + if (!Config.EnableLedChanging) return; + + if (Config.TurnOffLed) + ParentModel.SelectedGamepad.ClearLed(); + else + ParentModel.SelectedGamepad.SetLed(Config.LedColor.ToUInt32()); + }); + public void OnParentModelChanged() { IsLeft = ParentModel.IsLeft; diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 3d1bd5f4a..c59ec540c 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Svg.Skia; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; +using Gommon; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; @@ -54,7 +55,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public IGamepadDriver AvaloniaKeyboardDriver { get; } - public IGamepad SelectedGamepad { get; private set; } + + private IGamepad _selectedGamepad; + + public IGamepad SelectedGamepad + { + get => _selectedGamepad; + private set + { + _selectedGamepad = value; + OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed)); + } + } public ObservableCollection PlayerIndexes { get; set; } public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; } @@ -69,8 +81,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public bool IsRight { get; set; } public bool IsLeft { get; set; } - public bool HasLed => false; //temporary - //SelectedGamepad.Features.HasFlag(GamepadFeaturesFlag.Led); + public bool HasLed => SelectedGamepad.Features.HasFlag(GamepadFeaturesFlag.Led); + public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense"); public bool IsModified { get; set; } public event Action NotifyChangesEvent; diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml index db7040f4b..6b8673a9f 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml @@ -429,7 +429,7 @@ - + + @@ -505,8 +506,18 @@ IsChecked="{Binding Config.EnableLedChanging, Mode=TwoWay}"> - + + + diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs index 52a6d51b9..81483ce0e 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs @@ -4,11 +4,14 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using FluentAvalonia.UI.Controls; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels.Input; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Input; using Ryujinx.Input.Assigner; +using System.Linq; using StickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; namespace Ryujinx.Ava.UI.Views.Input @@ -82,7 +85,7 @@ namespace Ryujinx.Ava.UI.Views.Input private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) { - if (sender is ToggleButton button) + if (sender is ToggleButton button) { if (button.IsChecked is true) { @@ -103,7 +106,9 @@ namespace Ryujinx.Ava.UI.Views.Input var viewModel = (DataContext as ControllerInputViewModel); - IKeyboard keyboard = (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IKeyboard keyboard = + (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver + .GetGamepad("0"); // Open Avalonia keyboard for cancel operations. IButtonAssigner assigner = CreateButtonAssigner(isStick); _currentAssigner.ButtonAssigned += (sender, e) => @@ -231,8 +236,31 @@ namespace Ryujinx.Ava.UI.Views.Input protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); + + foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads()) + { + gamepad?.ClearLed(); + } + _currentAssigner?.Cancel(); _currentAssigner = null; } + + private void ColorPickerButton_OnColorChanged(ColorPickerButton sender, ColorButtonColorChangedEventArgs args) + { + if (!args.NewColor.HasValue) return; + if (DataContext is not ControllerInputViewModel cVm) return; + if (!cVm.Config.EnableLedChanging) return; + + cVm.ParentModel.SelectedGamepad.SetLed(args.NewColor.Value.ToUInt32()); + } + + private void ColorPickerButton_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + if (DataContext is not ControllerInputViewModel cVm) return; + if (!cVm.Config.EnableLedChanging) return; + + cVm.ParentModel.SelectedGamepad.SetLed(cVm.Config.LedColor.ToUInt32()); + } } } diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs index db8e0f6bb..67deb9723 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs @@ -4,9 +4,14 @@ using Avalonia.Input; using FluentAvalonia.Core; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.ViewModels.Input; using Ryujinx.HLE.FileSystem; +using Ryujinx.Input; using System; +using System.Linq; +using Key = Avalonia.Input.Key; namespace Ryujinx.Ava.UI.Windows { @@ -106,6 +111,12 @@ namespace Ryujinx.Ava.UI.Windows protected override void OnClosing(WindowClosingEventArgs e) { HotkeysPage.Dispose(); + + foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads()) + { + gamepad?.ClearLed(); + } + InputPage.Dispose(); base.OnClosing(e); } diff --git a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs index 1b00a8fa4..78e3dfc0f 100644 --- a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs @@ -1,3 +1,4 @@ +using Avalonia.Media; using Gommon; using Ryujinx.Ava.Utilities.Configuration.System; using Ryujinx.Ava.Utilities.Configuration.UI; @@ -421,7 +422,8 @@ namespace Ryujinx.Ava.Utilities.Configuration config.Led = new LedConfigController { EnableLed = false, - LedColor = 328189 + TurnOffLed = false, + LedColor = new Color(255, 5, 1, 253).ToUInt32() }; } })