Difference between revisions of "Custom Maid 3D2/Modding/Plugins"

From Hgames Wiki
Jump to: navigation, search
(Created page with "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 o...")
 
Line 1: Line 1:
 +
{{Custom Maid 3D 2}}
 +
 
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.  
 
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.  
  

Revision as of 00:02, 7 August 2015

KISS

all characters are at least 18


Custom Maid 3D2 [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: This tutorial was written with ReiPatcher 0.9.0.6 and UnityInjector 1.0.1.1. It is possible that in later versions of those tool there will have been some major changes in their design, which may or may not make this tutorial obsolete.

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 impossible to manipulate, which is due to the code being mostly unmanaged. 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.

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 CM3D2x84_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

  • 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 and Cecil Studio. In this tutorial we will be using ILSpy.

Designing the plug-in

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 Game's main directory
ExIni.dll Contains classes and methods for manipulating .ini files. Game's main directory
Mono.Cecil.dll Contains tools for manipulating CIL assemblies (reading classes, methods, etc) Game's main directory
Mono.Cecil.*.dll Miscellaneous classes and tools to extend Mono.Cecil's functionality. Game's main directory
ReiPatcherPlus.dll (optional) Extends the functionality of some classes found in ReiPatcher.exe Game's main 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.

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

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, 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. 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 programming choice, 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. 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. The number of requests is the same as the number of assemblies we want to patch.

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.

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 it has already been patched by our patch.
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.
All in all, injecting our OnClickHook(UIButton hook) requires the following steps:

  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, why not do all that with just two lines of code?
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)
        {
            this.HookMethod(args.Assembly.MainModule.GetType("UIButton"), "OnClick",
                hookAssembly.MainModule.GetType("CM3D2.HelloWorld.Hook.HelloWorldHooks"), "OnClickHook");
            SetPatchedAttribute(args.Assembly, TAG);
        }

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

Writing the plug-in DLL

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

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 a 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();
        }
    }
}

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 above
        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!");
        }
    }
}

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.

Method hooking

Consider the following class that contains the following methods:

namespace Game
{
    public class GameClass
    {
        public int numberOfCarrots = 42;
        
        public void GameMethod();
        public void GameMethod(string s, int i, bool b);
        private void GameMethod(byte b);
    }
}

Now imagine we want to do the simplest of tasks: alter GameMethod() so that it calls our own method MyMethod() that is located in our own assembly in namespace MyNamespace.MyClass.

In this case we can use HookMethod, a method that injects (hooks) a call to MyNamespace.MyClass.MyMethod() into GameClass.Game.GameMethod() (hook method) before the method's own instructions. That way, before GameClass.Game.GameMethod() proceeds with own logic, it will call MyNamespace.MyClass.MyMethod().
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).

Examples

Suppose we are patching Game assembly. Assume also that we have AssemblyDefinition of MyNamespace assembly as a variable named hookAssembly.

Therefore, we can patch the methods in patcher class's Patch(PatcherArguments args) method:

// MyMethod definition: void MyMethod(Game.GameClass thatGame)
this.HookMethod(args.Assembly.MainModule.GetType("Game.GameClass"), "GameMethod", 
                hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod");

// MyMethod definition: void MyMethod()
this.HookMethod(args.Assembly.MainModule.GetType("Game.GameClass"), "GameMethod", 
                hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod", false);

// MyMethod definition: void MyMethod(Game.GameClass thatGame, string s, int i, bool b) 
this.HookMethod(args.Assembly.MainModule.GetType("Game.GameClass"), "GameMethod",
                new[] { typeof(string), typeof(int), typeof(bool) },
                hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod");

// MyMethod definition: void MyMethod(byte b)
this.HookMethod(args.Assembly.MainModule.GetType("Game.GameClass"), "GameMethod",
                new[] { typeof(byte) }, 
                hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod", false);

Note that only the ones with Game.GameClass object will be able to access the numberOfCarrots variable.

Method redirecting

Let us consider the next class:

namespace Game
{
    public class Swallow
    {
        public float airSpeedVelocity = 11.2F;
        
        public float GetNeededSwallows(int coconuts);
        public bool IsSwallowEuropean(bool isNotAfrican);
        private void MakeFly(bool isUnladen, float distance);
    }
}

Suppose we want not to just hook a method — we want to replace it with a definition of our own! That can be achieved with ReiPatcherPlus' RedirectMethod methods. 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/code> is returned, the redirected method will proceed to its own instructions (just like in <code>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.

Examples

With the same rules as before, here is the code that is put into Patch(PatcherArguments args):

// MyMethod definition: bool MyMethod(Game.Swallow swallow, out float result)
this.RedirectMethod(args.Assembly.MainModule.GetType("Game.Swallow"), "GetNeededSwallows", 
                    hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod");

// MyMethod definition: bool MyMethod(out float result)
this.RedirectMethod(args.Assembly.MainModule.GetType("Game.Swallow"), "GetNeededSwallows", 
                    hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod", false);
			 
// MyMethod definition: bool MyMethod(Game.Swallow swallow, bool isUnladen, float distance)
this.RedirectMethod(args.Assembly.MainModule.GetType("Game.Swallow"), "MakeFly",
                    new[] { typeof(bool), typeof(float) },
                    hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod");

 // MyMethod definition: bool MyMethod(Game.Swallow swallow, out float result, int coconuts)
this.RedirectMethod(args.Assembly.MainModule.GetType("Game.Swallow"), "GetNeededSwallows",
                    new[] { typeof(int) },
                    hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod");

// MyMethod definition: bool MyMethod(Game.Swallow swallow, out bool result, bool isNotAfrican)
this.RedirectMethod(args.Assembly.MainModule.GetType("Game.Swallow"), "IsSwallowEuropean",
                    new[] { typeof(bool) },
                    hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod");

// MyMethod definition: bool MyMethod(out bool result, bool isNotAfrican)
this.RedirectMethod(args.Assembly.MainModule.GetType("Game.Swallow"), "IsSwallowEuropean",
                    new[] { typeof(bool) },
                    hookAssembly.MainModule.GetType("MyNamespace.MyClass"), "MyMethod", false);

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.

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.