-
Notifications
You must be signed in to change notification settings - Fork 9
SHORTTUTORIAL: Harmony
Roxx Ploxx edited this page Jun 1, 2017
·
12 revisions
Harmony is hard to understand, but easy to use once you get it. Except for the exceptional cases; they'll screw with you.
- Install via HugsLib: https://github.com/UnlimitedHugs/RimworldHugsLib
- Or, direct link to Harmony: https://github.com/pardeike/Harmony
- Other help pages: Harmony's Wiki and the HugsLib Wiki
- Harmony is great to run code before (Prefix) or after (Postfix) an existing method. Usually this is all you need.
- This doesn't change existing functionality (i.e. other mods), and can run in parallel with other Harmony patches.
- Harmony can transpile code to inject code INSIDE an existing method... but don't do this as it can impact existing code and (unless you're a pro) it's just a PITA.
- You can use Attributes to perform Harmony's magic but I am not showing that here (kinda seems like a PITA except for power users).
Here's a simple case that you can copy and paste to your code and get started with. This code example was created because I added a skill to RimWorld and now it doesn't show because the screen is too small. So, I want to make the screen change size based on the number of Skills in the game:
using Harmony;
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Verse;
namespace UnificaMagica
{
[StaticConstructorOnStartup]
static class HarmonyPatches
{
// this static constructor runs to create a HarmonyInstance and install a patch.
static HarmonyPatches()
{
HarmonyInstance harmony = HarmonyInstance.Create("rimworld.roxxploxx.unificamagica");
// find the FillTab method of the class RimWorld.ITab_Pawn_Character
MethodInfo targetmethod = AccessTools.Method(typeof(RimWorld.ITab_Pawn_Character),"FillTab");
// find the static method to call before (i.e. Prefix) the targetmethod
HarmonyMethod prefixmethod = new HarmonyMethod(typeof(UnificaMagica.HarmonyPatches).GetMethod("FillTab_Prefix"));
// patch the targetmethod, by calling prefixmethod before it runs, with no postfixmethod (i.e. null)
harmony.Patch( targetmethod, prefixmethod, null ) ;
}
// This method is now always called right before RimWorld.ITab_Pawn_Character.FillTab.
// So, before the ITab_Pawn_Character is instantiated, reset the height of the dialog window.
// The class RimWorld.ITab_Pawn_Character is static so there is no this __instance.
public static void FillTab_Prefix() {
RimWorld.CharacterCardUtility.PawnCardSize.y = DefDatabase<RimWorld.SkillDef>.AllDefsListForReading.Count * 47.5f;
}
}
}
The above example and these points will get you to the next level of understanding of Harmony.
-
this in a method is handled by the static prefix/postfix method having the 1st parameter be the '__instance' of the type of the class that is being targeted.
static void Method_PostFix(TargetClass __instance, ... ) { ... }
- Return values of the TargetMethod are handled by a ref to the __result parameter of the type of the TargetMethod's return type
- If target method is
int Bar(float a, string b)
in classFoo
, the Postfix is: -
static void Bar_PostFix(Foo __instance, float a, string b, ref int __result);
- NOTE:
Bar_Postfix
does not have to be the name of the method, as it can be whatever you want it to be.
- NOTE:
- If target method is
- Prefix methods ref all parameters
- If target method is
int Bar(float a, string b)
in classFoo
, the PreFix is: -
static void Bar_Prefix(Foo __instance, ref float a, ref string b);
- NOTE: the use of refs for parameters
- If target method is
Ok, you should now know enough to understand these other resources. Enjoy!
- Read the Harmony wiki page on patching: Patching
- especially read the bullet point lists at the bottom
- Read the HugsLib wiki page on patching: Patching
- There is a ton of syntactic sugar via C#'s Attributes that Harmony uses: About Harmony Attributes.
- Because methods can be overloaded (i.e. method names with different types of parameters), the Type.GetMethod(...) call can throw exceptions because of multiple matches. So, you specify the parameters as shown here.
- Now, just read the Wiki again.
- If you really think you understand this, figure this code example out from spdskatr:
using System;
using System.Collections.Generic;
using System.Linq;
using Verse;
using UnityEngine;
using RimWorld;
using Harmony;
using System.Reflection;
using System.Reflection.Emit;
namespace Swimming
{
[HarmonyPatch(typeof(PawnRenderer), "RenderPawnInternal", new Type[] { typeof(Vector3), typeof(Quaternion), typeof(bool), typeof(Rot4), typeof(Rot4), typeof(RotDrawMode), typeof(bool), typeof(bool)}), StaticConstructorOnStartup]
static class Patch_PawnRenderer
{
static Patch_PawnRenderer()
{
var harmonyInstance = HarmonyInstance.Create("com.spdskatr.swimming.patches");
harmonyInstance.PatchAll(Assembly.GetExecutingAssembly());
Log.Message(
"SS Raiders Can Swim Initialized. Patches:\n" +
"(Prefix non-destructive) Verse.PawnRenderer.RenderPawnInternal Overload with 7 parameters\n" +
"(Transpiler infix injection at IL_0041 (brtrue IL_007f)): Verse.Graphic_Shadow.DrawWorker\n" +
"(Transpiler infix injection at IL_0048 (ldc.r4 1))Verse.ShotReport.get_FactorFromPosture\n\n");
}
static void Prefix(ref bool renderBody, PawnRenderer __instance)
{
var pawn = Traverse.Create(__instance).Field("pawn").GetValue<Pawn>();
if (pawn != null
&& !pawn.Dead
&& pawn.Map != null
&& pawn.RaceProps.Humanlike
&& pawn.Position.GetTerrain(pawn.Map) != null
&& (pawn.Position.GetTerrain(pawn.Map).label == "deep water" || pawn.Position.GetTerrain(pawn.Map) == TerrainDefOf.WaterDeep))
{
renderBody = false;
}
}
}
[HarmonyPatch(typeof(Graphic_Shadow), "DrawWorker", new Type[] { typeof(Vector3), typeof(Rot4), typeof(ThingDef), typeof(Thing) })]
static class Patch_Shadows
{
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
var instructionsList = instructions.ToList();
for (var i = 0; i < instructionsList.Count; i++)
{
var instruction = instructionsList[i];
yield return instruction;
if (instruction.opcode == OpCodes.Brtrue
&& instructionsList[i - 1].operand == typeof(RoofGrid).GetMethod("Roofed", new Type[] { typeof(IntVec3) })) //Identifier for which IL line to inject to
{
//Start of injection
yield return new CodeInstruction(OpCodes.Ldarg_1);//First argument for both our method and its own
yield return new CodeInstruction(OpCodes.Ldarg_S, (byte)4);//Second argument for our method, fourth argument for its own: Thing thing
yield return new CodeInstruction(OpCodes.Call, typeof(Patch_Shadows).GetMethod("SatisfiesNoShadow"));//Injected code
yield return new CodeInstruction(OpCodes.Brtrue, instruction.operand);//If true, break to exactly where the original instruction went
}
}
}
public static bool SatisfiesNoShadow(IntVec3 loc, Thing thing)
{
var terrain = thing.Position.GetTerrain(thing.Map);
return thing is Pawn
&& (terrain == TerrainDefOf.WaterDeep
|| terrain.label.ToLower() == TerrainDefOf.WaterDeep.label.ToLower());
}
}
[HarmonyPatch]
static class Patch_ShotReport
{
static MethodInfo TargetMethod()
{
return typeof(ShotReport).GetProperty("FactorFromPosture", AccessTools.all).GetGetMethod(true);
}
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
var list = instructions.ToList();
for (int i = 0; i < list.Count; i++)
{
var instruction = list[i];
if (instruction.opcode == OpCodes.Ret && list[i-1].operand is float f && f - 0.9f > 0f)//f should be 1f
{
yield return new CodeInstruction(OpCodes.Ldarg_0);
yield return new CodeInstruction(OpCodes.Ldflda, AccessTools.Field(typeof(ShotReport), "target"));
//Since reflection doesnt work for this, I'm manually loading the private variable "target" with IL
yield return new CodeInstruction(OpCodes.Call, typeof(TargetInfo).GetProperty("Thing").GetGetMethod());
yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Patch_ShotReport), nameof(Manual)));
}
yield return instruction;
}
}
static float Manual(float result, Thing thing)
{
//0.2 factor for body size when in water
if (thing is Pawn &&
(thing.PositionHeld.GetTerrain(thing.Map).label == "deep water" ||
thing.PositionHeld.GetTerrain(thing.Map) == TerrainDefOf.WaterDeep))
{
result = 0.2f;
}
return result;
}
}
}