Skip to content

Commit

Permalink
Updated Harmony documentation and added a migration guide to HarmonyX.
Browse files Browse the repository at this point in the history
  • Loading branch information
CptMoore committed Mar 17, 2023
1 parent d14dc9b commit 93e796e
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 10 deletions.
111 changes: 110 additions & 1 deletion doc/HARMONY12X.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,120 @@
# HarmonyX support

BepInEx is a modding tool and a modding community that provides an alternative to the up-to-date Harmony2 called HarmonyX.
To allow support for mods using other versions, they implemented "shims" that redirect calls from older harmony versions to HarmonyX.
To allow support for mods using other versions, BepInEx provides "shims" that redirect calls from older harmony versions to HarmonyX.

ModTek supports loading up HarmonyX with the BepInEx shims for Harmony 1.2 and Harmony 2, see the ModTekPreloader configuration to enable that feature.
This allows mods written against those Harmony versions to coexist.

## Migration Guide

If your mod uses Harmony 1 and you want to migrate to HarmonyX instead of letting ModTek shim during runtime, one can follow the steps outlined below:

Preparation:
1. Add a global using file called `GlobalUsings.cs` and add `global using Harmony;` to it
2. Remove `using Harmony;` from any source files, best use Resharper and Rider to automatically find and remove un-used using statements.

Actual migration:
1. In the mods `csproj` file, replace the 0Harmony (Harmony 1) Reference with a HarmonyX PackageReference.
```csharp
<Reference Include="0Harmony">
<Private>False</Private>
</Reference>
```
HarmonyX:
```csharp
<PackageReference Include="HarmonyX" Version="2.10.1">
<PrivateAssets>all</PrivateAssets>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
```
2. Change the global using introduced in the preparation to `global using HarmonyLib;`
3. Update the Harmony patching mechanism, as it changed.
```csharp
var harmony = HarmonyInstance.Create("my harmony identifier);
harmony.PatchAll(Assembly.GetExecutingAssembly());
```
HarmonyX:
```csharp
Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "my harmony identifier");
```
4. In every patch prefix, add a __runOriginal check and return if a previous patch wants to skip the original method.
That emulates the behavior from Harmony1 and is necessary so other mods can make your prefix not execute.
Prefixes not skipping automatically anymore is the main difference between Harmony from pardeike and HarmonyX from BepInEx.

Optionally one can now replace any try/catch the game with the [HarmonySafeWrap] attribute, exception thrown by the patch will be logged
to the HBS Logger "HarmonyX" and thus will be available in the logs under `.modtek`.
```csharp
[HarmonyPrefix]
public static bool Prefix(MechDef mech, ref HashSet<string> __result)
{
if (mech == null)
{
return true;
}

try
{
// something complicated here
return false;
}
catch (Exception e)
{
Log.Main.Error?.Log("This should not have happened", e);
return true;
}
}
```
HarmonyX:
```csharp
[HarmonyPrefix]
[HarmonySafeWrap]
public static void Prefix(ref bool __runOriginal, MechDef mech, ref HashSet<string> __result)
{
if (!__runOriginal)
{
return;
}

if (mech == null)
{
return;
}

// something complicated here
__runOriginal = false;
}
```
5. One can also add `[HarmonySafeWrap]` to all postfixes to keep it consistent with the prefixes.
```csharp
[HarmonyPostfix]
public static bool Postfix(MechDef mech, ref HashSet<string> __result)
{
try
{
// something complicated here
}
catch
{
// maybe some logging is being done here
// or we just don't want to crash the whole game when our mod misbehaves
}
}
```
HarmonyX:
```csharp
[HarmonyPostfix]
[HarmonySafeWrap]
public static void Postfix(MechDef mech, ref HashSet<string> __result)
{
// something complicated here
}
```

See the BattleTech, ModTek or Harmony logs in `.modtek` in case you encounter errors.

## How it works internally

