ApplicationLoader.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. using ARMeilleure.Translation.PTC;
  2. using LibHac;
  3. using LibHac.Account;
  4. using LibHac.Common;
  5. using LibHac.Fs;
  6. using LibHac.FsSystem;
  7. using LibHac.FsSystem.NcaUtils;
  8. using LibHac.Ncm;
  9. using LibHac.Ns;
  10. using Ryujinx.Common.Configuration;
  11. using Ryujinx.Common.Logging;
  12. using Ryujinx.HLE.FileSystem;
  13. using Ryujinx.HLE.FileSystem.Content;
  14. using Ryujinx.HLE.Loaders.Executables;
  15. using Ryujinx.HLE.Loaders.Npdm;
  16. using System;
  17. using System.Collections.Generic;
  18. using System.IO;
  19. using System.Linq;
  20. using System.Reflection;
  21. using static LibHac.Fs.ApplicationSaveDataManagement;
  22. namespace Ryujinx.HLE.HOS
  23. {
  24. using JsonHelper = Common.Utilities.JsonHelper;
  25. public class ApplicationLoader
  26. {
  27. private readonly Switch _device;
  28. private readonly ContentManager _contentManager;
  29. private readonly VirtualFileSystem _fileSystem;
  30. public BlitStruct<ApplicationControlProperty> ControlData { get; set; }
  31. public string TitleName { get; private set; }
  32. public string DisplayVersion { get; private set; }
  33. public ulong TitleId { get; private set; }
  34. public string TitleIdText => TitleId.ToString("x16");
  35. public bool TitleIs64Bit { get; private set; }
  36. public bool EnablePtc => _device.System.EnablePtc;
  37. // Binaries from exefs are loaded into mem in this order. Do not change.
  38. private static readonly string[] ExeFsPrefixes = { "rtld", "main", "subsdk*", "sdk" };
  39. public ApplicationLoader(Switch device, VirtualFileSystem fileSystem, ContentManager contentManager)
  40. {
  41. _device = device;
  42. _contentManager = contentManager;
  43. _fileSystem = fileSystem;
  44. ControlData = new BlitStruct<ApplicationControlProperty>(1);
  45. // Clear Mods cache
  46. _fileSystem.ModLoader.Clear();
  47. }
  48. public void LoadCart(string exeFsDir, string romFsFile = null)
  49. {
  50. if (romFsFile != null)
  51. {
  52. _fileSystem.LoadRomFs(romFsFile);
  53. }
  54. LocalFileSystem codeFs = new LocalFileSystem(exeFsDir);
  55. Npdm metaData = ReadNpdm(codeFs);
  56. if (TitleId != 0)
  57. {
  58. EnsureSaveData(new TitleId(TitleId));
  59. }
  60. LoadExeFs(codeFs, metaData);
  61. }
  62. private (Nca main, Nca patch, Nca control) GetGameData(PartitionFileSystem pfs)
  63. {
  64. Nca mainNca = null;
  65. Nca patchNca = null;
  66. Nca controlNca = null;
  67. _fileSystem.ImportTickets(pfs);
  68. foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
  69. {
  70. pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
  71. Nca nca = new Nca(_fileSystem.KeySet, ncaFile.AsStorage());
  72. if (nca.Header.ContentType == NcaContentType.Program)
  73. {
  74. int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
  75. if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
  76. {
  77. patchNca = nca;
  78. }
  79. else
  80. {
  81. mainNca = nca;
  82. }
  83. }
  84. else if (nca.Header.ContentType == NcaContentType.Control)
  85. {
  86. controlNca = nca;
  87. }
  88. }
  89. return (mainNca, patchNca, controlNca);
  90. }
  91. public void LoadXci(string xciFile)
  92. {
  93. FileStream file = new FileStream(xciFile, FileMode.Open, FileAccess.Read);
  94. Xci xci = new Xci(_fileSystem.KeySet, file.AsStorage());
  95. if (!xci.HasPartition(XciPartitionType.Secure))
  96. {
  97. Logger.PrintError(LogClass.Loader, "Unable to load XCI: Could not find XCI secure partition");
  98. return;
  99. }
  100. PartitionFileSystem securePartition = xci.OpenPartition(XciPartitionType.Secure);
  101. Nca mainNca = null;
  102. Nca patchNca = null;
  103. Nca controlNca = null;
  104. try
  105. {
  106. (mainNca, patchNca, controlNca) = GetGameData(securePartition);
  107. }
  108. catch (Exception e)
  109. {
  110. Logger.PrintError(LogClass.Loader, $"Unable to load XCI: {e.Message}");
  111. return;
  112. }
  113. if (mainNca == null)
  114. {
  115. Logger.PrintError(LogClass.Loader, "Unable to load XCI: Could not find Main NCA");
  116. return;
  117. }
  118. _contentManager.LoadEntries(_device);
  119. _contentManager.ClearAocData();
  120. _contentManager.AddAocData(securePartition, xciFile, mainNca.Header.TitleId);
  121. LoadNca(mainNca, patchNca, controlNca);
  122. }
  123. public void LoadNsp(string nspFile)
  124. {
  125. FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read);
  126. PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
  127. Nca mainNca = null;
  128. Nca patchNca = null;
  129. Nca controlNca = null;
  130. try
  131. {
  132. (mainNca, patchNca, controlNca) = GetGameData(nsp);
  133. }
  134. catch (Exception e)
  135. {
  136. Logger.PrintError(LogClass.Loader, $"Unable to load NSP: {e.Message}");
  137. return;
  138. }
  139. if (mainNca == null)
  140. {
  141. Logger.PrintError(LogClass.Loader, "Unable to load NSP: Could not find Main NCA");
  142. return;
  143. }
  144. if (mainNca != null)
  145. {
  146. _contentManager.ClearAocData();
  147. _contentManager.AddAocData(nsp, nspFile, mainNca.Header.TitleId);
  148. LoadNca(mainNca, patchNca, controlNca);
  149. return;
  150. }
  151. // This is not a normal NSP, it's actually a ExeFS as a NSP
  152. LoadExeFs(nsp);
  153. }
  154. public void LoadNca(string ncaFile)
  155. {
  156. FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
  157. Nca nca = new Nca(_fileSystem.KeySet, file.AsStorage(false));
  158. LoadNca(nca, null, null);
  159. }
  160. private void LoadNca(Nca mainNca, Nca patchNca, Nca controlNca)
  161. {
  162. if (mainNca.Header.ContentType != NcaContentType.Program)
  163. {
  164. Logger.PrintError(LogClass.Loader, "Selected NCA is not a \"Program\" NCA");
  165. return;
  166. }
  167. IStorage dataStorage = null;
  168. IFileSystem codeFs = null;
  169. // Load Update
  170. string titleUpdateMetadataPath = Path.Combine(_fileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json");
  171. if (File.Exists(titleUpdateMetadataPath))
  172. {
  173. string updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected;
  174. if (File.Exists(updatePath))
  175. {
  176. FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
  177. PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
  178. _fileSystem.ImportTickets(nsp);
  179. foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
  180. {
  181. nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
  182. Nca nca = new Nca(_fileSystem.KeySet, ncaFile.AsStorage());
  183. if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != mainNca.Header.TitleId.ToString("x16"))
  184. {
  185. break;
  186. }
  187. if (nca.Header.ContentType == NcaContentType.Program)
  188. {
  189. patchNca = nca;
  190. }
  191. else if (nca.Header.ContentType == NcaContentType.Control)
  192. {
  193. controlNca = nca;
  194. }
  195. }
  196. }
  197. }
  198. // Load Aoc
  199. string titleAocMetadataPath = Path.Combine(_fileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "dlc.json");
  200. if (File.Exists(titleAocMetadataPath))
  201. {
  202. List<DlcContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(titleAocMetadataPath);
  203. foreach (DlcContainer dlcContainer in dlcContainerList)
  204. {
  205. foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
  206. {
  207. _contentManager.AddAocItem(dlcNca.TitleId, dlcContainer.Path, dlcNca.Path, dlcNca.Enabled);
  208. }
  209. }
  210. }
  211. if (patchNca == null)
  212. {
  213. if (mainNca.CanOpenSection(NcaSectionType.Data))
  214. {
  215. dataStorage = mainNca.OpenStorage(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
  216. }
  217. if (mainNca.CanOpenSection(NcaSectionType.Code))
  218. {
  219. codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, _device.System.FsIntegrityCheckLevel);
  220. }
  221. }
  222. else
  223. {
  224. if (patchNca.CanOpenSection(NcaSectionType.Data))
  225. {
  226. dataStorage = mainNca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
  227. }
  228. if (patchNca.CanOpenSection(NcaSectionType.Code))
  229. {
  230. codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, _device.System.FsIntegrityCheckLevel);
  231. }
  232. }
  233. if (codeFs == null)
  234. {
  235. Logger.PrintError(LogClass.Loader, "No ExeFS found in NCA");
  236. return;
  237. }
  238. Npdm metaData = ReadNpdm(codeFs);
  239. _fileSystem.ModLoader.CollectMods(TitleId, _fileSystem.GetBaseModsPath());
  240. if (controlNca != null)
  241. {
  242. ReadControlData(controlNca);
  243. }
  244. else
  245. {
  246. ControlData.ByteSpan.Clear();
  247. }
  248. if (dataStorage == null)
  249. {
  250. Logger.PrintWarning(LogClass.Loader, "No RomFS found in NCA");
  251. }
  252. else
  253. {
  254. IStorage newStorage = _fileSystem.ModLoader.ApplyRomFsMods(TitleId, dataStorage);
  255. _fileSystem.SetRomFs(newStorage.AsStream(FileAccess.Read));
  256. }
  257. if (TitleId != 0)
  258. {
  259. EnsureSaveData(new TitleId(TitleId));
  260. }
  261. LoadExeFs(codeFs, metaData);
  262. Logger.PrintInfo(LogClass.Loader, $"Application Loaded: {TitleName} v{DisplayVersion} [{TitleIdText}] [{(TitleIs64Bit ? "64-bit" : "32-bit")}]");
  263. }
  264. // Sets TitleId, so be sure to call before using it
  265. private Npdm ReadNpdm(IFileSystem fs)
  266. {
  267. Result result = fs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read);
  268. Npdm metaData;
  269. if (ResultFs.PathNotFound.Includes(result))
  270. {
  271. Logger.PrintWarning(LogClass.Loader, "NPDM file not found, using default values!");
  272. metaData = GetDefaultNpdm();
  273. }
  274. else
  275. {
  276. metaData = new Npdm(npdmFile.AsStream());
  277. }
  278. TitleId = metaData.Aci0.TitleId;
  279. TitleIs64Bit = metaData.Is64Bit;
  280. return metaData;
  281. }
  282. private void ReadControlData(Nca controlNca)
  283. {
  284. IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
  285. Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read);
  286. if (result.IsSuccess())
  287. {
  288. result = controlFile.Read(out long bytesRead, 0, ControlData.ByteSpan, ReadOption.None);
  289. if (result.IsSuccess() && bytesRead == ControlData.ByteSpan.Length)
  290. {
  291. TitleName = ControlData.Value
  292. .Titles[(int)_device.System.State.DesiredTitleLanguage].Name.ToString();
  293. if (string.IsNullOrWhiteSpace(TitleName))
  294. {
  295. TitleName = ControlData.Value.Titles.ToArray()
  296. .FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
  297. }
  298. DisplayVersion = ControlData.Value.DisplayVersion.ToString();
  299. }
  300. }
  301. else
  302. {
  303. ControlData.ByteSpan.Clear();
  304. }
  305. }
  306. private void LoadExeFs(IFileSystem codeFs, Npdm metaData = null)
  307. {
  308. if (_fileSystem.ModLoader.ReplaceExefsPartition(TitleId, ref codeFs))
  309. {
  310. metaData = null; //TODO: Check if we should retain old npdm
  311. }
  312. metaData ??= ReadNpdm(codeFs);
  313. List<NsoExecutable> nsos = new List<NsoExecutable>();
  314. foreach (string exePrefix in ExeFsPrefixes) // Load binaries with standard prefixes
  315. {
  316. foreach (DirectoryEntryEx file in codeFs.EnumerateEntries("/", exePrefix))
  317. {
  318. if (Path.GetExtension(file.Name) != string.Empty)
  319. {
  320. continue;
  321. }
  322. Logger.PrintInfo(LogClass.Loader, $"Loading {file.Name}...");
  323. codeFs.OpenFile(out IFile nsoFile, file.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
  324. NsoExecutable nso = new NsoExecutable(nsoFile.AsStorage(), file.Name);
  325. nsos.Add(nso);
  326. }
  327. }
  328. // ExeFs file replacements
  329. bool modified = _fileSystem.ModLoader.ApplyExefsMods(TitleId, nsos);
  330. var programs = nsos.ToArray();
  331. modified |= _fileSystem.ModLoader.ApplyNsoPatches(TitleId, programs);
  332. _contentManager.LoadEntries(_device);
  333. if (EnablePtc && modified)
  334. {
  335. Logger.PrintWarning(LogClass.Ptc, $"Detected exefs modifications. PPTC disabled.");
  336. }
  337. Ptc.Initialize(TitleIdText, DisplayVersion, EnablePtc && !modified);
  338. ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: programs);
  339. }
  340. public void LoadProgram(string filePath)
  341. {
  342. Npdm metaData = GetDefaultNpdm();
  343. bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
  344. IExecutable executable;
  345. if (isNro)
  346. {
  347. FileStream input = new FileStream(filePath, FileMode.Open);
  348. NroExecutable obj = new NroExecutable(input.AsStorage());
  349. executable = obj;
  350. // homebrew NRO can actually have some data after the actual NRO
  351. if (input.Length > obj.FileSize)
  352. {
  353. input.Position = obj.FileSize;
  354. BinaryReader reader = new BinaryReader(input);
  355. uint asetMagic = reader.ReadUInt32();
  356. if (asetMagic == 0x54455341)
  357. {
  358. uint asetVersion = reader.ReadUInt32();
  359. if (asetVersion == 0)
  360. {
  361. ulong iconOffset = reader.ReadUInt64();
  362. ulong iconSize = reader.ReadUInt64();
  363. ulong nacpOffset = reader.ReadUInt64();
  364. ulong nacpSize = reader.ReadUInt64();
  365. ulong romfsOffset = reader.ReadUInt64();
  366. ulong romfsSize = reader.ReadUInt64();
  367. if (romfsSize != 0)
  368. {
  369. _fileSystem.SetRomFs(new HomebrewRomFsStream(input, obj.FileSize + (long)romfsOffset));
  370. }
  371. if (nacpSize != 0)
  372. {
  373. input.Seek(obj.FileSize + (long)nacpOffset, SeekOrigin.Begin);
  374. reader.Read(ControlData.ByteSpan);
  375. ref ApplicationControlProperty nacp = ref ControlData.Value;
  376. metaData.TitleName = nacp.Titles[(int)_device.System.State.DesiredTitleLanguage].Name.ToString();
  377. if (string.IsNullOrWhiteSpace(metaData.TitleName))
  378. {
  379. metaData.TitleName = nacp.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
  380. }
  381. if (nacp.PresenceGroupId != 0)
  382. {
  383. metaData.Aci0.TitleId = nacp.PresenceGroupId;
  384. }
  385. else if (nacp.SaveDataOwnerId.Value != 0)
  386. {
  387. metaData.Aci0.TitleId = nacp.SaveDataOwnerId.Value;
  388. }
  389. else if (nacp.AddOnContentBaseId != 0)
  390. {
  391. metaData.Aci0.TitleId = nacp.AddOnContentBaseId - 0x1000;
  392. }
  393. else
  394. {
  395. metaData.Aci0.TitleId = 0000000000000000;
  396. }
  397. }
  398. }
  399. else
  400. {
  401. Logger.PrintWarning(LogClass.Loader, $"Unsupported ASET header version found \"{asetVersion}\"");
  402. }
  403. }
  404. }
  405. }
  406. else
  407. {
  408. executable = new NsoExecutable(new LocalStorage(filePath, FileAccess.Read), Path.GetFileNameWithoutExtension(filePath));
  409. }
  410. _contentManager.LoadEntries(_device);
  411. TitleName = metaData.TitleName;
  412. TitleId = metaData.Aci0.TitleId;
  413. TitleIs64Bit = metaData.Is64Bit;
  414. ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: executable);
  415. }
  416. private Npdm GetDefaultNpdm()
  417. {
  418. Assembly asm = Assembly.GetCallingAssembly();
  419. using (Stream npdmStream = asm.GetManifestResourceStream("Ryujinx.HLE.Homebrew.npdm"))
  420. {
  421. return new Npdm(npdmStream);
  422. }
  423. }
  424. private Result EnsureSaveData(TitleId titleId)
  425. {
  426. Logger.PrintInfo(LogClass.Application, "Ensuring required savedata exists.");
  427. Uid user = _device.System.State.Account.LastOpenedUser.UserId.ToLibHacUid();
  428. ref ApplicationControlProperty control = ref ControlData.Value;
  429. if (Util.IsEmpty(ControlData.ByteSpan))
  430. {
  431. // If the current application doesn't have a loaded control property, create a dummy one
  432. // and set the savedata sizes so a user savedata will be created.
  433. control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
  434. // The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
  435. control.UserAccountSaveDataSize = 0x4000;
  436. control.UserAccountSaveDataJournalSize = 0x4000;
  437. Logger.PrintWarning(LogClass.Application,
  438. "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
  439. }
  440. FileSystemClient fs = _fileSystem.FsClient;
  441. Result rc = fs.EnsureApplicationCacheStorage(out _, titleId, ref control);
  442. if (rc.IsFailure())
  443. {
  444. Logger.PrintError(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {rc.ToStringWithName()}");
  445. return rc;
  446. }
  447. rc = EnsureApplicationSaveData(fs, out _, titleId, ref control, ref user);
  448. if (rc.IsFailure())
  449. {
  450. Logger.PrintError(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {rc.ToStringWithName()}");
  451. }
  452. return rc;
  453. }
  454. }
  455. }