Difference between revisions of "Custom Maid 3D2/Modding/Plugins"
(→Pre-patching phase: Rephrased a sentence concerning the number of RequestAssembly calls) |
m (→Pre-patching checks: Added a minor explanation.) |
||
Line 313: | Line 313: | ||
=== Pre-patching checks === | === Pre-patching checks === | ||
The <code>CanPatch(PatcherArguments args)</code> method override is called just about before applying the patch. If we return <code>true</code>, ReiPatcher will call <code>Patch(PatcherArguments args)</code>. | The <code>CanPatch(PatcherArguments args)</code> method override is called just about before applying the patch. If we return <code>true</code>, ReiPatcher will call <code>Patch(PatcherArguments args)</code>. | ||
− | Note that <code>CanPatch</code> 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 <code>Patch(PatcherArguments args)</code> method will be called the right number of times. | + | Note that <code>CanPatch</code> 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 <code>Patch(PatcherArguments args)</code> method will be called the right number of times and on the right assemblies. |
To assist checking, ReiPatcher provides <code>PatcherArguments</code> parameter, that contains the following properties: | To assist checking, ReiPatcher provides <code>PatcherArguments</code> parameter, that contains the following properties: |
Revision as of 10:42, 7 August 2015
- Recent changes
- All pages
- Wiki tutorial
- Purge (this page)
all characters are at least 18
- Modding
- Patching 101: How to ReiPatch for dummies
- Writing plug-ins with ReiPatcher and UnityInjector
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.
Contents
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.
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
- 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.
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:
- Download and fire up ILSpy.
- Click
File->Open
(or pressCtrl+O
) and navigate to<Game's Main Directory>\CM3D2(x86/x64)_Data\Managed
(choose either x86 or x64 folder). - Choose
Assembly-CSharp.dll
and hit "Open". That will add the library to the list of loaded ones. - 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.
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:
- Open Visual Studio. Click
File->New->Project
(or pressCtrl+Shift+N
). - From templates, choose
Visual C#->Class Library
(orVisual C#->Windows Desktop->Empty Project
). - Above the list of project templates change .NET Framework version to 3.5 if it isn't already.
- Give a name to your project in the
Name
field. In the case of this tutorial it shall beCM3D2.HelloWorld.Hook
. - Give a name to your solution name in the
Solution name
field. In our case it isCM3D2.HelloWorld
. - Change location of the solution if needed.
- Make sure
Create directory for solution
is checked. Click "OK" and wait for solution to load up. - In Solution Explorer, right click on solution's name and go
Properties->Configuration Properties->Configuration->Configuration Manager
. - 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.
- Right click on each project and go to
Properties
and changeOutput type
to "Class Library". Save the project by pressingCtrl+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:
- In Solution Explorer, click on the small arrow left to the project name to expand it.
- Right click on
References
and clickAdd Reference...
. - In the newly-opened window go to
Browse
tab and clickBrowse...
button. - 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.
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 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:
- Get assembly's module, which is the property
args.Assembly.MainModule
, and use itsGetType(string fullName)
method to acquire the type definition forUIButton
. - Get hook assembly's module, which is the property
hookAssembly.MainModule
, and acquire the type definition forCM3D2.HelloWorld.Hook.HelloWorldHooks
(notice the use of full name containing the namespace(s) and type name). - Find the
MethodDefinition
forOnClick()
method usingUIButton
's type definition. - Repeat the same process for
OnClickHook(UIButton button)
method using acquired type definition. - Using hook assembly's module and method definition, get
MethodReference
forHelloWorldHooks.OnClickHook()
. - Using
UIButton.OnClick()
's method definition, get its method body and from it an instance ofILProcessor
. - Use the IL processor to insert
ldarg.0
andcall [method reference to OnClickHook()]
instructions before the method's original first instruction. - 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 MonoBehavior
s.
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 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.
Note that accessing ReiPatcherPlus's methods requires one to use this.
, or the extension method's won't be seen.
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 tofalse
, 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 tofalse
, 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 returnstrue
, the redirected method will return immediately without continuing the execution. Iffalse/code> is returned, the redirected method will proceed to its own instructions (just like in <code>HookMethod
). -
passSelf
acts the same as inHookMethod
and therefore requires an object of the type with the redirected method (ifpassSelf=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 (ifpassSelf=true
): reference type of the redirected method's return value (that is an argument that hasout
word before the type name). The value of this argument can be set by the redirect method. If redirect method returnstrue
, 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.
|