Decompiled source of CounterChargeTuner v1.5.2

CounterChargeTuner.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyFileVersion("1.5.2.0")]
[assembly: AssemblyTitle("CounterChargeTuner")]
[assembly: AssemblyProduct("CounterChargeTuner")]
[assembly: AssemblyVersion("1.5.2.0")]
namespace CounterChargeTuner;

public static class PluginInfo
{
	public const string PLUGIN_GUID = "supidowagon.CounterChargeTuner";

	public const string PLUGIN_NAME = "Counter Charge Tuner";

	public const string PLUGIN_VERSION = "1.5.2";

	public const string PARRY_COUNTER_ENHANCE_GUID = "Reits.ParryCounterEnhance";
}
[BepInDependency(/*Could not decode attribute arguments.*/)]
[BepInPlugin("supidowagon.CounterChargeTuner", "Counter Charge Tuner", "1.5.2")]
public class Plugin : BaseUnityPlugin
{
	[HarmonyPatch]
	private static class Patch_Player_ParryCounterCheck
	{
		private static MethodBase TargetMethod()
		{
			Type type = AccessTools.TypeByName("Player");
			if (type == null)
			{
				Logger.LogError((object)"Could not find the Player type; charge window unchanged.");
				return null;
			}
			MethodBase methodBase = AccessTools.Method(type, "ParryCounterCheck", (Type[])null, (Type[])null);
			if (methodBase == null)
			{
				Logger.LogError((object)"Could not find Player.ParryCounterCheck; charge window unchanged.");
			}
			return methodBase;
		}

		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			return RetimeHoldWindow(instructions, "Player.ParryCounterCheck", "CurrentMinHold", "CurrentMaxHold");
		}
	}

	[HarmonyPatch]
	private static class Patch_ParryCounterEnhance_SimpleCounterCheck
	{
		private static IEnumerable<MethodBase> TargetMethods()
		{
			Type type = AccessTools.TypeByName("ParryCounterEnhance.ParryCounterEnhance");
			if (type == null)
			{
				Logger.LogInfo((object)"ParryCounterEnhance not installed; nothing to align.");
				yield break;
			}
			MethodBase target = AccessTools.Method(type, "SimpleCounterCheck", (Type[])null, (Type[])null);
			if (target == null)
			{
				Logger.LogWarning((object)"ParryCounterEnhance is installed but SimpleCounterCheck was not found - its early-entry path keeps the vanilla window.");
			}
			else
			{
				yield return target;
			}
		}

		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			return RetimeHoldWindow(instructions, "ParryCounterEnhance.SimpleCounterCheck", "PceMinHold", "PceMaxHold");
		}
	}

	[HarmonyPatch]
	private static class Patch_PlayerCounterPrepareState_AnimationEvent
	{
		private static MethodBase TargetMethod()
		{
			Type type = AccessTools.TypeByName("PlayerCounterPrepareState");
			if (type == null)
			{
				Logger.LogError((object)"Could not find PlayerCounterPrepareState; the counter will still expire when its charge animation completes.");
				return null;
			}
			postLockField = AccessTools.Field(type, "postLock");
			if (postLockField == null)
			{
				Logger.LogError((object)"Could not find PlayerCounterPrepareState.postLock; the counter will still expire when its charge animation completes.");
				return null;
			}
			return AccessTools.Method(type, "AnimationEvent", (Type[])null, (Type[])null);
		}

		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			MethodInfo operand = AccessTools.Method(typeof(Plugin), "BaseAnimationEventShim", (Type[])null, (Type[])null);
			int num = 0;
			for (int i = 0; i < list.Count; i++)
			{
				MethodInfo methodInfo = list[i].operand as MethodInfo;
				if (!(methodInfo == null) && !(methodInfo.Name != "AnimationEvent") && !(methodInfo.DeclaringType == null) && !(methodInfo.DeclaringType.Name != "PlayerBaseState"))
				{
					list[i].operand = operand;
					num++;
				}
			}
			if (num == 1)
			{
				Logger.LogInfo((object)"Counter charge animation's Done event will be held open while parry is down.");
			}
			else
			{
				Logger.LogError((object)("Could not redirect base.AnimationEvent in PlayerCounterPrepareState - found " + num + " call(s). The counter will still expire when its charge animation ends."));
			}
			return list;
		}

		private static void Postfix(object __instance)
		{
			if (HoldCounterIndefinitely == null || !HoldCounterIndefinitely.Value || postLockField == null || __instance == null)
			{
				return;
			}
			object value = postLockField.GetValue(__instance);
			if (value is bool && (bool)value)
			{
				postLockField.SetValue(__instance, false);
				if (LogCounterDiagnostics != null && LogCounterDiagnostics.Value)
				{
					Logger.LogInfo((object)"Cleared postLock - counter stays releasable.");
				}
			}
		}
	}

	private const float VanillaMinHold = 0.33333f;

	private const float VanillaMaxHold = 1.4f;

	private const int TagDone = 0;

	private const int TagCanMove = 4;

	private const int StateNormal = 0;

	internal static ManualLogSource Logger;

	internal static ConfigEntry<float> MinHoldSeconds;

	internal static ConfigEntry<float> MaxHoldSeconds;

	internal static ConfigEntry<bool> AlignParryCounterEnhance;

	internal static ConfigEntry<bool> HoldCounterIndefinitely;

	internal static ConfigEntry<bool> LogCounterDiagnostics;

	internal static ConfigEntry<bool> LoopChargeAnimation;

	internal static ConfigEntry<float> ChargeLoopRestartAt;

	internal static ConfigEntry<string> ChargeAnimationName;

	private static FieldInfo postLockField;

	private static FieldInfo baseCanMoveField;

	private static FieldInfo basePlayerField;

	private static MethodInfo changeStateMethod;

	private static Type playerStateTypeEnum;

	private static bool baseLookupTried;

	private static MethodInfo playAnimationMethod;

	private static bool playAnimationTried;

	private static MethodInfo stateGetActions;

	private static FieldInfo parryActionField;

	private static MethodInfo parryIsPressedGetter;

	private static bool parryLookupTried;

	private static void EnsureBaseLookups()
	{
		if (!baseLookupTried)
		{
			baseLookupTried = true;
			Type type = AccessTools.TypeByName("PlayerBaseState");
			Type type2 = AccessTools.TypeByName("Player");
			playerStateTypeEnum = AccessTools.TypeByName("PlayerStateType");
			if (type != null)
			{
				baseCanMoveField = AccessTools.Field(type, "canMove");
				basePlayerField = AccessTools.Field(type, "player");
			}
			if (type2 != null)
			{
				changeStateMethod = AccessTools.Method(type2, "ChangeState", (Type[])null, (Type[])null);
			}
			if (baseCanMoveField == null || basePlayerField == null || changeStateMethod == null || playerStateTypeEnum == null)
			{
				Logger.LogError((object)"Could not mirror PlayerBaseState.AnimationEvent (canMove/player/ChangeState/PlayerStateType). Animation events other than the held-counter case will be dropped; disable this mod.");
			}
		}
	}

	private static void ReplayChargeAnimation(object state)
	{
		EnsureBaseLookups();
		if (basePlayerField == null)
		{
			return;
		}
		if (!playAnimationTried)
		{
			playAnimationTried = true;
			Type type = AccessTools.TypeByName("Player");
			if (type != null)
			{
				playAnimationMethod = AccessTools.Method(type, "PlayAnimation", new Type[3]
				{
					typeof(string),
					typeof(bool),
					typeof(float)
				}, (Type[])null);
			}
			if (playAnimationMethod == null)
			{
				Logger.LogWarning((object)"Could not resolve Actor.PlayAnimation(string, bool, float); the held counter will freeze on its last animation frame.");
			}
		}
		if (playAnimationMethod == null)
		{
			return;
		}
		try
		{
			object value = basePlayerField.GetValue(state);
			if (value != null)
			{
				string text = ((ChargeAnimationName == null || string.IsNullOrEmpty(ChargeAnimationName.Value)) ? "ParryCounterPrepare" : ChargeAnimationName.Value);
				float num = ((ChargeLoopRestartAt == null) ? 0f : ChargeLoopRestartAt.Value);
				if (num < 0f)
				{
					num = 0f;
				}
				if (num > 0.999f)
				{
					num = 0.999f;
				}
				playAnimationMethod.Invoke(value, new object[3] { text, true, num });
				if (LogCounterDiagnostics != null && LogCounterDiagnostics.Value)
				{
					Logger.LogInfo((object)("Replayed '" + text + "' from " + num + " to keep the charge looping."));
				}
			}
		}
		catch (Exception ex)
		{
			Logger.LogWarning((object)("Could not replay the charge animation: " + ex.Message));
		}
	}

	private static bool IsParryHeld(object state)
	{
		if (state == null)
		{
			return false;
		}
		if (!parryLookupTried)
		{
			parryLookupTried = true;
			Type type = AccessTools.TypeByName("PlayerBaseState");
			if (type != null)
			{
				stateGetActions = AccessTools.Method(type, "get_actions", (Type[])null, (Type[])null);
			}
			if (stateGetActions == null)
			{
				Logger.LogWarning((object)"Could not resolve PlayerBaseState.actions; the counter will keep expiring when its charge animation ends.");
			}
		}
		if (stateGetActions == null)
		{
			return false;
		}
		try
		{
			object obj = stateGetActions.Invoke(state, null);
			if (obj == null)
			{
				return false;
			}
			if (parryActionField == null)
			{
				parryActionField = AccessTools.Field(obj.GetType(), "Parry");
			}
			if (parryActionField == null)
			{
				return false;
			}
			object value = parryActionField.GetValue(obj);
			if (value == null)
			{
				return false;
			}
			if (parryIsPressedGetter == null)
			{
				parryIsPressedGetter = AccessTools.Method(value.GetType(), "get_IsPressed", (Type[])null, (Type[])null);
			}
			if (parryIsPressedGetter == null)
			{
				return false;
			}
			object obj2 = parryIsPressedGetter.Invoke(value, null);
			return obj2 is bool && (bool)obj2;
		}
		catch (Exception)
		{
			return false;
		}
	}

	internal static void BaseAnimationEventShim(object state, int tag)
	{
		if (tag == 0 && HoldCounterIndefinitely != null && HoldCounterIndefinitely.Value && IsParryHeld(state))
		{
			if (LogCounterDiagnostics != null && LogCounterDiagnostics.Value)
			{
				Logger.LogInfo((object)"Swallowed the charge animation's Done event - counter held open while parry is down.");
			}
			if (LoopChargeAnimation == null || LoopChargeAnimation.Value)
			{
				ReplayChargeAnimation(state);
			}
			return;
		}
		EnsureBaseLookups();
		switch (tag)
		{
		case 4:
			if (baseCanMoveField != null)
			{
				baseCanMoveField.SetValue(state, true);
			}
			break;
		case 0:
			if (!(basePlayerField == null) && !(changeStateMethod == null) && !(playerStateTypeEnum == null))
			{
				object value = basePlayerField.GetValue(state);
				if (value != null)
				{
					changeStateMethod.Invoke(value, new object[2]
					{
						Enum.ToObject(playerStateTypeEnum, 0),
						false
					});
				}
			}
			break;
		}
	}

	private void Awake()
	{
		Logger = ((BaseUnityPlugin)this).Logger;
		MinHoldSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "MinHoldSeconds", 0.2f, "How long parry must be held before the Unbounded Counter triggers. Vanilla is 0.33333.");
		MaxHoldSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "MaxHoldSeconds", 0f, "How long the counter stays available while parry is held. 0 or less means no limit - it stays available until you release the button. Vanilla is 1.4.");
		HoldCounterIndefinitely = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "HoldCounterIndefinitely", true, "Keep the counter releasable no matter how long you hold parry. The charge animation raises a 'postLock' flag at its CanMove event, and once that is set, releasing parry does nothing - that is what makes the counter fizzle out. Clearing it means releasing always fires the counter.");
		LoopChargeAnimation = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "LoopChargeAnimation", true, "Replay the charge animation when it finishes, so a held counter keeps animating instead of freezing on its last frame. Turn off to leave the frozen pose.");
		ChargeLoopRestartAt = ((BaseUnityPlugin)this).Config.Bind<float>("General", "ChargeLoopRestartAt", 0f, "Normalized time (0-1) the charge animation restarts from each loop. 0 replays the whole clip, which also re-fires its animation events - including the ShowEffect one that grants chi if you have the Charge Chi Jade. Raise this past that event to loop only the later frames.");
		ChargeAnimationName = ((BaseUnityPlugin)this).Config.Bind<string>("General", "ChargeAnimationName", "ParryCounterPrepare", "Animator state replayed to loop the charge. Only change this if the game renames the clip.");
		LogCounterDiagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "LogCounterDiagnostics", false, "Log every time the counter's postLock flag is cleared. Off by default because it is noisy; turn it on only when investigating.");
		AlignParryCounterEnhance = ((BaseUnityPlugin)this).Config.Bind<bool>("Compatibility", "AlignParryCounterEnhance", true, "Apply the same timings to ParryCounterEnhance's early-entry path, which carries its own copy of the vanilla window. Turn off to leave that path on 0.33333/1.4. Ignored when that mod is not installed.");
		Logger.LogInfo((object)"Plugin supidowagon.CounterChargeTuner is loaded!");
		Harmony val = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "supidowagon.CounterChargeTuner");
		foreach (MethodBase patchedMethod in val.GetPatchedMethods())
		{
			Logger.LogInfo((object)("Patched method: " + patchedMethod.DeclaringType.FullName + "." + patchedMethod.Name));
		}
		if (val.GetPatchedMethods().Count() == 0)
		{
			Logger.LogError((object)"Failed to apply Harmony patches.");
		}
	}

	internal static float CurrentMinHold()
	{
		float num = ((MinHoldSeconds == null) ? 0.2f : MinHoldSeconds.Value);
		if (!(num < 0f))
		{
			return num;
		}
		return 0f;
	}

	internal static float CurrentMaxHold()
	{
		float num = ((MaxHoldSeconds == null) ? 0f : MaxHoldSeconds.Value);
		if (!(num <= 0f))
		{
			return num;
		}
		return float.PositiveInfinity;
	}

	internal static float PceMinHold()
	{
		if (AlignParryCounterEnhance != null && !AlignParryCounterEnhance.Value)
		{
			return 0.33333f;
		}
		return CurrentMinHold();
	}

	internal static float PceMaxHold()
	{
		if (AlignParryCounterEnhance != null && !AlignParryCounterEnhance.Value)
		{
			return 1.4f;
		}
		return CurrentMaxHold();
	}

	private static IEnumerable<CodeInstruction> RetimeHoldWindow(IEnumerable<CodeInstruction> instructions, string label, string minGetterName, string maxGetterName)
	{
		List<CodeInstruction> list = new List<CodeInstruction>(instructions);
		MethodInfo operand = AccessTools.Method(typeof(Plugin), minGetterName, (Type[])null, (Type[])null);
		MethodInfo operand2 = AccessTools.Method(typeof(Plugin), maxGetterName, (Type[])null, (Type[])null);
		int num = 0;
		int num2 = 0;
		for (int i = 0; i < list.Count; i++)
		{
			if (!(list[i].opcode != OpCodes.Ldc_R4) && list[i].operand is float)
			{
				float num3 = (float)list[i].operand;
				if (Math.Abs(num3 - 0.33333f) < 0.001f)
				{
					list[i].opcode = OpCodes.Call;
					list[i].operand = operand;
					num++;
				}
				else if (Math.Abs(num3 - 1.4f) < 0.001f)
				{
					list[i].opcode = OpCodes.Call;
					list[i].operand = operand2;
					num2++;
				}
			}
		}
		if (num == 1 && num2 == 1)
		{
			Logger.LogInfo((object)(label + " retimed: min hold " + 0.33333f + "s -> " + CurrentMinHold() + "s, max hold " + 1.4f + "s -> " + ((MaxHoldSeconds != null && MaxHoldSeconds.Value <= 0f) ? "unlimited (until parry is released)" : (CurrentMaxHold() + "s"))));
		}
		else
		{
			Logger.LogError((object)("Could not retime " + label + " - expected one " + 0.33333f + " and one " + 1.4f + " constant, found " + num + " and " + num2 + ". That path's charge window is unchanged."));
		}
		return list;
	}
}