Note that in order for it to work, the ModTekPreloader has to intercept assembly load calls and rewrite the assembly. They are then saved to disk
Expand Down
18 changes: 9 additions & 9 deletions doc/MOD_DLL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This tutorial/walk-through uses:
* BattleTech itself
* [Visual Studio Community 2022](https://www.visualstudio.com/downloads/) to write and compile your mod's `.dll`
* [dnSpyEx](https://github.com/dnSpyEx/dnSpy) to decompile game assembly back to C#
* [Harmony](https://github.com/pardeike/Harmony) to patch methods at runtime
* [HarmonyX](https://github.com/BepInEx/HarmonyX) to patch methods at runtime

## Do Your Research

Expand All @@ -24,17 +24,19 @@ In our case, we want to simply add the condition that you cannot launch a missio

## Setting Your Project Up

Create a new project and solution with Visual Studio. You should target .NET 4.7.1, as this is what the game is using. See [ModTek.csproj](../ModTek/ModTek.csproj) on how your csproj can look like.
Copy the mod template found in the same directory as the following csproj file: [ModTemplateWithHarmonyX.csproj](../examples/ModTemplateWithHarmonyX/ModTemplateWithHarmonyX.csproj)

On the right, make sure that you add references to the `0Harmony.dll` and `Assembly-CSharp.dll`. If you will be using the settings from your `mod.json`, you will also need `Newtonsoft.Json.dll`.
Open your copy of the mod template with Visual Studio or Rider, it will complain about not finding the BattleTech directory.

It's a hassle to change the other references like System and such to match the installed game and unless you're doing something special, you can skip changing the references. You do not need to reference ModTek.
Copy [CHANGEME.Directory.Build.props](../CHANGEME.Directory.Build.props) as `Directory.Build.props` to your template copy and modify its contents to so that the BattleTechGameDir variable points to your BattleTech installation directory.

For more best practices see the [Development Guide](DEVELOPMENT_GUIDE.md)

## Actually Writing Your Mod

Now that you understand how the functionality works, you can change it. In our case, we want `ValidateLance` to return false when the lance is overweight, as well as fill in the same values that the function already does (i.e. we want to have exactly the same side effects as if the code was written in the method itself). It would be relatively easy to just hop into changing it in dnSpy and recompiling the method -- but we can't do that in this case, since we want to have a seperate `.dll` that does it at runtime.

That's why we're using [Harmony](https://github.com/pardeike/Harmony) -- it allows you to "hook" onto methods before and after they are called, as well as to directly modify the executed IL code with a transpiler. The 'hooks' before are called Prefixes; they can modify the parameters passed into the function, as well as actually prevent the original code from being called, and the hooks after are called Postfixes; which can modify what the function returns. You can learn more about Harmony from looking at it's [wiki](https://github.com/pardeike/Harmony/wiki) and looking through other people's Harmony-based mods.
That's why we're using [HarmonyX](https://github.com/BepInEx/HarmonyX) -- it allows you to "hook" onto methods before and after they are called, as well as to directly modify the executed IL code with a transpiler. The 'hooks' before are called Prefixes; they can modify the parameters passed into the function, as well as actually prevent the original code from being called, and the hooks after are called Postfixes; which can modify what the function returns. You can learn more about Harmony from looking at it's [wiki](https://github.com/pardeike/Harmony/wiki) and looking through other people's Harmony-based mods.

Since `ValidateLance` doesn't have parameters, we still want the original code to execute, and we want to change `ValidateLance`'s return value, we'll use a postfix patch, which something like this:

Expand All @@ -57,8 +59,7 @@ public static class DropLimit
{
public static void Init(string directory, string settingsJSON)
{
var harmony = HarmonyInstance.Create("io.github.mpstark.DropLimit");
harmony.PatchAll(Assembly.GetExecutingAssembly());
Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "io.github.mpstark.DropLimit");
}
}
```
Expand Down Expand Up @@ -119,8 +120,7 @@ internal class ModSettings
internal static ModSettings Settings = new ModSettings();
public static void Init(string directory, string settingsJSON)
{
var harmony = HarmonyInstance.Create("io.github.mpstark.DropLimit");
harmony.PatchAll(Assembly.GetExecutingAssembly());
var harmony = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "io.github.mpstark.DropLimit");

// read settings
try
Expand Down

0 comments on commit 93e796e

Please sign in to comment.