diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs index 7e728e7f72..1c6abc9878 100644 --- a/Ryujinx/Ui/ApplicationLibrary.cs +++ b/Ryujinx/Ui/ApplicationLibrary.cs @@ -1,4 +1,5 @@ -using LibHac; +using JsonPrettyPrinterPlus; +using LibHac; using LibHac.Fs; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; @@ -10,28 +11,20 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; - +using Utf8Json; +using Utf8Json.Resolvers; using SystemState = Ryujinx.HLE.HOS.SystemState; namespace Ryujinx.UI { public class ApplicationLibrary { - private static Keyset KeySet; - private static SystemState.TitleLanguage DesiredTitleLanguage; - - private const double SecondsPerMinute = 60.0; - private const double SecondsPerHour = SecondsPerMinute * 60; - private const double SecondsPerDay = SecondsPerHour * 24; - public static byte[] RyujinxNspIcon { get; private set; } public static byte[] RyujinxXciIcon { get; private set; } public static byte[] RyujinxNcaIcon { get; private set; } public static byte[] RyujinxNroIcon { get; private set; } public static byte[] RyujinxNsoIcon { get; private set; } - public static List ApplicationLibraryData { get; private set; } - public struct ApplicationData { public bool Fav; @@ -47,6 +40,24 @@ namespace Ryujinx.UI public string Path; } + public static List ApplicationLibraryData { get; private set; } + + private static Keyset KeySet; + private static SystemState.TitleLanguage DesiredTitleLanguage; + + private const double SecondsPerMinute = 60.0; + private const double SecondsPerHour = SecondsPerMinute * 60; + private const double SecondsPerDay = SecondsPerHour * 24; + + private struct ApplicationMetadata + { + public bool Fav; + public double TimePlayed; + public string LastPlayed; + } + + private static ApplicationMetadata AppMetadata; + public static void Init(List AppDirs, Keyset keySet, SystemState.TitleLanguage desiredTitleLanguage) { KeySet = keySet; @@ -297,7 +308,7 @@ namespace Ryujinx.UI if (Path.GetExtension(applicationPath) == ".nca") { Nca nca = new Nca(KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage(false)); - if (nca.Header.ContentType != ContentType.Program) + if (nca.Header.ContentType != NcaContentType.Program) { continue; } @@ -323,18 +334,18 @@ namespace Ryujinx.UI } } - string[] userData = GetUserData(titleId, "00000000000000000000000000000001"); + (bool, string, string) metaData = GetMetadata(titleId); ApplicationData data = new ApplicationData() { - Fav = bool.Parse(userData[2]), + Fav = metaData.Item1, Icon = applicationIcon, TitleName = titleName, TitleId = titleId, Developer = developer, Version = version, - TimePlayed = userData[0], - LastPlayed = userData[1], + TimePlayed = metaData.Item2, + LastPlayed = metaData.Item3, FileExt = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1), FileSize = (filesize < 1) ? (filesize * 1024).ToString("0.##") + "MB" : filesize.ToString("0.##") + "GB", Path = applicationPath, @@ -388,82 +399,53 @@ namespace Ryujinx.UI return controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); } - private static string[] GetUserData(string TitleId, string UserId) + private static (bool, string, string) GetMetadata(string TitleId) { - try + string metadataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "games", TitleId, "gui"); + string metadataFile = Path.Combine(metadataFolder, "metadata.json"); + + IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); + + if (!File.Exists(metadataFile)) { - string[] userData = new string[3]; - string savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "GUI", UserId, TitleId); + Directory.CreateDirectory(metadataFolder); - //Time Played - if (File.Exists(Path.Combine(savePath, "TimePlayed.dat")) == false) + AppMetadata = new ApplicationMetadata { - Directory.CreateDirectory(savePath); - using (FileStream file = File.OpenWrite(Path.Combine(savePath, "TimePlayed.dat"))) - { - file.Write(Encoding.ASCII.GetBytes("0")); - } - } + Fav = false, + TimePlayed = 0, + LastPlayed = "Never" + }; - using (FileStream fs = File.OpenRead(Path.Combine(savePath, "TimePlayed.dat"))) - { - using (StreamReader sr = new StreamReader(fs)) - { - float timePlayed = float.Parse(sr.ReadLine()); - - if (timePlayed < SecondsPerMinute) - { - userData[0] = $"{timePlayed}s"; - } - else if (timePlayed < SecondsPerHour) - { - userData[0] = $"{Math.Round(timePlayed / SecondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins"; - } - else if (timePlayed < SecondsPerDay) - { - userData[0] = $"{Math.Round(timePlayed / SecondsPerHour , 2, MidpointRounding.AwayFromZero)} hrs"; - } - else - { - userData[0] = $"{Math.Round(timePlayed / SecondsPerDay , 2, MidpointRounding.AwayFromZero)} days"; - } - } - } - - //Last Played - if (File.Exists(Path.Combine(savePath, "LastPlayed.dat")) == false) - { - Directory.CreateDirectory(savePath); - using (FileStream file = File.OpenWrite(Path.Combine(savePath, "LastPlayed.dat"))) - { - file.Write(Encoding.ASCII.GetBytes("Never")); - } - } - - using (FileStream fs = File.OpenRead(Path.Combine(savePath, "LastPlayed.dat"))) - { - using (StreamReader sr = new StreamReader(fs)) - { - userData[1] = sr.ReadLine(); - } - } - - //Fav Games - if (File.Exists(Path.Combine(savePath, "Fav.dat"))) - { - userData[2] = "true"; - } - else - { - userData[2] = "false"; - } - - return userData; + byte[] saveData = JsonSerializer.Serialize(AppMetadata, resolver); + File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson()); } - catch + + using (Stream stream = File.OpenRead(metadataFile)) { - return new string[] { "Unknown", "Unknown", "false" }; + AppMetadata = JsonSerializer.Deserialize(stream, resolver); } + + string timePlayed; + + if (AppMetadata.TimePlayed < SecondsPerMinute) + { + timePlayed = $"{AppMetadata.TimePlayed}s"; + } + else if (AppMetadata.TimePlayed < SecondsPerHour) + { + timePlayed = $"{Math.Round(AppMetadata.TimePlayed / SecondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins"; + } + else if (AppMetadata.TimePlayed < SecondsPerDay) + { + timePlayed = $"{Math.Round(AppMetadata.TimePlayed / SecondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs"; + } + else + { + timePlayed = $"{Math.Round(AppMetadata.TimePlayed / SecondsPerDay, 2, MidpointRounding.AwayFromZero)} days"; + } + + return (AppMetadata.Fav, timePlayed, AppMetadata.LastPlayed); } private static byte[] NspOrXciIcon(string applicationPath) diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index aee42364b8..5440267936 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -1,3 +1,4 @@ +using JsonPrettyPrinterPlus; using DiscordRPC; using Gtk; using GUI = Gtk.Builder.ObjectAttribute; @@ -13,6 +14,8 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading; +using Utf8Json; +using Utf8Json.Resolvers; namespace Ryujinx.UI { @@ -36,8 +39,6 @@ namespace Ryujinx.UI private static bool _firstLoadComplete = false; - private static string _userId = "00000000000000000000000000000001"; - private static TreeViewColumn favColumn; private static TreeViewColumn appColumn; private static TreeViewColumn devColumn; @@ -48,6 +49,15 @@ namespace Ryujinx.UI private static TreeViewColumn fileSizeColumn; private static TreeViewColumn pathColumn; + private struct ApplicationMetadata + { + public bool Fav; + public double TimePlayed; + public string LastPlayed; + } + + private static ApplicationMetadata AppMetadata; + public static bool DiscordIntegrationEnabled { get; set; } public static DiscordRpcClient DiscordClient; @@ -355,40 +365,35 @@ namespace Ryujinx.UI DiscordClient.SetPresence(DiscordPresence); } - try + string metadataFolder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "games", _device.System.TitleID, "gui"); + string metadataFile = System.IO.Path.Combine(metadataFolder, "metadata.json"); + + IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); + + if (!File.Exists(metadataFile)) { - string savePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "GUI", _userId, _device.System.TitleID); + Directory.CreateDirectory(metadataFolder); - if (File.Exists(System.IO.Path.Combine(savePath, "TimePlayed.dat")) == false) + AppMetadata = new ApplicationMetadata { - Directory.CreateDirectory(savePath); - using (FileStream stream = File.OpenWrite(System.IO.Path.Combine(savePath, "TimePlayed.dat"))) - { - stream.Write(Encoding.ASCII.GetBytes("0")); - } - } + Fav = false, + TimePlayed = 0, + LastPlayed = "Never" + }; - if (File.Exists(System.IO.Path.Combine(savePath, "LastPlayed.dat")) == false) - { - Directory.CreateDirectory(savePath); - using (FileStream stream = File.OpenWrite(System.IO.Path.Combine(savePath, "LastPlayed.dat"))) - { - stream.Write(Encoding.ASCII.GetBytes("Never")); - } - } - - using (FileStream stream = File.OpenWrite(System.IO.Path.Combine(savePath, "LastPlayed.dat"))) - { - using (StreamWriter writer = new StreamWriter(stream)) - { - writer.WriteLine(DateTime.UtcNow); - } - } + byte[] data = JsonSerializer.Serialize(AppMetadata, resolver); + File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson()); } - catch (ArgumentNullException) + + using (Stream stream = File.OpenRead(metadataFile)) { - Logger.PrintWarning(LogClass.Application, $"Could not access save path to retrieve time/last played data using: UserID: {_userId}, TitleID: {_device.System.TitleID}"); + AppMetadata = JsonSerializer.Deserialize(stream, resolver); } + + AppMetadata.LastPlayed = DateTime.UtcNow.ToString(); + + byte[] saveData = JsonSerializer.Serialize(AppMetadata, resolver); + File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson()); } } @@ -412,39 +417,35 @@ namespace Ryujinx.UI if (_gameLoaded) { - try - { - string savePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "GUI", _userId, _device.System.TitleID); - double currentPlayTime = 0; + string metadataFolder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "games", _device.System.TitleID, "gui"); + string metadataFile = System.IO.Path.Combine(metadataFolder, "metadata.json"); - using (FileStream stream = File.OpenRead(System.IO.Path.Combine(savePath, "LastPlayed.dat"))) + IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); + + if (!File.Exists(metadataFile)) + { + Directory.CreateDirectory(metadataFolder); + + AppMetadata = new ApplicationMetadata { - using (StreamReader reader = new StreamReader(stream)) - { - DateTime startTime = DateTime.Parse(reader.ReadLine()); + Fav = false, + TimePlayed = 0, + LastPlayed = "Never" + }; - using (FileStream lastPlayedStream = File.OpenRead(System.IO.Path.Combine(savePath, "TimePlayed.dat"))) - { - using (StreamReader lastPlayedReader = new StreamReader(lastPlayedStream)) - { - currentPlayTime = double.Parse(lastPlayedReader.ReadLine()); - } - } - - using (FileStream timePlayedStream = File.OpenWrite(System.IO.Path.Combine(savePath, "TimePlayed.dat"))) - { - using (StreamWriter timePlayedWriter = new StreamWriter(timePlayedStream)) - { - timePlayedWriter.WriteLine(currentPlayTime + Math.Round(DateTime.UtcNow.Subtract(startTime).TotalSeconds, MidpointRounding.AwayFromZero)); - } - } - } - } + byte[] data = JsonSerializer.Serialize(AppMetadata, resolver); + File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson()); } - catch (ArgumentNullException) + + using (Stream stream = File.OpenRead(metadataFile)) { - Logger.PrintWarning(LogClass.Application, $"Could not access save path to retrieve time/last played data using: UserID: {_userId}, TitleID: {_device.System.TitleID}"); + AppMetadata = JsonSerializer.Deserialize(stream, resolver); } + + AppMetadata.TimePlayed += Math.Round(DateTime.UtcNow.Subtract(DateTime.Parse(AppMetadata.LastPlayed)).TotalSeconds, MidpointRounding.AwayFromZero); + + byte[] saveData = JsonSerializer.Serialize(AppMetadata, resolver); + File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson()); } Profile.FinishProfiling(); @@ -480,26 +481,31 @@ namespace Ryujinx.UI private void FavToggle_Toggled(object o, ToggledArgs args) { _tableStore.GetIter(out TreeIter treeIter, new TreePath(args.Path)); - string savePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "GUI", _userId, _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower()); + string titleid = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); + string metadataPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "games", titleid, "gui", "metadata.json"); + + IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); + + using (Stream stream = File.OpenRead(metadataPath)) + { + AppMetadata = JsonSerializer.Deserialize(stream, resolver); + } if ((bool)_tableStore.GetValue(treeIter, 0)) { _tableStore.SetValue(treeIter, 0, false); - if (File.Exists(System.IO.Path.Combine(savePath, "Fav.dat"))) - { - File.Delete(System.IO.Path.Combine(savePath, "Fav.dat")); - } + AppMetadata.Fav = false; } else { _tableStore.SetValue(treeIter, 0, true); - if (!File.Exists(System.IO.Path.Combine(savePath, "Fav.dat"))) - { - using (File.Create(System.IO.Path.Combine(savePath, "Fav.dat"))) { }; - } + AppMetadata.Fav = true; } + + byte[] saveData = JsonSerializer.Serialize(AppMetadata, resolver); + File.WriteAllText(metadataPath, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson()); } private void Row_Activated(object o, RowActivatedArgs args) @@ -710,9 +716,6 @@ namespace Ryujinx.UI string aValue = model.GetValue(a, 5).ToString(); string bValue = model.GetValue(b, 5).ToString(); - if (aValue == "Unknown") { aValue = "0s"; } - if (bValue == "Unknown") { bValue = "0s"; } - if (aValue.Length > 4 && aValue.Substring(aValue.Length - 4) == "mins") { aValue = (float.Parse(aValue.Substring(0, aValue.Length - 5)) * 60).ToString();