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:
parent
a5d5ca0635
commit
0c1ea1212a
71 changed files with 2793 additions and 5 deletions
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue