diff --git a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs index 05bddc76f..cf99b0e7a 100644 --- a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs @@ -26,10 +26,20 @@ namespace Ryujinx.HLE.HOS.Applets { _normalSession = normalSession; _interactiveSession = interactiveSession; - - // TODO(jduncanator): Parse PlayerSelectConfig from input data - _normalSession.Push(BuildResponse()); - + + UserProfile selected = _system.Device.UIHandler.ShowPlayerSelectDialog(); + if (selected == null) + { + _normalSession.Push(BuildResponse()); + } + else if (selected.UserId == new UserId("00000000000000000000000000000080")) + { + _normalSession.Push(BuildGuestResponse()); + } + else + { + _normalSession.Push(BuildResponse(selected)); + } AppletStateChanged?.Invoke(this, null); _system.ReturnFocus(); @@ -37,16 +47,34 @@ namespace Ryujinx.HLE.HOS.Applets return ResultCode.Success; } - private byte[] BuildResponse() + private byte[] BuildResponse(UserProfile selectedUser) { - UserProfile currentUser = _system.AccountManager.LastOpenedUser; - using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); using BinaryWriter writer = new(stream); writer.Write((ulong)PlayerSelectResult.Success); - currentUser.UserId.Write(writer); + selectedUser.UserId.Write(writer); + + return stream.ToArray(); + } + + private byte[] BuildGuestResponse() + { + using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); + using BinaryWriter writer = new(stream); + + writer.Write(new byte()); + + return stream.ToArray(); + } + + private byte[] BuildResponse() + { + using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); + using BinaryWriter writer = new(stream); + + writer.Write((ulong)PlayerSelectResult.Failure); return stream.ToArray(); } diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg b/src/Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg new file mode 100644 index 000000000..8310994de Binary files /dev/null and b/src/Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg differ diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index d42ecf8b8..f551f1a18 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -48,6 +48,7 @@ + diff --git a/src/Ryujinx.HLE/UI/IHostUIHandler.cs b/src/Ryujinx.HLE/UI/IHostUIHandler.cs index 88af83735..8ccb5cf89 100644 --- a/src/Ryujinx.HLE/UI/IHostUIHandler.cs +++ b/src/Ryujinx.HLE/UI/IHostUIHandler.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; namespace Ryujinx.HLE.UI @@ -59,5 +60,11 @@ namespace Ryujinx.HLE.UI /// Gets fonts and colors used by the host. /// IHostUITheme HostUITheme { get; } + + + /// + /// Displays the player select dialog and returns the selected profile. + /// + UserProfile ShowPlayerSelectDialog(); } } diff --git a/src/Ryujinx/Headless/Windows/WindowBase.cs b/src/Ryujinx/Headless/Windows/WindowBase.cs index 7017d1f59..068c32062 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -9,6 +9,7 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.OpenGL; using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using Ryujinx.HLE.UI; using Ryujinx.Input; @@ -26,6 +27,7 @@ using static SDL2.SDL; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using Switch = Ryujinx.HLE.Switch; +using UserProfile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile; namespace Ryujinx.Headless { @@ -555,5 +557,10 @@ namespace Ryujinx.Headless SDL2Driver.Instance.Dispose(); } } + + public UserProfile ShowPlayerSelectDialog() + { + return AccountSaveDataManager.GetLastUsedUser(); + } } } diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 8929ba3eb..8c72d5a3c 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -173,4 +173,10 @@ + + + UserSelectorDialog.axaml + Code + + diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index 65f4c7795..2c63f6af0 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -1,17 +1,24 @@ using Avalonia.Controls; using Avalonia.Threading; using FluentAvalonia.UI.Controls; +using Gommon; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.ViewModels.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities.Configuration; +using Ryujinx.Common; using Ryujinx.HLE; using Ryujinx.HLE.HOS.Applets; using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; +using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using Ryujinx.HLE.UI; using System; +using System.Collections.ObjectModel; +using System.Linq; using System.Threading; namespace Ryujinx.Ava.UI.Applet @@ -253,5 +260,59 @@ namespace Ryujinx.Ava.UI.Applet } public IDynamicTextInputHandler CreateDynamicTextInputHandler() => new AvaloniaDynamicTextInputHandler(_parent); + + public UserProfile ShowPlayerSelectDialog() + { + UserId selected = UserId.Null; + byte[] defaultGuestImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg"); + UserProfile guest = new UserProfile(new UserId("00000000000000000000000000000080"), "Guest", defaultGuestImage); + + ManualResetEvent dialogCloseEvent = new(false); + + Dispatcher.UIThread.InvokeAsync(async () => + { + ObservableCollection profiles = []; + NavigationDialogHost nav = new(); + + _parent.AccountManager.GetAllUsers() + .OrderBy(x => x.Name) + .ForEach(profile => profiles.Add(new Models.UserProfile(profile, nav))); + + profiles.Add(new Models.UserProfile(guest, nav)); + UserSelectorDialogViewModel viewModel = new(); + viewModel.Profiles = profiles; + viewModel.SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId; + UserSelectorDialog content = new(viewModel); + (UserId id, _) = await UserSelectorDialog.ShowInputDialog(content); + + selected = id; + + dialogCloseEvent.Set(); + }); + + dialogCloseEvent.WaitOne(); + + UserProfile profile = _parent.AccountManager.LastOpenedUser; + if (selected == guest.UserId) + { + profile = guest; + } + else if (selected == UserId.Null) + { + profile = null; + } + else + { + foreach (UserProfile p in _parent.AccountManager.GetAllUsers()) + { + if (p.UserId == selected) + { + profile = p; + break; + } + } + } + return profile; + } } } diff --git a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml b/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml new file mode 100644 index 000000000..ed22fb088 --- /dev/null +++ b/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml.cs b/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml.cs new file mode 100644 index 000000000..6e25588ec --- /dev/null +++ b/src/Ryujinx/UI/Applet/UserSelectorDialog.axaml.cs @@ -0,0 +1,123 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; +using UserProfileSft = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile; + +namespace Ryujinx.Ava.UI.Applet +{ + public partial class UserSelectorDialog : UserControl, INotifyPropertyChanged + { + public UserSelectorDialogViewModel ViewModel { get; set; } + + public UserSelectorDialog(UserSelectorDialogViewModel viewModel) + { + InitializeComponent(); + ViewModel = viewModel; + DataContext = ViewModel; + } + + private void Grid_PointerEntered(object sender, PointerEventArgs e) + { + if (sender is Grid { DataContext: UserProfile profile }) + { + profile.IsPointerOver = true; + } + } + + private void Grid_OnPointerExited(object sender, PointerEventArgs e) + { + if (sender is Grid { DataContext: UserProfile profile }) + { + profile.IsPointerOver = false; + } + } + + private void ProfilesList_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is ListBox listBox) + { + int selectedIndex = listBox.SelectedIndex; + + if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count) + { + if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile) + { + ViewModel.SelectedUserId = userProfile.UserId; + Logger.Info?.Print(LogClass.UI, $"Selected user: {userProfile.UserId}"); + + ObservableCollection newProfiles = []; + + foreach (var item in ViewModel.Profiles) + { + if (item is UserProfile originalItem) + { + var profile = new UserProfileSft(originalItem.UserId, originalItem.Name, originalItem.Image); + + if (profile.UserId == ViewModel.SelectedUserId) + { + profile.AccountState = AccountState.Open; + } + + newProfiles.Add(new UserProfile(profile, new NavigationDialogHost())); + } + } + + ViewModel.Profiles = newProfiles; + } + } + } + } + + public static async Task<(UserId Id, bool Result)> ShowInputDialog(UserSelectorDialog content) + { + ContentDialog contentDialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle], + PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue], + SecondaryButtonText = string.Empty, + CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel], + Content = content, + Padding = new Thickness(0) + }; + + UserId result = UserId.Null; + bool input = false; + + void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs) + { + if (eventArgs.Result == ContentDialogResult.Primary) + { + if (contentDialog.Content is UserSelectorDialog view) + { + result = view.ViewModel.SelectedUserId; + input = true; + } + } + else + { + result = UserId.Null; + input = false; + } + } + + contentDialog.Closed += Handler; + + await ContentDialogHelper.ShowAsync(contentDialog); + + return (result, input); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/UserSelectorDialogViewModel.cs b/src/Ryujinx/UI/ViewModels/UserSelectorDialogViewModel.cs new file mode 100644 index 000000000..094aed5cf --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/UserSelectorDialogViewModel.cs @@ -0,0 +1,14 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System.Collections.ObjectModel; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public partial class UserSelectorDialogViewModel : BaseModel + { + + [ObservableProperty] private UserId _selectedUserId; + + [ObservableProperty] private ObservableCollection _profiles = []; + } +}