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

@ -57,6 +57,7 @@ namespace Ryujinx.Common.Logging
ServiceTime, ServiceTime,
ServiceVi, ServiceVi,
SurfaceFlinger, SurfaceFlinger,
TamperMachine,
Vic Vic
} }
} }

View File

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class CodeRegionTamperedException : TamperExecutionException
{
public CodeRegionTamperedException(string message) : base(message) { }
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class TamperCompilationException : Exception
{
public TamperCompilationException(string message) : base(message) { }
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class TamperExecutionException : Exception
{
public TamperExecutionException(string message) : base(message) { }
}
}

View File

@ -11,6 +11,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.FileSystem.Content;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Loaders.Npdm;
using System; using System;
@ -527,7 +528,9 @@ namespace Ryujinx.HLE.HOS
Ptc.Initialize(TitleIdText, DisplayVersion, usePtc); Ptc.Initialize(TitleIdText, DisplayVersion, usePtc);
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: programs); ProgramLoader.LoadNsos(_device.System.KernelContext, out ProcessTamperInfo tamperInfo, metaData, executables: programs);
_fileSystem.ModLoader.LoadCheats(TitleId, tamperInfo, _device.TamperMachine);
} }
public void LoadProgram(string filePath) public void LoadProgram(string filePath)
@ -626,7 +629,9 @@ namespace Ryujinx.HLE.HOS
Graphics.Gpu.GraphicsConfig.TitleId = null; Graphics.Gpu.GraphicsConfig.TitleId = null;
_device.Gpu.HostInitalized.Set(); _device.Gpu.HostInitalized.Set();
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: executable); ProgramLoader.LoadNsos(_device.System.KernelContext, out ProcessTamperInfo tamperInfo, metaData, executables: executable);
_fileSystem.ModLoader.LoadCheats(TitleId, tamperInfo, _device.TamperMachine);
} }
private Npdm GetDefaultNpdm() private Npdm GetDefaultNpdm()

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Kernel.Process
{
internal class ProcessTamperInfo
{
public KProcess Process { get; }
public IEnumerable<string> BuildIds { get; }
public IEnumerable<ulong> CodeAddresses { get; }
public ulong HeapAddress { get; }
public ProcessTamperInfo(KProcess process, IEnumerable<string> buildIds, IEnumerable<ulong> codeAddresses, ulong heapAddress)
{
Process = process;
BuildIds = buildIds;
CodeAddresses = codeAddresses;
HeapAddress = heapAddress;
}
}
}

View File

@ -13,6 +13,8 @@ using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.IO; using System.IO;
using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.HLE.HOS.Kernel.Process;
using System.Globalization;
namespace Ryujinx.HLE.HOS namespace Ryujinx.HLE.HOS
{ {
@ -20,9 +22,12 @@ namespace Ryujinx.HLE.HOS
{ {
private const string RomfsDir = "romfs"; private const string RomfsDir = "romfs";
private const string ExefsDir = "exefs"; private const string ExefsDir = "exefs";
private const string CheatDir = "cheats";
private const string RomfsContainer = "romfs.bin"; private const string RomfsContainer = "romfs.bin";
private const string ExefsContainer = "exefs.nsp"; private const string ExefsContainer = "exefs.nsp";
private const string StubExtension = ".stub"; private const string StubExtension = ".stub";
private const string CheatExtension = ".txt";
private const string DefaultCheatName = "<default>";
private const string AmsContentsDir = "contents"; private const string AmsContentsDir = "contents";
private const string AmsNsoPatchDir = "exefs_patches"; 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 // Title dependent mods
public class ModCache public class ModCache
{ {
@ -50,12 +73,15 @@ namespace Ryujinx.HLE.HOS
public List<Mod<DirectoryInfo>> RomfsDirs { get; } public List<Mod<DirectoryInfo>> RomfsDirs { get; }
public List<Mod<DirectoryInfo>> ExefsDirs { get; } public List<Mod<DirectoryInfo>> ExefsDirs { get; }
public List<Cheat> Cheats { get; }
public ModCache() public ModCache()
{ {
RomfsContainers = new List<Mod<FileInfo>>(); RomfsContainers = new List<Mod<FileInfo>>();
ExefsContainers = new List<Mod<FileInfo>>(); ExefsContainers = new List<Mod<FileInfo>>();
RomfsDirs = new List<Mod<DirectoryInfo>>(); RomfsDirs = new List<Mod<DirectoryInfo>>();
ExefsDirs = 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)); mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleDir.Name} ExeFs>", modDir));
types.Append('E'); types.Append('E');
} }
else if (StrEquals(CheatDir, modDir.Name))
{
for (int i = 0; i < QueryCheatsDir(mods, modDir); i++)
{
types.Append('C');
}
}
else else
{ {
var romfs = new DirectoryInfo(Path.Combine(modDir.FullName, RomfsDir)); var romfs = new DirectoryInfo(Path.Combine(modDir.FullName, RomfsDir));
var exefs = new DirectoryInfo(Path.Combine(modDir.FullName, ExefsDir)); var exefs = new DirectoryInfo(Path.Combine(modDir.FullName, ExefsDir));
var cheat = new DirectoryInfo(Path.Combine(modDir.FullName, CheatDir));
if (romfs.Exists) if (romfs.Exists)
{ {
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, romfs)); mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, romfs));
types.Append('R'); types.Append('R');
} }
if (exefs.Exists) if (exefs.Exists)
{ {
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, exefs)); mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, exefs));
types.Append('E'); 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}]"); 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 // Assumes searchDirPaths don't overlap
public static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths) public static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths)
{ {
@ -408,7 +540,6 @@ namespace Ryujinx.HLE.HOS
return modLoadResult; return modLoadResult;
} }
if (nsos.Length != ApplicationLoader.ExeFsPrefixes.Length) if (nsos.Length != ApplicationLoader.ExeFsPrefixes.Length)
{ {
throw new ArgumentOutOfRangeException("NSO Count is incorrect"); throw new ArgumentOutOfRangeException("NSO Count is incorrect");
@ -494,6 +625,41 @@ namespace Ryujinx.HLE.HOS
return ApplyProgramPatches(nsoMods, 0x100, programs); 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) private static bool ApplyProgramPatches(IEnumerable<Mod<DirectoryInfo>> mods, int protectedOffset, params IExecutable[] programs)
{ {
int count = 0; int count = 0;

View File

@ -1,13 +1,14 @@
using ARMeilleure.Translation.PTC; using ARMeilleure.Translation.PTC;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Kernel; using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Memory;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Loaders.Npdm;
using System;
using System.Linq;
namespace Ryujinx.HLE.HOS namespace Ryujinx.HLE.HOS
{ {
@ -124,13 +125,20 @@ namespace Ryujinx.HLE.HOS
return true; return true;
} }
public static bool LoadNsos(KernelContext context, Npdm metaData, byte[] arguments = null, params IExecutable[] executables) public static bool LoadNsos(KernelContext context, out ProcessTamperInfo tamperInfo, Npdm metaData, byte[] arguments = null, params IExecutable[] executables)
{ {
ulong argsStart = 0; ulong argsStart = 0;
uint argsSize = 0; uint argsSize = 0;
ulong codeStart = metaData.Is64Bit ? 0x8000000UL : 0x200000UL; ulong codeStart = metaData.Is64Bit ? 0x8000000UL : 0x200000UL;
uint codeSize = 0; uint codeSize = 0;
var buildIds = executables.Select(e => (e switch
{
NsoExecutable nso => BitConverter.ToString(nso.BuildId.Bytes.ToArray()),
NroExecutable nro => BitConverter.ToString(nro.Header.BuildId),
_ => ""
}).Replace("-", "").ToUpper());
ulong[] nsoBase = new ulong[executables.Length]; ulong[] nsoBase = new ulong[executables.Length];
for (int index = 0; index < executables.Length; index++) for (int index = 0; index < executables.Length; index++)
@ -202,6 +210,8 @@ namespace Ryujinx.HLE.HOS
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization failed setting resource limit values."); Logger.Error?.Print(LogClass.Loader, $"Process initialization failed setting resource limit values.");
tamperInfo = null;
return false; return false;
} }
@ -213,6 +223,8 @@ namespace Ryujinx.HLE.HOS
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization failed due to invalid ACID flags."); Logger.Error?.Print(LogClass.Loader, $"Process initialization failed due to invalid ACID flags.");
tamperInfo = null;
return false; return false;
} }
@ -229,6 +241,8 @@ namespace Ryujinx.HLE.HOS
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
tamperInfo = null;
return false; return false;
} }
@ -242,6 +256,8 @@ namespace Ryujinx.HLE.HOS
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
tamperInfo = null;
return false; return false;
} }
} }
@ -254,11 +270,18 @@ namespace Ryujinx.HLE.HOS
{ {
Logger.Error?.Print(LogClass.Loader, $"Process start returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process start returned error \"{result}\".");
tamperInfo = null;
return false; return false;
} }
context.Processes.TryAdd(process.Pid, process); context.Processes.TryAdd(process.Pid, process);
// Keep the build ids because the tamper machine uses them to know which process to associate a
// tamper to and also keep the starting address of each executable inside a process because some
// memory modifications are relative to this address.
tamperInfo = new ProcessTamperInfo(process, buildIds, nsoBase, process.MemoryManager.HeapRegionStart);
return true; return true;
} }

View File

@ -0,0 +1,130 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.CodeEmitters;
using Ryujinx.HLE.HOS.Tamper.Operations;
using System;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Tamper
{
class AtmosphereCompiler
{
public ITamperProgram Compile(IEnumerable<string> rawInstructions, ulong exeAddress, ulong heapAddress, ITamperedProcess process)
{
Logger.Debug?.Print(LogClass.TamperMachine, $"Executable address: {exeAddress:X16}");
Logger.Debug?.Print(LogClass.TamperMachine, $"Heap address: {heapAddress:X16}");
try
{
return CompileImpl(rawInstructions, exeAddress, heapAddress, process);
}
catch(TamperCompilationException exception)
{
// Just print the message without the stack trace.
Logger.Error?.Print(LogClass.TamperMachine, exception.Message);
}
catch (Exception exception)
{
Logger.Error?.Print(LogClass.TamperMachine, exception.ToString());
}
Logger.Error?.Print(LogClass.TamperMachine, "There was a problem while compiling the Atmosphere cheat");
return null;
}
private ITamperProgram CompileImpl(IEnumerable<string> rawInstructions, ulong exeAddress, ulong heapAddress, ITamperedProcess process)
{
CompilationContext context = new CompilationContext(exeAddress, heapAddress, process);
context.BlockStack.Push(new OperationBlock(null));
// Parse the instructions.
foreach (string rawInstruction in rawInstructions)
{
Logger.Debug?.Print(LogClass.TamperMachine, $"Compiling instruction {rawInstruction}");
byte[] instruction = InstructionHelper.ParseRawInstruction(rawInstruction);
CodeType codeType = InstructionHelper.GetCodeType(instruction);
switch (codeType)
{
case CodeType.StoreConstantToAddress:
StoreConstantToAddress.Emit(instruction, context);
break;
case CodeType.BeginMemoryConditionalBlock:
BeginConditionalBlock.Emit(instruction, context);
break;
case CodeType.EndConditionalBlock:
EndConditionalBlock.Emit(instruction, context);
break;
case CodeType.StartEndLoop:
StartEndLoop.Emit(instruction, context);
break;
case CodeType.LoadRegisterWithContant:
LoadRegisterWithConstant.Emit(instruction, context);
break;
case CodeType.LoadRegisterWithMemory:
LoadRegisterWithMemory.Emit(instruction, context);
break;
case CodeType.StoreConstantToMemory:
StoreConstantToMemory.Emit(instruction, context);
break;
case CodeType.LegacyArithmetic:
LegacyArithmetic.Emit(instruction, context);
break;
case CodeType.BeginKeypressConditionalBlock:
BeginConditionalBlock.Emit(instruction, context);
break;
case CodeType.Arithmetic:
Arithmetic.Emit(instruction, context);
break;
case CodeType.StoreRegisterToMemory:
StoreRegisterToMemory.Emit(instruction, context);
break;
case CodeType.BeginRegisterConditionalBlock:
BeginConditionalBlock.Emit(instruction, context);
break;
case CodeType.SaveOrRestoreRegister:
SaveOrRestoreRegister.Emit(instruction, context);
break;
case CodeType.SaveOrRestoreRegisterWithMask:
SaveOrRestoreRegisterWithMask.Emit(instruction, context);
break;
case CodeType.ReadOrWriteStaticRegister:
ReadOrWriteStaticRegister.Emit(instruction, context);
break;
case CodeType.PauseProcess:
PauseProcess.Emit(instruction, context);
break;
case CodeType.ResumeProcess:
ResumeProcess.Emit(instruction, context);
break;
case CodeType.DebugLog:
DebugLog.Emit(instruction, context);
break;
default:
throw new TamperCompilationException($"Code type {codeType} not implemented in Atmosphere cheat");
}
}
// Initialize only the registers used.
Value<ulong> zero = new Value<ulong>(0UL);
int position = 0;
foreach (Register register in context.Registers.Values)
{
context.CurrentOperations.Insert(position, new OpMov<ulong>(register, zero));
position++;
}
if (context.BlockStack.Count != 1)
{
throw new TamperCompilationException($"Reached end of compilation with unmatched conditional(s) or loop(s)");
}
return new AtmosphereProgram(process, context.PressedKeys, new Block(context.CurrentOperations));
}
}
}

View File

@ -0,0 +1,26 @@
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper
{
class AtmosphereProgram : ITamperProgram
{
private Parameter<long> _pressedKeys;
private IOperation _entryPoint;
public ITamperedProcess Process { get; }
public AtmosphereProgram(ITamperedProcess process, Parameter<long> pressedKeys, IOperation entryPoint)
{
Process = process;
_pressedKeys = pressedKeys;
_entryPoint = entryPoint;
}
public void Execute(ControllerKeys pressedKeys)
{
_pressedKeys.Value = (long)pressedKeys;
_entryPoint.Execute();
}
}
}

View File

@ -0,0 +1,105 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Operations;
using System;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 9 allows performing arithmetic on registers.
/// </summary>
class Arithmetic
{
private const int OperationWidthIndex = 1;
private const int OperationTypeIndex = 2;
private const int DestinationRegisterIndex = 3;
private const int LeftHandSideRegisterIndex = 4;
private const int UseImmediateAsRhsIndex = 5;
private const int RightHandSideRegisterIndex = 6;
private const int RightHandSideImmediateIndex = 8;
private const int RightHandSideImmediate8 = 8;
private const int RightHandSideImmediate16 = 16;
private const byte Add = 0; // lhs + rhs
private const byte Sub = 1; // lhs - rhs
private const byte Mul = 2; // lhs * rhs
private const byte Lsh = 3; // lhs << rhs
private const byte Rsh = 4; // lhs >> rhs
private const byte And = 5; // lhs & rhs
private const byte Or = 6; // lhs | rhs
private const byte Not = 7; // ~lhs (discards right-hand operand)
private const byte Xor = 8; // lhs ^ rhs
private const byte Mov = 9; // lhs (discards right-hand operand)
public static void Emit(byte[] instruction, CompilationContext context)
{
// 9TCRS0s0
// T: Width of arithmetic operation(1, 2, 4, or 8 bytes).
// C: Arithmetic operation to apply, see below.
// R: Register to store result in.
// S: Register to use as left - hand operand.
// s: Register to use as right - hand operand.
// 9TCRS100 VVVVVVVV (VVVVVVVV)
// T: Width of arithmetic operation(1, 2, 4, or 8 bytes).
// C: Arithmetic operation to apply, see below.
// R: Register to store result in.
// S: Register to use as left - hand operand.
// V: Value to use as right - hand operand.
byte operationWidth = instruction[OperationWidthIndex];
byte operation = instruction[OperationTypeIndex];
Register destinationRegister = context.GetRegister(instruction[DestinationRegisterIndex]);
Register leftHandSideRegister = context.GetRegister(instruction[LeftHandSideRegisterIndex]);
byte rightHandSideIsImmediate = instruction[UseImmediateAsRhsIndex];
IOperand rightHandSideOperand;
switch (rightHandSideIsImmediate)
{
case 0:
// Use a register as right-hand side.
rightHandSideOperand = context.GetRegister(instruction[RightHandSideRegisterIndex]);
break;
case 1:
// Use an immediate as right-hand side.
int immediateSize = operationWidth <= 4 ? RightHandSideImmediate8 : RightHandSideImmediate16;
ulong immediate = InstructionHelper.GetImmediate(instruction, RightHandSideImmediateIndex, immediateSize);
rightHandSideOperand = new Value<ulong>(immediate);
break;
default:
throw new TamperCompilationException($"Invalid right-hand side switch {rightHandSideIsImmediate} in Atmosphere cheat");
}
void Emit(Type operationType, IOperand rhs = null)
{
List<IOperand> operandList = new List<IOperand>();
operandList.Add(destinationRegister);
operandList.Add(leftHandSideRegister);
if (rhs != null)
{
operandList.Add(rhs);
}
InstructionHelper.Emit(operationType, operationWidth, context, operandList.ToArray());
}
switch (operation)
{
case Add: Emit(typeof(OpAdd<>), rightHandSideOperand); break;
case Sub: Emit(typeof(OpSub<>), rightHandSideOperand); break;
case Mul: Emit(typeof(OpMul<>), rightHandSideOperand); break;
case Lsh: Emit(typeof(OpLsh<>), rightHandSideOperand); break;
case Rsh: Emit(typeof(OpRsh<>), rightHandSideOperand); break;
case And: Emit(typeof(OpAnd<>), rightHandSideOperand); break;
case Or: Emit(typeof(OpOr<> ), rightHandSideOperand); break;
case Not: Emit(typeof(OpNot<>) ); break;
case Xor: Emit(typeof(OpXor<>), rightHandSideOperand); break;
case Mov: Emit(typeof(OpMov<>) ); break;
default:
throw new TamperCompilationException($"Invalid arithmetic operation {operation} in Atmosphere cheat");
}
}
}
}

View File

@ -0,0 +1,14 @@
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Marks the begin of a conditional block (started by Code Type 1, Code Type 8 or Code Type C0).
/// </summary>
class BeginConditionalBlock
{
public static void Emit(byte[] instruction, CompilationContext context)
{
// Just start a new compilation block and parse the instruction itself at the end.
context.BlockStack.Push(new OperationBlock(instruction));
}
}
}

View File

@ -0,0 +1,87 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 0xFFF writes a debug log.
/// </summary>
class DebugLog
{
private const int OperationWidthIndex = 3;
private const int LogIdIndex = 4;
private const int OperandTypeIndex = 5;
private const int RegisterOrMemoryRegionIndex = 6;
private const int OffsetRegisterOrImmediateIndex = 7;
private const int MemoryRegionWithOffsetImmediate = 0;
private const int MemoryRegionWithOffsetRegister = 1;
private const int AddressRegisterWithOffsetImmediate = 2;
private const int AddressRegisterWithOffsetRegister = 3;
private const int ValueRegister = 4;
private const int OffsetImmediateSize = 9;
public static void Emit(byte[] instruction, CompilationContext context)
{
// FFFTIX##
// FFFTI0Ma aaaaaaaa
// FFFTI1Mr
// FFFTI2Ra aaaaaaaa
// FFFTI3Rr
// FFFTI4V0
// T: Width of memory write (1, 2, 4, or 8 bytes).
// I: Log id.
// X: Operand Type, see below.
// M: Memory Type (operand types 0 and 1).
// R: Address Register (operand types 2 and 3).
// a: Relative Address (operand types 0 and 2).
// r: Offset Register (operand types 1 and 3).
// V: Value Register (operand type 4).
byte operationWidth = instruction[OperationWidthIndex];
byte logId = instruction[LogIdIndex];
byte operandType = instruction[OperandTypeIndex];
byte registerOrMemoryRegion = instruction[RegisterOrMemoryRegionIndex];
byte offsetRegisterIndex = instruction[OffsetRegisterOrImmediateIndex];
ulong immediate;
Register addressRegister;
Register offsetRegister;
IOperand sourceOperand;
switch (operandType)
{
case MemoryRegionWithOffsetImmediate:
// *(?x + #a)
immediate = InstructionHelper.GetImmediate(instruction, OffsetRegisterOrImmediateIndex, OffsetImmediateSize);
sourceOperand = MemoryHelper.EmitPointer((MemoryRegion)registerOrMemoryRegion, immediate, context);
break;
case MemoryRegionWithOffsetRegister:
// *(?x + $r)
offsetRegister = context.GetRegister(offsetRegisterIndex);
sourceOperand = MemoryHelper.EmitPointer((MemoryRegion)registerOrMemoryRegion, offsetRegister, context);
break;
case AddressRegisterWithOffsetImmediate:
// *($R + #a)
addressRegister = context.GetRegister(registerOrMemoryRegion);
immediate = InstructionHelper.GetImmediate(instruction, OffsetRegisterOrImmediateIndex, OffsetImmediateSize);
sourceOperand = MemoryHelper.EmitPointer(addressRegister, immediate, context);
break;
case AddressRegisterWithOffsetRegister:
// *($R + $r)
addressRegister = context.GetRegister(registerOrMemoryRegion);
offsetRegister = context.GetRegister(offsetRegisterIndex);
sourceOperand = MemoryHelper.EmitPointer(addressRegister, offsetRegister, context);
break;
case ValueRegister:
// $V
sourceOperand = context.GetRegister(registerOrMemoryRegion);
break;
default:
throw new TamperCompilationException($"Invalid operand type {operandType} in Atmosphere cheat");
}
InstructionHelper.Emit(typeof(OpLog<>), operationWidth, context, logId, sourceOperand);
}
}
}

View File

@ -0,0 +1,50 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Conditions;
using Ryujinx.HLE.HOS.Tamper.Operations;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 2 marks the end of a conditional block (started by Code Type 1, Code Type 8 or Code Type C0).
/// </summary>
class EndConditionalBlock
{
public static void Emit(byte[] instruction, CompilationContext context)
{
// 20000000
// Use the conditional begin instruction stored in the stack.
instruction = context.CurrentBlock.BaseInstruction;
CodeType codeType = InstructionHelper.GetCodeType(instruction);
// Pop the current block of operations from the stack so control instructions
// for the conditional can be emitted in the upper block.
IEnumerable<IOperation> operations = context.CurrentOperations;
context.BlockStack.Pop();
ICondition condition;
switch (codeType)
{
case CodeType.BeginMemoryConditionalBlock:
condition = MemoryConditional.Emit(instruction, context);
break;
case CodeType.BeginKeypressConditionalBlock:
condition = KeyPressConditional.Emit(instruction, context);
break;
case CodeType.BeginRegisterConditionalBlock:
condition = RegisterConditional.Emit(instruction, context);
break;
default:
throw new TamperCompilationException($"Conditional end does not match code type {codeType} in Atmosphere cheat");
}
// Create a conditional block with the current operations and nest it in the upper
// block of the stack.
IfBlock block = new IfBlock(condition, operations);
context.CurrentOperations.Add(block);
}
}
}

View File

@ -0,0 +1,26 @@
using Ryujinx.HLE.HOS.Tamper.Conditions;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 8 enters or skips a conditional block based on whether a key combination is pressed.
/// </summary>
class KeyPressConditional
{
private const int InputMaskIndex = 1;
private const int InputMaskSize = 7;
public static ICondition Emit(byte[] instruction, CompilationContext context)
{
// 8kkkkkkk
// k: Keypad mask to check against, see below.
// Note that for multiple button combinations, the bitmasks should be ORd together.
// The Keypad Values are the direct output of hidKeysDown().
ulong inputMask = InstructionHelper.GetImmediate(instruction, InputMaskIndex, InputMaskSize);
return new InputMask((long)inputMask, context.PressedKeys);
}
}
}

View File

@ -0,0 +1,57 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Operations;
using System;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 7 allows performing arithmetic on registers. However, it has been deprecated by Code
/// type 9, and is only kept for backwards compatibility.
/// </summary>
class LegacyArithmetic
{
const int OperationWidthIndex = 1;
const int DestinationRegisterIndex = 3;
const int OperationTypeIndex = 4;
const int ValueImmediateIndex = 8;
const int ValueImmediateSize = 8;
private const byte Add = 0; // reg += rhs
private const byte Sub = 1; // reg -= rhs
private const byte Mul = 2; // reg *= rhs
private const byte Lsh = 3; // reg <<= rhs
private const byte Rsh = 4; // reg >>= rhs
public static void Emit(byte[] instruction, CompilationContext context)
{
// 7T0RC000 VVVVVVVV
// T: Width of arithmetic operation(1, 2, 4, or 8 bytes).
// R: Register to apply arithmetic to.
// C: Arithmetic operation to apply, see below.
// V: Value to use for arithmetic operation.
byte operationWidth = instruction[OperationWidthIndex];
Register register = context.GetRegister(instruction[DestinationRegisterIndex]);
byte operation = instruction[OperationTypeIndex];
ulong immediate = InstructionHelper.GetImmediate(instruction, ValueImmediateIndex, ValueImmediateSize);
Value<ulong> rightHandSideValue = new Value<ulong>(immediate);
void Emit(Type operationType)
{
InstructionHelper.Emit(operationType, operationWidth, context, register, register, rightHandSideValue);
}
switch (operation)
{
case Add: Emit(typeof(OpAdd<>)); break;
case Sub: Emit(typeof(OpSub<>)); break;
case Mul: Emit(typeof(OpMul<>)); break;
case Lsh: Emit(typeof(OpLsh<>)); break;
case Rsh: Emit(typeof(OpRsh<>)); break;
default:
throw new TamperCompilationException($"Invalid arithmetic operation {operation} in Atmosphere cheat");
}
}
}
}

View File

@ -0,0 +1,28 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 4 allows setting a register to a constant value.
/// </summary>
class LoadRegisterWithConstant
{
const int RegisterIndex = 3;
const int ValueImmediateIndex = 8;
const int ValueImmediateSize = 16;
public static void Emit(byte[] instruction, CompilationContext context)
{
// 400R0000 VVVVVVVV VVVVVVVV
// R: Register to use.
// V: Value to load.
Register destinationRegister = context.GetRegister(instruction[RegisterIndex]);
ulong immediate = InstructionHelper.GetImmediate(instruction, ValueImmediateIndex, ValueImmediateSize);
Value<ulong> sourceValue = new Value<ulong>(immediate);
context.CurrentOperations.Add(new OpMov<ulong>(destinationRegister, sourceValue));
}
}
}

View File

@ -0,0 +1,58 @@
using Ryujinx.HLE.Exceptions;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 5 allows loading a value from memory into a register, either using a fixed address or by
/// dereferencing the destination register.
/// </summary>
class LoadRegisterWithMemory
{
private const int OperationWidthIndex = 1;
private const int MemoryRegionIndex = 2;
private const int DestinationRegisterIndex = 3;
private const int UseDestinationAsSourceIndex = 4;
private const int OffsetImmediateIndex = 6;
private const int OffsetImmediateSize = 10;
public static void Emit(byte[] instruction, CompilationContext context)
{
// 5TMR00AA AAAAAAAA
// T: Width of memory read (1, 2, 4, or 8 bytes).
// M: Memory region to write to (0 = Main NSO, 1 = Heap).
// R: Register to load value into.
// A: Immediate offset to use from memory region base.
// 5TMR10AA AAAAAAAA
// T: Width of memory read(1, 2, 4, or 8 bytes).
// M: Ignored.
// R: Register to use as base address and to load value into.
// A: Immediate offset to use from register R.
byte operationWidth = instruction[OperationWidthIndex];
MemoryRegion memoryRegion = (MemoryRegion)instruction[MemoryRegionIndex];
Register destinationRegister = context.GetRegister(instruction[DestinationRegisterIndex]);
byte useDestinationAsSourceIndex = instruction[UseDestinationAsSourceIndex];
ulong address = InstructionHelper.GetImmediate(instruction, OffsetImmediateIndex, OffsetImmediateSize);
Pointer sourceMemory;
switch (useDestinationAsSourceIndex)
{
case 0:
// Don't use the source register as an additional address offset.
sourceMemory = MemoryHelper.EmitPointer(memoryRegion, address, context);
break;
case 1:
// Use the source register as the base address.
sourceMemory = MemoryHelper.EmitPointer(destinationRegister, address, context);
break;
default:
throw new TamperCompilationException($"Invalid source mode {useDestinationAsSourceIndex} in Atmosphere cheat");
}
InstructionHelper.EmitMov(operationWidth, context, destinationRegister, sourceMemory);
}
}
}

View File

@ -0,0 +1,45 @@
using Ryujinx.HLE.HOS.Tamper.Conditions;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 1 performs a comparison of the contents of memory to a static value.
/// If the condition is not met, all instructions until the appropriate conditional block terminator
/// are skipped.
/// </summary>
class MemoryConditional
{
private const int OperationWidthIndex = 1;
private const int MemoryRegionIndex = 2;
private const int ComparisonTypeIndex = 3;
private const int OffsetImmediateIndex = 6;
private const int ValueImmediateIndex = 16;
private const int OffsetImmediateSize = 10;
private const int ValueImmediateSize4 = 8;
private const int ValueImmediateSize8 = 16;
public static ICondition Emit(byte[] instruction, CompilationContext context)
{
// 1TMC00AA AAAAAAAA VVVVVVVV (VVVVVVVV)
// T: Width of memory write (1, 2, 4, or 8 bytes).
// M: Memory region to write to (0 = Main NSO, 1 = Heap).
// C: Condition to use, see below.
// A: Immediate offset to use from memory region base.
// V: Value to compare to.
byte operationWidth = instruction[OperationWidthIndex];
MemoryRegion memoryRegion = (MemoryRegion)instruction[MemoryRegionIndex];
Comparison comparison = (Comparison)instruction[ComparisonTypeIndex];
ulong address = InstructionHelper.GetImmediate(instruction, OffsetImmediateIndex, OffsetImmediateSize);
Pointer sourceMemory = MemoryHelper.EmitPointer(memoryRegion, address, context);
int valueSize = operationWidth <= 4 ? ValueImmediateSize4 : ValueImmediateSize8;
ulong value = InstructionHelper.GetImmediate(instruction, ValueImmediateIndex, valueSize);
Value<ulong> compareToValue = new Value<ulong>(value);
return InstructionHelper.CreateCondition(comparison, operationWidth, sourceMemory, compareToValue);
}
}
}

View File

@ -0,0 +1,17 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 0xFF0 pauses the current process.
/// </summary>
class PauseProcess
{
// FF0?????
public static void Emit(byte[] instruction, CompilationContext context)
{
context.CurrentOperations.Add(new OpProcCtrl(context.Process, true));
}
}
}

View File

@ -0,0 +1,47 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 0xC3 reads or writes a static register with a given register.
/// NOTE: Registers are saved and restored to a different set of registers than the ones used
/// for the other opcodes (Static Registers).
/// </summary>
class ReadOrWriteStaticRegister
{
private const int StaticRegisterIndex = 5;
private const int RegisterIndex = 7;
private const byte FirstWriteRegister = 0x80;
private const int StaticRegisterSize = 2;
public static void Emit(byte[] instruction, CompilationContext context)
{
// C3000XXx
// XX: Static register index, 0x00 to 0x7F for reading or 0x80 to 0xFF for writing.
// x: Register index.
ulong staticRegisterIndex = InstructionHelper.GetImmediate(instruction, StaticRegisterIndex, StaticRegisterSize);
Register register = context.GetRegister(instruction[RegisterIndex]);
IOperand sourceRegister;
IOperand destinationRegister;
if (staticRegisterIndex < FirstWriteRegister)
{
// Read from static register.
sourceRegister = context.GetStaticRegister((byte)staticRegisterIndex);
destinationRegister = register;
}
else
{
// Write to static register.
sourceRegister = register;
destinationRegister = context.GetStaticRegister((byte)(staticRegisterIndex - FirstWriteRegister));
}
context.CurrentOperations.Add(new OpMov<ulong>(destinationRegister, sourceRegister));
}
}
}

View File

@ -0,0 +1,106 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Conditions;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 0xC0 performs a comparison of the contents of a register and another value.
/// This code support multiple operand types, see below. If the condition is not met,
/// all instructions until the appropriate conditional block terminator are skipped.
/// </summary>
class RegisterConditional
{
private const int OperationWidthIndex = 2;
private const int ComparisonTypeIndex = 3;
private const int SourceRegisterIndex = 4;
private const int OperandTypeIndex = 5;
private const int RegisterOrMemoryRegionIndex = 6;
private const int OffsetImmediateIndex = 7;
private const int ValueImmediateIndex = 8;
private const int MemoryRegionWithOffsetImmediate = 0;
private const int MemoryRegionWithOffsetRegister = 1;
private const int AddressRegisterWithOffsetImmediate = 2;
private const int AddressRegisterWithOffsetRegister = 3;
private const int OffsetImmediate = 4;
private const int AddressRegister = 5;
private const int OffsetImmediateSize = 9;
private const int ValueImmediateSize8 = 8;
private const int ValueImmediateSize16 = 16;
public static ICondition Emit(byte[] instruction, CompilationContext context)
{
// C0TcSX##
// C0TcS0Ma aaaaaaaa
// C0TcS1Mr
// C0TcS2Ra aaaaaaaa
// C0TcS3Rr
// C0TcS400 VVVVVVVV (VVVVVVVV)
// C0TcS5X0
// T: Width of memory write(1, 2, 4, or 8 bytes).
// c: Condition to use, see below.
// S: Source Register.
// X: Operand Type, see below.
// M: Memory Type(operand types 0 and 1).
// R: Address Register(operand types 2 and 3).
// a: Relative Address(operand types 0 and 2).
// r: Offset Register(operand types 1 and 3).
// X: Other Register(operand type 5).
// V: Value to compare to(operand type 4).
byte operationWidth = instruction[OperationWidthIndex];
Comparison comparison = (Comparison)instruction[ComparisonTypeIndex];
Register sourceRegister = context.GetRegister(instruction[SourceRegisterIndex]);
byte operandType = instruction[OperandTypeIndex];
byte registerOrMemoryRegion = instruction[RegisterOrMemoryRegionIndex];
byte offsetRegisterIndex = instruction[OffsetImmediateIndex];
ulong offsetImmediate;
ulong valueImmediate;
int valueImmediateSize;
Register addressRegister;
Register offsetRegister;
IOperand sourceOperand;
switch (operandType)
{
case MemoryRegionWithOffsetImmediate:
// *(?x + #a)
offsetImmediate = InstructionHelper.GetImmediate(instruction, OffsetImmediateIndex, OffsetImmediateSize);
sourceOperand = MemoryHelper.EmitPointer((MemoryRegion)registerOrMemoryRegion, offsetImmediate, context);
break;
case MemoryRegionWithOffsetRegister:
// *(?x + $r)
offsetRegister = context.GetRegister(offsetRegisterIndex);
sourceOperand = MemoryHelper.EmitPointer((MemoryRegion)registerOrMemoryRegion, offsetRegister, context);
break;
case AddressRegisterWithOffsetImmediate:
// *($R + #a)
addressRegister = context.GetRegister(registerOrMemoryRegion);
offsetImmediate = InstructionHelper.GetImmediate(instruction, OffsetImmediateIndex, OffsetImmediateSize);
sourceOperand = MemoryHelper.EmitPointer(addressRegister, offsetImmediate, context);
break;
case AddressRegisterWithOffsetRegister:
// *($R + $r)
addressRegister = context.GetRegister(registerOrMemoryRegion);
offsetRegister = context.GetRegister(offsetRegisterIndex);
sourceOperand = MemoryHelper.EmitPointer(addressRegister, offsetRegister, context);
break;
case OffsetImmediate:
valueImmediateSize = operationWidth <= 4 ? ValueImmediateSize8 : ValueImmediateSize16;
valueImmediate = InstructionHelper.GetImmediate(instruction, ValueImmediateIndex, valueImmediateSize);
sourceOperand = new Value<ulong>(valueImmediate);
break;
case AddressRegister:
// $V
sourceOperand = context.GetRegister(registerOrMemoryRegion);
break;
default:
throw new TamperCompilationException($"Invalid operand type {operandType} in Atmosphere cheat");
}
return InstructionHelper.CreateCondition(comparison, operationWidth, sourceRegister, sourceOperand);
}
}
}

View File

@ -0,0 +1,17 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 0xFF1 resumes the current process.
/// </summary>
class ResumeProcess
{
// FF1?????
public static void Emit(byte[] instruction, CompilationContext context)
{
context.CurrentOperations.Add(new OpProcCtrl(context.Process, false));
}
}
}

View File

@ -0,0 +1,65 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 0xC1 performs saving or restoring of registers.
/// NOTE: Registers are saved and restored to a different set of registers than the ones used
/// for the other opcodes (Save Registers).
/// </summary>
class SaveOrRestoreRegister
{
private const int DestinationRegisterIndex = 3;
private const int SourceRegisterIndex = 5;
private const int OperationTypeIndex = 6;
private const int RestoreRegister = 0;
private const int SaveRegister = 1;
private const int ClearSavedValue = 2;
private const int ClearRegister = 3;
public static void Emit(byte[] instruction, CompilationContext context)
{
// C10D0Sx0
// D: Destination index.
// S: Source index.
// x: Operand Type, see below.
byte destinationRegIndex = instruction[DestinationRegisterIndex];
byte sourceRegIndex = instruction[SourceRegisterIndex];
byte operationType = instruction[OperationTypeIndex];
Impl(operationType, destinationRegIndex, sourceRegIndex, context);
}
public static void Impl(byte operationType, byte destinationRegIndex, byte sourceRegIndex, CompilationContext context)
{
IOperand destinationOperand;
IOperand sourceOperand;
switch (operationType)
{
case RestoreRegister:
destinationOperand = context.GetRegister(destinationRegIndex);
sourceOperand = context.GetSavedRegister(sourceRegIndex);
break;
case SaveRegister:
destinationOperand = context.GetSavedRegister(destinationRegIndex);
sourceOperand = context.GetRegister(sourceRegIndex);
break;
case ClearSavedValue:
destinationOperand = new Value<ulong>(0);
sourceOperand = context.GetSavedRegister(sourceRegIndex);
break;
case ClearRegister:
destinationOperand = new Value<ulong>(0);
sourceOperand = context.GetRegister(sourceRegIndex);
break;
default:
throw new TamperCompilationException($"Invalid register operation type {operationType} in Atmosphere cheat");
}
context.CurrentOperations.Add(new OpMov<ulong>(destinationOperand, sourceOperand));
}
}
}

View File

@ -0,0 +1,33 @@
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 0xC2 performs saving or restoring of multiple registers using a bitmask.
/// NOTE: Registers are saved and restored to a different set of registers than the ones used
/// for the other opcodes (Save Registers).
/// </summary>
class SaveOrRestoreRegisterWithMask
{
private const int OperationTypeIndex = 2;
private const int RegisterMaskIndex = 4;
private const int RegisterMaskSize = 4;
public static void Emit(byte[] instruction, CompilationContext context)
{
// C2x0XXXX
// x: Operand Type, see below.
// X: 16-bit bitmask, bit i == save or restore register i.
byte operationType = instruction[OperationTypeIndex];
ulong mask = InstructionHelper.GetImmediate(instruction, RegisterMaskIndex, RegisterMaskSize);
for (byte regIndex = 0; mask != 0; mask >>= 1, regIndex++)
{
if ((mask & 0x1) != 0)
{
SaveOrRestoreRegister.Impl(operationType, regIndex, regIndex, context);
}
}
}
}
}

View File

@ -0,0 +1,72 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 3 allows for iterating in a loop a fixed number of times.
/// </summary>
class StartEndLoop
{
private const int StartOrEndIndex = 1;
private const int IterationRegisterIndex = 3;
private const int IterationsImmediateIndex = 8;
private const int IterationsImmediateSize = 8;
private const byte LoopBegin = 0;
private const byte LoopEnd = 1;
public static void Emit(byte[] instruction, CompilationContext context)
{
// 300R0000 VVVVVVVV
// R: Register to use as loop counter.
// V: Number of iterations to loop.
// 310R0000
byte mode = instruction[StartOrEndIndex];
byte iterationRegisterIndex = instruction[IterationRegisterIndex];
switch (mode)
{
case LoopBegin:
// Just start a new compilation block and parse the instruction itself at the end.
context.BlockStack.Push(new OperationBlock(instruction));
return;
case LoopEnd:
break;
default:
throw new TamperCompilationException($"Invalid loop {mode} in Atmosphere cheat");
}
// Use the loop begin instruction stored in the stack.
instruction = context.CurrentBlock.BaseInstruction;
CodeType codeType = InstructionHelper.GetCodeType(instruction);
if (codeType != CodeType.StartEndLoop)
{
throw new TamperCompilationException($"Loop end does not match code type {codeType} in Atmosphere cheat");
}
// Validate if the register in the beginning and end are the same.
byte oldIterationRegisterIndex = instruction[IterationRegisterIndex];
if (iterationRegisterIndex != oldIterationRegisterIndex)
{
throw new TamperCompilationException($"The register used for the loop changed from {oldIterationRegisterIndex} to {iterationRegisterIndex} in Atmosphere cheat");
}
Register iterationRegister = context.GetRegister(iterationRegisterIndex);
ulong immediate = InstructionHelper.GetImmediate(instruction, IterationsImmediateIndex, IterationsImmediateSize);
// Create a loop block with the current operations and nest it in the upper
// block of the stack.
ForBlock block = new ForBlock(immediate, iterationRegister, context.CurrentOperations);
context.BlockStack.Pop();
context.CurrentOperations.Add(block);
}
}
}

View File

@ -0,0 +1,41 @@
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 0 allows writing a static value to a memory address.
/// </summary>
class StoreConstantToAddress
{
private const int OperationWidthIndex = 1;
private const int MemoryRegionIndex = 2;
private const int OffsetRegisterIndex = 3;
private const int OffsetImmediateIndex = 6;
private const int ValueImmediateIndex = 16;
private const int OffsetImmediateSize = 10;
private const int ValueImmediateSize8 = 8;
private const int ValueImmediateSize16 = 16;
public static void Emit(byte[] instruction, CompilationContext context)
{
// 0TMR00AA AAAAAAAA VVVVVVVV (VVVVVVVV)
// T: Width of memory write(1, 2, 4, or 8 bytes).
// M: Memory region to write to(0 = Main NSO, 1 = Heap).
// R: Register to use as an offset from memory region base.
// A: Immediate offset to use from memory region base.
// V: Value to write.
byte operationWidth = instruction[OperationWidthIndex];
MemoryRegion memoryRegion = (MemoryRegion)instruction[MemoryRegionIndex];
Register offsetRegister = context.GetRegister(instruction[OffsetRegisterIndex]);
ulong offsetImmediate = InstructionHelper.GetImmediate(instruction, OffsetImmediateIndex, OffsetImmediateSize);
Pointer dstMem = MemoryHelper.EmitPointer(memoryRegion, offsetRegister, offsetImmediate, context);
int valueImmediateSize = operationWidth <= 4 ? ValueImmediateSize8 : ValueImmediateSize16;
ulong valueImmediate = InstructionHelper.GetImmediate(instruction, ValueImmediateIndex, valueImmediateSize);
Value<ulong> storeValue = new Value<ulong>(valueImmediate);
InstructionHelper.EmitMov(operationWidth, context, dstMem, storeValue);
}
}
}

View File

@ -0,0 +1,71 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 6 allows writing a fixed value to a memory address specified by a register.
/// </summary>
class StoreConstantToMemory
{
private const int OperationWidthIndex = 1;
private const int AddressRegisterIndex = 3;
private const int IncrementAddressRegisterIndex = 4;
private const int UseOffsetRegisterIndex = 5;
private const int OffsetRegisterIndex = 6;
private const int ValueImmediateIndex = 8;
private const int ValueImmediateSize = 16;
public static void Emit(byte[] instruction, CompilationContext context)
{
// 6T0RIor0 VVVVVVVV VVVVVVVV
// T: Width of memory write(1, 2, 4, or 8 bytes).
// R: Register used as base memory address.
// I: Increment register flag(0 = do not increment R, 1 = increment R by T).
// o: Offset register enable flag(0 = do not add r to address, 1 = add r to address).
// r: Register used as offset when o is 1.
// V: Value to write to memory.
byte operationWidth = instruction[OperationWidthIndex];
Register sourceRegister = context.GetRegister(instruction[AddressRegisterIndex]);
byte incrementAddressRegister = instruction[IncrementAddressRegisterIndex];
byte useOffsetRegister = instruction[UseOffsetRegisterIndex];
ulong immediate = InstructionHelper.GetImmediate(instruction, ValueImmediateIndex, ValueImmediateSize);
Value<ulong> storeValue = new Value<ulong>(immediate);
Pointer destinationMemory;
switch (useOffsetRegister)
{
case 0:
// Don't offset the address register by another register.
destinationMemory = MemoryHelper.EmitPointer(sourceRegister, context);
break;
case 1:
// Replace the source address by the sum of the base and offset registers.
Register offsetRegister = context.GetRegister(instruction[OffsetRegisterIndex]);
destinationMemory = MemoryHelper.EmitPointer(sourceRegister, offsetRegister, context);
break;
default:
throw new TamperCompilationException($"Invalid offset mode {useOffsetRegister} in Atmosphere cheat");
}
InstructionHelper.EmitMov(operationWidth, context, destinationMemory, storeValue);
switch (incrementAddressRegister)
{
case 0:
// Don't increment the address register by operationWidth.
break;
case 1:
// Increment the address register by operationWidth.
IOperand increment = new Value<ulong>(operationWidth);
context.CurrentOperations.Add(new OpAdd<ulong>(sourceRegister, sourceRegister, increment));
break;
default:
throw new TamperCompilationException($"Invalid increment mode {incrementAddressRegister} in Atmosphere cheat");
}
}
}
}

View File

@ -0,0 +1,99 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
{
/// <summary>
/// Code type 10 allows writing a register to memory.
/// </summary>
class StoreRegisterToMemory
{
private const int OperationWidthIndex = 1;
private const int SourceRegisterIndex = 2;
private const int AddressRegisterIndex = 3;
private const int IncrementAddressRegisterIndex = 4;
private const int AddressingTypeIndex = 5;
private const int RegisterOrMemoryRegionIndex = 6;
private const int OffsetImmediateIndex = 7;
private const int AddressRegister = 0;
private const int AddressRegisterWithOffsetRegister = 1;
private const int OffsetImmediate = 2;
private const int MemoryRegionWithOffsetRegister = 3;
private const int MemoryRegionWithOffsetImmediate = 4;
private const int MemoryRegionWithOffsetRegisterAndImmediate = 5;
private const int OffsetImmediateSize1 = 1;
private const int OffsetImmediateSize9 = 9;
public static void Emit(byte[] instruction, CompilationContext context)
{
// ATSRIOxa (aaaaaaaa)
// T: Width of memory write (1, 2, 4, or 8 bytes).
// S: Register to write to memory.
// R: Register to use as base address.
// I: Increment register flag (0 = do not increment R, 1 = increment R by T).
// O: Offset type, see below.
// x: Register used as offset when O is 1, Memory type when O is 3, 4 or 5.
// a: Value used as offset when O is 2, 4 or 5.
byte operationWidth = instruction[OperationWidthIndex];
Register sourceRegister = context.GetRegister(instruction[SourceRegisterIndex]);
Register addressRegister = context.GetRegister(instruction[AddressRegisterIndex]);
byte incrementAddressRegister = instruction[IncrementAddressRegisterIndex];
byte offsetType = instruction[AddressingTypeIndex];
byte registerOrMemoryRegion = instruction[RegisterOrMemoryRegionIndex];
int immediateSize = instruction.Length <= 8 ? OffsetImmediateSize1 : OffsetImmediateSize9;
ulong immediate = InstructionHelper.GetImmediate(instruction, OffsetImmediateIndex, immediateSize);
Pointer destinationMemory;
switch (offsetType)
{
case AddressRegister:
// *($R) = $S
destinationMemory = MemoryHelper.EmitPointer(addressRegister, context);
break;
case AddressRegisterWithOffsetRegister:
// *($R + $x) = $S
Register offsetRegister = context.GetRegister(registerOrMemoryRegion);
destinationMemory = MemoryHelper.EmitPointer(addressRegister, offsetRegister, context);
break;
case OffsetImmediate:
// *(#a) = $S
destinationMemory = MemoryHelper.EmitPointer(addressRegister, immediate, context);
break;
case MemoryRegionWithOffsetRegister:
// *(?x + $R) = $S
destinationMemory = MemoryHelper.EmitPointer((MemoryRegion)registerOrMemoryRegion, addressRegister, context);
break;
case MemoryRegionWithOffsetImmediate:
// *(?x + #a) = $S
destinationMemory = MemoryHelper.EmitPointer((MemoryRegion)registerOrMemoryRegion, immediate, context);
break;
case MemoryRegionWithOffsetRegisterAndImmediate:
// *(?x + #a + $R) = $S
destinationMemory = MemoryHelper.EmitPointer((MemoryRegion)registerOrMemoryRegion, addressRegister, immediate, context);
break;
default:
throw new TamperCompilationException($"Invalid offset type {offsetType} in Atmosphere cheat");
}
InstructionHelper.EmitMov(operationWidth, context, destinationMemory, sourceRegister);
switch (incrementAddressRegister)
{
case 0:
// Don't increment the address register by operationWidth.
break;
case 1:
// Increment the address register by operationWidth.
IOperand increment = new Value<ulong>(operationWidth);
context.CurrentOperations.Add(new OpAdd<ulong>(addressRegister, addressRegister, increment));
break;
default:
throw new TamperCompilationException($"Invalid increment mode {incrementAddressRegister} in Atmosphere cheat");
}
}
}
}

View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Tamper
{
/// <summary>
/// The opcodes specified for the Atmosphere Cheat VM.
/// </summary>
enum CodeType
{
/// <summary>
/// Code type 0 allows writing a static value to a memory address.
/// </summary>
StoreConstantToAddress = 0x0,
/// <summary>
/// Code type 1 performs a comparison of the contents of memory to a static value.
/// If the condition is not met, all instructions until the appropriate conditional block terminator
/// are skipped.
/// </summary>
BeginMemoryConditionalBlock = 0x1,
/// <summary>
/// Code type 2 marks the end of a conditional block (started by Code Type 1 or Code Type 8).
/// </summary>
EndConditionalBlock = 0x2,
/// <summary>
/// Code type 3 allows for iterating in a loop a fixed number of times.
/// </summary>
StartEndLoop = 0x3,
/// <summary>
/// Code type 4 allows setting a register to a constant value.
/// </summary>
LoadRegisterWithContant = 0x4,
/// <summary>
/// Code type 5 allows loading a value from memory into a register, either using a fixed address or by
/// dereferencing the destination register.
/// </summary>
LoadRegisterWithMemory = 0x5,
/// <summary>
/// Code type 6 allows writing a fixed value to a memory address specified by a register.
/// </summary>
StoreConstantToMemory = 0x6,
/// <summary>
/// Code type 7 allows performing arithmetic on registers. However, it has been deprecated by Code
/// type 9, and is only kept for backwards compatibility.
/// </summary>
LegacyArithmetic = 0x7,
/// <summary>
/// Code type 8 enters or skips a conditional block based on whether a key combination is pressed.
/// </summary>
BeginKeypressConditionalBlock = 0x8,
/// <summary>
/// Code type 9 allows performing arithmetic on registers.
/// </summary>
Arithmetic = 0x9,
/// <summary>
/// Code type 10 allows writing a register to memory.
/// </summary>
StoreRegisterToMemory = 0xA,
/// <summary>
/// Code type 0xC0 performs a comparison of the contents of a register and another value.
/// This code support multiple operand types, see below. If the condition is not met,
/// all instructions until the appropriate conditional block terminator are skipped.
/// </summary>
BeginRegisterConditionalBlock = 0xC0,
/// <summary>
/// Code type 0xC1 performs saving or restoring of registers.
/// NOTE: Registers are saved and restored to a different set of registers than the ones used
/// for the other opcodes (Save Registers).
/// </summary>
SaveOrRestoreRegister = 0xC1,
/// <summary>
/// Code type 0xC2 performs saving or restoring of multiple registers using a bitmask.
/// NOTE: Registers are saved and restored to a different set of registers than the ones used
/// for the other opcodes (Save Registers).
/// </summary>
SaveOrRestoreRegisterWithMask = 0xC2,
/// <summary>
/// Code type 0xC3 reads or writes a static register with a given register.
/// NOTE: Registers are saved and restored to a different set of registers than the ones used
/// for the other opcodes (Static Registers).
/// </summary>
ReadOrWriteStaticRegister = 0xC3,
/// <summary>
/// Code type 0xFF0 pauses the current process.
/// </summary>
PauseProcess = 0xFF0,
/// <summary>
/// Code type 0xFF1 resumes the current process.
/// </summary>
ResumeProcess = 0xFF1,
/// <summary>
/// Code type 0xFFF writes a debug log.
/// </summary>
DebugLog = 0xFFF
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Tamper
{
/// <summary>
/// The comparisons used by conditional operations.
/// </summary>
enum Comparison
{
Greater = 1,
GreaterOrEqual = 2,
Less = 3,
LessOrEqual = 4,
Equal = 5,
NotEqual = 6
}
}

View File

@ -0,0 +1,75 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Tamper
{
class CompilationContext
{
public OperationBlock CurrentBlock => BlockStack.Peek();
public List<IOperation> CurrentOperations => CurrentBlock.Operations;
public ITamperedProcess Process { get; }
public Parameter<long> PressedKeys { get; }
public Stack<OperationBlock> BlockStack { get; }
public Dictionary<byte, Register> Registers { get; }
public Dictionary<byte, Register> SavedRegisters { get; }
public Dictionary<byte, Register> StaticRegisters { get; }
public ulong ExeAddress { get; }
public ulong HeapAddress { get; }
public CompilationContext(ulong exeAddress, ulong heapAddress, ITamperedProcess process)
{
Process = process;
PressedKeys = new Parameter<long>(0);
BlockStack = new Stack<OperationBlock>();
Registers = new Dictionary<byte, Register>();
SavedRegisters = new Dictionary<byte, Register>();
StaticRegisters = new Dictionary<byte, Register>();
ExeAddress = exeAddress;
HeapAddress = heapAddress;
}
public Register GetRegister(byte index)
{
if (Registers.TryGetValue(index, out Register register))
{
return register;
}
register = new Register($"R_{index:X2}");
Registers.Add(index, register);
return register;
}
public Register GetSavedRegister(byte index)
{
if (SavedRegisters.TryGetValue(index, out Register register))
{
return register;
}
register = new Register($"S_{index:X2}");
SavedRegisters.Add(index, register);
return register;
}
public Register GetStaticRegister(byte index)
{
if (SavedRegisters.TryGetValue(index, out Register register))
{
return register;
}
register = new Register($"T_{index:X2}");
SavedRegisters.Add(index, register);
return register;
}
}
}

View File

@ -0,0 +1,21 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.Conditions
{
class CondEQ<T> : ICondition where T : unmanaged
{
private IOperand _lhs;
private IOperand _rhs;
public CondEQ(IOperand lhs, IOperand rhs)
{
_lhs = lhs;
_rhs = rhs;
}
public bool Evaluate()
{
return (dynamic)_lhs.Get<T>() == (dynamic)_rhs.Get<T>();
}
}
}

View File

@ -0,0 +1,21 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.Conditions
{
class CondGE<T> : ICondition where T : unmanaged
{
private IOperand _lhs;
private IOperand _rhs;
public CondGE(IOperand lhs, IOperand rhs)
{
_lhs = lhs;
_rhs = rhs;
}
public bool Evaluate()
{
return (dynamic)_lhs.Get<T>() >= (dynamic)_rhs.Get<T>();
}
}
}

View File

@ -0,0 +1,21 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.Conditions
{
class CondGT<T> : ICondition where T : unmanaged
{
private IOperand _lhs;
private IOperand _rhs;
public CondGT(IOperand lhs, IOperand rhs)
{
_lhs = lhs;
_rhs = rhs;
}
public bool Evaluate()
{
return (dynamic)_lhs.Get<T>() > (dynamic)_rhs.Get<T>();
}
}
}

View File

@ -0,0 +1,21 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.Conditions
{
class CondLE<T> : ICondition where T : unmanaged
{
private IOperand _lhs;
private IOperand _rhs;
public CondLE(IOperand lhs, IOperand rhs)
{
_lhs = lhs;
_rhs = rhs;
}
public bool Evaluate()
{
return (dynamic)_lhs.Get<T>() <= (dynamic)_rhs.Get<T>();
}
}
}

View File

@ -0,0 +1,21 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.Conditions
{
class CondLT<T> : ICondition where T : unmanaged
{
private IOperand _lhs;
private IOperand _rhs;
public CondLT(IOperand lhs, IOperand rhs)
{
_lhs = lhs;
_rhs = rhs;
}
public bool Evaluate()
{
return (dynamic)_lhs.Get<T>() < (dynamic)_rhs.Get<T>();
}
}
}

View File

@ -0,0 +1,21 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper.Conditions
{
class CondNE<T> : ICondition where T : unmanaged
{
private IOperand _lhs;
private IOperand _rhs;
public CondNE(IOperand lhs, IOperand rhs)
{
_lhs = lhs;
_rhs = rhs;
}
public bool Evaluate()
{
return (dynamic)_lhs.Get<T>() != (dynamic)_rhs.Get<T>();
}
}
}

View File

@ -0,0 +1,7 @@
namespace Ryujinx.HLE.HOS.Tamper.Conditions
{
interface ICondition
{
bool Evaluate();
}
}

View File

@ -0,0 +1,19 @@
namespace Ryujinx.HLE.HOS.Tamper.Conditions
{
class InputMask : ICondition
{
private long _mask;
private Parameter<long> _input;
public InputMask(long mask, Parameter<long> input)
{
_mask = mask;
_input = input;
}
public bool Evaluate()
{
return (_input.Value & _mask) != 0;
}
}
}

View File

@ -0,0 +1,10 @@
using Ryujinx.HLE.HOS.Services.Hid;
namespace Ryujinx.HLE.HOS.Tamper
{
interface ITamperProgram
{
ITamperedProcess Process { get; }
void Execute(ControllerKeys pressedKeys);
}
}

View File

@ -0,0 +1,13 @@
using Ryujinx.HLE.HOS.Kernel.Process;
namespace Ryujinx.HLE.HOS.Tamper
{
interface ITamperedProcess
{
ProcessState State { get; }
T ReadMemory<T>(ulong va) where T : unmanaged;
void WriteMemory<T>(ulong va, T value) where T : unmanaged;
void PauseProcess();
void ResumeProcess();
}
}

View File

@ -0,0 +1,134 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Conditions;
using Ryujinx.HLE.HOS.Tamper.Operations;
using System;
using System.Globalization;
namespace Ryujinx.HLE.HOS.Tamper
{
class InstructionHelper
{
private const int CodeTypeIndex = 0;
public static void Emit(IOperation operation, CompilationContext context)
{
context.CurrentOperations.Add(operation);
}
public static void Emit(Type instruction, byte width, CompilationContext context, params Object[] operands)
{
Emit((IOperation)Create(instruction, width, operands), context);
}
public static void EmitMov(byte width, CompilationContext context, IOperand destination, IOperand source)
{
Emit(typeof(OpMov<>), width, context, destination, source);
}
public static ICondition CreateCondition(Comparison comparison, byte width, IOperand lhs, IOperand rhs)
{
ICondition Create(Type conditionType)
{
return (ICondition)InstructionHelper.Create(conditionType, width, lhs, rhs);
}
switch (comparison)
{
case Comparison.Greater : return Create(typeof(CondGT<>));
case Comparison.GreaterOrEqual: return Create(typeof(CondGE<>));
case Comparison.Less : return Create(typeof(CondLT<>));
case Comparison.LessOrEqual : return Create(typeof(CondLE<>));
case Comparison.Equal : return Create(typeof(CondEQ<>));
case Comparison.NotEqual : return Create(typeof(CondNE<>));
default:
throw new TamperCompilationException($"Invalid comparison {comparison} in Atmosphere cheat");
}
}
public static Object Create(Type instruction, byte width, params Object[] operands)
{
Type realType;
switch (width)
{
case 1: realType = instruction.MakeGenericType(typeof(byte)); break;
case 2: realType = instruction.MakeGenericType(typeof(ushort)); break;
case 4: realType = instruction.MakeGenericType(typeof(uint)); break;
case 8: realType = instruction.MakeGenericType(typeof(ulong)); break;
default:
throw new TamperCompilationException($"Invalid instruction width {width} in Atmosphere cheat");
}
return Activator.CreateInstance(realType, operands);
}
public static ulong GetImmediate(byte[] instruction, int index, int nybbleCount)
{
ulong value = 0;
for (int i = 0; i < nybbleCount; i++)
{
value <<= 4;
value |= instruction[index + i];
}
return value;
}
public static CodeType GetCodeType(byte[] instruction)
{
int codeType = instruction[CodeTypeIndex];
if (codeType >= 0xC)
{
byte extension = instruction[CodeTypeIndex + 1];
codeType = (codeType << 4) | extension;
if (extension == 0xF)
{
extension = instruction[CodeTypeIndex + 2];
codeType = (codeType << 4) | extension;
}
}
return (CodeType)codeType;
}
public static byte[] ParseRawInstruction(string rawInstruction)
{
const int wordSize = 2 * sizeof(uint);
// Instructions are multi-word, with 32bit words. Split the raw instruction
// and parse each word into individual nybbles of bits.
var words = rawInstruction.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
byte[] instruction = new byte[wordSize * words.Length];
if (words.Length == 0)
{
throw new TamperCompilationException("Empty instruction in Atmosphere cheat");
}
for (int wordIndex = 0; wordIndex < words.Length; wordIndex++)
{
string word = words[wordIndex];
if (word.Length != wordSize)
{
throw new TamperCompilationException($"Invalid word length for {word} in Atmosphere cheat");
}
for (int nybbleIndex = 0; nybbleIndex < wordSize; nybbleIndex++)
{
int index = wordIndex * wordSize + nybbleIndex;
string byteData = word.Substring(nybbleIndex, 1);
instruction[index] = byte.Parse(byteData, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
}
return instruction;
}
}
}

View File

@ -0,0 +1,89 @@
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper
{
class MemoryHelper
{
public static ulong GetAddressShift(MemoryRegion source, CompilationContext context)
{
switch (source)
{
case MemoryRegion.NSO:
// Memory address is relative to the code start.
return context.ExeAddress;
case MemoryRegion.Heap:
// Memory address is relative to the heap.
return context.HeapAddress;
default:
throw new TamperCompilationException($"Invalid memory source {source} in Atmosphere cheat");
}
}
private static void EmitAdd(Value<ulong> finalValue, IOperand firstOperand, IOperand secondOperand, CompilationContext context)
{
context.CurrentOperations.Add(new OpAdd<ulong>(finalValue, firstOperand, secondOperand));
}
public static Pointer EmitPointer(ulong addressImmediate, CompilationContext context)
{
Value<ulong> addressImmediateValue = new Value<ulong>(addressImmediate);
return new Pointer(addressImmediateValue, context.Process);
}
public static Pointer EmitPointer(Register addressRegister, CompilationContext context)
{
return new Pointer(addressRegister, context.Process);
}
public static Pointer EmitPointer(Register addressRegister, ulong offsetImmediate, CompilationContext context)
{
Value<ulong> offsetImmediateValue = new Value<ulong>(offsetImmediate);
Value<ulong> finalAddressValue = new Value<ulong>(0);
EmitAdd(finalAddressValue, addressRegister, offsetImmediateValue, context);
return new Pointer(finalAddressValue, context.Process);
}
public static Pointer EmitPointer(Register addressRegister, Register offsetRegister, CompilationContext context)
{
Value<ulong> finalAddressValue = new Value<ulong>(0);
EmitAdd(finalAddressValue, addressRegister, offsetRegister, context);
return new Pointer(finalAddressValue, context.Process);
}
public static Pointer EmitPointer(Register addressRegister, Register offsetRegister, ulong offsetImmediate, CompilationContext context)
{
Value<ulong> offsetImmediateValue = new Value<ulong>(offsetImmediate);
Value<ulong> finalOffsetValue = new Value<ulong>(0);
EmitAdd(finalOffsetValue, offsetRegister, offsetImmediateValue, context);
Value<ulong> finalAddressValue = new Value<ulong>(0);
EmitAdd(finalAddressValue, addressRegister, finalOffsetValue, context);
return new Pointer(finalAddressValue, context.Process);
}
public static Pointer EmitPointer(MemoryRegion memoryRegion, ulong offsetImmediate, CompilationContext context)
{
offsetImmediate += GetAddressShift(memoryRegion, context);
return EmitPointer(offsetImmediate, context);
}
public static Pointer EmitPointer(MemoryRegion memoryRegion, Register offsetRegister, CompilationContext context)
{
ulong offsetImmediate = GetAddressShift(memoryRegion, context);
return EmitPointer(offsetRegister, offsetImmediate, context);
}
public static Pointer EmitPointer(MemoryRegion memoryRegion, Register offsetRegister, ulong offsetImmediate, CompilationContext context)
{
offsetImmediate += GetAddressShift(memoryRegion, context);
return EmitPointer(offsetRegister, offsetImmediate, context);
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Tamper
{
/// <summary>
/// The regions in the virtual address space of the process that are used as base address of memory operations.
/// </summary>
enum MemoryRegion
{
/// <summary>
/// The position of the NSO associated with the cheat in the virtual address space.
/// NOTE: A game can have several NSOs, but the cheat only associates itself with one.
/// </summary>
NSO = 0x0,
/// <summary>
/// The address of the heap, as determined by the kernel.
/// </summary>
Heap = 0x1
}
}

View File

@ -0,0 +1,17 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Tamper
{
struct OperationBlock
{
public byte[] BaseInstruction { get; }
public List<IOperation> Operations { get; }
public OperationBlock(byte[] baseInstruction)
{
BaseInstruction = baseInstruction;
Operations = new List<IOperation>();
}
}
}

View File

@ -0,0 +1,27 @@
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class Block : IOperation
{
private IEnumerable<IOperation> _operations;
public Block(IEnumerable<IOperation> operations)
{
_operations = operations;
}
public Block(params IOperation[] operations)
{
_operations = operations;
}
public void Execute()
{
foreach (IOperation op in _operations)
{
op.Execute();
}
}
}
}

View File

@ -0,0 +1,42 @@
using Ryujinx.HLE.HOS.Tamper.Conditions;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class ForBlock : IOperation
{
private ulong _count;
private Register _register;
private IEnumerable<IOperation> _operations;
public ForBlock(ulong count, Register register, IEnumerable<IOperation> operations)
{
_count = count;
_register = register;
_operations = operations;
}
public ForBlock(ulong count, Register register, params IOperation[] operations)
{
_count = count;
_register = register;
_operations = operations;
}
public void Execute()
{
for (ulong i = 0; i < _count; i++)
{
// Set the register and execute the operations so that changing the
// register during runtime does not break iteration.
_register.Set<ulong>(i);
foreach (IOperation op in _operations)
{
op.Execute();
}
}
}
}
}

View File

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
interface IOperand
{
public T Get<T>() where T : unmanaged;
public void Set<T>(T value) where T : unmanaged;
}
}

View File

@ -0,0 +1,7 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
interface IOperation
{
void Execute();
}
}

View File

@ -0,0 +1,35 @@
using Ryujinx.HLE.HOS.Tamper.Conditions;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class IfBlock : IOperation
{
private ICondition _condition;
private IEnumerable<IOperation> _operations;
public IfBlock(ICondition condition, IEnumerable<IOperation> operations)
{
_condition = condition;
_operations = operations;
}
public IfBlock(ICondition condition, params IOperation[] operations)
{
_operations = operations;
}
public void Execute()
{
if (!_condition.Evaluate())
{
return;
}
foreach (IOperation op in _operations)
{
op.Execute();
}
}
}
}

View File

@ -0,0 +1,21 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpAdd<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _lhs;
IOperand _rhs;
public OpAdd(IOperand destination, IOperand lhs, IOperand rhs)
{
_destination = destination;
_lhs = lhs;
_rhs = rhs;
}
public void Execute()
{
_destination.Set((T)((dynamic)_lhs.Get<T>() + (dynamic)_rhs.Get<T>()));
}
}
}

