aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.HLE/HOS/ModLoader.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Ryujinx.HLE/HOS/ModLoader.cs')
-rw-r--r--Ryujinx.HLE/HOS/ModLoader.cs168
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;