Tools Setup
C# Decompiler
You have a few options:
- dnSpy: https://github.com/0xd4d/dnSpy
- ILSpy: https://github.com/icsharpcode/ILSpy
- dotPeek: https://www.jetbrains.com/decompiler/
Each have their advantages and disadvantages. I find dnSpy the easiest to use, but occasionally use ILSpy for transpiler patches.
Whichever you use, Navigate to the Rogue Legacy 2_Data/Managed folder and open up Assembly-CSharp.dll in it. This is the dll that contains all the code for the game. Later we'll talk about patching methods with Harmony; for now, just know that this is how you figure out which methods to patch.
Unity Asset Explorer
(This is optional.)
There are a few asset explorers out there, but I use AssetStudio: https://github.com/Perfare/AssetStudio
This allows you to open the asset bundles in the game folder. These include not only the art and sound assets, but also the serialised objects with their default field values in MonoBehaviour assets.
Visual Studio
Load the mod template into Visual Studio.
You may need to edit solution_private.targets to point to your game folder. Just change this line:
<GamePath>C:\Program Files (x86)\Steam\steamapps\common\Rogue Legacy 2</GamePath>
Change the name of the project to your mod name, change the namespace, and change the details in the BepInPlugin annotation.
namespace Your_Mod_Here {
[BepInPlugin( "YourName.ModName", "Example Mod", "1.0.0" )]
public partial class Test : BaseUnityPlugin {
I have included a simple patch with a setting in the template as an example - either delete or replace it when you create your mod.
Patching Basics
In this guide I am going to use an example of making a mod that shows all spells, traits, and relics in the revealed/seen state when you start a new game.
Often the most difficult part of creating a mod is identifying what you need to change in the game code to have the effect that you want.
There are often many ways to acheive the same result - to demonstrate this the next few examples will all be different ways of showing spells in the seen state.
For detailed information on using Harmony, its official documentation is here: https://harmony.pardeike.net/articles/intro.html
The most common patches are Prefix and Postfix. A Prefix patch happens before the target method, and a Postfix patch happens after.
An advanced option is a Transpiler patch. This involves editing the IL code directly to change something inside the original method, rather than running code before or after the method. These are complex and easy to get wrong so should be avoided if possible, but sometimes it is still the best way of doing what you want.
Each patch is its own class. It has a Prefix, Postfix, or Transpiler method (or any combination of them). The method must be static, but it doesn't matter if it's public, private, etc. I use internal, as this suppresses Visual Studio's incorrect warnings about unused members.
To mark a class as being a Harmony patch, the easiest way is to put a HarmonyPatch annotation on it. There are several kinds, but the most common one is this:
[HarmonyPatch( typeof( ClassName ), "MethodName" )]
The Prefix and Postfix methods can take a number of different arguments:
- Any arguments the original method took, with the same type and name
- The instance of the class the method has been called on: ClassName __instance (two underscores)
- Any fields of the instance, whether public or private. They should have the same name and type as the original, but with three underscores before: FieldType ___fieldName
- The return value of the method, with the ref marker: ref ReturnType __result (two underscores)
All arguments can be marked as ref if you want to change the original value.
Prefix Patch
A Prefix patch runs before the original method. This is useful to change a value in a field before it is read or used in the original method.
In this example I am calling the method to set the ability to seen state (true) before it is read and returned by the original method.
[HarmonyPatch( typeof( PlayerSaveData ), nameof( PlayerSaveData.GetSpellSeenState ) )]
internal static class PlayerSaveData_GetSpellSeenState_Patch {
internal static void Prefix( PlayerSaveData __instance, AbilityType spellType ) {
// Before the method runs to get the seen state, make sure it is set to true (seen)
__instance.SetSpellSeenState( spellType, true );
}
}
Postfix Patch
A Postfix patch runs after the original method. This is useful to change the return value of the original method.
In this example I am overriding the return value with seen (true) in all cases.
[HarmonyPatch( typeof( PlayerSaveData ), nameof( PlayerSaveData.GetSpellSeenState ) )]
internal static class PlayerSaveData_GetSpellSeenState_Patch {
internal static void Postfix( ref bool __result ) {
// Ignore whether it is recorded as seen or not, just set the return value to true (seen)
__result = true;
}
}
Transpiler Patch
A Transpiler patch edits the IL code instructions of the original method. IL code is what is produced when C# is compiled. It is a stack based machine independant intermediate language. Understanding it is too big a topic for this tutorial - you should search for other resources if you are interested in learning it.
dnSpy and ILSpy have views to switch from decompiled C# code to IL code view to see what the instructions are. I find ILSpy better for this, as it has a view of IL code with the C# code lines beside them, making it much easier to find the right instructions in a large method. In dnSpy though, you can put your cursor on a C# code line then switch to IL view and it will place the cursor on the IL code lines related to it.
In this example I am searching the instructions for a field load (OpCodes.Ldloc_0) followed by a return (OpCodes.Ret). When found, I replace the field load with loading 1 (true) instead, so it always returns seen (true).
[HarmonyPatch( typeof( PlayerSaveData ), nameof( PlayerSaveData.GetSpellSeenState ) )]
internal static class PlayerSaveData_GetSpellSeenState_Patch {
internal static IEnumerable<CodeInstruction> Transpiler( IEnumerable<CodeInstruction> instructions ) {
// Put the instructions in a list for easier manipulation and matching using indexes
List<CodeInstruction> instructionList = new List<CodeInstruction>( instructions );
// Set up variables for looping through the instructions
bool found = false;
int i = 0;
// Loop through the instructions until a match has been found, or the end of the method has been reached
while( !found && i < instructionList.Count ) {
// Check for the desired instructions for 'return value;'
if( instructionList[i].opcode == OpCodes.Ldloc_0 && instructionList[i + 1].opcode == OpCodes.Ret ) {
// Change 'value' to '1' (true/seen);
instructionList[i].opcode = OpCodes.Ldc_I4_1;
// Set the found flag to exit the loop
found = true;
}
// Move to the next instruction
i++;
}
// Return the modified instructions
return instructionList.AsEnumerable();
}
}
Transpiler Patch - Alternative
When searching for large blocks of IL code in a transpiler, it can be difficult to keep track of instruction indexes, and very difficult to make easily readable code. To try to solve this I have created a set of transpiler helper classes.
This example is performing the same patch as the one above, but using my transpiler helper.
[HarmonyPatch( typeof( PlayerSaveData ), nameof( PlayerSaveData.GetSpellSeenState ) )]
internal static class PlayerSaveData_GetSpellSeenState_Patch {
internal static IEnumerable<CodeInstruction> Transpiler( IEnumerable<CodeInstruction> instructions ) {
WobPlugin.Log( "PlayerSaveData.GetSpellSeenState Transpiler Patch" );
// Set up the transpiler handler with the instruction list
WobTranspiler transpiler = new WobTranspiler( instructions );
// Perform the patching
transpiler.PatchAll(
// Define the series of IL code instructions that should be matched
new List<WobTranspiler.OpTest> {
/* 0 */ new WobTranspiler.OpTest( OpCodes.Ldloc_0 ), // value
/* 1 */ new WobTranspiler.OpTest( OpCodes.Ret ), // return value;
},
// Define the actions to take when an occurrence is found
new List<WobTranspiler.OpAction> {
new WobTranspiler.OpAction_SetOpcode( 0, OpCodes.Ldc_I4_1 ), // Change 'value' to '1' (true/seen);
} );
// Return the modified instructions
return transpiler.GetResult();
}
}
Special Cases
Single Run Prefix Patch
An alternative to editing a value every time a method is called is to edit all values once the first time only. This is particularly useful if you are multiplying an original field value by somthing, and you only want it it be multiplied once.
In this example I set all traits as seen by looping through the dictionary of them.
[HarmonyPatch( typeof( TraitManager ), nameof( TraitManager.GetTraitSeenState ) )]
internal static class TraitManager_GetTraitFoundState_Patch {
private static bool runOnce = false;
internal static void Prefix( PlayerSaveData __instance ) {
if( !runOnce ) {
// Loop through all traits
foreach( TraitType traitType in __instance.TraitSeenTable.Keys ) {
// Set the seen state to seen
__instance.TraitSeenTable[traitType] = TraitSeenState.SeenTwice;
}
// Record that this has been done, so no need to run again
runOnce = true;
}
}
}
Getters and Setters
Property get and set methods can be patched by adding a MethodType to the annotation.
Example getter annotation:
[HarmonyPatch( typeof( ClassName ), "MethodName", MethodType.Getter )]
Example setter annotation:
[HarmonyPatch( typeof( ClassName ), "MethodName", MethodType.Setter )]
In this example I edit a getter method for relic seen state. This also includes private field access and original method skip for the next parts.
[HarmonyPatch( typeof( RelicObj ), nameof( RelicObj.WasSeen ), MethodType.Getter )]
internal static class RelicObj_WasSeen_Get_Patch {
internal static bool Prefix( RelicObj __instance, ref bool __result ) {
// Navigate to the private field on the instance object, and set its value to true (seen)
Traverse.Create( __instance ).Field( "m_wasSeen" ).SetValue( true );
// Navigate to the private field on the instance object, get its value, and set the patched method's return value
__result = Traverse.Create( __instance ).Field( "m_wasSeen" ).GetValue<bool>();
// Return true to skip running the original method
return true;
}
}
Private Members
If you need edit the value of a private field or call a private method, this can be done using Harmony's Traverse.
// Get a private field value
Traverse.Create( ObjectReference ).Field( "FieldName" ).GetValue<FieldType>();
// Set a private field value
Traverse.Create( ObjectReference ).Field( "FieldName" ).SetValue( NewValue );
To call a method with no parameters:
Traverse.Create( ObjectReference ).Method( "MethodName" ).GetValue();
To call a method with 2 paramaters:
Traverse.Create( ObjectReference ).Method( "MethodName", new System.Type[] { typeof( Param1Type ), typeof( Param2Type ) } ).GetValue( new object[] { Param1Value, Param2Value } );
Skipping the Original Method
A Prefix patch can be made to skip the original method. This is done by replacing the void return type on the patch with bool, and returning a value of true. When you do this, you MUST set the __result parameter value in your patch.
Skipping the original should be avoided if possible. Harmony can allow multiple mods to edit the same method, but using a skip will interfere with this.
0 comments