diff options
Diffstat (limited to 'Ryujinx.HLE/HOS/ModLoader.cs')
| -rw-r--r-- | Ryujinx.HLE/HOS/ModLoader.cs | 168 |
1 files changed, 167 insertions, 1 deletions
diff --git a/Ryujinx.HLE/HOS/ModLoader.cs b/Ryujinx.HLE/HOS/ModLoader.cs index 1558ac76..430a0590 100644 --- a/Ryujinx.HLE/HOS/ModLoader.cs +++ b/Ryujinx.HLE/HOS/ModLoader.cs @@ -13,6 +13,8 @@ using System.Collections.Specialized; using System.Linq; using System.IO; using Ryujinx.HLE.Loaders.Npdm; +using Ryujinx.HLE.HOS.Kernel.Process; +using System.Globalization; namespace Ryujinx.HLE.HOS { @@ -20,9 +22,12 @@ namespace Ryujinx.HLE.HOS { private const string RomfsDir = "romfs"; private const string ExefsDir = "exefs"; + private const string CheatDir = "cheats"; private const string RomfsContainer = "romfs.bin"; private const string ExefsContainer = "exefs.nsp"; private const string StubExtension = ".stub"; + private const string CheatExtension = ".txt"; + private const string DefaultCheatName = "<default>"; private const string AmsContentsDir = "contents"; private const string AmsNsoPatchDir = "exefs_patches"; @@ -41,6 +46,24 @@ namespace Ryujinx.HLE.HOS } } + public struct Cheat + { + // Atmosphere identifies the executables with the first 8 bytes + // of the build id, which is equivalent to 16 hex digits. + public const int CheatIdSize = 16; + + public readonly string Name; + public readonly FileInfo Path; + public readonly IEnumerable<String> Instructions; + + public Cheat(string name, FileInfo path, IEnumerable<String> instructions) + { + Name = name; + Path = path; + Instructions = instructions; + } + } + // Title dependent mods public class ModCache { @@ -50,12 +73,15 @@ namespace Ryujinx.HLE.HOS public List<Mod<DirectoryInfo>> RomfsDirs { get; } public List<Mod<DirectoryInfo>> ExefsDirs { get; } + public List<Cheat> Cheats { get; } + public ModCache() { RomfsContainers = new List<Mod<FileInfo>>(); ExefsContainers = new List<Mod<FileInfo>>(); RomfsDirs = new List<Mod<DirectoryInfo>>(); ExefsDirs = new List<Mod<DirectoryInfo>>(); + Cheats = new List<Cheat>(); } } @@ -192,20 +218,38 @@ namespace Ryujinx.HLE.HOS mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleDir.Name} ExeFs>", modDir)); types.Append('E'); } + else if (StrEquals(CheatDir, modDir.Name)) + { + for (int i = 0; i < QueryCheatsDir(mods, modDir); i++) + { + types.Append('C'); + } + } else { var romfs = new DirectoryInfo(Path.Combine(modDir.FullName, RomfsDir)); var exefs = new DirectoryInfo(Path.Combine(modDir.FullName, ExefsDir)); + var cheat = new DirectoryInfo(Path.Combine(modDir.FullName, CheatDir)); + if (romfs.Exists) { mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, romfs)); types.Append('R'); } + if (exefs.Exists) { mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, exefs)); types.Append('E'); } + + if (cheat.Exists) + { + for (int i = 0; i < QueryCheatsDir(mods, cheat); i++) + { + types.Append('C'); + } + } } if (types.Length > 0) Logger.Info?.Print(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]"); @@ -226,6 +270,94 @@ namespace Ryujinx.HLE.HOS } } + private static int QueryCheatsDir(ModCache mods, DirectoryInfo cheatsDir) + { + if (!cheatsDir.Exists) + { + return 0; + } + + int numMods = 0; + + foreach (FileInfo file in cheatsDir.EnumerateFiles()) + { + if (!StrEquals(CheatExtension, file.Extension)) + { + continue; + } + + string cheatId = Path.GetFileNameWithoutExtension(file.Name); + + if (cheatId.Length != Cheat.CheatIdSize) + { + continue; + } + + if (!ulong.TryParse(cheatId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _)) + { + continue; + } + + // A cheat file can contain several cheats for the same executable, so the file must be parsed in + // order to properly enumerate them. + mods.Cheats.AddRange(GetCheatsInFile(file)); + } + + return numMods; + } + + private static IEnumerable<Cheat> GetCheatsInFile(FileInfo cheatFile) + { + string cheatName = DefaultCheatName; + List<string> instructions = new List<string>(); + List<Cheat> cheats = new List<Cheat>(); + + using (StreamReader cheatData = cheatFile.OpenText()) + { + string line; + while ((line = cheatData.ReadLine()) != null) + { + line = line.Trim(); + + if (line.StartsWith('[')) + { + // This line starts a new cheat section. + if (!line.EndsWith(']') || line.Length < 3) + { + // Skip the entire file if there's any error while parsing the cheat file. + + Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed"); + + return new List<Cheat>(); + } + + // Add the previous section to the list. + if (instructions.Count != 0) + { + cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions)); + } + + // Start a new cheat section. + cheatName = line.Substring(1, line.Length - 2); + instructions = new List<string>(); + } + else if (line.Length > 0) + { + // The line contains an instruction. + instructions.Add(line); + } + } + + // Add the last section being processed. + if (instructions.Count != 0) + { + cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions)); + } + } + + return cheats; + } + // Assumes searchDirPaths don't overlap public static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths) { @@ -408,7 +540,6 @@ namespace Ryujinx.HLE.HOS return modLoadResult; } - if (nsos.Length != ApplicationLoader.ExeFsPrefixes.Length) { throw new ArgumentOutOfRangeException("NSO Count is incorrect"); @@ -494,6 +625,41 @@ namespace Ryujinx.HLE.HOS return ApplyProgramPatches(nsoMods, 0x100, programs); } + internal void LoadCheats(ulong titleId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine) + { + if (tamperInfo == null || tamperInfo.BuildIds == null || tamperInfo.CodeAddresses == null) + { + Logger.Error?.Print(LogClass.ModLoader, "Unable to install cheat because the associated process is invalid"); + } + + Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for title {titleId:X16}:\n {String.Join("\n ", tamperInfo.BuildIds)}"); + + if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.Cheats.Count == 0) + { + return; + } + + var cheats = mods.Cheats; + var processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v }) + .ToDictionary(x => x.k.Substring(0, Math.Min(Cheat.CheatIdSize, x.k.Length)), x => x.v); + + foreach (var cheat in cheats) + { + string cheatId = Path.GetFileNameWithoutExtension(cheat.Path.Name).ToUpper(); + + if (!processExes.TryGetValue(cheatId, out ulong exeAddress)) + { + 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)"); + + continue; + } + + Logger.Info?.Print(LogClass.ModLoader, $"Installing cheat '{cheat.Name}'"); + + tamperMachine.InstallAtmosphereCheat(cheat.Instructions, tamperInfo, exeAddress); + } + } + private static bool ApplyProgramPatches(IEnumerable<Mod<DirectoryInfo>> mods, int protectedOffset, params IExecutable[] programs) { int count = 0; |
