Looking for Help with Harmony Transpiler

Started by dburgdorf, January 11, 2018, 02:04:33 PM

Previous topic - Next topic

dburgdorf

I posted this on the "Harmony" thread, but figured it couldn't hurt to post it here, as well.

In the "DrawCharacterCard" method of RimWorld.CharacterCardUtility, there is one line:

Find.WindowStack.Add(new Dialog_ChangeNameTriple(pawn));

Which I would like to change to point to a custom method. (I'm toying with the idea of making a pawn's "short title" editable along with his or her nickname, but that's incidental to the point of my question here.)

The problem is that my familiarity with transpilers is extremely limited. I've used them to make changes to static values, but I have no idea how to use them to change method references. Any help would be much appreciated.
- Rainbeau Flambe (aka Darryl Burgdorf) -
Old. Short. Grumpy. Bearded. "Yeah, I'm a dorf."



Buy me a Dr Pepper?

BrokenValkyrie

#1
I took this as an opportunity to learn harmony transpiler myself. You picked a difficult example. I want to be clear, this example is only scratching the surface of transpiler and I spent about 6-8 hours on it. This guide only addresses intercepting a creation of a new object. I am writing this as a guide for modder who are also interested in transpiler.

I highly recommend reading Pardieke transpiler tutorial if you hadn't already. https://gist.github.com/pardeike/c02e29f9e030e6a016422ca8a89eefc9 . A lot of what I am talking about won't make much sense otherwise.  Especially read about IL codes, because it is important. The use of assembly decompiler is mandatory, I'm assuming we are using ILSpy.

This is a custom class that I'd used to inject. All it does is change all first, "middle" and last name of the pawn to given name. Its silly but its for testing purpose.


using RimWorld;
using System;
using UnityEngine;
using Verse;

namespace TranspilerTest
{
public class CustomNameChange : Window
{
private Pawn pawn;

private string curName;

private const int MaxNameLength = 16;

private NameTriple CurPawnName
{
get
{
NameTriple nameTriple = this.pawn.Name as NameTriple;
if (nameTriple != null)
{
//Change the first and last name. Testing for mod purpose.
return new NameTriple(this.curName, this.curName, this.curName);
}
throw new InvalidOperationException();
}
}

public override Vector2 InitialSize
{
get
{
return new Vector2(500f, 175f);
}
}

public CustomNameChange(Pawn pawn)
{
this.pawn = pawn;
this.curName = ((NameTriple)pawn.Name).Nick;
this.forcePause = true;
this.absorbInputAroundWindow = true;
this.closeOnClickedOutside = true;
}

public override void DoWindowContents(Rect inRect)
{
bool flag = false;
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Return)
{
flag = true;
Event.current.Use();
}
Text.Font = GameFont.Medium;
Widgets.Label(new Rect(15f, 15f, 500f, 50f), this.CurPawnName.ToString().Replace(" '' ", " "));
Text.Font = GameFont.Small;
string text = Widgets.TextField(new Rect(15f, 50f, inRect.width / 2f - 20f, 35f), this.curName);
if (text.Length < 16)
{
this.curName = text;
}
if (Widgets.ButtonText(new Rect(inRect.width / 2f + 20f, inRect.height - 35f, inRect.width / 2f - 20f, 35f), "OK", true, false, true) || flag)
{
if (string.IsNullOrEmpty(this.curName))
{
this.curName = ((NameTriple)this.pawn.Name).First;
}
this.pawn.Name = this.CurPawnName;
Find.WindowStack.TryRemove(this, true);
Messages.Message("PawnGainsName".Translate(new object[]
{
this.curName
}), this.pawn, MessageTypeDefOf.PositiveEvent);
}
}
}
}



Here the harmony code.

using System.Collections.Generic;
using RimWorld;
using Verse;
using Harmony;
using System.Reflection;
using System.Reflection.Emit;



