ApplicationLibrary.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. using LibHac;
  2. using LibHac.Fs;
  3. using LibHac.FsSystem;
  4. using LibHac.FsSystem.NcaUtils;
  5. using LibHac.Spl;
  6. using Ryujinx.Common.Logging;
  7. using System;
  8. using System.Collections.Generic;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Reflection;
  12. using System.Text;
  13. using SystemState = Ryujinx.HLE.HOS.SystemState;
  14. namespace Ryujinx.UI
  15. {
  16. public class ApplicationLibrary
  17. {
  18. private static Keyset KeySet;
  19. private static SystemState.TitleLanguage DesiredTitleLanguage;
  20. private const double SecondsPerMinute = 60.0;
  21. private const double SecondsPerHour = SecondsPerMinute * 60;
  22. private const double SecondsPerDay = SecondsPerHour * 24;
  23. public static byte[] RyujinxNspIcon { get; private set; }
  24. public static byte[] RyujinxXciIcon { get; private set; }
  25. public static byte[] RyujinxNcaIcon { get; private set; }
  26. public static byte[] RyujinxNroIcon { get; private set; }
  27. public static byte[] RyujinxNsoIcon { get; private set; }
  28. public static List<ApplicationData> ApplicationLibraryData { get; private set; }
  29. public struct ApplicationData
  30. {
  31. public byte[] Icon;
  32. public string TitleName;
  33. public string TitleId;
  34. public string Developer;
  35. public string Version;
  36. public string TimePlayed;
  37. public string LastPlayed;
  38. public string FileExt;
  39. public string FileSize;
  40. public string Path;
  41. }
  42. public static void Init(List<string> AppDirs, Keyset keySet, SystemState.TitleLanguage desiredTitleLanguage)
  43. {
  44. KeySet = keySet;
  45. DesiredTitleLanguage = desiredTitleLanguage;
  46. // Loads the default application Icons
  47. RyujinxNspIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSPIcon.png");
  48. RyujinxXciIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxXCIIcon.png");
  49. RyujinxNcaIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNCAIcon.png");
  50. RyujinxNroIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNROIcon.png");
  51. RyujinxNsoIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSOIcon.png");
  52. // Builds the applications list with paths to found applications
  53. List<string> applications = new List<string>();
  54. foreach (string appDir in AppDirs)
  55. {
  56. if (Directory.Exists(appDir) == false)
  57. {
  58. Logger.PrintWarning(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\"");
  59. continue;
  60. }
  61. DirectoryInfo AppDirInfo = new DirectoryInfo(appDir);
  62. foreach (FileInfo App in AppDirInfo.GetFiles())
  63. {
  64. if ((Path.GetExtension(App.ToString()) == ".xci") ||
  65. (Path.GetExtension(App.ToString()) == ".nca") ||
  66. (Path.GetExtension(App.ToString()) == ".nsp") ||
  67. (Path.GetExtension(App.ToString()) == ".pfs0") ||
  68. (Path.GetExtension(App.ToString()) == ".nro") ||
  69. (Path.GetExtension(App.ToString()) == ".nso"))
  70. {
  71. applications.Add(App.ToString());
  72. }
  73. }
  74. }
  75. // Loops through applications list, creating a struct for each application and then adding the struct to a list of structs
  76. ApplicationLibraryData = new List<ApplicationData>();
  77. foreach (string applicationPath in applications)
  78. {
  79. double filesize = new FileInfo(applicationPath).Length * 0.000000000931;
  80. string titleName = null;
  81. string titleId = null;
  82. string developer = null;
  83. string version = null;
  84. byte[] applicationIcon = null;
  85. using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read))
  86. {
  87. if ((Path.GetExtension(applicationPath) == ".nsp") ||
  88. (Path.GetExtension(applicationPath) == ".pfs0") ||
  89. (Path.GetExtension(applicationPath) == ".xci"))
  90. {
  91. try
  92. {
  93. IFileSystem controlFs = null;
  94. // Store the ControlFS in variable called controlFs
  95. if (Path.GetExtension(applicationPath) == ".xci")
  96. {
  97. Xci xci = new Xci(KeySet, file.AsStorage());
  98. controlFs = GetControlFs(xci.OpenPartition(XciPartitionType.Secure));
  99. }
  100. else
  101. {
  102. controlFs = GetControlFs(new PartitionFileSystem(file.AsStorage()));
  103. }
  104. // Creates NACP class from the NACP file
  105. controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp", OpenMode.Read).ThrowIfFailure();
  106. Nacp controlData = new Nacp(controlNacpFile.AsStream());
  107. // Get the title name, title ID, developer name and version number from the NACP
  108. version = controlData.DisplayVersion;
  109. titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title;
  110. if (string.IsNullOrWhiteSpace(titleName))
  111. {
  112. titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
  113. }
  114. titleId = controlData.PresenceGroupId.ToString("x16");
  115. if (string.IsNullOrWhiteSpace(titleId))
  116. {
  117. titleId = controlData.SaveDataOwnerId.ToString("x16");
  118. }
  119. if (string.IsNullOrWhiteSpace(titleId))
  120. {
  121. titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
  122. }
  123. developer = controlData.Descriptions[(int)DesiredTitleLanguage].Developer;
  124. if (string.IsNullOrWhiteSpace(developer))
  125. {
  126. developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer;
  127. }
  128. // Read the icon from the ControlFS and store it as a byte array
  129. try
  130. {
  131. controlFs.OpenFile(out IFile icon, $"/icon_{DesiredTitleLanguage}.dat", OpenMode.Read).ThrowIfFailure();
  132. using (MemoryStream stream = new MemoryStream())
  133. {
  134. icon.AsStream().CopyTo(stream);
  135. applicationIcon = stream.ToArray();
  136. }
  137. }
  138. catch (HorizonResultException)
  139. {
  140. foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
  141. {
  142. if (entry.Name == "control.nacp")
  143. {
  144. continue;
  145. }
  146. controlFs.OpenFile(out IFile icon, entry.FullPath, OpenMode.Read).ThrowIfFailure();
  147. using (MemoryStream stream = new MemoryStream())
  148. {
  149. icon.AsStream().CopyTo(stream);
  150. applicationIcon = stream.ToArray();
  151. }
  152. if (applicationIcon != null)
  153. {
  154. break;
  155. }
  156. }
  157. if (applicationIcon == null)
  158. {
  159. applicationIcon = NspOrXciIcon(applicationPath);
  160. }
  161. }
  162. }
  163. catch (MissingKeyException exception)
  164. {
  165. titleName = "Unknown";
  166. titleId = "Unknown";
  167. developer = "Unknown";
  168. version = "?";
  169. applicationIcon = NspOrXciIcon(applicationPath);
  170. Logger.PrintWarning(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
  171. }
  172. catch (InvalidDataException)
  173. {
  174. titleName = "Unknown";
  175. titleId = "Unknown";
  176. developer = "Unknown";
  177. version = "?";
  178. applicationIcon = NspOrXciIcon(applicationPath);
  179. Logger.PrintWarning(LogClass.Application, $"The file is not an NCA file or the header key is incorrect. Errored File: {applicationPath}");
  180. }
  181. catch (Exception exception)
  182. {
  183. Logger.PrintWarning(LogClass.Application, $"This warning usualy means that you have a DLC in one of you game directories\n{exception}");
  184. continue;
  185. }
  186. }
  187. else if (Path.GetExtension(applicationPath) == ".nro")
  188. {
  189. BinaryReader reader = new BinaryReader(file);
  190. byte[] Read(long Position, int Size)
  191. {
  192. file.Seek(Position, SeekOrigin.Begin);
  193. return reader.ReadBytes(Size);
  194. }
  195. file.Seek(24, SeekOrigin.Begin);
  196. int AssetOffset = reader.ReadInt32();
  197. if (Encoding.ASCII.GetString(Read(AssetOffset, 4)) == "ASET")
  198. {
  199. byte[] IconSectionInfo = Read(AssetOffset + 8, 0x10);
  200. long iconOffset = BitConverter.ToInt64(IconSectionInfo, 0);
  201. long iconSize = BitConverter.ToInt64(IconSectionInfo, 8);
  202. ulong nacpOffset = reader.ReadUInt64();
  203. ulong nacpSize = reader.ReadUInt64();
  204. // Reads and stores game icon as byte array
  205. applicationIcon = Read(AssetOffset + iconOffset, (int)iconSize);
  206. // Creates memory stream out of byte array which is the NACP
  207. using (MemoryStream stream = new MemoryStream(Read(AssetOffset + (int)nacpOffset, (int)nacpSize)))
  208. {
  209. // Creates NACP class from the memory stream
  210. Nacp controlData = new Nacp(stream);
  211. // Get the title name, title ID, developer name and version number from the NACP
  212. version = controlData.DisplayVersion;
  213. titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title;
  214. if (string.IsNullOrWhiteSpace(titleName))
  215. {
  216. titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
  217. }
  218. titleId = controlData.PresenceGroupId.ToString("x16");
  219. if (string.IsNullOrWhiteSpace(titleId))
  220. {
  221. titleId = controlData.SaveDataOwnerId.ToString("x16");
  222. }
  223. if (string.IsNullOrWhiteSpace(titleId))
  224. {
  225. titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
  226. }
  227. developer = controlData.Descriptions[(int)DesiredTitleLanguage].Developer;
  228. if (string.IsNullOrWhiteSpace(developer))
  229. {
  230. developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer;
  231. }
  232. }
  233. }
  234. else
  235. {
  236. applicationIcon = RyujinxNroIcon;
  237. titleName = "Application";
  238. titleId = "0000000000000000";
  239. developer = "Unknown";
  240. version = "?";
  241. }
  242. }
  243. // If its an NCA or NSO we just set defaults
  244. else if ((Path.GetExtension(applicationPath) == ".nca") || (Path.GetExtension(applicationPath) == ".nso"))
  245. {
  246. if (Path.GetExtension(applicationPath) == ".nca")
  247. {
  248. applicationIcon = RyujinxNcaIcon;
  249. }
  250. else if (Path.GetExtension(applicationPath) == ".nso")
  251. {
  252. applicationIcon = RyujinxNsoIcon;
  253. }
  254. string fileName = Path.GetFileName(applicationPath);
  255. string fileExt = Path.GetExtension(applicationPath);
  256. StringBuilder titlename = new StringBuilder();
  257. titlename.Append(fileName);
  258. titlename.Remove(fileName.Length - fileExt.Length, fileExt.Length);
  259. titleName = titlename.ToString();
  260. titleId = "0000000000000000";
  261. version = "?";
  262. developer = "Unknown";
  263. }
  264. }
  265. string[] playedData = GetPlayedData(titleId, "00000000000000000000000000000001");
  266. ApplicationData data = new ApplicationData()
  267. {
  268. Icon = applicationIcon,
  269. TitleName = titleName,
  270. TitleId = titleId,
  271. Developer = developer,
  272. Version = version,
  273. TimePlayed = playedData[0],
  274. LastPlayed = playedData[1],
  275. FileExt = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
  276. FileSize = (filesize < 1) ? (filesize * 1024).ToString("0.##") + "MB" : filesize.ToString("0.##") + "GB",
  277. Path = applicationPath,
  278. };
  279. ApplicationLibraryData.Add(data);
  280. }
  281. }
  282. private static byte[] GetResourceBytes(string resourceName)
  283. {
  284. Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName);
  285. byte[] resourceByteArray = new byte[resourceStream.Length];
  286. resourceStream.Read(resourceByteArray);
  287. return resourceByteArray;
  288. }
  289. private static IFileSystem GetControlFs(PartitionFileSystem Pfs)
  290. {
  291. Nca controlNca = null;
  292. // Add keys to keyset if needed
  293. foreach (DirectoryEntryEx ticketEntry in Pfs.EnumerateEntries("/", "*.tik"))
  294. {
  295. Result result = Pfs.OpenFile(out IFile ticketFile, ticketEntry.FullPath, OpenMode.Read);
  296. if (result.IsSuccess())
  297. {
  298. Ticket ticket = new Ticket(ticketFile.AsStream());
  299. KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet)));
  300. }
  301. }
  302. // Find the Control NCA and store it in variable called controlNca
  303. foreach (DirectoryEntryEx fileEntry in Pfs.EnumerateEntries("/", "*.nca"))
  304. {
  305. Pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure();
  306. Nca nca = new Nca(KeySet, ncaFile.AsStorage());
  307. if (nca.Header.ContentType == NcaContentType.Control)
  308. {
  309. controlNca = nca;
  310. }
  311. }
  312. // Return the ControlFS
  313. return controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
  314. }
  315. private static string[] GetPlayedData(string TitleId, string UserId)
  316. {
  317. try
  318. {
  319. string[] playedData = new string[2];
  320. string savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "nand", "user", "save", "0000000000000000", UserId, TitleId);
  321. if (File.Exists(Path.Combine(savePath, "TimePlayed.dat")) == false)
  322. {
  323. Directory.CreateDirectory(savePath);
  324. using (FileStream file = File.OpenWrite(Path.Combine(savePath, "TimePlayed.dat")))
  325. {
  326. file.Write(Encoding.ASCII.GetBytes("0"));
  327. }
  328. }
  329. using (FileStream fs = File.OpenRead(Path.Combine(savePath, "TimePlayed.dat")))
  330. {
  331. using (StreamReader sr = new StreamReader(fs))
  332. {
  333. float timePlayed = float.Parse(sr.ReadLine());
  334. if (timePlayed < SecondsPerMinute)
  335. {
  336. playedData[0] = $"{timePlayed}s";
  337. }
  338. else if (timePlayed < SecondsPerHour)
  339. {
  340. playedData[0] = $"{Math.Round(timePlayed / SecondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins";
  341. }
  342. else if (timePlayed < SecondsPerDay)
  343. {
  344. playedData[0] = $"{Math.Round(timePlayed / SecondsPerHour , 2, MidpointRounding.AwayFromZero)} hrs";
  345. }
  346. else
  347. {
  348. playedData[0] = $"{Math.Round(timePlayed / SecondsPerDay , 2, MidpointRounding.AwayFromZero)} days";
  349. }
  350. }
  351. }
  352. if (File.Exists(Path.Combine(savePath, "LastPlayed.dat")) == false)
  353. {
  354. Directory.CreateDirectory(savePath);
  355. using (FileStream file = File.OpenWrite(Path.Combine(savePath, "LastPlayed.dat")))
  356. {
  357. file.Write(Encoding.ASCII.GetBytes("Never"));
  358. }
  359. }
  360. using (FileStream fs = File.OpenRead(Path.Combine(savePath, "LastPlayed.dat")))
  361. {
  362. using (StreamReader sr = new StreamReader(fs))
  363. {
  364. playedData[1] = sr.ReadLine();
  365. }
  366. }
  367. return playedData;
  368. }
  369. catch
  370. {
  371. return new string[] { "Unknown", "Unknown" };
  372. }
  373. }
  374. private static byte[] NspOrXciIcon(string applicationPath)
  375. {
  376. if (Path.GetExtension(applicationPath) == ".xci")
  377. {
  378. return RyujinxXciIcon;
  379. }
  380. else
  381. {
  382. return RyujinxNspIcon;
  383. }
  384. }
  385. }
  386. }