From e348f95495d3af9742010b1e44a2c3d7f30dba1a Mon Sep 17 00:00:00 2001
From: Alex Barney <thealexbarney@gmail.com>
Date: Sun, 12 Jan 2020 04:15:17 -0700
Subject: [PATCH] Call EnsureApplicationSaveData when launching a game (#871)

* Workaround for the lack of a program registry

* Call EnsureApplicationSaveData when launching a game
---
 Ryujinx.HLE/HOS/Horizon.cs                    | 51 +++++++++++++++++--
 .../Services/Arp/ApplicationLaunchProperty.cs |  6 +--
 .../HOS/Services/Fs/IFileSystemProxy.cs       | 30 +++++++++++
 Ryujinx/Ui/GLScreen.cs                        |  6 +--
 Ryujinx/Ui/MainWindow.cs                      |  6 +--
 5 files changed, 85 insertions(+), 14 deletions(-)

diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs
index 855d89148c..428e19227d 100644
--- a/Ryujinx.HLE/HOS/Horizon.cs
+++ b/Ryujinx.HLE/HOS/Horizon.cs
@@ -1,9 +1,11 @@
 using LibHac;
+using LibHac.Account;
 using LibHac.Common;
 using LibHac.Fs;
 using LibHac.FsService;
 using LibHac.FsSystem;
 using LibHac.FsSystem.NcaUtils;
+using LibHac.Ncm;
 using LibHac.Ns;
 using LibHac.Spl;
 using Ryujinx.Common.Logging;
@@ -32,6 +34,8 @@ using System.Threading;
 using TimeServiceManager = Ryujinx.HLE.HOS.Services.Time.TimeManager;
 using NxStaticObject     = Ryujinx.HLE.Loaders.Executables.NxStaticObject;
 
+using static LibHac.Fs.ApplicationSaveDataManagement;
+
 namespace Ryujinx.HLE.HOS
 {
     public class Horizon : IDisposable
@@ -109,7 +113,8 @@ namespace Ryujinx.HLE.HOS
 
         public string TitleName { get; private set; }
 
-        public string TitleId { get; private set; }
+        public ulong  TitleId { get; private set; }
+        public string TitleIdText => TitleId.ToString("x16");
 
         public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; }
 
@@ -513,7 +518,7 @@ namespace Ryujinx.HLE.HOS
 
             LoadExeFs(codeFs, out Npdm metaData);
             
-            TitleId = metaData.Aci0.TitleId.ToString("x16");
+            TitleId = metaData.Aci0.TitleId;
 
             if (controlNca != null)
             {
@@ -523,6 +528,11 @@ namespace Ryujinx.HLE.HOS
             {
                 ControlData.ByteSpan.Clear();
             }
+
+            if (TitleId != 0)
+            {
+                EnsureSaveData(new TitleId(TitleId));
+            }
         }
 
         private void LoadExeFs(IFileSystem codeFs, out Npdm metaData)
@@ -561,7 +571,7 @@ namespace Ryujinx.HLE.HOS
                 }
             }
 
-            TitleId = metaData.Aci0.TitleId.ToString("x16");
+            TitleId = metaData.Aci0.TitleId;
 
             LoadNso("rtld");
             LoadNso("main");
@@ -664,7 +674,7 @@ namespace Ryujinx.HLE.HOS
             ContentManager.LoadEntries();
 
             TitleName = metaData.TitleName;
-            TitleId   = metaData.Aci0.TitleId.ToString("x16");
+            TitleId   = metaData.Aci0.TitleId;
 
             ProgramLoader.LoadStaticObjects(this, metaData, new IExecutable[] { staticObject });
         }
@@ -679,6 +689,39 @@ namespace Ryujinx.HLE.HOS
             }
         }
 
