Custom Maid 3D 2: Plugins

From Hgames Wiki
Jump to: navigation, search
Icon msgbox notice.png This article is out of date and contains information that might be obsolete. You can find a newer guide for ReiPatcher here and for UnityInjector here..


KISS

all characters are at least 18


Custom Maid 3D 2 [edit]

Since Custom Maid 3D 2 was made using Unity Engine and scripted with .NET, it is possible to manipulate game's behaviour using .NET and some tools to inject code into game's own DLLs.

In this tutorial we will be using ReiPatcher, a general-purpose .NET assembly patcher by HongFire user usagirei, and UnityInjector, a simple plug-in injector, made by the same user, to create simple Unity plug-ins.

To simplify code, this tutorial will also use ReiPatcherPlus, an extension library for ReiPatcher to provide a handful of convenient methods. If one prefers to manipulate assemblies oneself, the same results may be achieved with Mono.Cecil, which ReiPathcer and ReiPatcherPlus use as well.

Note: Please note that owing to ever-so-evolving patching tools some parts of the tutorial may be outdated. The developers of said tools try their best to bring more features while trying to preserve the original programming routines. However, changes happen and the editors of this wiki attempt to update the documentation as fast as possible.

Overview

About this tutorial

In this tutorial we will use ReiPatcher and UnityInjector to create a simple plug-in called HelloWorld which will print "Hello, world!" on the screen every time a button is pressed in the game. We will also implement the ability to stop printing the message with a press of a key.

How ReiPatcher and UnityInjector work

While having some minor differences in purpose, both function as important tools to patch and manipulate CM3D2 (or any other .NET programs or Unity games).

ReiPatcher

ReiPatcher is an all-purpose tool for injecting CIL (Common Intermediate Language) instructions into existing managed assemblies and programs that were made using .NET. While the tool was initially made at the time of CM3D2, it is capable of patching almost any .NET assembly, as long as it is compiled into managed CIL, like C# and VB.NET do. Notice however, that C++/CLI assemblies might be almost impossible to manipulate, which is due to the code being mostly unmanaged (notice that the managed code is still compiled into CIL, albeit with unsafe tag.). By using ReiPatcher we can for instance hook methods (make them call other methods from outside their original assemblies) or even redirect them (make them use custom logic instead of game's original code). Furthermore, with the help of Mono.Cecil library, it is possible to add, remove and edit any original methods or assemblies without having to explicitly to decompile and recompile assemblies.

