aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx/Ui/ApplicationLibrary.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Ryujinx/Ui/ApplicationLibrary.cs')
-rw-r--r--Ryujinx/Ui/ApplicationLibrary.cs450
1 files changed, 450 insertions, 0 deletions
diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs
new file mode 100644
index 00000000..7e731f79
--- /dev/null
+++ b/Ryujinx/Ui/ApplicationLibrary.cs
@@ -0,0 +1,450 @@
+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<ApplicationData> 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<string> 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<string> applications = new List<string>();
+ 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<ApplicationData>();
+ 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;
+ }
+ }
+ }
+}