diff --git a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs index 67c68d8ec..61e1aed12 100644 --- a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs +++ b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs @@ -95,7 +95,7 @@ namespace Ryujinx.Input.SDL2 } private void HandleJoyStickConnected(int joystickDeviceId, int joystickInstanceId) - { + { if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE) { if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId)) diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs index 21219d91b..0286f023f 100644 --- a/src/Ryujinx.Input/HLE/NpadManager.cs +++ b/src/Ryujinx.Input/HLE/NpadManager.cs @@ -14,6 +14,7 @@ using ControllerType = Ryujinx.Common.Configuration.Hid.ControllerType; using PlayerIndex = Ryujinx.HLE.HOS.Services.Hid.PlayerIndex; using Switch = Ryujinx.HLE.Switch; + namespace Ryujinx.Input.HLE { public class NpadManager : IDisposable @@ -38,6 +39,8 @@ namespace Ryujinx.Input.HLE private bool _enableMouse; private Switch _device; + public bool AutoAssignEnabled { get; set; } = false; + public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver) { _controllers = new NpadController[MaxControllers]; @@ -84,12 +87,14 @@ namespace Ryujinx.Input.HLE } } + if (AutoAssignEnabled) return; ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); } } private void HandleOnGamepadConnected(string id) { + if (AutoAssignEnabled) return; // Force input reload ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); } @@ -171,7 +176,7 @@ namespace Ryujinx.Input.HLE _device.Hid.RefreshInputConfig(validInputs); } } - + public void UnblockInputUpdates() { lock (_lock) @@ -202,11 +207,13 @@ namespace Ryujinx.Input.HLE } } - public void Initialize(Switch device, List inputConfig, bool enableKeyboard, bool enableMouse) + public void Initialize(Switch device, List inputConfig, bool enableKeyboard, bool enableMouse, bool enableAutoAssign) { _device = device; _device.Configuration.RefreshInputConfig = RefreshInputConfigForHLE; + AutoAssignEnabled = enableAutoAssign; + ReloadConfiguration(inputConfig, enableKeyboard, enableMouse); } diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index b741eb977..7bcaa3e6e 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -463,7 +463,7 @@ namespace Ryujinx.Ava DisplaySleep.Prevent(); - NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); + NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse, ConfigurationState.Instance.Hid.EnableAutoAssign); TouchScreenManager.Initialize(Device); _viewModel.IsGameRunning = true; diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index d69c3ee9b..d583fa1d1 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -147,6 +147,31 @@ "zh_TW": "滑鼠直接存取" } }, + { + "ID": "SettingsTabInputAutoAssign", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Auto-assign controllers", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "Assegnamento controller automatico", + "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": "SettingsTabSystemMemoryManagerMode", "Translations": { @@ -16222,6 +16247,31 @@ "zh_TW": "支援滑鼠直接存取 (HID)。遊戲可將滑鼠作為指向裝置使用。\n\n僅適用於在 Switch 硬體上原生支援滑鼠控制的遊戲,這類遊戲很少。\n\n啟用後,觸控螢幕功能可能無法使用。\n\n如果不確定,請保持關閉狀態。" } }, + { + "ID": "AutoAssignTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Automatic controllers assignment support.\n\nAutomatically assigns connected controllers to each player.\n\nManual configuration remains available, even when this option is activated.\n\nLeave OFF if you prefer to manually assign controllers.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "Supporto per l'assegnazione automatica dei controller.\n\nAssegna automaticamente i controller connessi a ciascun giocatore.\n\nÈ possibile configurare manualmente i controller anche quando questa opzione è attivata.\n\nLascia disattivato se preferisci assegnare i controller manualmente.", + "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": "RegionTooltip", "Translations": { @@ -24298,4 +24348,4 @@ } } ] -} +} \ No newline at end of file diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.cs b/src/Ryujinx/Headless/HeadlessRyujinx.cs index 9a69c56dd..52c2f7eab 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.cs @@ -46,6 +46,7 @@ namespace Ryujinx.Headless private static List _inputConfiguration = []; private static bool _enableKeyboard; private static bool _enableMouse; + private static bool _enableAutoAssign; private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -230,6 +231,7 @@ namespace Ryujinx.Headless _inputConfiguration ??= []; _enableKeyboard = option.EnableKeyboard; _enableMouse = option.EnableMouse; + _enableAutoAssign = option.EnableAutoAssign; LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1); LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2); @@ -371,7 +373,7 @@ namespace Ryujinx.Headless DisplaySleep.Prevent(); - _window.Initialize(_emulationContext, _inputConfiguration, _enableKeyboard, _enableMouse); + _window.Initialize(_emulationContext, _inputConfiguration, _enableKeyboard, _enableMouse, _enableAutoAssign); _window.Execute(); diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs index 6658ba3ad..599f41d6e 100644 --- a/src/Ryujinx/Headless/Options.cs +++ b/src/Ryujinx/Headless/Options.cs @@ -26,6 +26,9 @@ namespace Ryujinx.Headless if (NeedsOverride(nameof(EnableMouse))) EnableMouse = configurationState.Hid.EnableMouse; + + if (NeedsOverride(nameof(EnableAutoAssign))) + EnableAutoAssign = configurationState.Hid.EnableAutoAssign; if (NeedsOverride(nameof(HideCursorMode))) HideCursorMode = configurationState.HideCursor; @@ -272,6 +275,9 @@ namespace Ryujinx.Headless [Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")] public bool EnableMouse { get; set; } + + [Option("enable-auto-assign", Required = false, Default = false, HelpText = "Enable or disable auto-assigning controllers to players.")] + public bool EnableAutoAssign { get; set; } [Option("hide-cursor", Required = false, Default = HideCursorMode.OnIdle, HelpText = "Change when the cursor gets hidden.")] public HideCursorMode HideCursorMode { get; set; } diff --git a/src/Ryujinx/Headless/Windows/WindowBase.cs b/src/Ryujinx/Headless/Windows/WindowBase.cs index 081998a00..e46ef3359 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -119,7 +119,7 @@ namespace Ryujinx.Headless SDL2Driver.Instance.Initialize(); } - public void Initialize(Switch device, List inputConfigs, bool enableKeyboard, bool enableMouse) + public void Initialize(Switch device, List inputConfigs, bool enableKeyboard, bool enableMouse, bool enableAutoAssign) { Device = device; @@ -132,7 +132,7 @@ namespace Ryujinx.Headless Renderer = renderer; - NpadManager.Initialize(device, inputConfigs, enableKeyboard, enableMouse); + NpadManager.Initialize(device, inputConfigs, enableKeyboard, enableMouse, enableAutoAssign); TouchScreenManager.Initialize(device); } diff --git a/src/Ryujinx/Input/AutoAssignController.cs b/src/Ryujinx/Input/AutoAssignController.cs new file mode 100644 index 000000000..e6fc90a16 --- /dev/null +++ b/src/Ryujinx/Input/AutoAssignController.cs @@ -0,0 +1,75 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.Utilities.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Logging; +using Ryujinx.Input; +using Ryujinx.Input.HLE; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Ava.Input +{ + public class AutoAssignController + { + private readonly InputManager _inputManager; + private readonly MainWindowViewModel _viewModel; + private readonly ConfigurationState _configurationState; + + public event Action ConfigurationUpdated; + + public AutoAssignController(InputManager inputManager, MainWindowViewModel mainWindowViewModel) + { + _inputManager = inputManager; + _viewModel = mainWindowViewModel; + _configurationState = ConfigurationState.Instance; + + _inputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; + _inputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + + RefreshControllers(); + } + + private void HandleOnGamepadConnected(string id) + { + Logger.Warning?.Print(LogClass.Application, $"Gamepad connected: {id}"); + RefreshControllers(); + } + + private void HandleOnGamepadDisconnected(string id) + { + Logger.Warning?.Print(LogClass.Application, $"Gamepad disconnected: {id}"); + RefreshControllers(); + } + + public void RefreshControllers() + { + if (!_configurationState.Hid.EnableAutoAssign) return; + + List controllers = _inputManager.GamepadDriver.GetGamepads().ToList(); + List oldConfig = _configurationState.Hid.InputConfig.Value.Where(x => x != null).ToList(); + + List newConfig = ControllerAssignmentManager.GetConfiguredControllers( + controllers, oldConfig, out bool hasNewControllersConnected); + + _viewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, _configurationState.Hid.EnableKeyboard, _configurationState.Hid.EnableMouse); + + if (!hasNewControllersConnected) + { + // there is no *new* controller, we must switch the order of the controllers in + // oldConfig to match the new order since probably a controller was disconnected + // or an old controller was reconnected + newConfig = ControllerAssignmentManager.ReorderControllers(newConfig, oldConfig); + } + + _configurationState.Hid.InputConfig.Value = newConfig; + + // we want to save the configuration only if a *new* controller was connected + if(hasNewControllersConnected) + { + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + ConfigurationUpdated?.Invoke(); + } + } +} diff --git a/src/Ryujinx/Input/ControllerAssignmentManager.cs b/src/Ryujinx/Input/ControllerAssignmentManager.cs new file mode 100644 index 000000000..d161b2e7d --- /dev/null +++ b/src/Ryujinx/Input/ControllerAssignmentManager.cs @@ -0,0 +1,274 @@ +using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Common.Logging; +using Ryujinx.Input; +using System.Collections.Generic; +using System.Linq; +using StickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; + +namespace Ryujinx.Ava.Input +{ + public static class ControllerAssignmentManager + { + private static readonly uint[] _playerColors = + [ + 0xFF0000FF, // Player 1 - Blue + 0xFFFF0000, // Player 2 - Red + 0xFF00FF00, // Player 3 - Green + 0xFFFFFF00, // Player 4 - Yellow + 0xFFFF00FF, // Player 5 - Magenta + 0xFFFFA500, // Player 6 - Orange + 0xFF00FFFF, // Player 7 - Cyan + 0xFF800080 // Player 8 - Purple + ]; + + private const int MaxControllers = 9; + + public static List ReorderControllers(List newConfig, List oldConfig) + { + if(newConfig == null || oldConfig == null || newConfig.Count == 0 || oldConfig.Count == 0) return []; + + List reorderedConfig = oldConfig.Select(config => new GamepadInputConfig(config).GetConfig()).ToList(); + + foreach (var config in newConfig) + { + InputConfig substitute = reorderedConfig.FirstOrDefault(x => x.Id == config.Id); + InputConfig toBeReplaced = reorderedConfig.FirstOrDefault(x => x.PlayerIndex == config.PlayerIndex); + + if (substitute == null || toBeReplaced == null || substitute.PlayerIndex == toBeReplaced.PlayerIndex) continue; + + (substitute.PlayerIndex, toBeReplaced.PlayerIndex) = (toBeReplaced.PlayerIndex, substitute.PlayerIndex); + } + + return reorderedConfig; + } + + public static List GetConfiguredControllers( + List controllers, + List oldConfig, + out bool hasNewControllersConnected) + { + if(controllers == null || controllers.Count == 0) + { + hasNewControllersConnected = false; + return []; + } + + Dictionary oldConfigMap = oldConfig + .Where(c => c?.Id != null) + .ToDictionary(x => x.Id); + + Dictionary playerIndexMap = new(); + HashSet usedIndices = []; + int recognizedControllersCount = 0; + + List remainingControllers = controllers.Where(c => c?.Id != null).ToList(); + + // Add controllers with existing configurations + AddExistingControllers(remainingControllers, oldConfigMap, playerIndexMap, usedIndices, ref recognizedControllersCount); + + // Add new controllers + AddNewControllers(remainingControllers, playerIndexMap, usedIndices); + + List orderedConfigs = playerIndexMap + .OrderBy(x => x.Key) + .Select(x => x.Value) + .ToList(); + + // Update player indices and LED colors + UpdatePlayerIndicesAndLEDs(orderedConfigs); + + hasNewControllersConnected = controllers.Count > recognizedControllersCount; + return orderedConfigs; + } + + private static void AddExistingControllers( + List controllers, + Dictionary oldConfigMap, + Dictionary playerIndexMap, + HashSet usedIndices, + ref int recognizedControllersCount) + { + foreach (var controller in controllers.ToList()) + { + if (!oldConfigMap.TryGetValue(controller.Id, out InputConfig existingConfig)) + { + continue; + } + + int desiredIndex = (int)existingConfig.PlayerIndex; + + // Ensure the index is valid and available + if (desiredIndex < 0 || desiredIndex >= MaxControllers || usedIndices.Contains(desiredIndex)) + { + desiredIndex = GetFirstAvailableIndex(usedIndices); + } + + if(desiredIndex == -1) continue; + + InputConfig config = new GamepadInputConfig(existingConfig).GetConfig(); + config.PlayerIndex = (PlayerIndex)desiredIndex; + usedIndices.Add(desiredIndex); + playerIndexMap[desiredIndex] = config; + recognizedControllersCount++; + + controllers.Remove(controller); + } + } + + private static void AddNewControllers( + List controllers, + Dictionary playerIndexMap, + HashSet usedIndices) + { + foreach (var controller in controllers) + { + InputConfig config = CreateConfigFromController(controller); + int freeIndex = GetFirstAvailableIndex(usedIndices); + config.PlayerIndex = (PlayerIndex)freeIndex; + usedIndices.Add(freeIndex); + playerIndexMap[freeIndex] = config; + } + } + + private static int GetFirstAvailableIndex(HashSet usedIndices) + { + for (int i = 0; i < MaxControllers; i++) + { + if (!usedIndices.Contains(i)) return i; + } + return -1; // Should not happen unless MaxControllers is exceeded + } + + private static void UpdatePlayerIndicesAndLEDs(List orderedConfigs) + { + for (int index = 0; index < orderedConfigs.Count; index++) + { + orderedConfigs[index].PlayerIndex = (PlayerIndex)index; + + if (orderedConfigs[index] is not StandardControllerInputConfig standardConfig || + standardConfig.Led.UseRainbow) + { + continue; + } + + Logger.Warning?.Print(LogClass.Application, $"Setting color for Player{index + 1}"); + standardConfig.Led = new LedConfigController + { + EnableLed = true, + LedColor = _playerColors[index] + }; + } + } + + private static InputConfig CreateConfigFromController(IGamepad controller) + { + if (controller == null) return null; + + Logger.Warning?.Print(LogClass.Application, $"Creating config for controller: {controller.Id}"); + + string id = controller.Id.Split(" ")[0]; + bool isNintendoStyle = controller.Name.Contains("Nintendo"); + ControllerType controllerType; + + if (isNintendoStyle && !controller.Name.Contains("(L/R)")) + { + if (controller.Name.Contains("(L)")) + { + controllerType = ControllerType.JoyconLeft; + } + else if (controller.Name.Contains("(R)")) + { + controllerType = ControllerType.JoyconRight; + } + else + { + controllerType = ControllerType.ProController; + } + } + else + { + // if it's not a nintendo controller, we assume it's a pro controller or a joy-con pair + controllerType = ControllerType.ProController; + } + + InputConfig config = new StandardControllerInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.GamepadSDL2, + Id = id, + ControllerType = controllerType, + DeadzoneLeft = 0.1f, + DeadzoneRight = 0.1f, + RangeLeft = 1.0f, + RangeRight = 1.0f, + TriggerThreshold = 0.5f, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = (controllerType == ControllerType.JoyconLeft) ? GamepadInputId.Y : GamepadInputId.DpadUp, + DpadDown = (controllerType == ControllerType.JoyconLeft) ? GamepadInputId.A : GamepadInputId.DpadDown, + DpadLeft = (controllerType == ControllerType.JoyconLeft) ? GamepadInputId.B : GamepadInputId.DpadLeft, + DpadRight = (controllerType == ControllerType.JoyconLeft) ? GamepadInputId.X : GamepadInputId.DpadRight, + ButtonMinus = (controllerType == ControllerType.JoyconLeft) ? GamepadInputId.Plus : GamepadInputId.Minus, + ButtonL = GamepadInputId.LeftShoulder, + ButtonZl = GamepadInputId.LeftTrigger, + ButtonSl = (controllerType == ControllerType.JoyconLeft) ? GamepadInputId.LeftShoulder : GamepadInputId.Unbound, + ButtonSr = (controllerType == ControllerType.JoyconLeft) ? GamepadInputId.RightShoulder : GamepadInputId.Unbound, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = StickInputId.Left, + StickButton = GamepadInputId.LeftStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = (controllerType == ControllerType.JoyconLeft), + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = GamepadInputId.B, + ButtonB = (controllerType == ControllerType.JoyconRight) ? GamepadInputId.Y : GamepadInputId.A, + ButtonX = (controllerType == ControllerType.JoyconRight) ? GamepadInputId.A : GamepadInputId.Y, + ButtonY = GamepadInputId.X, + ButtonPlus = GamepadInputId.Plus, + ButtonR = GamepadInputId.RightShoulder, + ButtonZr = GamepadInputId.RightTrigger, + ButtonSl = (controllerType == ControllerType.JoyconRight) ? GamepadInputId.LeftShoulder : GamepadInputId.Unbound, + ButtonSr = (controllerType == ControllerType.JoyconRight) ? GamepadInputId.RightShoulder : GamepadInputId.Unbound, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = (controllerType == ControllerType.JoyconRight) ? StickInputId.Left : StickInputId.Right, + StickButton = GamepadInputId.RightStick, + InvertStickX = (controllerType == ControllerType.JoyconRight), + InvertStickY = (controllerType == ControllerType.JoyconRight), + Rotate90CW = (controllerType == ControllerType.JoyconRight), + }, + Motion = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = true, + Sensitivity = 100, + GyroDeadzone = 1, + }, + Rumble = new RumbleConfigController + { + StrongRumble = 1f, + WeakRumble = 1f, + EnableRumble = false, + }, + Led = new LedConfigController + { + EnableLed = true, + TurnOffLed = false, + UseRainbow = false, + LedColor = 0, + }, + }; + + return config; + } + } + +} diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index edd46a7db..00a1ff083 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -134,7 +134,7 @@ namespace Ryujinx.Ava SDL2Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input); ReloadConfig(); - + WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor(); // Logging system information. diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 5b7bcfd32..469c7714c 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -252,6 +252,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + _mainWindow.AutoAssignController.ConfigurationUpdated += OnConfigurationUpdated; + _mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates(); _isLoaded = false; @@ -365,14 +367,47 @@ namespace Ryujinx.Ava.UI.ViewModels.Input private void HandleOnGamepadDisconnected(string id) { + if(ConfigurationState.Instance.Hid.EnableAutoAssign) return; Dispatcher.UIThread.Post(LoadDevices); } private void HandleOnGamepadConnected(string id) { + if(ConfigurationState.Instance.Hid.EnableAutoAssign) return; Dispatcher.UIThread.Post(LoadDevices); } + private void OnConfigurationUpdated() + { + Dispatcher.UIThread.Post(() => { + LoadDevices(); + _isLoaded = false; + LoadConfiguration(); + LoadDevice(); + _isLoaded = true; + + UpdateGamepadLed(); + }); + } + + private void UpdateGamepadLed() + { + if (ConfigViewModel is not ControllerInputViewModel controllerInputViewModel) return; + GamepadInputConfig inputConfig = controllerInputViewModel.Config; + + if (inputConfig is not { EnableLedChanging: true }) return; + + if (inputConfig.TurnOffLed) + { + SelectedGamepad.ClearLed(); + } + + if (!inputConfig.TurnOffLed && !inputConfig.UseRainbowLed) + { + SelectedGamepad.SetLed(inputConfig.LedColor.ToUInt32()); + } + } + private string GetCurrentGamepadId() { if (_device < 0) @@ -859,6 +894,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } + if (_mainWindow.ViewModel.AppHost != null) + { + _mainWindow.ViewModel.AppHost.NpadManager.AutoAssignEnabled = + ConfigurationState.Instance.Hid.EnableAutoAssign; + } + _mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); // Atomically replace and signal input change. @@ -890,6 +931,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; + _mainWindow.AutoAssignController.ConfigurationUpdated -= OnConfigurationUpdated; _mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates(); diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index d0a6c6d8a..5c22f57a7 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -140,6 +140,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } public bool EnableMouse { get; set; } + public bool EnableAutoAssign { get; set; } public bool DisableInputWhenOutOfFocus { get; set; } public int FocusLostActionType { get; set; } @@ -563,6 +564,7 @@ namespace Ryujinx.Ava.UI.ViewModels EnableDockedMode = config.System.EnableDockedMode; EnableKeyboard = config.Hid.EnableKeyboard; EnableMouse = config.Hid.EnableMouse; + EnableAutoAssign = config.Hid.EnableAutoAssign; DisableInputWhenOutOfFocus = config.Hid.DisableInputWhenOutOfFocus; // Keyboard Hotkeys @@ -668,6 +670,8 @@ namespace Ryujinx.Ava.UI.ViewModels config.System.EnableDockedMode.Value = EnableDockedMode; config.Hid.EnableKeyboard.Value = EnableKeyboard; config.Hid.EnableMouse.Value = EnableMouse; + bool activatingAutoAssign = EnableAutoAssign && !config.Hid.EnableAutoAssign; + config.Hid.EnableAutoAssign.Value = EnableAutoAssign; config.Hid.DisableInputWhenOutOfFocus.Value = DisableInputWhenOutOfFocus; // Keyboard Hotkeys @@ -690,7 +694,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.System.DramSize.Value = DramSize; config.System.IgnoreMissingServices.Value = IgnoreMissingServices; - config.System.IgnoreControllerApplet.Value = IgnoreApplet; + config.System.IgnoreControllerApplet.Value = (EnableAutoAssign) || IgnoreApplet; // CPU config.System.EnablePtc.Value = EnablePptc; @@ -766,7 +770,14 @@ namespace Ryujinx.Ava.UI.ViewModels MainWindow.UpdateGraphicsConfig(); RyujinxApp.MainWindow.ViewModel.VSyncModeSettingChanged(); - SaveSettingsEvent?.Invoke(); + if(activatingAutoAssign) + { + RyujinxApp.MainWindow.AutoAssignController.RefreshControllers(); + } + else + { + SaveSettingsEvent?.Invoke(); + } GameListNeedsRefresh = false; } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml index b0edc51a5..920c5994e 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml @@ -58,6 +58,12 @@ + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index dd6858ee6..2d2727a51 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -319,6 +319,7 @@ diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 241a0c0dc..20ec9384f 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -64,6 +64,7 @@ namespace Ryujinx.Ava.UI.Windows public LibHacHorizonManager LibHacHorizonManager { get; private set; } public InputManager InputManager { get; private set; } + public AutoAssignController AutoAssignController { get; private set; } public SettingsWindow SettingsWindow { get; set; } @@ -110,6 +111,7 @@ namespace Ryujinx.Ava.UI.Windows if (Program.PreviewerDetached) { InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); + AutoAssignController = new AutoAssignController(InputManager, ViewModel); _ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it); this.ScalingChanged += OnScalingChanged; diff --git a/src/Ryujinx/Utilities/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Utilities/Configuration/ConfigurationFileFormat.cs index 814a48e53..c40ce3436 100644 --- a/src/Ryujinx/Utilities/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Utilities/Configuration/ConfigurationFileFormat.cs @@ -389,6 +389,11 @@ namespace Ryujinx.Ava.Utilities.Configuration /// public bool EnableMouse { get; set; } + /// + /// Enable or disable automatic controller assignment for players + /// + public bool EnableAutoAssign { get; set; } + /// /// Enable/disable the ability to control Ryujinx when it's not the currently focused window. /// diff --git a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs index 8a0ddb560..ba13a2865 100644 --- a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Migration.cs @@ -144,6 +144,7 @@ namespace Ryujinx.Ava.Utilities.Configuration Hid.EnableKeyboard.Value = cff.EnableKeyboard; Hid.EnableMouse.Value = cff.EnableMouse; + Hid.EnableAutoAssign.Value = cff.EnableAutoAssign; Hid.DisableInputWhenOutOfFocus.Value = shouldLoadFromFile ? cff.DisableInputWhenOutOfFocus: Hid.DisableInputWhenOutOfFocus.Value; // Get from global config only Hid.Hotkeys.Value = shouldLoadFromFile ? cff.Hotkeys : Hid.Hotkeys.Value; // Get from global config only Hid.InputConfig.Value = cff.InputConfig ?? []; diff --git a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs index ead99fbac..d8f2d52a7 100644 --- a/src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Utilities/Configuration/ConfigurationState.Model.cs @@ -449,6 +449,11 @@ namespace Ryujinx.Ava.Utilities.Configuration /// public ReactiveObject EnableMouse { get; private set; } + /// + /// Enable or disable auto-assigning controllers to players + /// + public ReactiveObject EnableAutoAssign { get; private set; } + /// /// Enable/disable the ability to control Ryujinx when it's not the currently focused window. /// @@ -475,6 +480,7 @@ namespace Ryujinx.Ava.Utilities.Configuration { EnableKeyboard = new ReactiveObject(); EnableMouse = new ReactiveObject(); + EnableAutoAssign = new ReactiveObject(); DisableInputWhenOutOfFocus = new ReactiveObject(); Hotkeys = new ReactiveObject(); InputConfig = new ReactiveObject>(); diff --git a/src/Ryujinx/Utilities/Configuration/ConfigurationState.cs b/src/Ryujinx/Utilities/Configuration/ConfigurationState.cs index 4fdf7c4f0..e7cdac8fa 100644 --- a/src/Ryujinx/Utilities/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Utilities/Configuration/ConfigurationState.cs @@ -133,6 +133,7 @@ namespace Ryujinx.Ava.Utilities.Configuration ShowConsole = UI.ShowConsole, EnableKeyboard = Hid.EnableKeyboard, EnableMouse = Hid.EnableMouse, + EnableAutoAssign = Hid.EnableAutoAssign, DisableInputWhenOutOfFocus = Hid.DisableInputWhenOutOfFocus, Hotkeys = Hid.Hotkeys, InputConfig = Hid.InputConfig, @@ -249,6 +250,7 @@ namespace Ryujinx.Ava.Utilities.Configuration UI.WindowStartup.WindowMaximized.Value = false; Hid.EnableKeyboard.Value = false; Hid.EnableMouse.Value = false; + Hid.EnableAutoAssign.Value = false; Hid.DisableInputWhenOutOfFocus.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys {