diff --git a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs
index 05bddc76f..9d5b53177 100644
--- a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs
+++ b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs
@@ -27,9 +27,19 @@ 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 9c5d0c4c6..5ec79a57c 100644
--- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -59,6 +59,7 @@
+
diff --git a/src/Ryujinx.HLE/UI/IHostUIHandler.cs b/src/Ryujinx.HLE/UI/IHostUIHandler.cs
index f33c8d7aa..35ec69135 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
@@ -65,5 +66,10 @@ namespace Ryujinx.HLE.UI
/// Takes a screenshot from the current renderer and saves it in the screenshots folder.
///
void TakeScreenshot();
+
+ ///
+ /// Displays the player select dialog and returns the selected profile.
+ ///
+ UserProfile ShowPlayerSelectDialog();
}
}
diff --git a/src/Ryujinx/Headless/WindowBase.cs b/src/Ryujinx/Headless/WindowBase.cs
index 77b7e5a81..49e465569 100644
--- a/src/Ryujinx/Headless/WindowBase.cs
+++ b/src/Ryujinx/Headless/WindowBase.cs
@@ -10,6 +10,7 @@ using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
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.Loaders.Processes;
using Ryujinx.HLE.UI;
@@ -28,6 +29,7 @@ using static SDL.SDL3;
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
{
@@ -590,5 +592,10 @@ namespace Ryujinx.Headless
{
throw new NotImplementedException();
}
+
+ public UserProfile ShowPlayerSelectDialog()
+ {
+ return AccountSaveDataManager.GetLastUsedUser();
+ }
}
}
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index 1c04aa02e..4685c84a7 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -169,4 +169,10 @@
+
+
+ UserSelectorDialog.axaml
+ Code
+
+
diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
index 21bac3935..db984595c 100644
--- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
@@ -1,17 +1,23 @@
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.Windows;
+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 Ryujinx.UI.Common.Configuration;
using System;
+using System.Collections.ObjectModel;
+using System.Linq;
using System.Threading;
namespace Ryujinx.Ava.UI.Applet
@@ -256,11 +262,61 @@ namespace Ryujinx.Ava.UI.Applet
return showDetails;
}
- public IDynamicTextInputHandler CreateDynamicTextInputHandler()
- {
- return new AvaloniaDynamicTextInputHandler(_parent);
- }
+ 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));
+ ProfileSelectorDialogViewModel viewModel = new()
+ {
+ Profiles = profiles,
+ SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
+ };
+ (selected, _) = await ProfileSelectorDialog.ShowInputDialog(viewModel);
+
+ 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;
+ }
+
public void TakeScreenshot()
{
_parent.ViewModel.AppHost.ScreenshotRequested = true;
diff --git a/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml
new file mode 100644
index 000000000..369790053
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs
new file mode 100644
index 000000000..8d2c54088
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml.cs
@@ -0,0 +1,118 @@
+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.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+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 ProfileSelectorDialog : UserControl
+ {
+ public ProfileSelectorDialogViewModel ViewModel { get; set; }
+
+ public ProfileSelectorDialog(ProfileSelectorDialogViewModel viewModel)
+ {
+ DataContext = ViewModel = viewModel;
+
+ InitializeComponent();
+ }
+
+ 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: {userProfile.UserId}", "ProfileSelector");
+
+ ObservableCollection newProfiles = [];
+
+ foreach (BaseModel item in ViewModel.Profiles)
+ {
+ if (item is UserProfile originalItem)
+ {
+ UserProfileSft profile = new(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(ProfileSelectorDialogViewModel viewModel)
+ {
+ FAContentDialog contentDialog = new()
+ {
+ Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
+ PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
+ SecondaryButtonText = string.Empty,
+ CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
+ Content = new ProfileSelectorDialog(viewModel),
+ Padding = new Thickness(0)
+ };
+
+ UserId result = UserId.Null;
+ bool input = false;
+
+ contentDialog.Closed += Handler;
+
+ await ContentDialogHelper.ShowAsync(contentDialog);
+
+ return (result, input);
+
+ void Handler(FAContentDialog sender, FAContentDialogClosedEventArgs eventArgs)
+ {
+ if (eventArgs.Result == FAContentDialogResult.Primary)
+ {
+ result = viewModel.SelectedUserId;
+ input = true;
+ }
+ else
+ {
+ result = UserId.Null;
+ input = false;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.cs b/src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.cs
new file mode 100644
index 000000000..979e1616a
--- /dev/null
+++ b/src/Ryujinx/UI/ViewModels/ProfileSelectorDialogViewModel.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 ProfileSelectorDialogViewModel : BaseModel
+ {
+
+ [ObservableProperty] private UserId _selectedUserId;
+
+ [ObservableProperty] private ObservableCollection _profiles = [];
+ }
+}