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;
}
}