Skip to content

Instantly share code, notes, and snippets.

@egocarib
Created August 20, 2020 04:56
Show Gist options
  • Save egocarib/d4ea351536a44e164625fefeae415fe5 to your computer and use it in GitHub Desktop.
Save egocarib/d4ea351536a44e164625fefeae415fe5 to your computer and use it in GitHub Desktop.
Example Qud Transpiler
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Collections.Generic;
using HarmonyLib;
using XRL.Core;
using XRL.World.Parts;
using UnityEngine;
namespace Egocarib.Junkbunker
{
public static class Patch_Helpers
{
// Checks whether two CodeInstructions are equivalent. Supports only certain OpCodes.
public static bool InstructionsAreEqual(CodeInstruction i1, CodeInstruction i2)
{
if (i1.opcode == i2.opcode)
{
if (i1.opcode == OpCodes.Ldstr)
{
return ((string)i1.operand) == ((string)i2.operand);
}
else if (i1.opcode == OpCodes.Call || i1.opcode == OpCodes.Callvirt || i1.opcode == OpCodes.Ldsfld || i1.opcode == OpCodes.Ldfld)
{
return ((MemberInfo)i1.operand).MetadataToken == ((MemberInfo)i2.operand).MetadataToken;
}
else if (i1.operand == i2.operand)
{
return true;
}
}
return false;
}
}
/*
* The following patch bypasses the swim confirmation message that appears before entering a
* swim-depth body of water, if the player currently has a flexiboard equipped. We just need to
* insert two new IL instructions in the appropriate spot to perform that equip check and
* suppress the message in GameObject.Move().
*
*/
[HarmonyPatch(typeof(XRL.World.GameObject))]
public class Patch_XRL_World_GameObject
{
public static MethodInfo Method_HasEffect = typeof(XRL.World.GameObject).GetMethod("HasEffect", new Type[] { typeof(string) });
public static MethodInfo Method_HasBridge = typeof(XRL.World.Cell).GetMethod("HasBridge");
public static FieldInfo Field_XRLCore = typeof(XRLCore).GetField("Core");
public static FieldInfo Field_MoveConfirmDirection = typeof(XRLCore).GetField("MoveConfirmDirection");
public static MethodInfo Method_IsFlexiboardEquipped = typeof(Egcb_Flexiboard).GetMethod("PlayerHasFlexiboardEquipped");
public static MethodInfo Method_StringNotEqual = typeof(System.String).GetMethod("op_Inequality");
//instruction sequence we are looking for. We'll insert our patch after this.
private readonly static List<CodeInstruction> TargetInstructions = new List<CodeInstruction>()
{
new CodeInstruction(OpCodes.Ldstr, "Swimming"),
new CodeInstruction(OpCodes.Call, Method_HasEffect),
new CodeInstruction(OpCodes.Callvirt, Method_HasBridge),
new CodeInstruction(OpCodes.Ldsfld, Field_XRLCore),
new CodeInstruction(OpCodes.Ldfld, Field_MoveConfirmDirection),
new CodeInstruction(OpCodes.Ldarg_1, null),
new CodeInstruction(OpCodes.Call, Method_StringNotEqual)
};
//max allowed distance between individual instructions in the above sequence
private static int AllowedInstructionDistance = 10;
[HarmonyTranspiler]
[HarmonyPatch("Move")]
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
int idx = 0;
int gap = 0;
object jumpTarget = null;
bool found = false;
bool patchComplete = false;
foreach (var instruction in instructions)
{
if (found)
{
if (!patchComplete)
{
if (jumpTarget == null)
{
if (instruction.opcode != OpCodes.Brfalse)
{
throw new Exception("Exception in Junkbunker Mod - Unexpected opcode found in GameObject.Move patch transpiler.");
}
jumpTarget = instruction.operand;
}
else
{
//insert our patch code
yield return new CodeInstruction(OpCodes.Call, Method_IsFlexiboardEquipped);
yield return new CodeInstruction(OpCodes.Brtrue, jumpTarget);
patchComplete = true;
}
}
}
else if (Patch_Helpers.InstructionsAreEqual(instruction, TargetInstructions[idx]))
{
if (++idx == TargetInstructions.Count())
{
found = true;
}
gap = 0;
}
else
{
if (++gap > AllowedInstructionDistance)
{
idx = 0;
}
}
yield return instruction;
}
if (patchComplete == false)
{
XRLCore.Log("Junkbunker Mod: (Warning) Failed to patch swim behavior for the flexiboard item. If you "
+ "have the \"Confirm Swimming\" option turned on, the flexiboard may be somewhat annoying to use "
+ "in large bodies of water (You'll have to confirm your movement each tile).");
}
}
}
/*
* The following patch allows Egcb_ConstrainedLiquidVolume to be interpreted as a valid target for
* "pour" events. The game by default only looks for objects with a "LiquidVolume" part to pour into.
*
*/
[HarmonyPatch(typeof(XRL.World.Parts.LiquidVolume))]
public class Patch_XRL_World_Parts_LiquidVolume
{
[HarmonyPatch("PerformFill")]
static void Prefix()
{
//XRLCore.Log("LiquidVolume.PerformFill PREFIX called");
Patch_XRL_World_IPart.PerformFillActive = true;
//Egcb_ConstrainedLiquidVolume.PretendToBeALiquidVolume();
}
[HarmonyPatch("PerformFill")]
static void Postfix()
{
//XRLCore.Log("LiquidVolume.PerformFill POSTFIX called");
//Egcb_ConstrainedLiquidVolume.StopPretendingToBeALiquidVolume();
Patch_XRL_World_IPart.PerformFillActive = false;
}
}
[HarmonyPatch(typeof(XRL.World.IPart))]
public class Patch_XRL_World_IPart
{
public static bool PerformFillActive = false;
private static string NameOf_Egcb_ConstrainedLiquidVolume = typeof(Egcb_ConstrainedLiquidVolume).Name;
private static string NameOf_LiquidVolume = typeof(LiquidVolume).Name;
[HarmonyPatch("Name", MethodType.Getter)]
static void Postfix(ref string __result)
{
if (PerformFillActive == true)
{
if (__result == NameOf_Egcb_ConstrainedLiquidVolume)
{
//XRLCore.Log("ADJUSTING Egcb_ConstrainedLiquidVolume type name to 'LiquidVolume'.");
__result = NameOf_LiquidVolume;
}
}
}
}
//[HarmonyPatch(typeof(XRL.World.GameObject))]
//public class Patch_XRL_World_GameObject_2
//{
// [HarmonyPatch("IsAutogetLiquid")]
// static void Postfix(XRL.World.GameObject __instance)
// {
// LiquidVolume liquidVolume = __instance.GetPart("LiquidVolume") as LiquidVolume;
// if (liquidVolume != null && liquidVolume.IsOpenVolume() && liquidVolume.IsMixed())
// {
// }
// }
//}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment