This repo contains the code to build and test the dotnet-patcher
tool.
I like modding games, and many games are now using .Net as a scripting language. It can a home-made solution or just because the game is developped using the UnityEngine. I'm also a Linux user. I don't have any Windows machine. So it means that I play on Linux, using Steam's Proton, or Wine when the game isn't natively supported on Linux.
Some games have official mod support like Cities: Skylines. Unfortunately you may be quite limited to what you can mod because no extension points are provided for everything.
Some games have unofficial, closed-source, mod loaders like, for example Raft, which is using RaftModLoader. Unfortunately, it doesn't play well with Linux/Proton. I failed to make it work. Some documentation do mentions some step to make it work involving installing it on a Windows, to then copy on the Linux. I don't find this an acceptable workaround. Documentation also mentions that it may work better if using the compiler from a Mono installation but that would require too much work then it won't be done. As it's closed-source I can't contribute to solve the situation.
Some other games rely on Harmony to provide mod support. This is by far the least problematic as it's a framework to patch assemblies. The only issues I have with this it that its a bit complex to use. Sometimes I juste want to make a minor change so I don't want to create a full new project, dealing with dependencies, installing Harmony in the game and all that stuff just to change two or three opcodes.
And finally, some other games or applications just don't have any mod support. Technically, I could make it work using Harmony, as it's supposed to be working on any .Net or Mono application. But I still finds it overkill for small patching.
Also, I am disappointed by the Reflexil's UI/UX on Windows and even more disappointed that there is no Linux's version of it. The AvaloniaILSpy fork is crashing very often, it's UI is full of bugs. It's a huge beast, relying on technologies I dislike (WPF for example), so that I don't want to get involved into it to solve my issues.
For all these reasons, I created this tool. I aim at making it as lightweight, easy to use, portable as possible.
This project depends on the following:
As this is written using .Net Core, it should run everywhere this framework is available. Including Windows, Linux and Mac OS. Thus being said, I develop and test only on Linux. I don't have any other OS to test, so I may not be able to reproduce your issue.
git clone [email protected]:ctxnop/dotnet-patcher.git
cd dotnet-patcher
dotnet build
Create a class that implement the IPatch
interface. It may be located anywhere
in the project's folder, but you may want to keep things tidy so put it in the
Patches
folder.
Rebuild the tool using dotnet build
, then you can patch an assembly using the
following command:
./dotnet-patcher/out/net6.0/dp patch <patch-id> <assembly-to-patch>.dll
Currently, patching an assembly means using Mono.Cecil and requires understanding the IL code which is a kind-of assembler. This can be tricky, so you probably want to get some help from tools like ILSpy or Reflexil, to check what was the original code, what is the patched code, how does it translate from C# to IL and vice-versa...
public bool Apply(AssemblyDefinition asm)
{
asm.Patch(
(td) => {
// This is a selection predicate: return 'true' to select this TypeDefinition
// Called for all TypeDefinition found in the AssemblyDefinition
return string.CompareOrdinal(td.FullName, "TypeToPatch") == 0;
},
(md) => {
// This is a selection predicate: return 'true' to select this MethodDefinition
// Called for all TypeDefinition selected by the above predicate
return string.CompareOrdinal(md.Name, "MethodToPatch") == 0; },
(ilp) => {
ilp.Clear(); // Remove all previous instructions
ilp.Emit(OpCodes.Ret); // This method will now just return and do nothing.
}
);
}
A very early and incomplete support for C# compilation is added, so that you can provide a method's body code. It will generate a source code corresponding to the original but with the code you provided as the method's body. It will then compiles everything into an in-memory assembly using Roslyn. And finally it use Mono.Cecil back to read the resulting assembly and replace the original intructions with the one founds in the generated assembly.
When invoking the patch
command, a backup of the original assembly is created
in the same directory, with a .dporg
extension. The backup is made only if not
already existed. Patches are applies on the backup file, so that calling patch
a second time on the same assembly won't result in a different patched assembly.
This means that you can revert any change by restoring the backup.