Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ParryStrike v0.1.0
plugins/ParryStrike/ParryStrike.dll
Decompiled a day agousing System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("Narukami")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © 2026 Narukami")] [assembly: AssemblyDescription("Parry enemy attacks with your own swing.")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0")] [assembly: AssemblyProduct("Parry Strike")] [assembly: AssemblyTitle("Parry Strike")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/Narukami/ParryStrike")] [assembly: AssemblyVersion("0.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ParryStrike { internal static class ParryHelper { internal static bool IsLocalPlayer(Player player) { if ((Object)(object)player != (Object)null && (Object)(object)player == (Object)(object)Player.m_localPlayer && ((Character)player).IsOwner()) { return !((Character)player).IsDead(); } return false; } internal static bool CanUseWeaponForParry(Player player) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Invalid comparison between Unknown and I4 //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Invalid comparison between Unknown and I4 //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Invalid comparison between Unknown and I4 if (!IsLocalPlayer(player)) { return false; } if (((Character)player).IsStaggering() || ((Character)player).InDodge() || ((Character)player).IsEncumbered()) { return false; } ItemData currentWeapon = ((Humanoid)player).GetCurrentWeapon(); if (currentWeapon == null) { return false; } if (Settings.AllowBowParry.Value) { return true; } if ((int)currentWeapon.m_shared.m_itemType == 4) { return false; } SkillType skillType = currentWeapon.m_shared.m_skillType; if ((int)skillType != 8) { return (int)skillType != 14; } return false; } internal static bool IsFacingHit(Character defender, HitData hit) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) Vector3 dir = hit.m_dir; dir.y = 0f; if (((Vector3)(ref dir)).sqrMagnitude < 0.0001f) { return true; } return Vector3.Dot(dir, ((Component)defender).transform.forward) <= 0f; } internal static Vector3 DirectionTo(Character from, Character to) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) Vector3 val = ((Component)to).transform.position - ((Component)from).transform.position; val.y = 0f; if (!(((Vector3)(ref val)).sqrMagnitude < 0.0001f)) { return ((Vector3)(ref val)).normalized; } return ((Component)to).transform.forward; } internal static void Trace(string message) { if (Settings.DebugLog.Value) { Plugin.Log.LogInfo((object)("[ParryStrike] " + message)); } } } [HarmonyPatch(typeof(Character), "RPC_Damage")] internal static class ParryStrikePatch { private static bool Prefix(Character __instance, HitData hit) { if (!Settings.Enabled.Value || hit == null) { return true; } Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val == null) { return true; } if ((Object)(object)val != (Object)(object)Player.m_localPlayer || !((Character)val).IsOwner() || ((Character)val).IsDead()) { return true; } if (!ParryWindow.IsActiveFor(val)) { return true; } Character attacker = hit.GetAttacker(); if (!IsParryCandidate(val, attacker, hit)) { return true; } if (!TryParry(val, attacker, hit)) { return true; } return false; } private static bool IsParryCandidate(Player player, Character attacker, HitData hit) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Invalid comparison between Unknown and I4 //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Invalid comparison between Unknown and I4 if ((Object)(object)attacker == (Object)null || (Object)(object)attacker == (Object)(object)player || attacker.IsDead()) { return false; } if (!hit.m_blockable) { return false; } if (Settings.OnlyMelee.Value && hit.m_ranged) { return false; } if ((int)hit.m_hitType != 1 && (int)hit.m_hitType != 2) { return false; } if (attacker.IsPlayer() && !((Character)player).IsPVPEnabled()) { return false; } if (!ParryHelper.IsFacingHit((Character)(object)player, hit)) { return false; } return !((Character)player).IsBlocking(); } private static bool TryParry(Player player, Character attacker, HitData hit) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Expected O, but got Unknown //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) ItemData currentWeapon = ((Humanoid)player).GetCurrentWeapon(); ItemData val = currentWeapon ?? ((Humanoid)player).LeftItem; if (val == null) { return false; } float num = Mathf.Max(0f, Settings.StaminaCost.Value); if (num > 0f && !((Character)player).HaveStamina(num)) { ParryHelper.Trace("Parry failed: not enough stamina."); return false; } if (num > 0f) { ((Character)player).UseStamina(num); } float totalDamage = hit.GetTotalDamage(); val.m_shared.m_blockEffect.Create(hit.m_point, Quaternion.identity, (Transform)null, 1f, -1, ((Character)player).GetZDOID()); ((Humanoid)player).m_perfectBlockEffect.Create(hit.m_point, Quaternion.identity, (Transform)null, 1f, -1, ((Character)player).GetZDOID()); if (Settings.ShowDamageText.Value && (Object)(object)DamageText.instance != (Object)null) { DamageText.instance.ShowText((TextType)6, hit.m_point + Vector3.up * 0.5f, totalDamage, true); } if (Settings.DeflectAttacker.Value) { float num2 = Mathf.Max(new float[3] { val.GetDeflectionForce(), hit.m_pushForce, 5f }) * Settings.DeflectForceMultiplier.Value; if (num2 > 0f) { HitData val2 = new HitData { m_pushForce = num2, m_dir = ParryHelper.DirectionTo((Character)(object)player, attacker) }; attacker.Damage(val2); } } if (Settings.StaggerAttacker.Value && attacker.m_staggerWhenBlocked) { attacker.Stagger(-hit.m_dir); } if (Settings.RaiseBlockingSkill.Value) { ((Character)player).RaiseSkill((SkillType)6, 2f); } if (Settings.AdrenalineGain.Value > 0f) { ((Character)player).AddAdrenaline(Settings.AdrenalineGain.Value); } if (Settings.DurabilityLoss.Value && currentWeapon != null && currentWeapon.m_shared.m_useDurability) { currentWeapon.m_durability -= currentWeapon.m_shared.m_useDurabilityDrain * Game.m_durabilityRate; } if ((Object)(object)val.m_shared.m_perfectBlockStatusEffect != (Object)null) { ((Character)player).GetSEMan().AddStatusEffect(val.m_shared.m_perfectBlockStatusEffect, true, val.m_worldLevel, ((Character)player).GetSkillLevel((SkillType)6), (short)(-1)); } if (Settings.FreezeFrame.Value) { ((Character)player).FreezeFrame(0.1f); } if (Settings.OneParryPerSwing.Value) { ParryWindow.Lock(); } Plugin.ReportParry(((Object)attacker).name, totalDamage); return true; } } internal static class ParryWindow { private const float WindupGrace = 0.15f; private const float MaxWindupTime = 2f; private static Player m_player; private static float m_expireAt; private static float m_graceUntil; private static bool m_open; private static bool m_pastHitFrame; private static bool m_locked; internal static void OnNewSwing(Player player, bool openDuringWindup) { m_player = player; m_locked = false; m_pastHitFrame = false; m_open = openDuringWindup; m_graceUntil = Time.time + 0.15f; m_expireAt = (openDuringWindup ? (Time.time + 2f) : 0f); } internal static void OnHitFrame(Player player, float duration) { if (!m_locked && !((Object)(object)player == (Object)null)) { if ((Object)(object)m_player != (Object)(object)player) { m_player = player; m_locked = false; } m_open = true; m_pastHitFrame = true; m_expireAt = Time.time + Mathf.Max(0.01f, duration); } } internal static bool IsActiveFor(Player player) { if (!m_open || m_locked || (Object)(object)player == (Object)null || (Object)(object)m_player != (Object)(object)player) { return false; } if (((Character)player).IsDead() || ((Character)player).IsStaggering() || ((Character)player).InDodge() || ((Character)player).IsEncumbered()) { Close(); return false; } if (Time.time > m_expireAt) { Close(); return false; } if (!m_pastHitFrame && !((Character)player).InAttack() && Time.time > m_graceUntil) { Close(); return false; } return true; } internal static void Lock() { m_open = false; m_locked = true; m_expireAt = 0f; } internal static void Close() { m_player = null; m_expireAt = 0f; m_graceUntil = 0f; m_open = false; m_pastHitFrame = false; } } [HarmonyPatch(typeof(Humanoid), "StartAttack")] internal static class ParryWindowStartPatch { private static void Postfix(Humanoid __instance, bool __result) { if (__result && Settings.Enabled.Value) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null && ParryHelper.IsLocalPlayer(val)) { bool flag = ParryHelper.CanUseWeaponForParry(val); ParryWindow.OnNewSwing(val, flag && Settings.ParryOnWindup.Value); ParryHelper.Trace(flag ? "New swing, window reset." : "New swing, parrying is unavailable with this weapon."); } } } } [HarmonyPatch(typeof(Humanoid), "OnAttackTrigger")] internal static class ParryWindowTriggerPatch { private static void Postfix(Humanoid __instance) { if (!Settings.Enabled.Value) { return; } Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null && ParryHelper.IsLocalPlayer(val)) { if (!ParryHelper.CanUseWeaponForParry(val)) { ParryWindow.Close(); return; } ParryWindow.OnHitFrame(val, Settings.WindowDuration.Value); ParryHelper.Trace($"Parry window opened ({Settings.WindowDuration.Value:0.00} s)."); } } } [BepInPlugin("narukami.parrystrike", "Parry Strike", "0.1.0")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "narukami.parrystrike"; public const string DisplayName = "Parry Strike"; public const string Version = "0.1.0"; private static bool m_firstParryLogged; internal static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal static Harmony Harmony { get; private set; } private void Awake() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Settings.Bind(((BaseUnityPlugin)this).Config); Harmony = new Harmony("narukami.parrystrike"); Harmony.PatchAll(); Log.LogInfo((object)("Parry Strike v0.1.0 loaded. Strike parrying: " + (Settings.Enabled.Value ? "on" : "off") + ", " + $"window {Settings.WindowDuration.Value:0.00} s.")); } private void OnDestroy() { ParryWindow.Close(); Harmony harmony = Harmony; if (harmony != null) { harmony.UnpatchSelf(); } } internal static void ReportParry(string attackerName, float prevented) { if (!m_firstParryLogged) { m_firstParryLogged = true; Log.LogInfo((object)($"First parry: {attackerName}, prevented {prevented:0.#} damage. " + "The mechanic is working.")); } if (Settings.DebugLog.Value) { Log.LogInfo((object)$"[ParryStrike] Parry: {attackerName}, prevented {prevented:0.#} damage."); } } } internal static class Settings { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<float> WindowDuration; internal static ConfigEntry<bool> ParryOnWindup; internal static ConfigEntry<bool> OnlyMelee; internal static ConfigEntry<bool> OneParryPerSwing; internal static ConfigEntry<bool> AllowBowParry; internal static ConfigEntry<float> StaminaCost; internal static ConfigEntry<bool> StaggerAttacker; internal static ConfigEntry<bool> DeflectAttacker; internal static ConfigEntry<float> DeflectForceMultiplier; internal static ConfigEntry<bool> RaiseBlockingSkill; internal static ConfigEntry<float> AdrenalineGain; internal static ConfigEntry<bool> DurabilityLoss; internal static ConfigEntry<bool> ShowDamageText; internal static ConfigEntry<bool> FreezeFrame; internal static ConfigEntry<bool> DebugLog; internal static void Bind(ConfigFile cfg) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Expected O, but got Unknown //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Expected O, but got Unknown //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01b3: Expected O, but got Unknown Enabled = cfg.Bind<bool>("General", "Enabled", true, "Enable parrying enemy attacks with your own swing.\nВключить парирование атаки врага собственной атакой."); WindowDuration = cfg.Bind<float>("General", "ParryWindow", 0.3f, new ConfigDescription("Parry window length in seconds. Counted from the hit frame of your attack (and from the start of the swing when ParryOnWindup is on).\nДлительность окна парирования в секундах. Отсчитывается от кадра удара (и от начала замаха, если включено ParryOnWindup).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 1.5f), Array.Empty<object>())); ParryOnWindup = cfg.Bind<bool>("General", "ParryOnWindup", true, "Open the parry window already during the wind-up, not only on the hit frame. Turn off for strict strike-for-strike parrying.\nОткрывать окно парирования уже на замахе, а не только в момент удара. Выключите для строгого парирования «удар в удар»."); OnlyMelee = cfg.Bind<bool>("General", "OnlyMelee", true, "Only melee hits can be parried. Arrows and projectiles cannot.\nПарировать можно только ближние атаки. Стрелы и снаряды парировать нельзя."); OneParryPerSwing = cfg.Bind<bool>("General", "OneParryPerSwing", true, "A single swing can parry only one enemy hit.\nОдна ваша атака может парировать только один удар врага."); AllowBowParry = cfg.Bind<bool>("General", "AllowBowParry", false, "Allow parrying while drawing a bow or crossbow.\nРазрешить парирование во время выстрела из лука или арбалета."); StaminaCost = cfg.Bind<float>("Balance", "StaminaCost", 10f, new ConfigDescription("Stamina spent per successful parry. 0 means free.\nСколько выносливости тратится за успешное парирование. 0 — бесплатно.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); StaggerAttacker = cfg.Bind<bool>("Balance", "StaggerAttacker", true, "Stagger the attacker on a successful parry.\nОглушать врага при успешном парировании."); DeflectAttacker = cfg.Bind<bool>("Balance", "DeflectAttacker", true, "Knock the attacker back on a successful parry.\nОтбрасывать врага назад при успешном парировании."); DeflectForceMultiplier = cfg.Bind<float>("Balance", "DeflectForceMultiplier", 1f, new ConfigDescription("Multiplier for the knockback force applied to the attacker.\nМножитель силы отбрасывания врага при парировании.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>())); RaiseBlockingSkill = cfg.Bind<bool>("Balance", "RaiseBlockingSkill", true, "Raise the Blocking skill on a successful parry.\nПовышать навык «Блокирование» при успешном парировании."); AdrenalineGain = cfg.Bind<float>("Balance", "AdrenalineGain", 5f, new ConfigDescription("Adrenaline granted per successful parry.\nСколько адреналина даётся за успешное парирование.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 50f), Array.Empty<object>())); DurabilityLoss = cfg.Bind<bool>("Balance", "DurabilityLoss", true, "Spend weapon durability on a successful parry.\nТратить прочность оружия при успешном парировании."); ShowDamageText = cfg.Bind<bool>("Visual", "ShowDamageText", true, "Show the floating \"Blocked\" number on a parry.\nПоказывать всплывающий текст «Заблокировано» при парировании."); FreezeFrame = cfg.Bind<bool>("Visual", "FreezeFrame", true, "Brief hit-stop on a parry for impact.\nКороткая заморозка анимации в момент парирования (эффект «звон клинков»)."); DebugLog = cfg.Bind<bool>("Debug", "DebugLog", false, "Verbose parry logging to the BepInEx console.\nПодробный лог парирований в консоль BepInEx."); } } }