Skip to content

SHORTTUTORIAL: Harmony

roxxploxx edited this page May 19, 2017 · 12 revisions

Harmony is a way to non-desctructively (mostly) inject code into an existing DLL.

It is hard to understand, but easy to use once you get it. Except for the exceptional cases; they'll screw with you.


Links

How To Use

  • 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 regular coding styles or Attributes to perform Harmony's magic.

Simple Example

Here's a simple case. 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;
        }
    }
}

Further Steps of Learning

  • Read the Harmony wiki page on patching: Patching
    • especially read the bullet point lists at the bottom
  • 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;
        }
    }
}

Clone this wiki locally