+        private Result EnsureSaveData(TitleId titleId)
+        {
+            Logger.PrintInfo(LogClass.Application, "Ensuring required savedata exists.");
+
+            UInt128 lastOpenedUser = State.Account.LastOpenedUser.UserId;
+            Uid user = new Uid((ulong)lastOpenedUser.Low, (ulong)lastOpenedUser.High);
+
+            ref ApplicationControlProperty control = ref ControlData.Value;
+
+            if (LibHac.Util.IsEmpty(ControlData.ByteSpan))
+            {
+                // If the current application doesn't have a loaded control property, create a dummy one
+                // and set the savedata sizes so a user savedata will be created.
+                control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
+
+                // The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
+                control.UserAccountSaveDataSize = 0x4000;
+                control.UserAccountSaveDataJournalSize = 0x4000;
+
+                Logger.PrintWarning(LogClass.Application,
+                    "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
+            }
+
+            Result rc = EnsureApplicationSaveData(FsClient, out _, titleId, ref ControlData.Value, ref user);
+
+            if (rc.IsFailure())
+            {
+                Logger.PrintError(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {rc.ToStringWithName()}");
+            }
+
+            return rc;
+        }
+
         public void LoadKeySet()
         {
             string keyFile        = null;
diff --git a/Ryujinx.HLE/HOS/Services/Arp/ApplicationLaunchProperty.cs b/Ryujinx.HLE/HOS/Services/Arp/ApplicationLaunchProperty.cs
index 4962e3ffdc..686577d7e3 100644
--- a/Ryujinx.HLE/HOS/Services/Arp/ApplicationLaunchProperty.cs
+++ b/Ryujinx.HLE/HOS/Services/Arp/ApplicationLaunchProperty.cs
@@ -1,12 +1,10 @@
 using Ryujinx.HLE.FileSystem;
-using Ryujinx.HLE.Utilities;
-using System;
 
 namespace Ryujinx.HLE.HOS.Services.Arp
 {
     class ApplicationLaunchProperty
     {
-        public long  TitleId;
+        public ulong TitleId;
         public int   Version;
         public byte  BaseGameStorageId;
         public byte  UpdateGameStorageId;
@@ -33,7 +31,7 @@ namespace Ryujinx.HLE.HOS.Services.Arp
 
             return new ApplicationLaunchProperty
             {
-                TitleId             = BitConverter.ToInt64(StringUtils.HexToBytes(context.Device.System.TitleId), 0),
+                TitleId             = context.Device.System.TitleId,
                 Version             = 0x00,
                 BaseGameStorageId   = (byte)StorageId.NandSystem,
                 UpdateGameStorageId = (byte)StorageId.None
diff --git a/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs b/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
index 60f4a3f434..4e96788644 100644
--- a/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
+++ b/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
@@ -133,6 +133,20 @@ namespace Ryujinx.HLE.HOS.Services.Fs
             SaveDataCreateInfo createInfo     = context.RequestData.ReadStruct<SaveDataCreateInfo>();
             SaveMetaCreateInfo metaCreateInfo = context.RequestData.ReadStruct<SaveMetaCreateInfo>();
 
+            // TODO: There's currently no program registry for FS to reference.
+            // Workaround that by setting the application ID and owner ID if they're not already set
+            if (attribute.TitleId == TitleId.Zero)
+            {
+                attribute.TitleId = new TitleId(context.Process.TitleId);
+            }
+
+            if (createInfo.OwnerId == TitleId.Zero)
+            {
+                createInfo.OwnerId = new TitleId(context.Process.TitleId);
+            }
+
+            Logger.PrintInfo(LogClass.ServiceFs, $"Creating save with title ID {attribute.TitleId.Value:x16}");
+
             Result result = _baseFileSystemProxy.CreateSaveDataFileSystem(ref attribute, ref createInfo, ref metaCreateInfo);
 
             return (ResultCode)result.Value;
@@ -196,6 +210,18 @@ namespace Ryujinx.HLE.HOS.Services.Fs
             SaveMetaCreateInfo metaCreateInfo = context.RequestData.ReadStruct<SaveMetaCreateInfo>();
             HashSalt           hashSalt       = context.RequestData.ReadStruct<HashSalt>();
 
+            // TODO: There's currently no program registry for FS to reference.
+            // Workaround that by setting the application ID and owner ID if they're not already set
+            if (attribute.TitleId == TitleId.Zero)
+            {
+                attribute.TitleId = new TitleId(context.Process.TitleId);
+            }
+
+            if (createInfo.OwnerId == TitleId.Zero)
+            {
+                createInfo.OwnerId = new TitleId(context.Process.TitleId);
+            }
+
             Result result = _baseFileSystemProxy.CreateSaveDataFileSystemWithHashSalt(ref attribute, ref createInfo, ref metaCreateInfo, ref hashSalt);
 
             return (ResultCode)result.Value;
@@ -208,6 +234,8 @@ namespace Ryujinx.HLE.HOS.Services.Fs
             SaveDataSpaceId   spaceId   = (SaveDataSpaceId)context.RequestData.ReadInt64();
             SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
 
+            // TODO: There's currently no program registry for FS to reference.
+            // Workaround that by setting the application ID if it's not already set
             if (attribute.TitleId == TitleId.Zero)
             {
                 attribute.TitleId = new TitleId(context.Process.TitleId);
@@ -247,6 +275,8 @@ namespace Ryujinx.HLE.HOS.Services.Fs
             SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64();
             SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
 
+            // TODO: There's currently no program registry for FS to reference.
+            // Workaround that by setting the application ID if it's not already set
             if (attribute.TitleId == TitleId.Zero)
             {
                 attribute.TitleId = new TitleId(context.Process.TitleId);
diff --git a/Ryujinx/Ui/GLScreen.cs b/Ryujinx/Ui/GLScreen.cs
index 6c0c28586b..8e39126224 100644
--- a/Ryujinx/Ui/GLScreen.cs
+++ b/Ryujinx/Ui/GLScreen.cs
@@ -307,10 +307,10 @@ namespace Ryujinx.Ui
             string titleNameSection = string.IsNullOrWhiteSpace(_device.System.TitleName) ? string.Empty
                 : " | " + _device.System.TitleName;
 
-            string titleIDSection = string.IsNullOrWhiteSpace(_device.System.TitleId) ? string.Empty
-                : " | " + _device.System.TitleId.ToUpper();
+            string titleIdSection = string.IsNullOrWhiteSpace(_device.System.TitleIdText) ? string.Empty
+                : " | " + _device.System.TitleIdText.ToUpper();
 
-            _newTitle = $"Ryujinx{titleNameSection}{titleIDSection} | Host FPS: {hostFps:0.0} | Game FPS: {gameFps:0.0} | " +
+            _newTitle = $"Ryujinx{titleNameSection}{titleIdSection} | Host FPS: {hostFps:0.0} | Game FPS: {gameFps:0.0} | " +
                 $"Game Vsync: {(_device.EnableDeviceVsync ? "On" : "Off")}";
 
             _titleEvent = true;
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index c914a4a7a3..af7dd524ab 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -305,9 +305,9 @@ namespace Ryujinx.Ui
                 _firmwareInstallFile.Sensitive      = false;
                 _firmwareInstallDirectory.Sensitive = false;
 
-                DiscordIntegrationModule.SwitchToPlayingState(_device.System.TitleId, _device.System.TitleName);
+                DiscordIntegrationModule.SwitchToPlayingState(_device.System.TitleIdText, _device.System.TitleName);
 
-                ApplicationLibrary.LoadAndSaveMetaData(_device.System.TitleId, appMetadata =>
+                ApplicationLibrary.LoadAndSaveMetaData(_device.System.TitleIdText, appMetadata =>
                 {
                     appMetadata.LastPlayed = DateTime.UtcNow.ToString();
                 });
@@ -337,7 +337,7 @@ namespace Ryujinx.Ui
 
             if (_gameLoaded)
             {
-                ApplicationLibrary.LoadAndSaveMetaData(_device.System.TitleId, appMetadata =>
+                ApplicationLibrary.LoadAndSaveMetaData(_device.System.TitleIdText, appMetadata =>
                 {
                     DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed);
                     double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;