diff --git a/Ryujinx.HLE/Loaders/Npdm/ACI0.cs b/Ryujinx.HLE/Loaders/Npdm/ACI0.cs index af426bcfc8..29fc309a4a 100644 --- a/Ryujinx.HLE/Loaders/Npdm/ACI0.cs +++ b/Ryujinx.HLE/Loaders/Npdm/ACI0.cs @@ -3,7 +3,7 @@ using System.IO; namespace Ryujinx.HLE.Loaders.Npdm { - class Aci0 + public class Aci0 { private const int Aci0Magic = 'A' << 0 | 'C' << 8 | 'I' << 16 | '0' << 24; diff --git a/Ryujinx.HLE/Loaders/Npdm/ACID.cs b/Ryujinx.HLE/Loaders/Npdm/ACID.cs index 4a181b294f..365495c60b 100644 --- a/Ryujinx.HLE/Loaders/Npdm/ACID.cs +++ b/Ryujinx.HLE/Loaders/Npdm/ACID.cs @@ -3,7 +3,7 @@ using System.IO; namespace Ryujinx.HLE.Loaders.Npdm { - class Acid + public class Acid { private const int AcidMagic = 'A' << 0 | 'C' << 8 | 'I' << 16 | 'D' << 24; diff --git a/Ryujinx.HLE/Loaders/Npdm/FsAccessControl.cs b/Ryujinx.HLE/Loaders/Npdm/FsAccessControl.cs index 3359435dcb..d0f349eaf3 100644 --- a/Ryujinx.HLE/Loaders/Npdm/FsAccessControl.cs +++ b/Ryujinx.HLE/Loaders/Npdm/FsAccessControl.cs @@ -2,7 +2,7 @@ namespace Ryujinx.HLE.Loaders.Npdm { - class FsAccessControl + public class FsAccessControl { public int Version { get; private set; } public ulong PermissionsBitmask { get; private set; } diff --git a/Ryujinx.HLE/Loaders/Npdm/FsAccessHeader.cs b/Ryujinx.HLE/Loaders/Npdm/FsAccessHeader.cs index 564b8dc391..57d1208db9 100644 --- a/Ryujinx.HLE/Loaders/Npdm/FsAccessHeader.cs +++ b/Ryujinx.HLE/Loaders/Npdm/FsAccessHeader.cs @@ -4,7 +4,7 @@ using System.IO; namespace Ryujinx.HLE.Loaders.Npdm { - class FsAccessHeader + public class FsAccessHeader { public int Version { get; private set; } public ulong PermissionsBitmask { get; private set; } diff --git a/Ryujinx.HLE/Loaders/Npdm/KernelAccessControl.cs b/Ryujinx.HLE/Loaders/Npdm/KernelAccessControl.cs index d8e40d0b99..39803642c9 100644 --- a/Ryujinx.HLE/Loaders/Npdm/KernelAccessControl.cs +++ b/Ryujinx.HLE/Loaders/Npdm/KernelAccessControl.cs @@ -2,7 +2,7 @@ namespace Ryujinx.HLE.Loaders.Npdm { - class KernelAccessControl + public class KernelAccessControl { public int[] Capabilities { get; private set; } diff --git a/Ryujinx.HLE/Loaders/Npdm/Npdm.cs b/Ryujinx.HLE/Loaders/Npdm/Npdm.cs index 36449e40a5..d2cce2c39e 100644 --- a/Ryujinx.HLE/Loaders/Npdm/Npdm.cs +++ b/Ryujinx.HLE/Loaders/Npdm/Npdm.cs @@ -4,10 +4,10 @@ using System.Text; namespace Ryujinx.HLE.Loaders.Npdm { - // https://github.com/SciresM/hactool/blob/master/npdm.c - // https://github.com/SciresM/hactool/blob/master/npdm.h - // http://switchbrew.org/index.php?title=NPDM - class Npdm + //https://github.com/SciresM/hactool/blob/master/npdm.c + //https://github.com/SciresM/hactool/blob/master/npdm.h + //http://switchbrew.org/index.php?title=NPDM + public class Npdm { private const int MetaMagic = 'M' << 0 | 'E' << 8 | 'T' << 16 | 'A' << 24; diff --git a/Ryujinx.HLE/Loaders/Npdm/ServiceAccessControl.cs b/Ryujinx.HLE/Loaders/Npdm/ServiceAccessControl.cs index 03f62ff7c5..54012b8a97 100644 --- a/Ryujinx.HLE/Loaders/Npdm/ServiceAccessControl.cs +++ b/Ryujinx.HLE/Loaders/Npdm/ServiceAccessControl.cs @@ -5,7 +5,7 @@ using System.Text; namespace Ryujinx.HLE.Loaders.Npdm { - class ServiceAccessControl + public class ServiceAccessControl { public IReadOnlyDictionary Services { get; private set; } diff --git a/Ryujinx/ApplicationLibrary.cs b/Ryujinx/ApplicationLibrary.cs index cd9194efb9..217e3fbfd7 100644 --- a/Ryujinx/ApplicationLibrary.cs +++ b/Ryujinx/ApplicationLibrary.cs @@ -2,6 +2,7 @@ using LibHac.Fs; using LibHac.Fs.NcaUtils; using Ryujinx.Common.Logging; +using Ryujinx.HLE.Loaders.Npdm; using System; using System.Collections.Generic; using System.IO; @@ -25,6 +26,7 @@ namespace Ryujinx { public Gdk.Pixbuf Icon; public string GameName; + public string GameId; public string TimePlayed; public string LastPlayed; public string FileSize; @@ -72,10 +74,14 @@ namespace Ryujinx using (FileStream file = new FileStream(GamePath, FileMode.Open, FileAccess.Read)) { + Nca mainNca = null; + Nca patchNca = null; Nca controlNca = null; PartitionFileSystem pfs = null; IFileSystem controlFs = null; + Npdm metaData = null; string TitleName = null; + string TitleId = "010000000000100D"; Gdk.Pixbuf GameIcon = null; if ((Path.GetExtension(GamePath) == ".nsp") || (Path.GetExtension(GamePath) == ".pfs0")) @@ -104,13 +110,45 @@ namespace Ryujinx foreach (DirectoryEntry fileEntry in pfs.EnumerateEntries("*.nca")) { Nca nca = new Nca(MainWindow._device.System.KeySet, pfs.OpenFile(fileEntry.FullPath, OpenMode.Read).AsStorage()); - if (nca.Header.ContentType == ContentType.Control) + if (nca.Header.ContentType == ContentType.Program) + { + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, ContentType.Program); + + if (nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + patchNca = nca; + } + else + { + mainNca = nca; + } + } + else if (nca.Header.ContentType == ContentType.Control) { controlNca = nca; } } controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, MainWindow._device.System.FsIntegrityCheckLevel); + + if (patchNca == null) + { + if (mainNca.CanOpenSection(NcaSectionType.Code)) + { + IFileSystem codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, MainWindow._device.System.FsIntegrityCheckLevel); + metaData = new Npdm(codeFs.OpenFile("/main.npdm", OpenMode.Read).AsStream()); + TitleId = metaData.Aci0.TitleId.ToString("x16"); + } + } + else + { + if (patchNca.CanOpenSection(NcaSectionType.Code)) + { + IFileSystem codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, MainWindow._device.System.FsIntegrityCheckLevel); + metaData = new Npdm(codeFs.OpenFile("/main.npdm", OpenMode.Read).AsStream()); + TitleId = metaData.Aci0.TitleId.ToString("x16"); + } + } } if ((Path.GetExtension(GamePath) == ".nca") || (Path.GetExtension(GamePath) == ".nro") || (Path.GetExtension(GamePath) == ".nso")) { TitleName = Path.GetFileName(GamePath); } @@ -183,8 +221,9 @@ namespace Ryujinx { Icon = GameIcon, GameName = TitleName, - TimePlayed = "", - LastPlayed = "", + GameId = TitleId, + TimePlayed = GetPlayedData(TitleId)[0], + LastPlayed = GetPlayedData(TitleId)[1], FileSize = (filesize < 1) ? (filesize * 1024).ToString("0.##") + "MB" : filesize.ToString("0.##") + "GB", Path = GamePath, }; @@ -193,5 +232,45 @@ namespace Ryujinx } } } + + private static string[] GetPlayedData(string TitleId) + { + string[] playedData = new string[2]; + string appdataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string savePath = Path.Combine(appdataPath, "RyuFs", "nand", "user", "save", "0000000000000000", "savecommon", TitleId); + + if (File.Exists(Path.Combine(savePath, "TimePlayed.dat")) == false) + { + Directory.CreateDirectory(savePath); + using (FileStream file = File.OpenWrite(Path.Combine(savePath, "TimePlayed.dat"))) { file.Write(Encoding.ASCII.GetBytes("0")); } + } + using (FileStream fs = File.OpenRead(Path.Combine(savePath, "TimePlayed.dat"))) + { + using (StreamReader sr = new StreamReader(fs)) + { + float timePlayed = float.Parse(sr.ReadLine()); + + if (timePlayed <= 60.0) { playedData[0] = $"{timePlayed}s"; } + else if(timePlayed <= 3600.0) { playedData[0] = $"{Math.Round(timePlayed / 60 , 2, MidpointRounding.AwayFromZero)}mins"; } + else if(timePlayed <= 86400.0) { playedData[0] = $"{Math.Round(timePlayed / 3600 , 2, MidpointRounding.AwayFromZero)}hrs"; } + else { playedData[0] = $"{Math.Round(timePlayed / 86400, 2, MidpointRounding.AwayFromZero)}days"; } + } + } + + 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)) + { + playedData[1] = sr.ReadLine(); + } + } + + return playedData; + } } } diff --git a/Ryujinx/GUI/MainWindow.cs b/Ryujinx/GUI/MainWindow.cs index af86fcfc5f..9ce8ba6ad6 100644 --- a/Ryujinx/GUI/MainWindow.cs +++ b/Ryujinx/GUI/MainWindow.cs @@ -99,12 +99,12 @@ namespace Ryujinx Nfc.Sensitive = false; - GameTable.AppendColumn("Icon", new CellRendererPixbuf(), "pixbuf", 0); - GameTable.AppendColumn("Game", new CellRendererText(), "text", 1); - //GameTable.AppendColumn("Time Played", new CellRendererText(), "text", 2); - //GameTable.AppendColumn("Last Played", new CellRendererText(), "text", 3); - GameTable.AppendColumn("File Size", new CellRendererText(), "text", 4); - GameTable.AppendColumn("Path", new CellRendererText(), "text", 5); + GameTable.AppendColumn("Icon" , new CellRendererPixbuf(), "pixbuf", 0); + GameTable.AppendColumn("Game" , new CellRendererText() , "text" , 1); + GameTable.AppendColumn("Time Played", new CellRendererText() , "text" , 2); + GameTable.AppendColumn("Last Played", new CellRendererText() , "text" , 3); + GameTable.AppendColumn("File Size" , new CellRendererText() , "text" , 4); + GameTable.AppendColumn("Path" , new CellRendererText() , "text" , 5); _TableStore = new ListStore(typeof(Gdk.Pixbuf), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string)); GameTable.Model = _TableStore; @@ -120,7 +120,7 @@ namespace Ryujinx foreach (ApplicationLibrary.ApplicationData AppData in ApplicationLibrary.ApplicationLibraryData) { - _TableStore.AppendValues(AppData.Icon, AppData.GameName, AppData.TimePlayed, AppData.LastPlayed, AppData.FileSize, AppData.Path); + _TableStore.AppendValues(AppData.Icon, $"{AppData.GameName}\n{AppData.GameId.ToUpper()}", AppData.TimePlayed, AppData.LastPlayed, AppData.FileSize, AppData.Path); } } @@ -157,7 +157,7 @@ namespace Ryujinx { if (_GameLoaded) { - MessageDialog eRrOr = new MessageDialog(null, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "A game has already been loaded, please close the game and try again"); + MessageDialog eRrOr = new MessageDialog(null, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "A game has already been loaded, please unload the game and try again"); eRrOr.SetSizeRequest(100, 20); eRrOr.Title = "Ryujinx - Error"; eRrOr.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.GUI.assets.ryujinxIcon.png"); @@ -235,10 +235,20 @@ namespace Ryujinx DiscordPresence.Assets.LargeImageText = _device.System.TitleName; DiscordPresence.Assets.SmallImageKey = "ryujinx"; DiscordPresence.Assets.SmallImageText = "Ryujinx is an emulator for the Nintendo Switch"; - DiscordPresence.Timestamps = new Timestamps(DateTime.UtcNow); + DiscordPresence.Timestamps = new Timestamps(DateTime.UnixEpoch); DiscordClient.SetPresence(DiscordPresence); } + + string appdataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string savePath = System.IO.Path.Combine(appdataPath, "RyuFs", "nand", "user", "save", "0000000000000000", "savecommon", _device.System.TitleID); + using (FileStream fs = File.OpenWrite(System.IO.Path.Combine(savePath, "LastPlayed.dat"))) + { + using (StreamWriter sr = new StreamWriter(fs)) + { + sr.WriteLine(DateTime.UtcNow); + } + } } private static void CreateGameWindow() @@ -254,6 +264,42 @@ namespace Ryujinx _audioOut.Dispose(); } } + + private static void End() + { + string appdataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string savePath = System.IO.Path.Combine(appdataPath, "RyuFs", "nand", "user", "save", "0000000000000000", "savecommon", _device.System.TitleID); + double currentPlayTime = 0; + + using (FileStream fs = File.OpenRead(System.IO.Path.Combine(savePath, "LastPlayed.dat"))) + { + using (StreamReader sr = new StreamReader(fs)) + { + DateTime startTime = DateTime.Parse(sr.ReadLine()); + + using (FileStream lpfs = File.OpenRead(System.IO.Path.Combine(savePath, "TimePlayed.dat"))) + { + using (StreamReader lpsr = new StreamReader(lpfs)) + { + currentPlayTime = double.Parse(lpsr.ReadLine()); + } + } + + using (FileStream tpfs = File.OpenWrite(System.IO.Path.Combine(savePath, "TimePlayed.dat"))) + { + using (StreamWriter tpsr = new StreamWriter(tpfs)) + { + tpsr.WriteLine(currentPlayTime + Math.Round(DateTime.UtcNow.Subtract(startTime).TotalSeconds, MidpointRounding.AwayFromZero)); + } + } + } + } + + _audioOut.Dispose(); + DiscordClient.Dispose(); + Logger.Shutdown(); + Environment.Exit(0); + } //Events private void Row_Activated(object obj, RowActivatedArgs args) @@ -303,21 +349,9 @@ namespace Ryujinx fc.Destroy(); } - private void Exit_Pressed(object o, EventArgs args) - { - _audioOut.Dispose(); - DiscordClient.Dispose(); - Logger.Shutdown(); - Environment.Exit(0); - } + private void Exit_Pressed(object o, EventArgs args) { End(); } - private void Window_Close(object obj, DeleteEventArgs args) - { - _audioOut.Dispose(); - DiscordClient.Dispose(); - Logger.Shutdown(); - Environment.Exit(0); - } + private void Window_Close(object obj, DeleteEventArgs args) { End(); } private void ReturnMain_Pressed(object o, EventArgs args) {