As of this writing, ReiPatcher operates as follows. The programmer creates two DLLs: the one that contains custom hook or redirect methods, and the one that is used by ReiPatcher itself to patch the assemblies to call the custom methods. While the former DLL can have a structure of its own, the latter requires to have patcher classes (classes, that extend ReiPatcher.exe's PatchBase class). Every class that extends PatchBase is considered a patch in itself, which means that the patch DLL can contain as many patches as the programmer wants.

For further information on how to use ReiPatcher to hook or redirect methods, refer to the next sections of this tutorial.

ReiPatcherPlus

ReiPatcher's purpose is to provide an API for easy code injection. However, while ReiPatcher manages patching, assembly manipulation itself must be implemented by the programmer with the help of Mono.Cecil. Unfortunately, anything beyond injecting simple function calls increases the complexity of the code to the point where it is almost impossible to maintain it.

Therefore, an extension for ReiPatcher was created to encapsulate the most common routine methods for patching, injecting and altering assembly members.

ReiPatcherPlus allows to inject two types of functions:

  • Hooks: Methods which the injected function will call before continuing with its own routine
  • Redirects: Methods which the injected function will call. However, the return value of the redirect function determines whether the injected function will continue or return. That means that the redirect function can "redirect" game logic to programmer's own.

In addition to injection, ReiPatcherPlus allows to easily change visibility of class members to public. Furthermore, it is possible to make methods virtual.

This tutorial uses ReiPatcherPlus to shorten the code. Of course, full source code will be provided, where possible.

NOTE: Since ReiPatcherPlus 2.0, the library has undergone some major changes in structure. Therefore, note that some of parts of this page concerning ReiPatcherPlus might be outdated.

UnityInjector

Sometimes one does not need to hook or redirect in-game methods, but instead just needs to manipulate in-game objects, like textures, buttons, models, cameras or input, every tick (game update, usualy called every 1/60th of a second). In that case, patching game's assemblies is not required; instead, UnityInjector can be used.

UnityInjector is a plug-in manager, that hooks into a Unity Engine game and creates a game object of itself that is capable of being updated and accessing other game objects. In other words, UnityInjector uses ReiPatcher to be inserted into the game and allow programmers to create simple plug-ins without the need of patching any assemblies over and over again. UnityInjector can be used to process user input, create new UI and change properties of objects that are loaded into the game.

Tools

Required

  • Custom Maid 3D 2 (or just the DLLs found in CM3D2(x84/x64)_Data\Managed folder at the very least.
  • ReiPatcher and UnityInjector.
  • .NET Framework 3.5 or later.
  • IDE to program with a .NET-compatible language. Some of the more popular ones are Visual Studio (VS2015 Community is free), MonoDevelop and SharpDevelop. This tutorial was initially written for Visual Studio.
  • ReiPatcher and UnityInjector. Mono.Cecil is included in ReiPatcher and thus is not required to be downloaded by itself.
  • Basic knowledge of at least one programming language that supports .NET Framework. This tutorial is written in C#, but the plug-ins may be written in VB.NET and other .NET-compliant languages alike.

Additional

  • Basic knowledge of CIL (Common Intermediate Language). That is needed when writing own custom patches with Mono.Cecil.
  • ReiPatcherPlus, which provides methods to hook and redirect methods, load assemblies, etc.
  • ReSharper which is a productivity tool for Visual Studio. The tool provides for a more efficient and faster development.
  • .NET decompiler to view game's original code, which will be needed to create own plug-ins. Such tools are, for instance, ILSpy, Cecil Studio, dotPeek (included in ReSharper by default, but this is the standalone version) and .NET Reflector (paid, but includes patch testing tools like ILSpy). In this tutorial we will be using ILSpy.

Inspecting original code with a decompiler

Before we can start programming, we need to gather information about how to implement our plug-in. For that, we will use ILSpy (alternatively, you can use any other .NET code viewer) to find the classes and methods to hook. In the case of CM3D2, most important code is located in <Game's Main Directory>\CM3D2(x86/x64)_Data\Managed\Assembly-CSharp.dll. To view the code with ILSpy, do the following:

  1. Download and fire up ILSpy.
  2. Click File->Open (or press Ctrl+O) and navigate to <Game's Main Directory>\CM3D2(x86/x64)_Data\Managed (choose either x86 or x64 folder).
  3. Choose Assembly-CSharp.dll and hit "Open". That will add the library to the list of loaded ones.
  4. Click on the "+" button left to the loaded assembly to expand the view and see namespaces contained in the assembly.

From here it is the programmer's task to find the class and method to hook or redirect. Sometimes it is not even necessary to hook anything at all, for manipulating in-game property with UnityInjector may do the trick. However, for the purpose of this tutorial we shall use ReiPatcher to hook methods nonetheless.

While trying to find the needed classes or methods, one can use the following tools in ILSpy to simplify searching:

  • Clicking on the "+" left to any item will expand it to show the sub-items. Namespaces expand into types (classes, structs, etc) and those expand into members (variables, methods, properties). Clicking on the type or its member will show its definition in C#, VB.NET or pure CIL depending on chosen settings.
  • Right-clicking any item and choosing "Analyze" will bring up the "Analyzer" window which will contain such information as where the item is used, where it is defined or where it is exposed to other types. That will significantly speed up the process of finding methods to hook, redirect or use.
  • Read the names and definitions, for most of the time they reveal the purpose of the type or the method.

For the purpose of this tutorial, we can find that the class UIButton in the general namespace (notated as {} by ILSpy) has a method called OnClick(). After looking at the definition and analysing the method we can be rather certain that this is the method we are looking for. Therefore, by hooking the method (not redirecting — we'll discuss the difference later!) we can detect whether a button has been pressed. Now that we know what methods to hook, we can proceed to projects for ReiPatcher and UnityInjector.

Setting up

Installing the tools

Download and install .NET Framework, an IDE of your choice and possibly a decompiler. Follow installation instructions provided with the programs. Thereafter, download and install ReiPatcher and UnityInjector according to the instructions found on their respective download pages. Verify that the patcher was installed successfully by patching UnityInjector and launching the game. Remember to follow all README files provided with the mods.

Gathering required libraries

In addition to DLLs provided with ReiPatcher, we will need some additional assemblies in order to apply own plug-ins. Below is the list of required DLLs and their locations:

Assembly Name Purpose Location
ReiPatcher.exe Contains main patcher class ReiPatcher's install directory
ExIni.dll Contains classes and methods for manipulating .ini files. ReiPatcher's install directory
Mono.Cecil.dll Contains tools for manipulating CIL assemblies (reading classes, methods, etc) ReiPatcher's install directory
Mono.Cecil.*.dll Miscellaneous classes and tools to extend Mono.Cecil's functionality. ReiPatcher's install directory
ReiPatcherPlus.dll (optional) Extends the functionality of some classes found in ReiPatcher.exe ReiPatcher's install directory
Assembly-CSharp.dll Contains game's logic and scripts <Game's Main Directory>\CM3D2(x86/x64)_Data\Managed (Either x86 or x64 will suffice)
UnityEngine.dll Contains Unity Engine's wrapper classes for .NET <Game's Main Directory>\CM3D2x86_Data\Managed
UnityInjector.dll Contains classes for writing and loading plug-ins <Game's Main Directory>\CM3D2x86_Data\Managed
Miscellaneous Assemblies Additional libraries to inject into or to use Most likely <Game's Main Directory>\CM3D2(x86/x64)_Data\Managed

Copy the above-mentioned assemblies into a single folder. It will be used later in the tutorial.</br> Note: You can copy Assembly-CSharp.dll as well, but remember that if you intend on using UnityInjector along with ReiPatcher, consider referencing the assembly directly from game directory instead. That way, after ReiPatcher had applied the patches, the IDE will automatically pick up updated method and visibility definitions.

Creating the projects

Create an empty project in the IDE of your choice. We are going to call it CM3D2.HelloWorld.Hook. When creating the project, we'll set solution name to CM3D2.HelloWorld. Thereafter, we'll create two other empty projects: CM3D2.HelloWorld.Patcher and CM3D2.HelloWorld.Plugin. Having done that, set solution's active configuration to "Release" and platform to "Any CPU". Finally, set .NET Framework version to 3.5 if you haven't done so when creating the solution.

If you are unsure of how to perform such a task, refer to the IDE's own help pages. In Visual Studio 2013, the aforementioned can be done as follows:

  1. Open Visual Studio. Click File->New->Project (or press Ctrl+Shift+N).
  2. From templates, choose Visual C#->Class Library (or Visual C#->Windows Desktop->Empty Project).
  3. Above the list of project templates change .NET Framework version to 3.5 if it isn't already.
  4. Give a name to your project in the Name field. In the case of this tutorial it shall be CM3D2.HelloWorld.Hook.
  5. Give a name to your solution name in the Solution name field. In our case it is CM3D2.HelloWorld.
  6. Change location of the solution if needed.
  7. Make sure Create directory for solution is checked. Click "OK" and wait for solution to load up.
  8. In Solution Explorer, right click on solution's name and go Properties->Configuration Properties->Configuration->Configuration Manager.
  9. Change "Active solution configuration" to "Release". Under "Active solution platform", make sure it is "Any CPU" and click "OK". Close Configuration Manager and click OK in solution's configuration.
  10. Right click on each project and go to Properties and change Output type to "Class Library". Save the project by pressing Ctrl+S. Close the tabs.

When creating other projects, refer to the next subsection about naming and their meaning.

DLL types and naming conventions

As of this writing, ReiPatcher and UnityInjector require at most three different DLLs to be created in order to apply plug-ins. Those libraries' function can be described as follows:

  • Hook — Used by ReiPatcher. Contains methods (hooks) which will be called by the game before running its own logic. Ready DLLs are placed to <Game's Main Directory>\CM3D2(x86/x64)_Data\Managed.
  • Patcher — Used by ReiPatcher. Contains procedures that will add hook methods into game's original methods (also called hooking in programming argot). Ready DLLs are placed to <Game's Main Directory>\Patches
  • Plugin — User by UnityInjector. Contains Unity script that is loaded up by UnityInjector. Ready DLLs are placed to <Game's Main Directory>\UnityInjector

Note, that if one does not need to alter the execution of any in-game methods, only Plugin DLL is needed.

Each library needs a project of its own to be created. Unfortunately, as of this moment, no naming conventions exist. Therefore, it is up to the programmer to decide how to name them. Nonetheless, most of the plug-in creators seem to name their DLLs as follows:

CM3D2.PluginName.Type.dll

In this case PluginName is self-explanatory. Type, on the other hand, refers to one of the three types of DLL. Note: Some people seem to prefer to replace the Hook word with Core, while others leave the type out altogether.

Importing the DLLs

Remember the DLLs we gathered into a single folder? Move it in the same folder as the solution. After that return to your IDE and add those libraries to projects' references. In Visual Studio it can be done as follows:

  1. In Solution Explorer, click on the small arrow left to the project name to expand it.
  2. Right click on References and click Add Reference....
  3. In the newly-opened window go to Browse tab and click Browse... button.
  4. Choose the assemblies to import (multiple can be chosen at the same time). And click "OK".

Below is the table of basic set of assemblies to reference for each type of DLL:

Project type Assemblies to reference
Hook Assembly-CSharp.dll, UnityEngine.dll
Patcher Assembly-CSharp.dll, UnityEngine.dll, Mono.Cecil.dll, ReiPatcher.exe, ReiPatcherPlus.dll (if you want convenience methods)
Plugin Assembly-CSharp.dll, UnityEngine.dll, UnityInjector.dll, ExIni.dll, Hook DLL (if using ReiPatcher to hook methods. Can be added by referencing the Hook project)

Of course, if any other assemblies are to be used, they must be referenced as well.

Writing the hook DLL

Firstly, we shall write the hook method. The hook DLL acts more often than not as a bridge between the game's own internal API and our plug-in code.

If there are not any classes (.cs files) in your hook project, create a new source code file. We shall name it HelloWorldHooks.

In it we shall create a basic class which is also named HelloWorldHooks. The structure should be the following:

namespace CM3D2.HelloWorld.Hook
{
    public static class HelloWorldHooks
    {
    }
}

Notice how the the class is notated as static. It is not compulsory, but is a good practice, as we do not intend to create an instance of HelloWorldHooks.

Let us define the hook method. As we have seen the definition of UIButton.OnClick(), we know that the button has a property UIButton.isEnabled. We will want to access that property so that we will not print anything when the button is disabled. To access the property, we will need to have the reference of the pressed button at our disposal. Therefore, we will create a hook method with a single parameter: reference to the UIButton object. The prototype of the method will then look something like this:

public static void OnClickHook(UIButton button)
{
    // Called when the game calls UIButton.OnClick(). 
    // The parameter contains the reference to UIButton in which UIButton.OnClick() was called.
}

Next, we shall use .NET event handlers to create a custom event to which we can then add functions to call when OnClickHook() is called. Contrary to .NET Framework guidelines, we shall do it by declaring a custom delegate (event handler prototype) and an event above of OnClickHook() definition:

public delegate void ButtonClickHandler(UIButton button); // Function prototype for event handlers
public static event ButtonClickHandler ButtonClicked;     // Event itself

public static void OnClickHook(UIButton button)
{
    // ...
}

It must be noted, however, that while this way is shorter, it is against .NET guidelines and is compact only when there are just a few events to handle. Refer to MSDN Events tutorial if you want to do it the .NET way.

Finally, we can call the event handlers in our defined hook. The final structure of HelloWorldHooks.cs will look like this:

namespace CM3D2.HelloWorld.Hook
{
    public static class HelloWorldHooks
    {
        public delegate void ButtonClickHandler(UIButton button); // Function prototype for event handlers
        public static event ButtonClickHandler ButtonClicked;     // Event itself

        public static void OnClickHook(UIButton button)
        {
            // Check if there are even handlers. If true, call them.
            if (ButtonClicked != null) ButtonClicked(button);
        }
    }
}

This is the only hook method we will need. Save the class and proceed to writing the patcher.

Writing the patcher DLL

As discussed in the previous sections, ReiPatcher uses an API of its own to provide an easy way for patching assemblies. The patcher DLL is used by ReiPatcher to allow the plug-in developer to inject custom calls to the hook methods, change member field access and adding own code.

In our patcher project, we shall create a class HelloWorldPatch. To turn the class into a patcher, inherit PatchBase located in ReiPatcher.Patch namespace. Since the class is abstract, it requires the following members to be extended/defined:

  • string Name: A get property that specifies the name of the patch
  • string Version: A get property that specifies the version of the patch.
  • bool CanPatch(PatcherArguments args): A method that determines whether the patch can be applied.
  • void Patch(PatcherArguments args): A method in which one performs the patching.

Here is the basic structure of our patch:

using System.Reflection;
using ReiPatcher.Patch;
using ReiPatcherPlus; // Extends PatchBase to simplify patching. Not required to make ReiPatcher work.

namespace CM3D2.HelloWorld.Patcher
{
    public class HelloWorldPatch : PatchBase
    {
        public override string Version
        {
            get
            {
                // A simple way: just gets the version of CM3D2.HelloWorld.Patcher assembly
                return Assembly.GetExecutingAssembly().GetName().Version.ToString();
            }
        }
        public override string Name
        {
            get { return "Hello, world! Patch for CM3D2"; }
        }

        public override bool CanPatch(PatcherArguments args)
        {
            // Checks to determine whether this patch can be applied
            // Return ture if ReiPatch may proceed to patch with this class
        }

        public override void Patch(PatcherArguments args)
        {
            // Patches the assemblies using Mono.Cecil
        }
    }
}

In addition, we can override two more methods:

  • PrePatch(): Called after the patch has been loaded into memory, but before it is applied. Used to request assemblies that we want to patch. Also used to load our hook DLL.
  • PostPatch(): Called after the patch has been applied and the patched assembly saved. Can be used to run some clean-up code.

In our case, overriding only PrePatch() will suffice.

Pre-patching phase

Before patching we need to ask ReiPatcher to load up the assemblies we want to patch. In our case it is Assembly-CSharp.dll. Moreover, we need to load our own assembly that contains the hook method.

Assembly request is done with RPConfig.RequestAssembly(string name), where name is path to the assembly to patch. If the exact path is not specified, ReiPatcher will attempt to find the assembly from the path specified in AssembliesDir attribute in patcher's INI file. Every assembly that one wishes to be patched must be requested. Otherwise it might not be included into the patching cycle.

Having requested the assembly, we need to load our hook assembly.

ReiPatcherPlus provides a convenient method LoadAssembly(string name) where name is the path of the assembly. If exact path is not specified, the method will attempt to find the assembly from the path specified in AssembliesDir attribute in patcher's INI file. If the method fails to find or load the assembly, it will throw an exception.
The functionality of LoadAssembly can be mimicked with Mono.Cecil as follows:

public static AssemblyDefinition LoadAssembly(string name)
{
    string path = Path.Combine(patch.AssembliesDir, name);
    if (!File.Exists(path)) throw new FileNotFoundException("Missing DLL: " + path);
    using (Stream s = File.OpenRead(path))
	     result = AssemblyDefinition.ReadAssembly(s);
    return result;
}

In the end, this is how our PrePatch() method should look like:

// Below the class definition
private AssemblyDefinition hookAssembly;    // Our loaded hook assembly
// ...

// Below Patch(PatcherArguments args)
public override void PrePatch()
{
    //Request assemblies from ReiPatcher
    RPConfig.RequestAssembly("Assembly-CSharp.dll");
 
    // Load our own assemblies (like hooks, etc.)
    hookAssembly = this.LoadAssembly("CM3D2.HelloWorld.Hook.dll");
}

Pre-patching checks

The CanPatch(PatcherArguments args) method override is called just about before applying the patch. If we return true, ReiPatcher will call Patch(PatcherArguments args). Note that CanPatch can be called multiple times — one time for each assembly that was requested (either by our own patch or some other one). That is why it is crucial to check that we are about to patch the right assembly. That way Patch(PatcherArguments args) method will be called the right number of times and on the right assemblies.

To assist checking, ReiPatcher provides PatcherArguments parameter, that contains the following properties:

Property Description
Assembly Assembly that ReiPatcher is about to patch. Can be used to check whether we actually want to patch it.
Location Full path to the assembly
FromBackup True, if the assembly was loaded from a back-up
WasPatched True, if that assembly has been patched at least once during this patch cycle

In addition to checking whether the assembly is right, we also need to check if our patch has already been applied to the specified assembly. That way we can avoid injecting the same code twice.
ReiPatcherPlus contains another convenience method: HasAttribute(AssemblyDefinition assembly, string attribute).
The method loads all attributes from the given assembly and attempts to find a match. If there is an attribute data of which matches attribute, the method returns true.
The functionality of the method is implemented as follows:

public static bool HasAttribute(AssemblyDefinition assembly, string attribute)
{
    return patch.GetPatchedAttributes(assembly).Any(a => a.Info == attribute);
}

We shall define an arbitrary tag, "CM3D2_HELLO_WORLD", that we will add as an attribute to the assembly after our patch has been successfully applied. Next time ReiPatcher is run, we check whether our tag exists in the assembly. If it does, we know that our patch has already been applied.

That way, our definition of CanPatch becomes:

// Below the class definition
private const string TAG = "CM3D2_HELLO_WORLD";

//...

public override bool CanPatch(PatcherArguments args)
{
    //Check that we are patching the right assembly and it doesn't have our tag
    return args.Assembly.Name.Name == "Assembly-CSharp" && !this.HasAttribute(args.Assembly, TAG);
}

Patching phase

Having done all the checks, it is time to finally patch the assembly, which is done in Patch(PatcherArguments args) method.
The contents of the method depend on the situation. However, in almost all cases patching requires using various methods provided by Mono.Cecil to alter the contents of classes and methods.
All in all, injecting our OnClickHook(UIButton hook) requires the following steps if we are to use pure Cecil and CIL:

  1. Get assembly's module, which is the property args.Assembly.MainModule, and use its GetType(string fullName) method to acquire the type definition for UIButton.
  2. Get hook assembly's module, which is the property hookAssembly.MainModule, and acquire the type definition for CM3D2.HelloWorld.Hook.HelloWorldHooks (notice the use of full name containing the namespace(s) and type name).
  3. Find the MethodDefinition for OnClick() method using UIButton's type definition.
  4. Repeat the same process for OnClickHook(UIButton button) method using acquired type definition.
  5. Using hook assembly's module and method definition, get MethodReference for HelloWorldHooks.OnClickHook().
  6. Using UIButton.OnClick()'s method definition, get its method body and from it an instance of ILProcessor.
  7. Use the IL processor to insert ldarg.0 and call [method reference to OnClickHook()] instructions before the method's original first instruction.
  8. Add our patch tag to the assembly.

The process requires to write about a dozen of lines to complete the injection. However, method search and injecting can be done easier with ReiPatcherPlus.

Here are the main steps in using ReiPatcherPlus:

1. Acquiring the type definitions
This is done simply with Mono.Cecil's ModuleDefinition.GetType(string name):

TypeDefinition uiButton = args.Assembly.MainModule.GetType("UIButton");
TypeDefinition hooks = hookAssembly.MainModule.GetType("CM3D2.HelloWorld.Hook.HelloWorldHooks");

2. Getting the target method and the appropriate hook
ReiPatcherPlus provides an extension to Mono.Cecil: TypeDefinition.GetMethod(string name) which searches for the first method with the given name.
Furthermore, to get a suiting hook method, we can use GetHookMethod(MethodDefinition target, TypeDefinition hookType, string hookName, MethodFeatures features) which finds a suitable hook method that matches both the target method and the given criteria. Since our hook method has only a parameter of the target type, only using the feature MethodFeatures.PassTargetType will suffice:

MethodDefinition onClick = uiButton.GetMethod("OnClick");
MethodHook hookMethod = this.GetHookMethod(onClick, hooks, "OnClickHook", MethodFeatures.PassTargetType);

For more detailed discussion on method features and GetHookMethod, refer to the later sections.

3. Injecting the code
Having created an instance of MethodHook, we can now simply use ReiPatcherPlus's AttachMethod(MethodHook hook, int codeOffset). Since the hook method was searched with GetHookMethod, one can be sure that the hook method is in fact injectable into the target method. Note that int codeOffset specifies the index of the IL code to start the injection from. Since we want to add the hook at the beginning of the OnClick(), we simply pass 0:

this.AttachMethod(hookMethod, 0);

Putting it all together we get a fancy patch method:

public override void Patch(PatcherArguments args)
{
    // Get type definitions for the UIButton and the hook class
    TypeDefinition uiButton = args.Assembly.MainModule.GetType("UIButton");
    TypeDefinition hooks = hookAssembly.MainModule.GetType("CM3D2.HelloWorld.Hook.HelloWorldHooks");
    
    // Get the method definition for OnClick and OnClickHook
    // GetHookMethod assures that the created MethodHook instance contains a valid hook for OnClick
    MethodDefinition onClick = uiButton.GetMethod("OnClick");
    MethodHook hookMethod = this.GetHookMethod(onClick, hooks, "OnClickHook", MethodFeatures.PassTargetType);
    
    // Passing 0 will inject the hook method at the beginning of OnClick method
    this.AttachMethod(hookMethod, 0);
    
    // Add our tag to the assembly attribute to signify that the patch has been applied successfully
    SetPatchedAttribute(args.Assembly, TAG);
}

NOTE: The alternative below is listed only for legacy reasons. ReiPatcherPlus 2.0+ marks HookMethod and RedirectMethod as obsolete. Consider using the above-mentioned code.

Alternatively, ReiPatcherPlus provides yet another method: HookMethod(TypeDefinition targetType, string targetMethod, TypeDefinition hookType, string hookMethod). This method takes the type definitions created in steps 1. and 2., and automatically hooks the function! This version of the method searches for the first methods that match the given method names and hooks them together. If one wants better control of which method to hook, refer to the later sections where the capabilities of ReiPatcherPlus are exposed in greater detail.

Everything considered, our Patch(PatcherArguments args) method turns into:

public override void Patch(PatcherArguments args)
{
    // Hook UIButton.OnClick method to call CM3D2.HelloWorld.Hook.HelloWorldHooks.OnClickHook()
    this.HookMethod(args.Assembly.MainModule.GetType("UIButton"),
                    "OnClick", 
                    hookAssembly.MainModule.GetType("CM3D2.HelloWorld.Hook.HelloWorldHooks"), 
                    "OnClickHook");

    // Add our tag to the assembly attribute to signify that the patch has been applied successfully
    SetPatchedAttribute(args.Assembly, TAG);
}

Summary

By combining the code from the subsections above we will get a fully working patcher! Of course, it is a simple (it does not even override PostPatch()!), yet fully working class. In the next section we shall briefly discuss writing plug-in DLLs for UnityInjector.

The final source of our patcher class:

using System.Reflection;
using Mono.Cecil;
using ReiPatcher;
using ReiPatcher.Patch;
using ReiPatcherPlus;

namespace CM3D2.HelloWorld.Patcher
{
    public class HelloWorldPatch : PatchBase
    {
        private const string TAG = "CM3D2_HELLO_WORLD";
        private AssemblyDefinition hookAssembly;

        public override string Version
        {
            get { return Assembly.GetExecutingAssembly().GetName().Version.ToString();}
        }

        public override string Name
        {
            get { return "Hello, world! Patch for CM3D2"; }
        }

        public override bool CanPatch(PatcherArguments args)
        {
            return args.Assembly.Name.Name == "Assembly-CSharp" && !this.HasAttribute(args.Assembly, TAG);
        }

        public override void Patch(PatcherArguments args)
        {
            TypeDefinition uiButton = args.Assembly.MainModule.GetType("UIButton");
            TypeDefinition hooks = hookAssembly.MainModule.GetType("CM3D2.HelloWorld.Hook.HelloWorldHooks");
            
            MethodDefinition onClick = uiButton.GetMethod("OnClick");
            MethodHook hookMethod = this.GetHookMethod(onClick, hooks, "OnClickHook", MethodFeatures.PassTargetType);
            
            this.AttachMethod(hookMethod, 0);
            
            SetPatchedAttribute(args.Assembly, TAG);
        }

        public override void PrePatch()
        {
            RPConfig.RequestAssembly("Assembly-CSharp.dll");
            hookAssembly = this.LoadAssembly("CM3D2.HelloWorld.Hook");
        }
    }
}

Writing the plug-in DLL (using UnityInjector)

Finally, by using UnityInjector one can affect game's logic and objects using Unity's own scripting API.
It is important to notice that although UnityInjector relies on ReiPatcher to be installed, both are completely standalone tools in plug-in development. It is possible to create plug-ins that are only installed by ReiPatcher, plug-ins that act as Unity's game objects loaded on-the-fly with UnityInjector, or plug-ins that take advantage of both tools to create versatile and diverse additions into Unity Engine games.

As mentioned, UnityInjector plug-ins are not patched into the game. Instead, they are loaded dynamically and added to the game as custom MonoBehaviours. In other words, UnityInjector allows to insert custom scripts into the game.

Writing plug-ins is simple: create a custom class that extends PluginBase. That is it. Alternatively, one can add one or many of the following attributes to the class:

Attribute example Description
[PluginFilter("<executable>")] The plug-in will be loaded only when the name of the game executable is the same as <executable>. If not specified, UnityInjector will always load the plug-in.
[PluginName("<name>")] Specifies the name of the plug-in. Useful for debugging. If not specified, the name will be that of the plug-in class.

On UnityInjector 1.0.1.0 and newer, will now fallback to the Assembly Name if not specified.

[PluginVersion("<version>")] Specifies the version of the plug-in. Useful for debugging. If not specified, the version will be set to "1.0".

On UnityInjector 1.0.1.0 and newer, will now fallback to the Assembly Version if not specified.

Therefore, in our case the class will look like this:

using System;
using UnityInjector;
using UnityInjector.Attributes;
// If you use ReiPatcher to make custom hooks, remember to reference your hook assembly
using CM3D2.HelloWorld.Hook;
using ExIni;

namespace CM3D2.HelloWorld.Plugin
{
    [PluginName("Hello, world! Unity Plug-In"), PluginVersion("0.0.0.1")]
    public class HelloWorldPlugin : PluginBase
    {
    }
}

About scripting

Unlike ReiPatcher, or any other tool discussed so far, Unity does not use C# in a conventional manner. Instead of providing method overrides, the engine simply calls the methods with certain names when an event occurs. These methods are referred to as "messages" in Unity.

There is a foison of different messages one can use to script the behaviour of the plug-in. All of them are well documented on Unity's own documentation website. Furthermore, UnityInjector contains Message enumeration with all the scriptable message names which can be used as a quick reference while creating own plug-ins.

All in all, it is highly advised to read Unity's tutorial on scripting to fully take advantage of engine's capabilities.

Manipulating INI files

ReiPatcher comes bundled with ExIni, a simple library that provides the ability to work with INI configuration files.

In our case, we want to give the user the ability to redefine the key which toggles "Hello, world!" messages on and off.

Fortunately, ExIni and UnityInjector will create missing configuration files and properties if they do not exist. That significantly simplifies scripting. Let us simply create a method LoadConfig() that will load the key configuration or create it if there isn't one:

// In the beginning of the class
private const KeyCode TOGGLE_KEY_INITIAL = KeyCode.K;
private KeyCode toggleKey = TOGGLE_KEY_INITIAL;
private bool displayText = true; // We will alter the value of this variable with the key
//...

private void LoadConfig()
{
    // Preferences is an instance of ExIni.IniFile that is defined in PluginBase.
    // If no INI file is found, UnityInjector will create one automatically.
    // That is why we don't need to perform any checks.
    // This command will attempt to find a key "ToggleKey" in section "Key_Mappings".
    // If such section/key does not exist, it will be created on-the-fly.
    IniKey key = Preferences["Key_Mappings"]["ToggleKey"];

    // If no key existed or it was left empty, the value will be null
    if (key.Value == null)
    {
        key.Value = Enum.GetName(typeof(KeyCode), toggleKey);
        // Note that ExIni nor UnityInjector save the configuration. Remember to do it yourself!
        SaveConfig();
    }
    else
    {
        try
        {
            // If the key is found, we attempt to parse it into UnityEngine's KeyCode enum
            toggleKey = (KeyCode)Enum.Parse(typeof(KeyCode), key.Value, true);
        }
        catch (Exception)
        {
            // If we fail, set reset it to the initial value and save our work
            toggleKey = TOGGLE_KEY_INITIAL;
            key.Value = Enum.GetName(typeof(KeyCode), toggleKey);
            SaveConfig();
        }
    }
}

Note, that the name of plug-in's INI configuration file will be the same as plug-in class name. In our case, calling SaveConfig() will create a configuration file named HelloWorldPlugin.ini which will be saved to <Game's main directory>\UnityInjector\Config.

Adding the event handler

If you used ReiPatcher to create a custom hook, it is high time we added an event handler for it. In that case, remember to reference the hook assembly in the plug-in project to access the hooked event. In the case of this tutorial, we shall reference "CM3D2.HelloWorld.Hook" and use the namespace.
After that, we create an event handler called OnButtonClick(UIButton button) in our plug-in. In it, we simply check that the button is enabled (that is, clickable) and we can display the text. If all lights are green, we print our message.

Having written our event handler, it is only left to be added to the event itself. That is done in Awake() method, that in Unity acts as a constructor. In fact, there are not a lot of differences between the two, one of them being that Awake() guarantees that all game objects have been initialised and are accessible from this method.
In the Awake() method we firstly call LoadConfig() to load the configuration file; only thereafter we add the event handler to the event.

In the end we have ended up with two new methods in our HelloWorldPlugin:

// Used instead of constructor
public void Awake()
{
    LoadConfig(); // Load the configuration
    HelloWorldHooks.ButtonClicked += OnButtonClick; // Add our own event handler to ButtonClicked event
}

// This is our event handler. It will be called every time HelloWorldHooks.OnClickHook() will be called.
private void OnButtonClick(UIButton button)
{
    // Check that button is enabled and text displaying is not disabled
    if (button.isEnabled && displayText)
        Console.WriteLine("Beep! Hello, world!");
}

Final steps: getting input

To finish our patch, we shall implement simple input processing. Since input is updated every tick, it makes sense to put our input checking into Update() message.

The code below speaks for itself:

// On top of the class
private bool isKeyPressed = false; // Helper boolean to prevent multiple toggles

//...

public void Update()
{
    if (!isKeyPressed && Input.GetKeyDown(toggleKey))
    {
        // We can still use in-game classes without hooking them.
        // For instance, in CM3D2 we can emit button click sounds with the command below
        GameMain.Instance.SoundMgr.PlaySystem(displayText ? "SE001.ogg" : "SE002.ogg");

        displayText = !displayText;
        isKeyPressed = true;
    }
    else if (isKeyPressed && Input.GetKeyUp(toggleKey))
        isKeyPressed = false;
}

Summary

In this section we discussed how to create plug-ins for UnityInjector. As we have witnessed, the process is rather simple and just requires some knowledge of Unity scripting API.

Here is the source code for our plug-in class:

using System;
using CM3D2.HelloWorld.Hook;
using ExIni;
using UnityEngine;
using UnityInjector;
using UnityInjector.Attributes;

namespace CM3D2.HelloWorld.Plugin
{
    [PluginName("Hello, world! Unity Plug-In"), PluginVersion("0.0.0.1")]
    public class HelloWorldPlugin : PluginBase
    {
        private const KeyCode TOGGLE_KEY_INITIAL = KeyCode.K;

        private KeyCode toggleKey = TOGGLE_KEY_INITIAL;
        private bool displayText = true;
        private bool isKeyPressed;

        public void Awake()
        {
            LoadConfig();
            HelloWorldHooks.ButtonClicked += OnButtonClick;
        }

        public void Update()
        {
            if (!isKeyPressed && Input.GetKeyDown(toggleKey))
            {
                GameMain.Instance.SoundMgr.PlaySystem(displayText ? "SE001.ogg" : "SE002.ogg");
                displayText = !displayText;
                isKeyPressed = true;
            }
            else if (isKeyPressed && Input.GetKeyUp(toggleKey))
                isKeyPressed = false;
        }

        private void LoadConfig()
        {
            IniKey key = Preferences["Key_Mappings"]["ToggleKey"];
            if (key.Value == null)
            {
                key.Value = Enum.GetName(typeof (KeyCode), toggleKey);
                SaveConfig();
            }
            else
            {
                try
                {
                    toggleKey = (KeyCode) Enum.Parse(typeof (KeyCode), key.Value, true);
                }
                catch (Exception)
                {
                    toggleKey = TOGGLE_KEY_INITIAL;
                    key.Value = Enum.GetName(typeof (KeyCode), toggleKey);
                    SaveConfig();
                }
            }
        }

        private void OnButtonClick(UIButton button)
        {
            if (button.isEnabled && displayText)
                Console.WriteLine("Beep! Hello, world!");
        }
    }
}

Source code and example projects

Below is a list of different projects written for ReiPatcher and UnityInjector. The projects can be used as templates for own projects.

ReiPatcherPlus: methods overview

The previous sections have covered all of the ReiPatcher and UnityInjector API, while ReiPatcherPlus and ExIni have been covered only briefly. In the following two sections (Note: ExIni will come later) we will discuss the possibilities that ReiPatcherPlus and ExIni provide.

This section is dedicated to ReiPatcherPlus: a ReiPatcher extension that condenses complicated Mono.Cecil code into simple-to-use methods.

ReiPatcherPlus is used alongside ReiPatcher to add additional functionality to programmer's patcher class that inherits ReiPatcher's PatchBase. As of this writing, such functionality includes method hooking, redirecting, modifying accessibility of class members, assembly loading and attribute checking.

Note that accessing ReiPatcherPlus's methods requires one to use this., or the extension method's won't be seen.

In the examples we shall use the following class that contains potential methods to hook:

namespace Game
{
    public class Swallow
    {
        protected enum SwallowType
        {
            European,
            African
        }
        
        private float airSpeedVelocity = 11.2F;
        
        public float GetNeededSwallows(int coconuts);
        public float GetNeededSwallows(float distance);
        public bool IsSwallowEuropean(bool isNotAfrican);
        public static T FirstSwallow<T>(List<T> swallows);
        public static T FirstSwallow<T>(List<T> swallows, T addition);
        private void MakeFly(bool isUnladen, float distance);
    }
}

Getting methods and fields

ReiPatcherPlus adds two new extension methods to Mono.Cecil that allow to locate method definitions in their respected types. Those methods are added as extensions to Mono.Cecil.TypeDefinition type.
Those two methods are:

MethodDefinition GetMethod(string methodName);
MethodDefinition GetMethod(string methodName, params TypeReference[] paramTypes);

where the arguments are:

Argument Description
methodName Name of the method to search for.
paramTypes TypeReference representation of method parameters. Used to search for the exact overload of a method.

Furthermore, one can easily acquire member fields (delegates, variables) of the type. That is done with the method:

FieldDefinition GetField(string memberName);

where memberName is name of the member to search for.

In both cases, the methods will return null, if the fitting method/field has not been found. Therefore, it is up to the developer to make sure the provided parameters point to a valid method or field. In some cases, the programmer may need to perform null checking.

Examples

TypeDefinition swallowType = MyModule.GetType("Game.Swallow");

// Will return GetNeededSwallows(int)
MethodDefinition m1 = swallowType.GetMethod("GetNeededSwallows");

// Will return GetNeededSwallows(float)
MethodDefinition m2 = swallowType.GetMethod("GetNeededSwallows", Parameter.FromType<float>());

FieldDefinition f1 = swallowType.GetField("airSpeedVelocity");

Handling parameters

To make turning System.Type and normal type declarations into dummy TypeReferences to use with ReiPatcherPlus, Parameter class was created. The class houses a panoply of useful methods to create "dummy type references" (they are not actual references to the real types -- instead, they are containers for type information used method search methods).
Such methods are:

TypeReference FromType(Type type, params string[] genericTypeNames);
TypeReference FromType<T>();
TypeReference FromTypeRef(Type type, params string[] genericTypeNames);

TypeReference CreateGeneric(string name);

For the first four methods, the parameter descriptions are as follows:

Argument Description
type System.Type representation of the given type. Can be acquired with typeof(TypeName).
genericTypeNames Names of the generic types. Used only, if the provided type has undefined generic parameters.
<T> Type name of the type to be converted. Calling FromType<T>() has the same effect as FromType(typeof(T)).

As for CreateGeneric:

Argument Description
name Name of the generic type to create.

Note that in order for GetMethod to succeed with generic types, the names of those must stay the same. If the original method had a generic type T, one must make the type with Parameter.MakeGeneric("T") in order for the method to be found. The same principle must hold when matching target method parameters with the hook method parameters.

Examples

// t1 and t2 are technically the same 
TypeReference t1 = Parameter.FromType(typeof(int));
TypeReference t2 = Parameter.FromType<int>();

// Creates a type of List<T>
TypeReference t3 = Parameter.FromType(typeof(List<>), "T");

// Creates a reference type System.Boolean&
TypeReference t4 = Parameter.FromTypeRef(typeof(bool));

TypeDefinition swallowType = MyModule.GetType("Game.Swallow");

MethodDefinition m3 = swallowType.GetMethod("MakeFly", Parameter.FromType<bool>(), Parameter.FromType<float>());
MethodDefinition m4 = swallowType.GetMethod("FirstSwallow", Parameter.FromType(typeof(List<>), "T"), Parameter.CreateGeneric("T"));

// WILL NOT WORK: The provided generic types differ from the original definition
MethodDefinition m5 = swallowType.GetMethod("FirstSwallow", Parameter.FromType(typeof(List<>),"A"), Parameter.CreateGeneric("G"));

The MethodHook type and getting hooks

To provide an easy patching experience, ReiPatcherPlus includes MethodHook container that exposes the following get-only properties:

Property Description
Hook Method definition of the hook method. Will be hooked to the target.
Features Additional criteria by which the hook method was chosen. Refer to the next subsection for more information.
Target Method definition of the method to hook (target method).

The creation of the MethodHook instance is controlled internally from ReiPatcherPlus. That way the library guarantees that the specified hook-target pair is valid and injectable.

Currently there are two ways of getting the instance: by using PatchBase extension GetHookMethod or by calling MethodHook.FromMethodDefinition.

The exact definitions are:

MethodHook GetHookMethod(MethodDefinition target, 
                         TypeDefinition hookType, 
                         string hookName, 
                         MethodFeatures features, 
                         params FieldDefinition[] memberReferences);

MethodHook MethodHook.FromMethodDefinition(MethodDefinition target, 
                                           MethodDefinition hook, 
                                           MethodFeatures features, 
                                           params FieldDefinition[] memberReferences);

The parameter description is the following:

Argument Description
target The method definition of the method which is to be hooked.
hookType The type (class/struct) that possesses the hook method.
features The criteria by which the hook method should be chosen. Defines the structure of the hook method. Refer to the next subsection for more information.
memberReferences If one of the criteria is MethodFeatures.PassMemberReferences, specifies the fields that are to be passed to the method.
hook Method definition of the hook method.

While using the methods, note the following:

  • When using GetHookMethod, the final features might be different from the ones specified. That is simply because the method adapts to its passed parameters to find the best hook method.

However, the method attempts to find the best hook without throwing any exceptions (for the exception of ArgumentException if the provided member fields do not belong to the target type).

  • MethodHook.FromMethodDefinition strictly checks whether the provided target and hook are injectable. If a discrepancy is found, the method will throw an exception containing the reason the method failed.
  • Prefer using GetHookMethod, as it mostly exception-free and will return either a working instance of MethodHook or null.

Method Features

To allow the developer to freely choose which kind of hook to use, what parameters to pass and what to return, ReiPatcherPlus allows to specify certain criteria that define the layot of the hook method. Albeit allowing different set of parameters to be passed, the hook method must have its parameters in a certain order for ReiPatcherPlus to work with them.

Below is the hook method template with numbered positions and a table of what types are allowed in place of those positions and which MethodFeatures fields to use to enable those positions:

public static <2.1> MyHookMethod(<1>, <2.2>, <3>, <4>);

MethodFeatures field Position value replacement, if feature specified (only types and prefixes specified) Description
MethodFeatures.None <1>, <2.2>, <3>, <4> = Empty
<2.1> = void
The method may not have any of the features.
MethodFeatures.PassTargetType <1> = Target type The method may be passed a reference to the target type that initially called the hooked method.
MethodFeatures.PassReturn <2.1> = bool
If target has a return type:
<2.2> = out <return type>
Otherwise:
<2.2> = Empty
The hook method is a redirect and thus may be passed a reference to the return value (if there is such). If the the target method has a return value of type <return type>, a parameter of the same type is passed as a reference to the hook method.
Note: If the feature is not specified, <2.1> must be void.
MethodFeatures.PassMemberReferences <3> = ref <type1>, ref <type2>, ... The method may pass references to the target type's member fields (assuming target method is not static) specified by the MethodHook creation method.
MethodFeatures.PassMethodParametersByValue <4> = <type1>, <type2>, ... The method may pass its own parameters to the hook. Since they are passed by value, they cannot be altered.
MethodFeatures.PassMethodParametersByReference <4> = ref <type1>, ref <type2>, ... The method may pass its own parameters to the hook. Since they are passed by reference, they can be altered.

Some important things to point out:

  • The hook method must be public and static! Otherwise it is impossible to hook it anywhere.
  • Some of the above-mentioned features can be left out. However the order of the parameters must be always preserved!
  • If MethodFeatures.PassReturn is set, the hook method must return a bool. If the hook method returns true, the execution of the original target method's instructions is interrupted; the target method simply returns the result value it got from the hook method (if any). In the contrary, if the hook method return false, the original target method will continue its execution after the hook method has completed executing its instructions.
  • Legacy notice: MethodFeatures.PassReturn makes ReiPatcherPlus search for a redirect method. However, since both hooking and redirecting have been combined into one single method, there is no point in differentiating between them anymore.

The features may be combined with a logical OR operator (|). In addition to the features above, the MethodFeatures enumeration has the following feature combinations:

Feature Combination equivalent
MethodFeatures.All_ByValue MethodFeatures.PassTargetType | MethodFeatures.PassReturn | MethodFeatures.PassMemberReferences | MethodFeatures.PassMethodParametersByValue
MethodFeatures.All_ByReference MethodFeatures.PassTargetType | MethodFeatures.PassReturn | MethodFeatures.PassMemberReferences | MethodFeatures.PassMethodParametersByReference

Examples

TypeDefinition swallowType = MyModule.GetType("Game.Swallow");
// Getting type definition for the class that contains all the hook methods
TypeDefinition hookType = HookModule.GetType("MyHookNamespace.Hook");

MethodDefinition m1 = swallowType.GetMethod("GetNeededSwallows");
MethodDefinition m2 = swallowType.GetMethod("MakeFly");
 
// Valid hook method prototype: public static void GetNeededSwallowsHook1()
MethodHook hookM1 = this.GetHookMethod(m1, 
                                       hookType, 
                                       "GetNeededSwallowsHook1",
                                       MethodFeatures.None);

// Valid hook method: public static bool GetNeededSwallowsHook2(Swallow self, out float result, int coconuts)
MethodHook hookM2 = this.GetHookMethod(m1,
                                       hookType,
                                       "GetNeededSwallowsHook2",
                                       MethodFeatures.PassTargetType
                                       | MethodFeatures.PassReturn
                                       | MethodFeatures.PassMethodParametersByValue);

// Valid hook method: public static bool GetNeededSwallowsHook3(Swallow self, out float result, ref int coconuts)									
MethodHook hookM3 = this.GetHookMethod(m1,
                                       hookType,
                                       "GetNeededSwallowsHook3",
                                       MethodFeatures.PassTargetType
                                       | MethodFeatures.PassReturn
                                       | MethodFeatures.PassMethodParametersByReference);

// Valid hook method: public static bool MakeFlyHook(Swallow self, out float result, ref float airSpeedVelocity, int coconuts)									
MethodHook hookM4 = this.GetHookMethod(m2,
                                       hookType,
                                       "MakeFlyHook",
                                       MethodFeatures.PassTargetType
                                       | MethodFeatures.PassReturn
                                       | MethodFeatures.PassMemberReferences
                                       | MethodFeatures.PassMethodParametersByValue,
                                       swallowType.GetField("airSpeedVelocity"));

MethodDefinition hookMethod = hookType.GetMethod("MakeFlyHook2");

// Valid hook method: public static bool MakeFlyHook(Swallow self, out float result, ref float airSpeedVelocity, ref int coconuts)									
MethodHook hookM5 = MethodHook.FromMethodDefinition(m2,
                                                    hookMethod,
                                                    MethodFeatures.All_ByReference,
                                                    swallowType.GetField("airSpeedVelocity"))

Injecting the methods

Finally, ReiPatcherPlus provides a functionality to patch methods. That can be done with AttachMethod method.

The exact definitions and parameter descriptions are below:

void AttachMethod(MethodHook hook, int codeOffset);
void AttachMethod(MethodDefinition target, 
                  MethodDefinition hook, 
                  int codeOffset, 
                  MethodFeatures features,
                  params FieldDefinition[] memberReferences);

Argument Description
target The method definition of the method which is to be hooked.
MethodDefinition hook Method definition of the hook method.
features The criteria by which the hook method should be chosen. Defines the structure of the hook method. Default: MethodFeatures.None
memberReferences If one of the criteria is MethodFeatures.PassMemberReferences, specifies the fields that are to be passed to the method.
MethodHook hook An instance of MethodHook that contains all the appropriate information.
codeOffset An index value of an IL instruction in the target method from which to begin injection. If set to negative, will begin counting from the last instruction of the target method, where -1 is being the last instruction.

Elementary, these two methods will automatically inject the needed CIL instructions depending on the chosen hook/target method.
The latter method will verify the compatibility between the target and the hook before applying the patch. That method is equivalent to calling MethodHook.FromMethodDefinition and then AttachMethod.

Most of the time setting code offset to 0 will be sufficient for plug-ins. However, if one wants to place the hook at a certain place, refer to the previous sections on how to decompile the .NET code. Most of the decompilers come with IL decompilers bundled (like ILSpy), which allows to inspect pure IL instructions to find the perfect attachment point.

Note as well that AttachMethod will always add its own CIL instructions before the specified code offset! That is done to prevent the developers from inserting method hooks after the ret instruction that is always present as a last IL instruction in every method. Any code insterted after the said instruction is simply neglected by the machine.

Examples

TypeDefinition swallowType = MyModule.GetType("Game.Swallow");
TypeDefinition hookType = HookModule.GetType("MyHookNamespace.Hook");

MethodDefinition m1 = swallowType.GetMethod("GetNeededSwallows");

// Valid hook method prototype: public static void GetNeededSwallowsHook1()
MethodHook hookM1 = this.GetHookMethod(m1, 
                                       hookType, 
                                       "GetNeededSwallowsHook1",
                                       MethodFeatures.None);

this.AttachMethod(hookM1, 0);

Changing accessibility

When searching through assemblies you might encounter a member or a method that is set to be private, even though you would want to access outside the class.

ReiPatcherPlus provides a simple method to make such private types accessible:

void ChangeAccess(TypeDefinition type, string member, bool makePublic = true, bool makeVirtual = true);

The method has the following arguments:

Argument Description
type Type (class/struct) that contains the member to manipulate. Can be gained from an assembly with the AssemblyDefinition.MainModule.GetType(string typeName) method.
member Name of the member to manipulate.
makePublic If true, will make the member public. Default: true.
makeVirtual If true and member is a method, will make it virtual. Default: true.

Debugging functions

ReiPatcherPlus provides simple methods for patcher message logging. All of the logging methods are located in RPPLogger class.

However, to enable logging, one must use the DEV version of the ReiPatcherPlus (slightly larger file size than normal ReiPatcherPlus). After that, one can set RPPLogger.VERBOSE_LEVEL variable to enable logging of certain methods.

Possible variable values are:

Flag Description
RPPLogger.VerboseLevel.None Don't output any messages
RPPLogger.VerboseLevel.Patcher Output patcher-related messages (getting hook methods, attaching methods, etc.).
RPPLogger.VerboseLevel.Cecil Output Mono.Cecil-related extension method messages. Includes parameter equality checking and method searching.

The flags can be combined with a logical OR (|) operator.

If one whishes to output own logging messages, it is possible through the following methods located in RPPLogger>/code>: <code>

void Log(string message, VerboseLevel level, bool addPrefix);
void LogLine(string message, VerboseLevel level, bool addPrefix);

And the parameters:

Argument Description
message Message to output.
level The level to output the message to. Used to filter the output based on selected levels.
addPrefix If true, will append a prefix specified on RPPLogger.PREFIX before the specified message.

The difference between the methods is that the latter will add a line break to the message, while the former one will not.

Examples

// Set verbose level to patcher only
RPPLogger.VERBOSE_LEVEL = RPPLogger.VerboseLevel.Patcher;

// Will be shown in ReiPatcher console
RPPLogger.LogLine("Hello, log!", RPPLogger.VerboseLevel.Patcher);

// Won't be seen in ReiPatcher
RPPLogger.LogLine("I am hidden!", RPPLogger.VerboseLevel.Cecil);

Miscellaneous methods

Here are some other small methods that simplify the workflow:

Method Description
bool HasAttribute(AssemblyDefinition assembly, string attribute) Checks whether the assembly has a string attribute set. Can be used in CanPatch(PatcherArguments args) to determine if the patch has already been applied.
AssemblyDefinition LoadAssembly(string name) Loads a custom assembly from the AssembliesDir path set by ReiPatcher. Can be used in PrePatch() to load hook assemblies.

Legacy and other obsolete methods

These methods are marked as obsolete and must not be used. Instead, use the newer functionality of ReiPatcherPlus.

Method hooking

ReiPatcherPlus has two overrides of this method:

void HookMethod(TypeDefinition targetType, string targetMethod, 
           TypeDefinition hookType, string hookMethod, bool passSelf = true);

void HookMethod(TypeDefinition targetType, string targetMethod, 
           Type[] targetParams, TypeDefinition hookType, string hookMethod, 
           bool passSelf = true);

Where the arguments are:

Argument Description
targetType Type (class/struct) which contains the method to be hooked. Can be gained from an assembly with the AssemblyDefinition.MainModule.GetType(string typeName) method.
targetMethod Name of the method to hook.
targetParams An array containing the Type of each target method's arguments in the order they are defined in the method. Used to find the exact method to hook.
hookType Type (class/struct) which contains the hook method.
hookMethod Name of the hook method.
passSelf If true, the hooked method will pass an instance of an object in which the method was called to the hook. Default: true.

In order for HookMethod to succeed in hooking the method, the hook must be defined with the right arguments. The type and the number of arguments needed depends on the choice of the override:

  • If the former override is chosen, the hook must contain only one argument: object of the type that contains the hooked method. However, if passSelf is set to false, the hook must have no arguments at all.
  • If the latter override is chosen, the hook must contain one more argument than the method it hooks: the first is the object of the type that contains the hooked method, and the rest are the same arguments as those of the method that is to be hooked. However, if passSelf is set to false, the hook must have exactly the same arguments as the method that is hooked.
  • If the former override is chosen and the hooked method contains some arguments, they will not be passed to the hook. For that, use the latter override.

In all cases, the hook must be a method without any return values (void, that is).

Method redirecting

The overrides are akin to HookMethod and the arguments are too:

void RedirectMethod(TypeDefinition targetType, string targetMethod, 
           TypeDefinition redirectType, string redirectMethod, bool passSelf = true);

void RedirectMethod(TypeDefinition targetType, string targetMethod, 
           Type[] targetParams, TypeDefinition redirectType, string redirectMethod, 
           bool passSelf = true);

The biggest difference is in how the redirect method MyMethod should be defined:

  • The return type must be bool. If the redirect returns true, the redirected method will return immediately without continuing the execution. If false is returned, the redirected method will proceed to its own instructions (just like in HookMethod).
  • passSelf acts the same as in HookMethod and therefore requires an object of the type with the redirected method (if passSelf=true).
  • If the redirected method has a return value (not void, that is), the redirect method must have an additional argument after the object type (if passSelf=true): reference type of the redirected method's return value (that is an argument that has out word before the type name). The value of this argument can be set by the redirect method. If redirect method returns true, the redirected method will use that value as its own return value.