Add the TamperMachine module for runtime mods and cheats (#1928)

* Add initial implementation of the Tamper Machine

* Implement Atmosphere opcodes 0, 4 and 9

* Add missing TamperCompilationException class

* Implement Atmosphere conditional and loop opcodes 1, 2 and 3

* Inplement input conditional opcode 8

* Add register store opcode A

* Implement extended pause/resume opcodes FF0 and FF1

* Implement extended log opcode FFF

* Implement extended register conditional opcode C0

* Refactor TamperProgram to an interface

* Moved Atmosphere classes to a separate subdirectory

* Fix OpProcCtrl class not setting process

* Implement extended register save/restore opcodes C1, C2 and C3

* Refactor code emitters to separate classes

* Supress memory access errors from the Tamper Machine

* Add debug information to tamper register and memory writes

* Add block stack check to Atmosphere Cheat compiler

* Add handheld input support to Tamper Machine

* Fix code styling

* Fix build id and cheat case mismatch

* Fix invalid immediate size selection

* Print build ids of the title

* Prevent Tamper Machine from change code regions

* Remove Atmosphere namespace

* Remove empty cheats from the list

* Prevent code modification without disabling the tampering

* Fix missing addressing mode in LoadRegisterWithMemory

* Fix wrong addressing in RegisterConditional

* Add name to the tamper machine thread

* Fix code styling
This commit is contained in:
Caian Benedicto 2021-03-27 11:12:05 -03:00 committed by GitHub
parent a5d5ca0635
commit 0c1ea1212a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 2793 additions and 5 deletions

View file

@ -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;