namespace TranspilerTest
{
[StaticConstructorOnStartup]
internal static class TranspilerTest
{
static TranspilerTest()
{
HarmonyInstance harmony = HarmonyInstance.Create("com.github.rimworld.mod.TranspilerTest");
harmony.PatchAll(Assembly.GetExecutingAssembly());
}


[HarmonyPatch(typeof(CharacterCardUtility))]
[HarmonyPatch("DrawCharacterCard")]
public static class staticIntercept_DrawCharacterCard
{
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions )
{
//We are going with the simplest strategy, we are count the number of instance of brfalse
int startIndex = -1, brFalseCount = 0;

var codes = new List<CodeInstruction>(instructions);
for (int i = 0; i < codes.Count; i++)
{
if (codes[i].opcode == OpCodes.Brfalse)
{
brFalseCount++;
if (brFalseCount >= 14)
{
//this place the index directly on IL_0411: call class Verse.WindowStack Verse.Find::get_WindowStack() for convenience
startIndex = i + 1;
Log.Message("14th brFalse found");
break;
}

}
}


if (startIndex > -1)
{
/* For reference. Find.WindowStack.Add(new Dialog_ChangeNameTriple(pawn));
IL_0411: call class Verse.WindowStack Verse.Find::get_WindowStack()
IL_0416: ldloc.0
IL_0417: ldfld class Verse.Pawn RimWorld.CharacterCardUtility/'<DrawCharacterCard>c__AnonStorey1'::pawn
IL_041c: newobj instance void Verse.Dialog_ChangeNameTriple::.ctor(class Verse.Pawn)
IL_0421: callvirt instance void Verse.WindowStack::Add(class Verse.Window)
*/


Log.Message("Value call at " + codes[startIndex + 2]);
Log.Message("Found new object call at " + codes[startIndex + 3]);

//at IL_0417 we get the reference to the pawn

//Pawn ILPawn =  codes[startIndex + 2].operand as Pawn;

//operand is the object reference


//Create a constructor for IL code
var constructorInfo =  typeof(CustomNameChange).GetConstructor(
  new[] { typeof(Pawn)});

//Replace index 3 IL_041c: newobj instance void Verse.Dialog_ChangeNameTriple::.ctor(class Verse.Pawn) to call our new class

codes[startIndex + 3] = new CodeInstruction(OpCodes.Newobj, constructorInfo); //codes[startIndex + 3].opcode = OpCodes.Nop;
Log.Message("Found new object call at " + codes[startIndex + 3]);
}

return codes;
//return null;
}
}
}
}



The harmony code won't make sense on its own,  so here the explanation.

Open up ILspy and look in "DrawCharacterCard",  there this segment that interest us.

if (Widgets.ButtonImage(rect8, TexButton.Rename))
{
Find.WindowStack.Add(new Dialog_ChangeNameTriple(pawn));
}


Change viewing mode to IL(Intermediate Language) and the code look like this. You may have to use search to find the code of interest.

IL_03d2: ldloca.s 13
IL_03d4: ldloc.s 11
IL_03d6: ldc.r4 0.0
IL_03db: ldc.r4 30
IL_03e0: ldc.r4 30
IL_03e5: call instance void [UnityEngine]UnityEngine.Rect::.ctor(float32, float32, float32, float32)
IL_03ea: ldloc.s 13
IL_03ec: ldstr "RenameColonist"
IL_03f1: call string Verse.Translator::Translate(string)
IL_03f6: call valuetype Verse.TipSignal Verse.TipSignal::op_Implicit(string)
IL_03fb: call void Verse.TooltipHandler::TipRegion(valuetype [UnityEngine]UnityEngine.Rect, valuetype Verse.TipSignal)
IL_0400: ldloc.s 13
IL_0402: ldsfld class [UnityEngine]UnityEngine.Texture2D Verse.TexButton::Rename
IL_0407: call bool Verse.Widgets::ButtonImage(valuetype [UnityEngine]UnityEngine.Rect, class [UnityEngine]UnityEngine.Texture2D)
IL_040c: brfalse IL_0426

