ModLoader.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. using LibHac.Common;
  2. using LibHac.Fs;
  3. using LibHac.Fs.Fsa;
  4. using LibHac.FsSystem;
  5. using LibHac.Loader;
  6. using LibHac.Tools.FsSystem;
  7. using LibHac.Tools.FsSystem.RomFs;
  8. using Ryujinx.Common.Configuration;
  9. using Ryujinx.Common.Logging;
  10. using Ryujinx.Common.Utilities;
  11. using Ryujinx.HLE.HOS.Kernel.Process;
  12. using Ryujinx.HLE.Loaders.Executables;
  13. using Ryujinx.HLE.Loaders.Mods;
  14. using Ryujinx.HLE.Loaders.Processes;
  15. using System;
  16. using System.Collections.Generic;
  17. using System.Collections.Specialized;
  18. using System.Globalization;
  19. using System.IO;
  20. using System.Linq;
  21. using LazyFile = Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy.LazyFile;
  22. using Path = System.IO.Path;
  23. namespace Ryujinx.HLE.HOS
  24. {
  25. public class ModLoader
  26. {
  27. private const string RomfsDir = "romfs";
  28. private const string ExefsDir = "exefs";
  29. private const string CheatDir = "cheats";
  30. private const string RomfsContainer = "romfs.bin";
  31. private const string ExefsContainer = "exefs.nsp";
  32. private const string StubExtension = ".stub";
  33. private const string CheatExtension = ".txt";
  34. private const string DefaultCheatName = "<default>";
  35. private const string AmsContentsDir = "contents";
  36. private const string AmsNsoPatchDir = "exefs_patches";
  37. private const string AmsNroPatchDir = "nro_patches";
  38. private const string AmsKipPatchDir = "kip_patches";
  39. private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
  40. public readonly struct Mod<T> where T : FileSystemInfo
  41. {
  42. public readonly string Name;
  43. public readonly T Path;
  44. public readonly bool Enabled;
  45. public Mod(string name, T path, bool enabled)
  46. {
  47. Name = name;
  48. Path = path;
  49. Enabled = enabled;
  50. }
  51. }
  52. public struct Cheat
  53. {
  54. // Atmosphere identifies the executables with the first 8 bytes
  55. // of the build id, which is equivalent to 16 hex digits.
  56. public const int CheatIdSize = 16;
  57. public readonly string Name;
  58. public readonly FileInfo Path;
  59. public readonly IEnumerable<String> Instructions;
  60. public Cheat(string name, FileInfo path, IEnumerable<String> instructions)
  61. {
  62. Name = name;
  63. Path = path;
  64. Instructions = instructions;
  65. }
  66. }
  67. // Application dependent mods
  68. public class ModCache
  69. {
  70. public List<Mod<FileInfo>> RomfsContainers { get; }
  71. public List<Mod<FileInfo>> ExefsContainers { get; }
  72. public List<Mod<DirectoryInfo>> RomfsDirs { get; }
  73. public List<Mod<DirectoryInfo>> ExefsDirs { get; }
  74. public List<Cheat> Cheats { get; }
  75. public ModCache()
  76. {
  77. RomfsContainers = new List<Mod<FileInfo>>();
  78. ExefsContainers = new List<Mod<FileInfo>>();
  79. RomfsDirs = new List<Mod<DirectoryInfo>>();
  80. ExefsDirs = new List<Mod<DirectoryInfo>>();
  81. Cheats = new List<Cheat>();
  82. }
  83. }
  84. // Application independent mods
  85. private class PatchCache
  86. {
  87. public List<Mod<DirectoryInfo>> NsoPatches { get; }
  88. public List<Mod<DirectoryInfo>> NroPatches { get; }
  89. public List<Mod<DirectoryInfo>> KipPatches { get; }
  90. internal bool Initialized { get; set; }
  91. public PatchCache()
  92. {
  93. NsoPatches = new List<Mod<DirectoryInfo>>();
  94. NroPatches = new List<Mod<DirectoryInfo>>();
  95. KipPatches = new List<Mod<DirectoryInfo>>();
  96. Initialized = false;
  97. }
  98. }
  99. private readonly Dictionary<ulong, ModCache> _appMods; // key is ApplicationId
  100. private PatchCache _patches;
  101. private static readonly EnumerationOptions _dirEnumOptions;
  102. static ModLoader()
  103. {
  104. _dirEnumOptions = new EnumerationOptions
  105. {
  106. MatchCasing = MatchCasing.CaseInsensitive,
  107. MatchType = MatchType.Simple,
  108. RecurseSubdirectories = false,
  109. ReturnSpecialDirectories = false,
  110. };
  111. }
  112. public ModLoader()
  113. {
  114. _appMods = new Dictionary<ulong, ModCache>();
  115. _patches = new PatchCache();
  116. }
  117. private void Clear()
  118. {
  119. _appMods.Clear();
  120. _patches = new PatchCache();
  121. }
  122. private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
  123. public static string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath());
  124. public static string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath());
  125. private static string EnsureBaseDirStructure(string modsBasePath)
  126. {
  127. var modsDir = new DirectoryInfo(modsBasePath);
  128. modsDir.CreateSubdirectory(AmsContentsDir);
  129. modsDir.CreateSubdirectory(AmsNsoPatchDir);
  130. modsDir.CreateSubdirectory(AmsNroPatchDir);
  131. // TODO: uncomment when KIPs are supported
  132. // modsDir.CreateSubdirectory(AmsKipPatchDir);
  133. return modsDir.FullName;
  134. }
  135. private static DirectoryInfo FindApplicationDir(DirectoryInfo contentsDir, string applicationId)
  136. => contentsDir.EnumerateDirectories(applicationId, _dirEnumOptions).FirstOrDefault();
  137. private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, ModMetadata modMetadata)
  138. {
  139. System.Text.StringBuilder types = new();
  140. foreach (var modDir in dir.EnumerateDirectories())
  141. {
  142. types.Clear();
  143. Mod<DirectoryInfo> mod = new("", null, true);
  144. if (StrEquals(RomfsDir, modDir.Name))
  145. {
  146. var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path));
  147. var enabled = modData?.Enabled ?? true;
  148. mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>(dir.Name, modDir, enabled));
  149. types.Append('R');
  150. }
  151. else if (StrEquals(ExefsDir, modDir.Name))
  152. {
  153. var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path));
  154. var enabled = modData?.Enabled ?? true;
  155. mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(dir.Name, modDir, enabled));
  156. types.Append('E');
  157. }
  158. else if (StrEquals(CheatDir, modDir.Name))
  159. {
  160. types.Append('C', QueryCheatsDir(mods, modDir));
  161. }
  162. else
  163. {
  164. AddModsFromDirectory(mods, modDir, modMetadata);
  165. }
  166. if (types.Length > 0)
  167. {
  168. Logger.Info?.Print(LogClass.ModLoader, $"Found {(mod.Enabled ? "enabled" : "disabled")} mod '{mod.Name}' [{types}]");
  169. }
  170. }
  171. }
  172. public static string GetApplicationDir(string modsBasePath, string applicationId)
  173. {
  174. var contentsDir = new DirectoryInfo(Path.Combine(modsBasePath, AmsContentsDir));
  175. var applicationModsPath = FindApplicationDir(contentsDir, applicationId);
  176. if (applicationModsPath == null)
  177. {
  178. Logger.Info?.Print(LogClass.ModLoader, $"Creating mods directory for Application {applicationId.ToUpper()}");
  179. applicationModsPath = contentsDir.CreateSubdirectory(applicationId);
  180. }
  181. return applicationModsPath.FullName;
  182. }
  183. // Static Query Methods
  184. private static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir)
  185. {
  186. if (cache.Initialized || !patchDir.Exists)
  187. {
  188. return;
  189. }
  190. List<Mod<DirectoryInfo>> patches;
  191. string type;
  192. if (StrEquals(AmsNsoPatchDir, patchDir.Name))
  193. {
  194. patches = cache.NsoPatches;
  195. type = "NSO";
  196. }
  197. else if (StrEquals(AmsNroPatchDir, patchDir.Name))
  198. {
  199. patches = cache.NroPatches;
  200. type = "NRO";
  201. }
  202. else if (StrEquals(AmsKipPatchDir, patchDir.Name))
  203. {
  204. patches = cache.KipPatches;
  205. type = "KIP";
  206. }
  207. else
  208. {
  209. return;
  210. }
  211. foreach (var modDir in patchDir.EnumerateDirectories())
  212. {
  213. patches.Add(new Mod<DirectoryInfo>(modDir.Name, modDir, true));
  214. Logger.Info?.Print(LogClass.ModLoader, $"Found {type} patch '{modDir.Name}'");
  215. }
  216. }
  217. private static void QueryApplicationDir(ModCache mods, DirectoryInfo applicationDir, ulong applicationId)
  218. {
  219. if (!applicationDir.Exists)
  220. {
  221. return;
  222. }
  223. string modJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationId.ToString("x16"), "mods.json");
  224. ModMetadata modMetadata = new();
  225. if (File.Exists(modJsonPath))
  226. {
  227. try
  228. {
  229. modMetadata = JsonHelper.DeserializeFromFile(modJsonPath, _serializerContext.ModMetadata);
  230. }
  231. catch
  232. {
  233. Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {applicationId:X16} at {modJsonPath}");
  234. }
  235. }
  236. var fsFile = new FileInfo(Path.Combine(applicationDir.FullName, RomfsContainer));
  237. if (fsFile.Exists)
  238. {
  239. var modData = modMetadata.Mods.Find(x => fsFile.FullName.Contains(x.Path));
  240. var enabled = modData == null || modData.Enabled;
  241. mods.RomfsContainers.Add(new Mod<FileInfo>($"<{applicationDir.Name} RomFs>", fsFile, enabled));
  242. }
  243. fsFile = new FileInfo(Path.Combine(applicationDir.FullName, ExefsContainer));
  244. if (fsFile.Exists)
  245. {
  246. var modData = modMetadata.Mods.Find(x => fsFile.FullName.Contains(x.Path));
  247. var enabled = modData == null || modData.Enabled;
  248. mods.ExefsContainers.Add(new Mod<FileInfo>($"<{applicationDir.Name} ExeFs>", fsFile, enabled));
  249. }
  250. AddModsFromDirectory(mods, applicationDir, modMetadata);
  251. }
  252. public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong applicationId)
  253. {
  254. if (!contentsDir.Exists)
  255. {
  256. return;
  257. }
  258. Logger.Info?.Print(LogClass.ModLoader, $"Searching mods for {((applicationId & 0x1000) != 0 ? "DLC" : "Application")} {applicationId:X16} in \"{contentsDir.FullName}\"");
  259. var applicationDir = FindApplicationDir(contentsDir, $"{applicationId:x16}");
  260. if (applicationDir != null)
  261. {
  262. QueryApplicationDir(mods, applicationDir, applicationId);
  263. }
  264. }
  265. private static int QueryCheatsDir(ModCache mods, DirectoryInfo cheatsDir)
  266. {
  267. if (!cheatsDir.Exists)
  268. {
  269. return 0;
  270. }
  271. int numMods = 0;
  272. foreach (FileInfo file in cheatsDir.EnumerateFiles())
  273. {
  274. if (!StrEquals(CheatExtension, file.Extension))
  275. {
  276. continue;
  277. }
  278. string cheatId = Path.GetFileNameWithoutExtension(file.Name);
  279. if (cheatId.Length != Cheat.CheatIdSize)
  280. {
  281. continue;
  282. }
  283. if (!ulong.TryParse(cheatId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
  284. {
  285. continue;
  286. }
  287. int oldCheatsCount = mods.Cheats.Count;
  288. // A cheat file can contain several cheats for the same executable, so the file must be parsed in
  289. // order to properly enumerate them.
  290. mods.Cheats.AddRange(GetCheatsInFile(file));
  291. if (mods.Cheats.Count - oldCheatsCount > 0)
  292. {
  293. numMods++;
  294. }
  295. }
  296. return numMods;
  297. }
  298. private static IEnumerable<Cheat> GetCheatsInFile(FileInfo cheatFile)
  299. {
  300. string cheatName = DefaultCheatName;
  301. List<string> instructions = new();
  302. List<Cheat> cheats = new();
  303. using StreamReader cheatData = cheatFile.OpenText();
  304. while (cheatData.ReadLine() is { } line)
  305. {
  306. line = line.Trim();
  307. if (line.StartsWith('['))
  308. {
  309. // This line starts a new cheat section.
  310. if (!line.EndsWith(']') || line.Length < 3)
  311. {
  312. // Skip the entire file if there's any error while parsing the cheat file.
  313. Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed");
  314. return Array.Empty<Cheat>();
  315. }
  316. // Add the previous section to the list.
  317. if (instructions.Count > 0)
  318. {
  319. cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
  320. }
  321. // Start a new cheat section.
  322. cheatName = line[1..^1];
  323. instructions = new List<string>();
  324. }
  325. else if (line.Length > 0)
  326. {
  327. // The line contains an instruction.
  328. instructions.Add(line);
  329. }
  330. }
  331. // Add the last section being processed.
  332. if (instructions.Count > 0)
  333. {
  334. cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
  335. }
  336. return cheats;
  337. }
  338. // Assumes searchDirPaths don't overlap
  339. private static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths)
  340. {
  341. static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) ||
  342. StrEquals(AmsNroPatchDir, name) ||
  343. StrEquals(AmsKipPatchDir, name);
  344. static bool IsContentsDir(string name) => StrEquals(AmsContentsDir, name);
  345. static bool TryQuery(DirectoryInfo searchDir, PatchCache patches, Dictionary<ulong, ModCache> modCaches)
  346. {
  347. if (IsContentsDir(searchDir.Name))
  348. {
  349. foreach ((ulong applicationId, ModCache cache) in modCaches)
  350. {
  351. QueryContentsDir(cache, searchDir, applicationId);
  352. }
  353. return true;
  354. }
  355. else if (IsPatchesDir(searchDir.Name))
  356. {
  357. QueryPatchDirs(patches, searchDir);
  358. return true;
  359. }
  360. return false;
  361. }
  362. foreach (var path in searchDirPaths)
  363. {
  364. var searchDir = new DirectoryInfo(path);
  365. if (!searchDir.Exists)
  366. {
  367. Logger.Warning?.Print(LogClass.ModLoader, $"Mod Search Dir '{searchDir.FullName}' doesn't exist");
  368. return;
  369. }
  370. if (!TryQuery(searchDir, patches, modCaches))
  371. {
  372. foreach (var subdir in searchDir.EnumerateDirectories())
  373. {
  374. TryQuery(subdir, patches, modCaches);
  375. }
  376. }
  377. }
  378. patches.Initialized = true;
  379. }
  380. public void CollectMods(IEnumerable<ulong> applications, params string[] searchDirPaths)
  381. {
  382. Clear();
  383. foreach (ulong applicationId in applications)
  384. {
  385. _appMods[applicationId] = new ModCache();
  386. }
  387. CollectMods(_appMods, _patches, searchDirPaths);
  388. }
  389. internal IStorage ApplyRomFsMods(ulong applicationId, IStorage baseStorage)
  390. {
  391. if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0)
  392. {
  393. return baseStorage;
  394. }
  395. var fileSet = new HashSet<string>();
  396. var builder = new RomFsBuilder();
  397. int count = 0;
  398. Logger.Info?.Print(LogClass.ModLoader, $"Applying RomFS mods for Application {applicationId:X16}");
  399. // Prioritize loose files first
  400. foreach (var mod in mods.RomfsDirs)
  401. {
  402. if (!mod.Enabled)
  403. {
  404. continue;
  405. }
  406. using (IFileSystem fs = new LocalFileSystem(mod.Path.FullName))
  407. {
  408. AddFiles(fs, mod.Name, mod.Path.FullName, fileSet, builder);
  409. }
  410. count++;
  411. }
  412. // Then files inside images
  413. foreach (var mod in mods.RomfsContainers)
  414. {
  415. if (!mod.Enabled)
  416. {
  417. continue;
  418. }
  419. Logger.Info?.Print(LogClass.ModLoader, $"Found 'romfs.bin' for Application {applicationId:X16}");
  420. using (IFileSystem fs = new RomFsFileSystem(mod.Path.OpenRead().AsStorage()))
  421. {
  422. AddFiles(fs, mod.Name, mod.Path.FullName, fileSet, builder);
  423. }
  424. count++;
  425. }
  426. if (fileSet.Count == 0)
  427. {
  428. Logger.Info?.Print(LogClass.ModLoader, "No files found. Using base RomFS");
  429. return baseStorage;
  430. }
  431. Logger.Info?.Print(LogClass.ModLoader, $"Replaced {fileSet.Count} file(s) over {count} mod(s). Processing base storage...");
  432. // And finally, the base romfs
  433. var baseRom = new RomFsFileSystem(baseStorage);
  434. foreach (var entry in baseRom.EnumerateEntries()
  435. .Where(f => f.Type == DirectoryEntryType.File && !fileSet.Contains(f.FullPath))
  436. .OrderBy(f => f.FullPath, StringComparer.Ordinal))
  437. {
  438. using var file = new UniqueRef<IFile>();
  439. baseRom.OpenFile(ref file.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
  440. builder.AddFile(entry.FullPath, file.Release());
  441. }
  442. Logger.Info?.Print(LogClass.ModLoader, "Building new RomFS...");
  443. IStorage newStorage = builder.Build();
  444. Logger.Info?.Print(LogClass.ModLoader, "Using modded RomFS");
  445. return newStorage;
  446. }
  447. private static void AddFiles(IFileSystem fs, string modName, string rootPath, ISet<string> fileSet, RomFsBuilder builder)
  448. {
  449. foreach (var entry in fs.EnumerateEntries()
  450. .AsParallel()
  451. .Where(f => f.Type == DirectoryEntryType.File)
  452. .OrderBy(f => f.FullPath, StringComparer.Ordinal))
  453. {
  454. var file = new LazyFile(entry.FullPath, rootPath, fs);
  455. if (fileSet.Add(entry.FullPath))
  456. {
  457. builder.AddFile(entry.FullPath, file);
  458. }
  459. else
  460. {
  461. Logger.Warning?.Print(LogClass.ModLoader, $" Skipped duplicate file '{entry.FullPath}' from '{modName}'", "ApplyRomFsMods");
  462. }
  463. }
  464. }
  465. internal bool ReplaceExefsPartition(ulong applicationId, ref IFileSystem exefs)
  466. {
  467. if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.ExefsContainers.Count == 0)
  468. {
  469. return false;
  470. }
  471. if (mods.ExefsContainers.Count > 1)
  472. {
  473. Logger.Warning?.Print(LogClass.ModLoader, "Multiple ExeFS partition replacements detected");
  474. }
  475. Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition");
  476. var pfs = new PartitionFileSystem();
  477. pfs.Initialize(mods.ExefsContainers[0].Path.OpenRead().AsStorage()).ThrowIfFailure();
  478. exefs = pfs;
  479. return true;
  480. }
  481. public struct ModLoadResult
  482. {
  483. public BitVector32 Stubs;
  484. public BitVector32 Replaces;
  485. public MetaLoader Npdm;
  486. public bool Modified => (Stubs.Data | Replaces.Data) != 0;
  487. }
  488. internal ModLoadResult ApplyExefsMods(ulong applicationId, NsoExecutable[] nsos)
  489. {
  490. ModLoadResult modLoadResult = new()
  491. {
  492. Stubs = new BitVector32(),
  493. Replaces = new BitVector32(),
  494. };
  495. if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.ExefsDirs.Count == 0)
  496. {
  497. return modLoadResult;
  498. }
  499. if (nsos.Length != ProcessConst.ExeFsPrefixes.Length)
  500. {
  501. throw new ArgumentOutOfRangeException(nameof(nsos), nsos.Length, "NSO Count is incorrect");
  502. }
  503. var exeMods = mods.ExefsDirs;
  504. foreach (var mod in exeMods)
  505. {
  506. if (!mod.Enabled)
  507. {
  508. continue;
  509. }
  510. for (int i = 0; i < ProcessConst.ExeFsPrefixes.Length; ++i)
  511. {
  512. var nsoName = ProcessConst.ExeFsPrefixes[i];
  513. FileInfo nsoFile = new(Path.Combine(mod.Path.FullName, nsoName));
  514. if (nsoFile.Exists)
  515. {
  516. if (modLoadResult.Replaces[1 << i])
  517. {
  518. Logger.Warning?.Print(LogClass.ModLoader, $"Multiple replacements to '{nsoName}'");
  519. continue;
  520. }
  521. modLoadResult.Replaces[1 << i] = true;
  522. nsos[i] = new NsoExecutable(nsoFile.OpenRead().AsStorage(), nsoName);
  523. Logger.Info?.Print(LogClass.ModLoader, $"NSO '{nsoName}' replaced");
  524. }
  525. modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension));
  526. }
  527. FileInfo npdmFile = new(Path.Combine(mod.Path.FullName, "main.npdm"));
  528. if (npdmFile.Exists)
  529. {
  530. if (modLoadResult.Npdm != null)
  531. {
  532. Logger.Warning?.Print(LogClass.ModLoader, "Multiple replacements to 'main.npdm'");
  533. continue;
  534. }
  535. modLoadResult.Npdm = new MetaLoader();
  536. modLoadResult.Npdm.Load(File.ReadAllBytes(npdmFile.FullName));
  537. Logger.Info?.Print(LogClass.ModLoader, "main.npdm replaced");
  538. }
  539. }
  540. for (int i = ProcessConst.ExeFsPrefixes.Length - 1; i >= 0; --i)
  541. {
  542. if (modLoadResult.Stubs[1 << i] && !modLoadResult.Replaces[1 << i]) // Prioritizes replacements over stubs
  543. {
  544. Logger.Info?.Print(LogClass.ModLoader, $" NSO '{nsos[i].Name}' stubbed");
  545. nsos[i] = null;
  546. }
  547. }
  548. return modLoadResult;
  549. }
  550. internal void ApplyNroPatches(NroExecutable nro)
  551. {
  552. var nroPatches = _patches.NroPatches;
  553. if (nroPatches.Count == 0)
  554. {
  555. return;
  556. }
  557. // NRO patches aren't offset relative to header unlike NSO
  558. // according to Atmosphere's ro patcher module
  559. ApplyProgramPatches(nroPatches, 0, nro);
  560. }
  561. internal bool ApplyNsoPatches(ulong applicationId, params IExecutable[] programs)
  562. {
  563. IEnumerable<Mod<DirectoryInfo>> nsoMods = _patches.NsoPatches;
  564. if (_appMods.TryGetValue(applicationId, out ModCache mods))
  565. {
  566. nsoMods = nsoMods.Concat(mods.ExefsDirs);
  567. }
  568. // NSO patches are created with offset 0 according to Atmosphere's patcher module
  569. // But `Program` doesn't contain the header which is 0x100 bytes. So, we adjust for that here
  570. return ApplyProgramPatches(nsoMods, 0x100, programs);
  571. }
  572. internal void LoadCheats(ulong applicationId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine)
  573. {
  574. if (tamperInfo?.BuildIds == null || tamperInfo.CodeAddresses == null)
  575. {
  576. Logger.Error?.Print(LogClass.ModLoader, "Unable to install cheat because the associated process is invalid");
  577. return;
  578. }
  579. Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for application {applicationId:X16}:\n {String.Join("\n ", tamperInfo.BuildIds)}");
  580. if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.Cheats.Count == 0)
  581. {
  582. return;
  583. }
  584. var cheats = mods.Cheats;
  585. var processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v })
  586. .ToDictionary(x => x.k[..Math.Min(Cheat.CheatIdSize, x.k.Length)], x => x.v);
  587. foreach (var cheat in cheats)
  588. {
  589. string cheatId = Path.GetFileNameWithoutExtension(cheat.Path.Name).ToUpper();
  590. if (!processExes.TryGetValue(cheatId, out ulong exeAddress))
  591. {
  592. Logger.Warning?.Print(LogClass.ModLoader, $"Skipping cheat '{cheat.Name}' because no executable matches its BuildId {cheatId} (check if the game title and version are correct)");
  593. continue;
  594. }
  595. Logger.Info?.Print(LogClass.ModLoader, $"Installing cheat '{cheat.Name}'");
  596. tamperMachine.InstallAtmosphereCheat(cheat.Name, cheatId, cheat.Instructions, tamperInfo, exeAddress);
  597. }
  598. EnableCheats(applicationId, tamperMachine);
  599. }
  600. internal static void EnableCheats(ulong applicationId, TamperMachine tamperMachine)
  601. {
  602. var contentDirectory = FindApplicationDir(new DirectoryInfo(Path.Combine(GetModsBasePath(), AmsContentsDir)), $"{applicationId:x16}");
  603. string enabledCheatsPath = Path.Combine(contentDirectory.FullName, CheatDir, "enabled.txt");
  604. if (File.Exists(enabledCheatsPath))
  605. {
  606. tamperMachine.EnableCheats(File.ReadAllLines(enabledCheatsPath));
  607. }
  608. }
  609. private static bool ApplyProgramPatches(IEnumerable<Mod<DirectoryInfo>> mods, int protectedOffset, params IExecutable[] programs)
  610. {
  611. int count = 0;
  612. MemPatch[] patches = new MemPatch[programs.Length];
  613. for (int i = 0; i < patches.Length; ++i)
  614. {
  615. patches[i] = new MemPatch();
  616. }
  617. var buildIds = programs.Select(p => p switch
  618. {
  619. NsoExecutable nso => Convert.ToHexString(nso.BuildId.ItemsRo.ToArray()).TrimEnd('0'),
  620. NroExecutable nro => Convert.ToHexString(nro.Header.BuildId).TrimEnd('0'),
  621. _ => string.Empty,
  622. }).ToList();
  623. int GetIndex(string buildId) => buildIds.FindIndex(id => id == buildId); // O(n) but list is small
  624. // Collect patches
  625. foreach (var mod in mods)
  626. {
  627. if (!mod.Enabled)
  628. {
  629. continue;
  630. }
  631. var patchDir = mod.Path;
  632. foreach (var patchFile in patchDir.EnumerateFiles())
  633. {
  634. if (StrEquals(".ips", patchFile.Extension)) // IPS|IPS32
  635. {
  636. string filename = Path.GetFileNameWithoutExtension(patchFile.FullName).Split('.')[0];
  637. string buildId = filename.TrimEnd('0');
  638. int index = GetIndex(buildId);
  639. if (index == -1)
  640. {
  641. continue;
  642. }
  643. Logger.Info?.Print(LogClass.ModLoader, $"Matching IPS patch '{patchFile.Name}' in '{mod.Name}' bid={buildId}");
  644. using var fs = patchFile.OpenRead();
  645. using var reader = new BinaryReader(fs);
  646. var patcher = new IpsPatcher(reader);
  647. patcher.AddPatches(patches[index]);
  648. }
  649. else if (StrEquals(".pchtxt", patchFile.Extension)) // IPSwitch
  650. {
  651. using var fs = patchFile.OpenRead();
  652. using var reader = new StreamReader(fs);
  653. var patcher = new IPSwitchPatcher(reader);
  654. int index = GetIndex(patcher.BuildId);
  655. if (index == -1)
  656. {
  657. continue;
  658. }
  659. Logger.Info?.Print(LogClass.ModLoader, $"Matching IPSwitch patch '{patchFile.Name}' in '{mod.Name}' bid={patcher.BuildId}");
  660. patcher.AddPatches(patches[index]);
  661. }
  662. }
  663. }
  664. // Apply patches
  665. for (int i = 0; i < programs.Length; ++i)
  666. {
  667. count += patches[i].Patch(programs[i].Program, protectedOffset);
  668. }
  669. return count > 0;
  670. }
  671. }
  672. }