using LibHac; using LibHac.Fs; using LibHac.Fs.NcaUtils; using Ryujinx.Common.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; 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 byte[] Icon; public string TitleName; public string TitleId; public string Developer; public string Version; public string TimePlayed; public string LastPlayed; public string FileExt; public string FileSize; public string Path; } public static void Init(List AppDirs, Keyset keySet, SystemState.TitleLanguage desiredTitleLanguage) { KeySet = keySet; DesiredTitleLanguage = desiredTitleLanguage; // Loads the default application Icons RyujinxNspIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSPIcon.png"); RyujinxXciIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxXCIIcon.png"); RyujinxNcaIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNCAIcon.png"); RyujinxNroIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNROIcon.png"); RyujinxNsoIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSOIcon.png"); // Builds the applications list with paths to found applications List applications = new List(); foreach (string appDir in AppDirs) { if (Directory.Exists(appDir) == false) { Logger.PrintWarning(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); continue; } DirectoryInfo AppDirInfo = new DirectoryInfo(appDir); foreach (FileInfo App in AppDirInfo.GetFiles()) { if ((Path.GetExtension(App.ToString()) == ".xci") || (Path.GetExtension(App.ToString()) == ".nca") || (Path.GetExtension(App.ToString()) == ".nsp") || (Path.GetExtension(App.ToString()) == ".pfs0") || (Path.GetExtension(App.ToString()) == ".nro") || (Path.GetExtension(App.ToString()) == ".nso")) { applications.Add(App.ToString()); } } } // Loops through applications list, creating a struct for each application and then adding the struct to a list of structs ApplicationLibraryData = new List(); foreach (string applicationPath in applications) { double filesize = new FileInfo(applicationPath).Length * 0.000000000931; string titleName = null; string titleId = null; string developer = null; string version = null; byte[] applicationIcon = null; using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read)) { if ((Path.GetExtension(applicationPath) == ".nsp") || (Path.GetExtension(applicationPath) == ".pfs0") || (Path.GetExtension(applicationPath) == ".xci")) { try { IFileSystem controlFs = null; // Store the ControlFS in variable called controlFs if (Path.GetExtension(applicationPath) == ".xci") { Xci xci = new Xci(KeySet, file.AsStorage()); controlFs = GetControlFs(xci.OpenPartition(XciPartitionType.Secure)); } else { controlFs = GetControlFs(new PartitionFileSystem(file.AsStorage())); } // Creates NACP class from the NACP file IFile controlNacp = controlFs.OpenFile("/control.nacp", OpenMode.Read); Nacp controlData = new Nacp(controlNacp.AsStream()); // Get the title name, title ID, developer name and version number from the NACP version = controlData.DisplayVersion; titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title; if (string.IsNullOrWhiteSpace(titleName)) { titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; } titleId = controlData.PresenceGroupId.ToString("x16"); if (string.IsNullOrWhiteSpace(titleId)) { titleId = controlData.SaveDataOwnerId.ToString("x16"); } if (string.IsNullOrWhiteSpace(titleId)) { titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); } developer = controlData.Descriptions[(int)DesiredTitleLanguage].Developer; if (string.IsNullOrWhiteSpace(developer)) { developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer; } // Read the icon from the ControlFS and store it as a byte array try { IFile icon = controlFs.OpenFile($"/icon_{DesiredTitleLanguage}.dat", OpenMode.Read); using (MemoryStream stream = new MemoryStream()) { icon.AsStream().CopyTo(stream); applicationIcon = stream.ToArray(); } } catch (HorizonResultException) { IDirectory controlDir = controlFs.OpenDirectory("./", OpenDirectoryMode.All); foreach (DirectoryEntry entry in controlDir.Read()) { if (entry.Name == "control.nacp") { continue; } IFile icon = controlFs.OpenFile(entry.FullPath, OpenMode.Read); using (MemoryStream stream = new MemoryStream()) { icon.AsStream().CopyTo(stream); applicationIcon = stream.ToArray(); } if (applicationIcon != null) { break; } } if (applicationIcon == null) { applicationIcon = NspOrXciIcon(applicationPath); } } } catch (MissingKeyException exception) { titleName = "Unknown"; titleId = "Unknown"; developer = "Unknown"; version = "?"; applicationIcon = NspOrXciIcon(applicationPath); Logger.PrintWarning(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); } catch (InvalidDataException) { titleName = "Unknown"; titleId = "Unknown"; developer = "Unknown"; version = "?"; applicationIcon = NspOrXciIcon(applicationPath); Logger.PrintWarning(LogClass.Application, $"The file is not an NCA file or the header key is incorrect. Errored File: {applicationPath}"); } catch (Exception exception) { Logger.PrintWarning(LogClass.Application, $"This warning usualy means that you have a DLC in one of you game directories\n{exception}"); continue; } } else if (Path.GetExtension(applicationPath) == ".nro") { BinaryReader reader = new BinaryReader(file); byte[] Read(long Position, int Size) { file.Seek(Position, SeekOrigin.Begin); return reader.ReadBytes(Size); } file.Seek(24, SeekOrigin.Begin); int AssetOffset = reader.ReadInt32(); if (Encoding.ASCII.GetString(Read(AssetOffset, 4)) == "ASET") { byte[] IconSectionInfo = Read(AssetOffset + 8, 0x10); long iconOffset = BitConverter.ToInt64(IconSectionInfo, 0); long iconSize = BitConverter.ToInt64(IconSectionInfo, 8); ulong nacpOffset = reader.ReadUInt64(); ulong nacpSize = reader.ReadUInt64(); // Reads and stores game icon as byte array applicationIcon = Read(AssetOffset + iconOffset, (int)iconSize); // Creates memory stream out of byte array which is the NACP using (MemoryStream stream = new MemoryStream(Read(AssetOffset + (int)nacpOffset, (int)nacpSize))) { // Creates NACP class from the memory stream Nacp controlData = new Nacp(stream); // Get the title name, title ID, developer name and version number from the NACP version = controlData.DisplayVersion; titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title; if (string.IsNullOrWhiteSpace(titleName)) { titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; } titleId = controlData.PresenceGroupId.ToString("x16"); if (string.IsNullOrWhiteSpace(titleId)) { titleId = controlData.SaveDataOwnerId.ToString("x16"); } if (string.IsNullOrWhiteSpace(titleId)) { titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); } developer = controlData.Descriptions[(int)DesiredTitleLanguage].Developer; if (string.IsNullOrWhiteSpace(developer)) { developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer; } } } else { applicationIcon = RyujinxNroIcon; titleName = "Application"; titleId = "0000000000000000"; developer = "Unknown"; version = "?"; } } // If its an NCA or NSO we just set defaults else if ((Path.GetExtension(applicationPath) == ".nca") || (Path.GetExtension(applicationPath) == ".nso")) { if (Path.GetExtension(applicationPath) == ".nca") { applicationIcon = RyujinxNcaIcon; } else if (Path.GetExtension(applicationPath) == ".nso") { applicationIcon = RyujinxNsoIcon; } string fileName = Path.GetFileName(applicationPath); string fileExt = Path.GetExtension(applicationPath); StringBuilder titlename = new StringBuilder(); titlename.Append(fileName); titlename.Remove(fileName.Length - fileExt.Length, fileExt.Length); titleName = titlename.ToString(); titleId = "0000000000000000"; version = "?"; developer = "Unknown"; } } string[] playedData = GetPlayedData(titleId, "00000000000000000000000000000001"); ApplicationData data = new ApplicationData() { Icon = applicationIcon, TitleName = titleName, TitleId = titleId, Developer = developer, Version = version, TimePlayed = playedData[0], LastPlayed = playedData[1], FileExt = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1), FileSize = (filesize < 1) ? (filesize * 1024).ToString("0.##") + "MB" : filesize.ToString("0.##") + "GB", Path = applicationPath, }; ApplicationLibraryData.Add(data); } } private static byte[] GetResourceBytes(string resourceName) { Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); byte[] resourceByteArray = new byte[resourceStream.Length]; resourceStream.Read(resourceByteArray); return resourceByteArray; } private static IFileSystem GetControlFs(PartitionFileSystem Pfs) { Nca controlNca = null; // Add keys to keyset if needed foreach (DirectoryEntry ticketEntry in Pfs.EnumerateEntries("*.tik")) { Ticket ticket = new Ticket(Pfs.OpenFile(ticketEntry.FullPath, OpenMode.Read).AsStream()); if (!KeySet.TitleKeys.ContainsKey(ticket.RightsId)) { KeySet.TitleKeys.Add(ticket.RightsId, ticket.GetTitleKey(KeySet)); } } // Find the Control NCA and store it in variable called controlNca foreach (DirectoryEntry fileEntry in Pfs.EnumerateEntries("*.nca")) { Nca nca = new Nca(KeySet, Pfs.OpenFile(fileEntry.FullPath, OpenMode.Read).AsStorage()); if (nca.Header.ContentType == ContentType.Control) { controlNca = nca; } } // Return the ControlFS return controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); } private static string[] GetPlayedData(string TitleId, string UserId) { try { string[] playedData = new string[2]; string savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "nand", "user", "save", "0000000000000000", UserId, 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 < SecondsPerMinute) { playedData[0] = $"{timePlayed}s"; } else if (timePlayed < SecondsPerHour) { playedData[0] = $"{Math.Round(timePlayed / SecondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins"; } else if (timePlayed < SecondsPerDay) { playedData[0] = $"{Math.Round(timePlayed / SecondsPerHour , 2, MidpointRounding.AwayFromZero)} hrs"; } else { playedData[0] = $"{Math.Round(timePlayed / SecondsPerDay , 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; } catch { return new string[] { "Unknown", "Unknown" }; } } private static byte[] NspOrXciIcon(string applicationPath) { if (Path.GetExtension(applicationPath) == ".xci") { return RyujinxXciIcon; } else { return RyujinxNspIcon; } } } }