IL_0411: call class Verse.WindowStack Verse.Find::get_WindowStack()
IL_0416: ldloc.0
IL_0417: ldfld class Verse.Pawn RimWorld.CharacterCardUtility/'<DrawCharacterCard>c__AnonStorey1'::pawn
IL_041c: newobj instance void Verse.Dialog_ChangeNameTriple::.ctor(class Verse.Pawn)
IL_0421: callvirt instance void Verse.WindowStack::Add(class Verse.Window)


This is the line we are interested in. newobj will Allocate uninitialized and call the constructor method.

IL_041c: newobj instance void Verse.Dialog_ChangeNameTriple::.ctor(class Verse.Pawn)


There is more than one way to handle this, my way involves just replacing this line with constructor to my own class. There no need to worry about passing in the parameter as line before it prepares it for us.
This pushes the value of a field in a specified object onto the stack. In this case the method pawn value.

IL_0417: ldfld class Verse.Pawn RimWorld.CharacterCardUtility/'<DrawCharacterCard>c__AnonStorey1'::pawn


I am still new to IL so If I get something wrong correct me.
This wikipedia link will come in handy when working in IL code https://en.wikipedia.org/wiki/List_of_CIL_instructions . ILspy gives tooltip to instruction when you hover over it.

Now for one of the hardest and dangerous part, getting the index of the line that we wish to modify. If you read the tutorial you'll know we want find an anchor point. There are many strategy to getting the anchor point to index of interest.

The strategy I am using is to count the occurrence of brfalse, which leads neatly to the bottom block of the IL code. I copied the entire Method IL code to a text editor and then use word search to count from the beginning to IL_40c: brfalse IL_0426 . From this I know there is 14 occurrences.

int startIndex = -1, brFalseCount = 0;
var codes = new List<CodeInstruction>(instructions);
for (int i = 0; i < codes.Count; i++)
{
if (codes[i].opcode == OpCodes.Brfalse)
{
brFalseCount++;
if (brFalseCount >= 14)
{
//this place the index directly on IL_0411: call class Verse.WindowStack Verse.Find::get_WindowStack() for convenience
startIndex = i + 1;
Log.Message("14th brFalse found");
break;
}

}
}

This should place the index at IL_0411: call class Verse.WindowStack Verse.Find::get_WindowStack(). The code I'm interested is 3 index down.

I recommend confirming the correct index before patching as patching the wrong place will cause rimworld to crash.

Log.Message("Value call at " + codes[startIndex + 2]);
Log.Message("Found new object call at " + codes[startIndex + 3]);


Now that I know I have the right index, I create ConstructorInfo to place in our replacement. Then its the matter of just replacing it.

//Create a constructor for IL code
var constructorInfo =  typeof(CustomNameChange).GetConstructor(
  new[] { typeof(Pawn)});

//Replace index 3 IL_041c: newobj instance void Verse.Dialog_ChangeNameTriple::.ctor(class Verse.Pawn) to call our new constructor
codes[startIndex + 3] = new CodeInstruction(OpCodes.Newobj, constructorInfo);


Knowing all this, I am going to stress that transpiler code are version sensitive. Changes to core code can easily break transpiler patch. This is the closest thing modder have to nuclear reactor.

dburgdorf

Valkyrie:

Thanks very much, not only for addressing my question, but also for writing up such a detailed explanation!

As I said above, I've used transpilers myself already, albeit only for simple value replacements or to block certain code segments, so some of what you wrote I'm already familiar with. But your post should be quite valuable to others who haven't yet played with them but want to try.

I can't do much now, as I'm at work, but I look forward to giving this a try this evening.
- Rainbeau Flambe (aka Darryl Burgdorf) -
Old. Short. Grumpy. Bearded. "Yeah, I'm a dorf."



Buy me a Dr Pepper?