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 Transpiler(IEnumerable instructions) { return RetimeHoldWindow(instructions, "Player.ParryCounterCheck", "CurrentMinHold", "CurrentMaxHold"); } } [HarmonyPatch] private static class Patch_ParryCounterEnhance_SimpleCounterCheck { private static IEnumerable 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 Transpiler(IEnumerable 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 Transpiler(IEnumerable instructions) { List list = new List(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 MinHoldSeconds; internal static ConfigEntry MaxHoldSeconds; internal static ConfigEntry AlignParryCounterEnhance; internal static ConfigEntry HoldCounterIndefinitely; internal static ConfigEntry LogCounterDiagnostics; internal static ConfigEntry LoopChargeAnimation; internal static ConfigEntry ChargeLoopRestartAt; internal static ConfigEntry 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("General", "MinHoldSeconds", 0.2f, "How long parry must be held before the Unbounded Counter triggers. Vanilla is 0.33333."); MaxHoldSeconds = ((BaseUnityPlugin)this).Config.Bind("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("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("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("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("General", "ChargeAnimationName", "ParryCounterPrepare", "Animator state replayed to loop the charge. Only change this if the game renames the clip."); LogCounterDiagnostics = ((BaseUnityPlugin)this).Config.Bind("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("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 RetimeHoldWindow(IEnumerable instructions, string label, string minGetterName, string maxGetterName) { List list = new List(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; } }