From 263eb97f79f02db12c2499ceec7e957b91bc008b Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Sat, 3 Aug 2024 23:31:34 +0200
Subject: Avoid race conditions while launching games directly from the command
 line (#7116)

* optimization: Load application metadata only for applications with IDs

* Load applications when necessary

This prevents loading applications when launching an application
directly from the command line (or a shortcut).
Instead, applications will be loaded after the emulation was stopped by the user.

* Show the title in the configured language when launching an application

* Rename DesiredTitleLanguage to DesiredLanguage
---
 src/Ryujinx.Gtk3/Program.cs                     |  6 +++
 src/Ryujinx.Gtk3/UI/MainWindow.cs               |  9 ++--
 src/Ryujinx.UI.Common/App/ApplicationLibrary.cs | 57 +++++++++++++------------
 src/Ryujinx/UI/Windows/MainWindow.axaml.cs      | 22 ++++++++--
 4 files changed, 61 insertions(+), 33 deletions(-)

(limited to 'src')

diff --git a/src/Ryujinx.Gtk3/Program.cs b/src/Ryujinx.Gtk3/Program.cs
index 0fb71288..8bb65164 100644
--- a/src/Ryujinx.Gtk3/Program.cs
+++ b/src/Ryujinx.Gtk3/Program.cs
@@ -256,6 +256,12 @@ namespace Ryujinx
             MainWindow mainWindow = new();
             mainWindow.Show();
 
+            // Load the game table if no application was requested by the command line
+            if (CommandLineState.LaunchPathArg == null)
+            {
+                mainWindow.UpdateGameTable();
+            }
+
             if (OperatingSystem.IsLinux())
             {
                 int currentVmMaxMapCount = LinuxHelper.VmMaxMapCount;
diff --git a/src/Ryujinx.Gtk3/UI/MainWindow.cs b/src/Ryujinx.Gtk3/UI/MainWindow.cs
index 7f9eceb3..66c0afae 100644
--- a/src/Ryujinx.Gtk3/UI/MainWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/MainWindow.cs
@@ -187,7 +187,10 @@ namespace Ryujinx.UI
                 : IntegrityCheckLevel.None;
 
             // Instantiate GUI objects.
-            ApplicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel);
+            ApplicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel)
+            {
+                DesiredLanguage = ConfigurationState.Instance.System.Language,
+            };
             _uiHandler = new GtkHostUIHandler(this);
             _deviceExitStatus = new AutoResetEvent(false);
 
@@ -325,7 +328,6 @@ namespace Ryujinx.UI
             _hideUI.Label = _hideUI.Label.Replace("SHOWUIKEY", ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI.ToString());
 
             UpdateColumns();
-            UpdateGameTable();
 
             ConfigurationState.Instance.UI.GameDirs.Event += (sender, args) =>
             {
@@ -738,7 +740,8 @@ namespace Ryujinx.UI
 
             Thread applicationLibraryThread = new(() =>
             {
-                ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language);
+                ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language;
+                ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs);
 
                 _updatingGameTable = false;
             })
diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
index cd95c4d8..2defc1f6 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
@@ -34,6 +34,7 @@ namespace Ryujinx.UI.App.Common
 {
     public class ApplicationLibrary
     {
+        public Language DesiredLanguage { get; set; }
         public event EventHandler<ApplicationAddedEventArgs> ApplicationAdded;
         public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
 
@@ -45,7 +46,6 @@ namespace Ryujinx.UI.App.Common
 
         private readonly VirtualFileSystem _virtualFileSystem;
         private readonly IntegrityCheckLevel _checkLevel;
-        private Language _desiredTitleLanguage;
         private CancellationTokenSource _cancellationToken;
 
         private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@@ -221,7 +221,7 @@ namespace Ryujinx.UI.App.Common
                 {
                     using UniqueRef<IFile> icon = new();
 
-                    controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+                    controlFs.OpenFile(ref icon.Ref, $"/icon_{DesiredLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
 
                     using MemoryStream stream = new();
 
@@ -432,35 +432,40 @@ namespace Ryujinx.UI.App.Common
 
             foreach (var data in applications)
             {
-                ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata =>
+                // Only load metadata for applications with an ID
+                if (data.Id != 0)
                 {
-                    appMetadata.Title = data.Name;
-
-                    // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
-                    if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
+                    ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata =>
                     {
-                        appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
-                        appMetadata.TimePlayedOld = default;
-                    }
+                        appMetadata.Title = data.Name;
 
-                    // Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
-                    if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
-                    {
-                        // Migrate from string-based last_played to DateTime-based last_played_utc.
-                        if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+                        // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
+                        if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
                         {
-                            appMetadata.LastPlayed = lastPlayedOldParsed;
+                            appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
+                            appMetadata.TimePlayedOld = default;
+                        }
+
+                        // Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
+                        if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
+                        {
+                            // Migrate from string-based last_played to DateTime-based last_played_utc.
+                            if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+                            {
+                                appMetadata.LastPlayed = lastPlayedOldParsed;
+
+                                // Migration successful: deleting last_played from the metadata file.
+                                appMetadata.LastPlayedOld = default;
+                            }
 
-                            // Migration successful: deleting last_played from the metadata file.
-                            appMetadata.LastPlayedOld = default;
                         }
+                    });
 
-                    }
-                });
+                    data.Favorite = appMetadata.Favorite;
+                    data.TimePlayed = appMetadata.TimePlayed;
+                    data.LastPlayed = appMetadata.LastPlayed;
+                }
 
-                data.Favorite = appMetadata.Favorite;
-                data.TimePlayed = appMetadata.TimePlayed;
-                data.LastPlayed = appMetadata.LastPlayed;
                 data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper();
                 data.FileSize = fileSize;
                 data.Path = applicationPath;
@@ -482,13 +487,11 @@ namespace Ryujinx.UI.App.Common
             controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
         }
 
-        public void LoadApplications(List<string> appDirs, Language desiredTitleLanguage)
+        public void LoadApplications(List<string> appDirs)
         {
             int numApplicationsFound = 0;
             int numApplicationsLoaded = 0;
 
-            _desiredTitleLanguage = desiredTitleLanguage;
-
             _cancellationToken = new CancellationTokenSource();
 
             // Builds the applications list with paths to found applications
@@ -847,7 +850,7 @@ namespace Ryujinx.UI.App.Common
 
         private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data)
         {
-            _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
+            _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
 
             if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage)
             {
diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
index dc5336ab..348412e7 100644
--- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
@@ -37,6 +37,7 @@ namespace Ryujinx.Ava.UI.Windows
         internal static MainWindowViewModel MainWindowViewModel { get; private set; }
 
         private bool _isLoading;
+        private bool _applicationsLoadedOnce;
 
         private UserChannelPersistence _userChannelPersistence;
         private static bool _deferLoad;
@@ -224,7 +225,10 @@ namespace Ryujinx.Ava.UI.Windows
                 ? IntegrityCheckLevel.ErrorOnInvalid
                 : IntegrityCheckLevel.None;
 
-            ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel);
+            ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel)
+            {
+                DesiredLanguage = ConfigurationState.Instance.System.Language,
+            };
 
             // Save data created before we supported extra data in directory save data will not work properly if
             // given empty extra data. Luckily some of that extra data can be created using the data from the
@@ -472,7 +476,11 @@ namespace Ryujinx.Ava.UI.Windows
 
             ViewModel.RefreshFirmwareStatus();
 
-            LoadApplications();
+            // Load applications if no application was requested by the command line
+            if (!_deferLoad)
+            {
+                LoadApplications();
+            }
 
 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
             CheckLaunchState();
@@ -485,6 +493,12 @@ namespace Ryujinx.Ava.UI.Windows
 
             if (MainContent.Content != content)
             {
+                // Load applications while switching to the GameLibrary if we haven't done that yet
+                if (!_applicationsLoadedOnce && content == GameLibrary)
+                {
+                    LoadApplications();
+                }
+
                 MainContent.Content = content;
             }
         }
@@ -581,6 +595,7 @@ namespace Ryujinx.Ava.UI.Windows
 
         public void LoadApplications()
         {
+            _applicationsLoadedOnce = true;
             ViewModel.Applications.Clear();
 
             StatusBarView.LoadProgressBar.IsVisible = true;
@@ -622,7 +637,8 @@ namespace Ryujinx.Ava.UI.Windows
 
             Thread applicationLibraryThread = new(() =>
             {
-                ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language);
+                ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language;
+                ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs);
 
                 _isLoading = false;
             })
-- 
cgit v1.2.3-70-g09d2