Patching Utilities
RogueLibs provides several utilities to help you with patching. Whether you use Harmony's attributes, Harmony instances directly, RogueLibs' stuff or something else, is your choice. All of them have their own pros and cons. You can learn more about Harmony here.
RLSetup
attribute
Since RogueLibs handles custom stuff as classes, you might forget to initialize a new class in your plugin's Awake
. That's why RLSetup
attribute is here. You can just add it to a static method, and initialize your custom thing in there.
public class MyCustomItem : CustomItem
{
[RLSetup]
public static void Setup()
{
RogueLibs.CreateCustomItem<MyCustomItem>()
.WithName(new CustomNameInfo("Name"))
.WithDescription(new CustomNameInfo("Description"))
.WithSprite(Properties.Resources.Sprite)
.WithUnlock(new ItemUnlock());
RogueLibs.CreateCustomName("SomeName", "Dialogue", new CustomNameInfo("Text"));
}
}
You'll just have to call the following method in your plugin's Awake
:
public void Awake()
{
RogueLibs.LoadFromAssembly();
/* ... */
}
Seriously, you should use it. It helps with versioning too. All of the logic in one place.
RoguePatcher
RoguePatcher
is a small helper class that makes writing patches a little bit faster and easier. If you need more control (patch order, priority, etc.), then you should use the original Harmony methods.
- RoguePatcher
- Harmony
RoguePatcher patcher = new RoguePatcher(this);
patcher.Postfix(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));
patcher.Postfix(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
Harmony harmony = new Harmony(pluginGUID);
MethodInfo original = AccessTools.Method(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));
MethodInfo patch = AccessTools.Method(GetType(), nameof(MyPatchMethod));
harmony.Patch(original, new HarmonyMethod(patch));
original = AccessTools.Method(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
patch = AccessTools.Method(GetType(), nameof(MyPatchMethod2));
harmony.Patch(original, new HarmonyMethod(patch));
Instead of specifying method names using strings, you should specify them using the nameof
keyword. Use string names only if the method you're trying to patch is not public.
Patch methods should have the following name: <TargetType>_<TargetMethod>
. In the example above, RoguePatcher
will search for patch methods called StatusEffects_hasStatusEffect
and InvDatabase_ChooseArmor
in your plugin's class.
You can change the type to search patch methods in. Specify it in the constructor or set the property between patches:
- RoguePatcher
- Harmony
public class MyCoolPlugin : BaseUnityPlugin
{
public void Awake()
{
RoguePatcher patcher = new RoguePatcher(this, typeof(MyCoolPatches));
patcher.Postfix(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));
patcher.TypeWithPatches = typeof(MyEvenCoolerPatches);
patcher.Postfix(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
}
}
public class MyCoolPatches
{
public static void StatusEffects_hasStatusEffect(StatusEffects __instance)
{
/* ... */
}
}
public class MyEvenCoolerPatches
{
public static void InvDatabase_ChooseArmor(InvDatabase __instance, string previousArmorName)
{
/* ... */
}
}
public class MyCoolPlugin : BaseUnityPlugin
{
public void Awake()
{
Harmony harmony = new Harmony(pluginGUID);
MethodInfo original = AccessTools.Method(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));
MethodInfo patch = AccessTools.Method(typeof(MyCoolPatches), nameof(MyPatchMethod));
harmony.Patch(original, new HarmonyMethod(patch));
original = AccessTools.Method(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
patch = AccessTools.Method(typeof(MyEvenCoolerPatches), nameof(MyPatchMethod2));
harmony.Patch(original, new HarmonyMethod(patch));
}
}
public class MyCoolPatches
{
public static void MyPatchMethod(StatusEffects __instance)
{
/* ... */
}
}
public class MyEvenCoolerPatches
{
public static void MyPatchMethod2(InvDatabase __instance, string previousArmorName)
{
/* ... */
}
}
Transpiler helper methods
Transpilers are kind of complicated, since they use a low-level Intermediate Language (IL) instead of C# (branches, loops and conditions are the hardest part in here). As an example, here's one of the simplest transpilers from RogueLibs:
- Helper methods
- Harmony
public static IEnumerable<CodeInstruction> StatusEffects_AddStatusEffect(IEnumerable<CodeInstruction> codeEnumerable)
=> codeEnumerable.AddRegionAfter(
new Func<CodeInstruction, bool>[]
{
i => i.IsLdloc(),
i => i.opcode == OpCodes.Ldarg_3,
i => i.opcode == OpCodes.Stfld && i.StoresField(causingAgentField),
},
new Func<CodeInstruction[], CodeInstruction>[]
{
a => a[0],
_ => new CodeInstruction(OpCodes.Ldarg_0),
_ => new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(SetupEffectHook))),
});
private static readonly FieldInfo causingAgentField = typeof(StatusEffect).GetField(nameof(StatusEffect.causingAgent));
public static IEnumerable<CodeInstruction> StatusEffects_AddStatusEffect(IEnumerable<CodeInstruction> code)
{
bool searching = true;
int current = 0;
CodeInstruction[] matches = new CodeInstruction[after.Length];
foreach (CodeInstruction instr in code)
{
yield return instr;
if (searching)
{
if (current is 0 ? instr.IsLdloc()
: current is 1 ? instr.opcode == OpCodes.Ldarg_3
: instr.opcode == OpCodes.Stfld && instr.StoresField(causingAgentField))
{
matches[current] = instr;
if (++current is 3)
{
searching = false;
yield return matches[0];
yield return new CodeInstruction(OpCodes.Ldarg_0);
yield return new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(SetupEffectHook)));
}
}
else current = 0;
}
}
}
private static readonly FieldInfo causingAgentField = typeof(StatusEffect).GetField(nameof(StatusEffect.causingAgent));
Yeah, it looks easy. But that's only because it's a very simple example.
When writing predicates, keep in mind that they might get called hundreds or thousands of times. For example, you can pre-calculate the FieldInfo
value, used by your predicate, just put it in a static readonly field, like in the example above.
Heavy calculations like that can cost you hundreds of milliseconds of start-up time (or even entire seconds, if you're working on a big project).
Here's another example from RogueLibs:
- Helper methods
- Harmony
public static IEnumerable<CodeInstruction> Unlocks_LoadInitialUnlocks(IEnumerable<CodeInstruction> codeEnumerable)
=> codeEnumerable.ReplaceRegion(
new Func<CodeInstruction, bool>[]
{
i => i.opcode == OpCodes.Callvirt && i.Calls(List_Unlock_GetEnumerator),
i => i.IsStloc(),
},
new Func<CodeInstruction, bool>[]
{
i => i.opcode == OpCodes.Callvirt,
i => i.opcode == OpCodes.Endfinally,
i => i.opcode == OpCodes.Ldarg_0,
},
new CodeInstruction[]
{
new CodeInstruction(OpCodes.Pop),
new CodeInstruction(OpCodes.Pop),
new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(LoadUnlockWrappersAndCategorize))),
});
private static readonly MethodInfo List_Unlock_GetEnumerator = typeof(List<Unlock>).GetMethod("GetEnumerator");
public static IEnumerable<CodeInstruction> Unlocks_LoadInitialUnlocks(IEnumerable<CodeInstruction> code)
{
int state = 0;
int current = 0;
CodeInstruction[] beginCache = new CodeInstruction[2];
foreach (CodeInstruction instr in code)
{
if (state is 2)
yield return instr;
else if (state is 0)
{
if (current is 0 ? instr.opcode == OpCodes.Callvirt && instr.Calls(List_Unlock_GetEnumerator)
: instr.IsStloc())
{
beginCache[current] = instr;
if (++current == 2)
{
state = 1;
current = 0;
}
}
else
{
if (current > 0)
{
for (int i = 0; i < current; i++)
yield return beginCache[i];
current = 0;
}
yield return instr;
}
}
else
{
if (current is 0 ? instr.opcode == OpCodes.Callvirt
: current is 1 ? instr.opcode == OpCodes.Endfinally
: instr.opcode == OpCodes.Ldarg_0)
{
if (++current == 3)
{
yield return new CodeInstruction(OpCodes.Pop);
yield return new CodeInstruction(OpCodes.Pop);
yield return new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(LoadUnlockWrappersAndCategorize)));
}
}
else current = 0;
}
}
}
private static readonly MethodInfo List_Unlock_GetEnumerator = typeof(List<Unlock>).GetMethod("GetEnumerator");
Still relatively simple. I just don't want to spend my time writing a really complicated example.
You might also want to consider using BlazingTwist's BTHarmonyUtils. It provides several useful transpiler-patching utilities, similar to the ones in RogueLibs. It is easier to use, but I don't think there's any documentation on it.