[Tutorial](A16) Detouring a method (without HugsLib/CCL)

Started by Thirite, February 08, 2017, 08:52:21 PM

Previous topic - Next topic

Thirite

Here's a bit of a quick run through for detouring a method from the base game. Detouring (replacing base game code with your own) is essential for mods that outright change how the game works rather than simply adding new content. Credit goes directly to RawCode and 1000101 for originally writing the code/developing the technique for the CCL which is used in this tutorial. Though the code is from CCL, we can still use a portion of the library that we need without having a dependancy for CCL itself- which isn't fully updated for A16. And while the info for how to do all this is all somewhere scattered around the forum, I figure it would be better to have it all it one place.

This tutorial will assume:
- You have a basic understanding of programming and C#
- You are using MonoDevelop
- You have read the tutorial here on configuring monodevelop to make a mod for RimWorld

First things first, you'll need to add these as .cs files into your solution.
Detours.cs
DetourInjector.cs

These files I graciously stole found somewhere on the forum during my wild goose chase on learning how to mod RimWorld. You don't need to touch Detours.cs at all, it does all the heavy lifting here. All we need to do is modify the DetourInjector.cs which will send instructions to it on what methods to detour and what methods they are replaced with.

Now we have to find the method we want to detour. For this example we'll modify the DropBloodFilth method in the Pawn_HealthTracker class. It will look similar to this:
public void DropBloodFilth ()
{
if ((this.pawn.Spawned || (this.pawn.holder != null && this.pawn.holder.owner is Pawn_CarryTracker)) && this.pawn.PositionHeld.InBounds () && this.pawn.RaceProps.BloodDef != null && !this.pawn.InContainer) {
FilthMaker.MakeFilth (this.pawn.PositionHeld, this.pawn.RaceProps.BloodDef, this.pawn.LabelIndefinite (), 1);
}
}


But we have to convert it so that it will act as a detour. So we need to change a few things. In the end our class file will look something like this:

using System;
using Verse;
using RimWorld;

namespace MyRimworldMod
{
public static class MyDetours
{
internal static void _DropBloodFilth (this Pawn_HealthTracker _this)
{
if ((_this.pawn.Spawned || (_this.pawn.holder != null && _this.pawn.holder.owner is Pawn_CarryTracker)) && _this.pawn.PositionHeld.InBounds () && _this.pawn.RaceProps.BloodDef != null && !_this.pawn.InContainer) {
FilthMaker.MakeFilth (_this.pawn.PositionHeld, _this.pawn.RaceProps.BloodDef, _this.pawn.LabelIndefinite (), 1);
}
}

}
}


Notice we added a parameter! If the detoured method had parameters already, the "this Class _this" parameter would come before them. Where Class is the class holding the method we're detouring. eg:    internal static void DrawEquipment (this PawnRenderer _this, Vector3 rootLoc)

But if you put this in MonoDevelop, oh shit! Why can't we access the field "pawn" in the Pawn_HealthTracker class? Well, for whatever annoying reason it's been set to private. If it was public, this code would already work. However, looks like we need to make a private field accessor. Put this before the _DropBloodFilth method:
static FieldInfo pht_pawn = typeof(Pawn_HealthTracker).GetField ("pawn", BindingFlags.NonPublic | BindingFlags.Instance);

Notice you will have to add "System.Reflection" to the "using" section at the top. MonoDevelop should give you a notice to Resolve this when you right click on the error-marked "BindingFlags..." bit.

Basically what this is doing is for the Pawn_HealthTracker class, it looks inside it for a field named "pawn" which is NonPublic and an Instance. From here we can access it inside our detour method. Now inside our method (at the start) we'll add in a variable to get the 'pawn' field of the arbitrary instance of the Pawn_HealthTracker class (each pawn in game has a copy) that is calling our method.

Pawn _pawn = (Pawn)pht_pawn.GetValue (_this);

Alright! From here on we can now modify the detoured method to use this field accessor. It will look like this in the end.

