using System; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; [assembly: AssemblyFileVersion("1.3.1.0")] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: CompilationRelaxations(8)] [assembly: AssemblyTitle("PerfectParry")] [assembly: AssemblyProduct("PerfectParry")] [assembly: AssemblyCopyright("Perfect window rewrite and imperfect-parry damage by supidowagon. Original Perfect Parry mod by nozwock.")] [assembly: AssemblyVersion("1.3.1.0")] namespace PerfectParry; public static class PluginInfo { public const string PLUGIN_GUID = "supidowagon.PerfectParry"; public const string PLUGIN_NAME = "Perfect Parry"; public const string PLUGIN_VERSION = "1.3.1"; } [BepInPlugin("supidowagon.PerfectParry", "Perfect Parry", "1.3.1")] public class Plugin : BaseUnityPlugin { [HarmonyPatch(typeof(PlayerParryState), "Parried")] private static class Patch_PlayerParryState_Parried { private static readonly FieldInfo SpamDatasField = AccessTools.Field(typeof(PlayerParryState), "spamDatas"); private static readonly ConditionalWeakTable Baselines = new ConditionalWeakTable(); private static string lastReported; private static void Prefix(PlayerParryState __instance) { if (SpamDatasField == null) { return; } float[] windows = SpamDatasField.GetValue(__instance) as float[]; if (windows == null || windows.Length == 0) { return; } float[] value = Baselines.GetValue(__instance, (object _) => (float[])windows.Clone()); if (value.Length != windows.Length) { return; } bool flag = Enabled == null || Enabled.Value; float num = WindowSeconds(); for (int num2 = 0; num2 < windows.Length; num2++) { windows[num2] = ((!flag || value[num2] <= 0f) ? value[num2] : num); } if (Logger != null) { string text = (flag ? (Describe(value) + " -> " + Describe(windows) + " (" + PerfectParryFrames.Value + " frames @ " + FramesPerSecond.Value + "fps)") : ("disabled, stock windows " + Describe(value))); if (text != lastReported) { lastReported = text; Logger.LogInfo((object)("Perfect parry window: " + text)); } } } private static string Describe(float[] windows) { StringBuilder stringBuilder = new StringBuilder("["); for (int i = 0; i < windows.Length; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(windows[i].ToString("0.####")); } return stringBuilder.Append("]").ToString(); } } [HarmonyPatch(typeof(PlayerParryState), "NonAccurateParry")] private static class Patch_PlayerParryState_NonAccurateParry { [HarmonyPrefix] private static void Prefix() { EnterNonAccurateParry(); } [HarmonyPostfix] private static void Postfix() { LeaveNonAccurateParry(); } } [HarmonyPatch(typeof(PlayerParryState), "AccurateParry")] private static class Patch_PlayerParryState_AccurateParry { [HarmonyPrefix] private static void Prefix() { LeaveNonAccurateParry(); } } [HarmonyPatch(typeof(PlayerParryState), "TakeInternalInjury")] private static class Patch_PlayerParryState_TakeInternalInjury { [HarmonyPrefix] private static bool Prefix(PlayerParryState __instance, float oriDamage) { return ConvertImperfectParryDamage(__instance, oriDamage); } } private const float VanillaFrames = 8f; internal static ManualLogSource Logger; internal static ConfigEntry Enabled; internal static ConfigEntry PerfectParryFrames; internal static ConfigEntry FramesPerSecond; internal static ConfigEntry ImperfectParryTakesRealDamage; internal static ConfigEntry ImperfectParryDamageScale; internal static ConfigEntry IgnoreChipDamageRatio; private static bool inNonAccurateParry; private static FieldInfo chipRatioField; private static FieldInfo statePlayerField; private static bool injuryLookupTried; private static FieldInfo healthField; private static MethodInfo receiveDotDamage; private static PropertyInfo statValueProp; private void Awake() { Logger = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Turn off to restore the stock windows exactly, without removing the mod."); PerfectParryFrames = ((BaseUnityPlugin)this).Config.Bind("General", "PerfectParryFrames", 12f, "How long after pressing parry a hit still counts as a perfect parry, in frames. Vanilla is 8 (0.1333s). Fractions are allowed. Lower values make perfect parries harder than stock."); FramesPerSecond = ((BaseUnityPlugin)this).Config.Bind("General", "FramesPerSecond", 60f, "Frames-per-second used to turn the setting above into seconds. The game's own check is time-based, so this is purely the unit for PerfectParryFrames."); ImperfectParryTakesRealDamage = ((BaseUnityPlugin)this).Config.Bind("Imperfect parry", "ImperfectParryTakesRealDamage", false, "Take real health damage on an imperfect parry instead of internal injury. Internal injury heals back by attacking; real health needs a potion, so this makes the game harder. The parry still happens - no knockback, no hurt animation, no invincibility frames - it just costs you health you cannot regenerate."); ImperfectParryDamageScale = ((BaseUnityPlugin)this).Config.Bind("Imperfect parry", "ImperfectParryDamageScale", 1f, "Multiplies the damage an imperfect parry deals. 1 is the same number the game would have applied as internal injury. Only used when ImperfectParryTakesRealDamage is on."); IgnoreChipDamageRatio = ((BaseUnityPlugin)this).Config.Bind("Imperfect parry", "IgnoreChipDamageRatio", false, "Take the attack's full damage rather than the chip-damage slice of it. The game multiplies the incoming hit by ChipDamageRatio before applying it as internal injury; turning this on skips that, so an imperfect parry hurts like a clean hit. Only used when ImperfectParryTakesRealDamage is on."); Logger.LogInfo((object)"Plugin supidowagon.PerfectParry is loaded!"); Harmony val = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "supidowagon.PerfectParry"); 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 WindowSeconds() { float num = ((FramesPerSecond == null) ? 60f : FramesPerSecond.Value); if (num <= 0f) { num = 60f; } float num2 = ((PerfectParryFrames == null) ? 12f : PerfectParryFrames.Value); if (num2 < 0f) { num2 = 0f; } return num2 / num; } private static void EnsureInjuryLookups() { if (!injuryLookupTried) { injuryLookupTried = true; chipRatioField = AccessTools.Field(typeof(PlayerParryState), "ChipDamageRatio"); statePlayerField = AccessTools.Field(typeof(PlayerParryState), "player"); if (statePlayerField == null) { Logger.LogError((object)"Could not find PlayerBaseState.player; imperfect parries keep taking internal injury."); } if (chipRatioField == null) { Logger.LogWarning((object)"Could not find PlayerParryState.ChipDamageRatio; imperfect parry damage will use the attack's full value."); } } } private static float ChipRatio(PlayerParryState state) { if (chipRatioField == null) { return 1f; } try { object value = chipRatioField.GetValue(state); if (value == null) { return 1f; } if (statValueProp == null) { statValueProp = AccessTools.Property(value.GetType(), "Value"); if (statValueProp == null) { return 1f; } } return (float)statValueProp.GetValue(value, null); } catch { return 1f; } } private static bool DealRealDamage(PlayerParryState state, float amount) { if (statePlayerField == null) { return false; } try { object value = statePlayerField.GetValue(state); if (value == null) { return false; } if (healthField == null) { healthField = AccessTools.Field(value.GetType(), "health"); if (healthField == null) { Logger.LogError((object)"Could not find Player.health; imperfect parries keep taking internal injury."); return false; } } object value2 = healthField.GetValue(value); if (value2 == null) { return false; } if (receiveDotDamage == null) { receiveDotDamage = AccessTools.Method(value2.GetType(), "ReceiveDOT_Damage", (Type[])null, (Type[])null); if (receiveDotDamage == null) { Logger.LogError((object)"Could not find PlayerHealth.ReceiveDOT_Damage; imperfect parries keep taking internal injury."); return false; } } receiveDotDamage.Invoke(value2, new object[1] { amount }); return true; } catch (Exception ex) { Logger.LogError((object)("Could not deal imperfect parry damage: " + ex.Message)); return false; } } internal static bool ConvertImperfectParryDamage(PlayerParryState __instance, float oriDamage) { if (!inNonAccurateParry) { return true; } inNonAccurateParry = false; if (ImperfectParryTakesRealDamage == null || !ImperfectParryTakesRealDamage.Value) { return true; } EnsureInjuryLookups(); float num = oriDamage; if (IgnoreChipDamageRatio == null || !IgnoreChipDamageRatio.Value) { num *= ChipRatio(__instance); } float num2 = ((ImperfectParryDamageScale == null) ? 1f : ImperfectParryDamageScale.Value); if (num2 < 0f) { num2 = 0f; } num *= num2; if (!DealRealDamage(__instance, num)) { return true; } return false; } internal static void EnterNonAccurateParry() { inNonAccurateParry = true; } internal static void LeaveNonAccurateParry() { inNonAccurateParry = false; } }