View File

@ -0,0 +1,21 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpAnd<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _lhs;
IOperand _rhs;
public OpAnd(IOperand destination, IOperand lhs, IOperand rhs)
{
_destination = destination;
_lhs = lhs;
_rhs = rhs;
}
public void Execute()
{
_destination.Set((T)((dynamic)_lhs.Get<T>() & (dynamic)_rhs.Get<T>()));
}
}
}

View File

@ -0,0 +1,21 @@
using Ryujinx.Common.Logging;
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpLog<T> : IOperation where T : unmanaged
{
int _logId;
IOperand _source;
public OpLog(int logId, IOperand source)
{
_logId = logId;
_source = source;
}
public void Execute()
{
Logger.Debug?.Print(LogClass.TamperMachine, $"Tamper debug log id={_logId} value={(dynamic)_source.Get<T>():X}");
}
}
}

View File

@ -0,0 +1,21 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpLsh<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _lhs;
IOperand _rhs;
public OpLsh(IOperand destination, IOperand lhs, IOperand rhs)
{
_destination = destination;
_lhs = lhs;
_rhs = rhs;
}
public void Execute()
{
_destination.Set((T)((dynamic)_lhs.Get<T>() << (dynamic)_rhs.Get<T>()));
}
}
}

View File

@ -0,0 +1,19 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpMov<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _source;
public OpMov(IOperand destination, IOperand source)
{
_destination = destination;
_source = source;
}
public void Execute()
{
_destination.Set(_source.Get<T>());
}
}
}

View File

@ -0,0 +1,21 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpMul<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _lhs;
IOperand _rhs;
public OpMul(IOperand destination, IOperand lhs, IOperand rhs)
{
_destination = destination;
_lhs = lhs;
_rhs = rhs;
}
public void Execute()
{
_destination.Set((T)((dynamic)_lhs.Get<T>() * (dynamic)_rhs.Get<T>()));
}
}
}

View File

@ -0,0 +1,19 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpNot<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _source;
public OpNot(IOperand destination, IOperand source)
{
_destination = destination;
_source = source;
}
public void Execute()
{
_destination.Set((T)(~(dynamic)_source.Get<T>()));
}
}
}

View File

@ -0,0 +1,21 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpOr<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _lhs;
IOperand _rhs;
public OpOr(IOperand destination, IOperand lhs, IOperand rhs)
{
_destination = destination;
_lhs = lhs;
_rhs = rhs;
}
public void Execute()
{
_destination.Set((T)((dynamic)_lhs.Get<T>() | (dynamic)_rhs.Get<T>()));
}
}
}

View File

@ -0,0 +1,26 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpProcCtrl : IOperation
{
private ITamperedProcess _process;
private bool _pause;
public OpProcCtrl(ITamperedProcess process, bool pause)
{
_process = process;
_pause = pause;
}
public void Execute()
{
if (_pause)
{
_process.PauseProcess();
}
else
{
_process.ResumeProcess();
}
}
}
}

View File

@ -0,0 +1,21 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpRsh<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _lhs;
IOperand _rhs;
public OpRsh(IOperand destination, IOperand lhs, IOperand rhs)
{
_destination = destination;
_lhs = lhs;
_rhs = rhs;
}
public void Execute()
{
_destination.Set((T)((dynamic)_lhs.Get<T>() >> (dynamic)_rhs.Get<T>()));
}
}
}

View File

@ -0,0 +1,21 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpSub<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _lhs;
IOperand _rhs;
public OpSub(IOperand destination, IOperand lhs, IOperand rhs)
{
_destination = destination;
_lhs = lhs;
_rhs = rhs;
}
public void Execute()
{
_destination.Set((T)((dynamic)_lhs.Get<T>() - (dynamic)_rhs.Get<T>()));
}
}
}

View File

@ -0,0 +1,21 @@
namespace Ryujinx.HLE.HOS.Tamper.Operations
{
class OpXor<T> : IOperation where T : unmanaged
{
IOperand _destination;
IOperand _lhs;
IOperand _rhs;
public OpXor(IOperand destination, IOperand lhs, IOperand rhs)
{
_destination = destination;
_lhs = lhs;
_rhs = rhs;
}
public void Execute()
{
_destination.Set((T)((dynamic)_lhs.Get<T>() ^ (dynamic)_rhs.Get<T>()));
}
}
}

View File

@ -0,0 +1,12 @@
namespace Ryujinx.HLE.HOS.Tamper
{
class Parameter<T>
{
public T Value { get; set; }
public Parameter(T value)
{
Value = value;
}
}
}

View File

@ -0,0 +1,32 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Tamper.Operations;
using System.Runtime.CompilerServices;
namespace Ryujinx.HLE.HOS.Tamper
{
class Pointer : IOperand
{
private IOperand _position;
private ITamperedProcess _process;
public Pointer(IOperand position, ITamperedProcess process)
{
_position = position;
_process = process;
}
public T Get<T>() where T : unmanaged
{
return _process.ReadMemory<T>(_position.Get<ulong>());
}
public void Set<T>(T value) where T : unmanaged
{
ulong position = _position.Get<ulong>();
Logger.Debug?.Print(LogClass.TamperMachine, $"0x{position:X16}@{Unsafe.SizeOf<T>()}: {value:X}");
_process.WriteMemory(position, value);
}
}
}

View File

@ -0,0 +1,28 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper
{
class Register : IOperand
{
private ulong _register = 0;
private string _alias;
public Register(string alias)
{
_alias = alias;
}
public T Get<T>() where T : unmanaged
{
return (T)(dynamic)_register;
}
public void Set<T>(T value) where T : unmanaged
{
Logger.Debug?.Print(LogClass.TamperMachine, $"{_alias}: {value}");
_register = (ulong)(dynamic)value;
}
}
}

View File

@ -0,0 +1,66 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Kernel.Process;
using System.Runtime.CompilerServices;
namespace Ryujinx.HLE.HOS.Tamper
{
class TamperedKProcess : ITamperedProcess
{
private KProcess _process;
public ProcessState State => _process.State;
public TamperedKProcess(KProcess process)
{
this._process = process;
}
private void AssertMemoryRegion<T>(ulong va, bool isWrite) where T : unmanaged
{
ulong size = (ulong)Unsafe.SizeOf<T>();
// TODO (Caian): This double check is workaround because CpuMemory.IsRangeMapped reports
// some addresses as mapped even though they are not, i. e. 4 bytes from 0xffffffffffffff70.
if (!_process.CpuMemory.IsMapped(va) || !_process.CpuMemory.IsRangeMapped(va, size))
{
throw new TamperExecutionException($"Unmapped memory access of {size} bytes at 0x{va:X16}");
}
if (!isWrite)
{
return;
}
// TODO (Caian): It is unknown how PPTC behaves if the tamper modifies memory regions
// belonging to code. So for now just prevent code tampering.
if ((va >= _process.MemoryManager.CodeRegionStart) && (va + size <= _process.MemoryManager.CodeRegionEnd))
{
throw new CodeRegionTamperedException($"Writing {size} bytes to address 0x{va:X16} alters code");
}
}
public T ReadMemory<T>(ulong va) where T : unmanaged
{
AssertMemoryRegion<T>(va, false);
return _process.CpuMemory.Read<T>(va);
}
public void WriteMemory<T>(ulong va, T value) where T : unmanaged
{
AssertMemoryRegion<T>(va, true);
_process.CpuMemory.Write(va, value);
}
public void PauseProcess()
{
Logger.Warning?.Print(LogClass.TamperMachine, "Process pausing is not supported!");
}
public void ResumeProcess()
{
Logger.Warning?.Print(LogClass.TamperMachine, "Process resuming is not supported!");
}
}
}

View File

@ -0,0 +1,24 @@
using Ryujinx.HLE.HOS.Tamper.Operations;
namespace Ryujinx.HLE.HOS.Tamper
{
class Value<P> : IOperand where P : unmanaged
{
private P _value;
public Value(P value)
{
_value = value;
}
public T Get<T>() where T : unmanaged
{
return (T)(dynamic)_value;
}
public void Set<T>(T value) where T : unmanaged
{
_value = (P)(dynamic)value;
}
}
}

View File

@ -0,0 +1,161 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.Tamper;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Ryujinx.HLE.HOS
{
public class TamperMachine
{
private Thread _tamperThread = null;
private ConcurrentQueue<ITamperProgram> _programs = new ConcurrentQueue<ITamperProgram>();
private long _pressedKeys = 0;
private void Activate()
{
if (_tamperThread == null || !_tamperThread.IsAlive)
{
_tamperThread = new Thread(this.TamperRunner);
_tamperThread.Name = "HLE.TamperMachine";
_tamperThread.Start();
}
}
internal void InstallAtmosphereCheat(IEnumerable<string> rawInstructions, ProcessTamperInfo info, ulong exeAddress)
{
if (!CanInstallOnPid(info.Process.Pid))
{
return;
}
ITamperedProcess tamperedProcess = new TamperedKProcess(info.Process);
AtmosphereCompiler compiler = new AtmosphereCompiler();
ITamperProgram program = compiler.Compile(rawInstructions, exeAddress, info.HeapAddress, tamperedProcess);
if (program != null)
{
_programs.Enqueue(program);
}
Activate();
}
private bool CanInstallOnPid(long pid)
{
// Do not allow tampering of kernel processes.
if (pid < KernelConstants.InitialProcessId)
{
Logger.Warning?.Print(LogClass.TamperMachine, $"Refusing to tamper kernel process {pid}");
return false;
}
return true;
}
private bool IsProcessValid(ITamperedProcess process)
{
return process.State != ProcessState.Crashed && process.State != ProcessState.Exiting && process.State != ProcessState.Exited;
}
private void TamperRunner()
{
Logger.Info?.Print(LogClass.TamperMachine, "TamperMachine thread running");
int sleepCounter = 0;
while (true)
{
// Sleep to not consume too much CPU.
if (sleepCounter == 0)
{
sleepCounter = _programs.Count;
Thread.Sleep(1);
}
else
{
sleepCounter--;
}
if (!AdvanceTamperingsQueue())
{
// No more work to be done.
Logger.Info?.Print(LogClass.TamperMachine, "TamperMachine thread exiting");
return;
}
}
}
private bool AdvanceTamperingsQueue()
{
if (!_programs.TryDequeue(out ITamperProgram program))
{
// No more programs in the queue.
return false;
}
// Check if the process is still suitable for running the tamper program.
if (!IsProcessValid(program.Process))
{
// Exit without re-enqueuing the program because the process is no longer valid.
return true;
}
// Re-enqueue the tampering program because the process is still valid.
_programs.Enqueue(program);
Logger.Debug?.Print(LogClass.TamperMachine, "Running tampering program");
try
{
ControllerKeys pressedKeys = (ControllerKeys)Thread.VolatileRead(ref _pressedKeys);
program.Execute(pressedKeys);
}
catch (CodeRegionTamperedException ex)
{
Logger.Debug?.Print(LogClass.TamperMachine, $"Prevented tampering program from modifing code memory");
if (!String.IsNullOrEmpty(ex.Message))
{
Logger.Debug?.Print(LogClass.TamperMachine, ex.Message);
}
}
catch (Exception ex)
{
Logger.Debug?.Print(LogClass.TamperMachine, $"The tampering program crashed, this can happen while the game is starting");
if (!String.IsNullOrEmpty(ex.Message))
{
Logger.Debug?.Print(LogClass.TamperMachine, ex.Message);
}
}
return true;
}
public void UpdateInput(List<GamepadInput> gamepadInputs)
{
// Look for the input of the player one or the handheld.
foreach (GamepadInput input in gamepadInputs)
{
if (input.PlayerId == PlayerIndex.Player1 || input.PlayerId == PlayerIndex.Handheld)
{
Thread.VolatileWrite(ref _pressedKeys, (long)input.Buttons);
return;
}
}
// Clear the input because player one is not conected.
Thread.VolatileWrite(ref _pressedKeys, 0);
}
}
}

View File

@ -46,6 +46,8 @@ namespace Ryujinx.HLE
public Hid Hid { get; private set; } public Hid Hid { get; private set; }
public TamperMachine TamperMachine { get; private set; }
public IHostUiHandler UiHandler { get; set; } public IHostUiHandler UiHandler { get; set; }
public bool EnableDeviceVsync { get; set; } = true; public bool EnableDeviceVsync { get; set; } = true;
@ -109,6 +111,8 @@ namespace Ryujinx.HLE
Hid.InitDevices(); Hid.InitDevices();
Application = new ApplicationLoader(this, fileSystem, contentManager); Application = new ApplicationLoader(this, fileSystem, contentManager);
TamperMachine = new TamperMachine();
} }
public void Initialize() public void Initialize()

View File

@ -659,6 +659,7 @@ namespace Ryujinx.Ui
_device.Hid.Npads.Update(gamepadInputs); _device.Hid.Npads.Update(gamepadInputs);
_device.Hid.Npads.UpdateSixAxis(motionInputs); _device.Hid.Npads.UpdateSixAxis(motionInputs);
_device.TamperMachine.UpdateInput(gamepadInputs);
if(_isFocused) if(_isFocused)
{ {