public static class MyDetours
{
static FieldInfo pht_pawn = typeof(Pawn_HealthTracker).GetField ("pawn", BindingFlags.NonPublic | BindingFlags.Instance);
internal static void _DropBloodFilth (this Pawn_HealthTracker _this)
{
Pawn _pawn = (Pawn)pht_pawn.GetValue (_this);
if ((_pawn.Spawned || (_pawn.holder != null && _pawn.holder.owner is Pawn_CarryTracker)) && _pawn.PositionHeld.InBounds () && _pawn.RaceProps.BloodDef != null && !_pawn.InContainer) {
FilthMaker.MakeFilth (_pawn.PositionHeld, _pawn.RaceProps.BloodDef, _pawn.LabelIndefinite (), 1);
}
}

}


Alright, so now we should have successfully copied/converted the method to work as a detour. But obviously we want to change something, so maybe triple blood spray or something.
internal static void _DropBloodFilth (this Pawn_HealthTracker _this)
{
Pawn _pawn = (Pawn)pht_pawn.GetValue (_this);
if ((_pawn.Spawned || (_pawn.holder != null && _pawn.holder.owner is Pawn_CarryTracker)) && _pawn.PositionHeld.InBounds () && _pawn.RaceProps.BloodDef != null && !_pawn.InContainer) {
for (int i = 3; i != 0; i--) {
FilthMaker.MakeFilth (_pawn.PositionHeld, _pawn.RaceProps.BloodDef, _pawn.LabelIndefinite (), 1);
}
Log.Message("Working as intended :^)");
}
}


Alright, now we just have to go into the DetourInjector.cs file and make sure everything is set up properly to 'inject' our code. For the sake of laziness in writing this tutorial it will be if you copied the file from above. Take a good look at what it's doing to make sure you understand it. It's actually pretty simple. Now compile your dll (make sure it's in an Assemblies folder of an activated mod) and run the game. Now whenever someone drops blood they will drop three times as much, and you will get an annoying message in the log to boot.


Detouring methods with overloads is even more of a pain, but still very possible. If you understand C# and can google, you'll figure it out easily enough. Good luck.


Thirite

Hm, looks like I did it a different way. I used this to get a specific overload of RenderPawnInternal:

typeof(Verse.PawnRenderer).GetMethod("RenderPawnInternal", BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(Vector3), typeof(Quaternion), typeof(Boolean), typeof(Rot4), typeof(Rot4), typeof(RotDrawMode), typeof(Boolean) }, null);

1000101

fyi, while RawCode figured out the 32-bit detours and the basic methodology behind the process, the code provided is from CCL.  I know this because I'm the one who figured out the required changes for 64-bit platforms further, the code and comments in the code you provide is the code and comments I wrote for CCL.  They are free to use but CCL should be credited for the code you have taken as per it's terms of use and license.

For those who are interested in the original code taken from CCL, you can also see CCLs full github code base here.

Remember, it's better to learn than copy-pasta and always give credit where due, not just to a hand-picked few.
(2*b)||!(2*b) - That is the question.
There are 10 kinds of people in this world - those that understand binary and those that don't.

Powered By

Thirite

Right, thanks. I have a terrible memory so I can't remember where I found the files but someone else must have taken them from CCL. I've updated the OP to reflect proper attribution.

1000101

(2*b)||!(2*b) - That is the question.
There are 10 kinds of people in this world - those that understand binary and those that don't.

Powered By

vietarmis

I'm using the injection code from here, but it just swaps out the function calls entirely, right? Is there a way to run the original code as well?

Thirite

You're supposed to copy/paste/convert the method to work as a detour, like I describe in the tutorial. Then modify it afterwards. The purpose of detouring isn't to simply append code to an existing function call, but to replace the function call entirely.

vietarmis

That's the issue I'm having. I want to add functionality without changing the existing functions.

RawCode

Quote from: vietarmis on February 11, 2017, 10:00:05 PM
That's the issue I'm having. I want to add functionality without changing the existing functions.

methods hooked fully and completely, you are not allowed to hook part of method, if you need part hooking, well, time to learn x86 assembler (or CLI.EMIT) and write your own library.

it's perfectly possible, both IL and x86 levels, but, what reason to inject opcode A after opcode B when you can provide entire function with correct sequence of opcodes?

Thirite

Quote from: vietarmis on February 11, 2017, 10:00:05 PM
That's the issue I'm having. I want to add functionality without changing the existing functions.

Well, that's kind of the entire point of detouring... changing existing methods. Whether you're tacking on some more code to it without actually changing the rest, you still have to detour the entire method.