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 GoosCombatOverhaul v1.0.0
BepInEx/plugins/GooCombatOverhaul/GooCombatOverhaul.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; 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(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("GooCombatOverhaul")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("GooCombatOverhaul")] [assembly: AssemblyTitle("GooCombatOverhaul")] [assembly: AssemblyVersion("1.0.0.0")] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace GooCombatOverhaul { [BepInPlugin("goo.valheim.gooscombatoverhaul", "Goo's Combat Overhaul", "1.0.0")] public sealed class GooCombatOverhaulPlugin : BaseUnityPlugin { internal sealed class CategoryConfig { public ConfigEntry<StaggerApplicationMode> PrimaryStaggerMode { get; } public ConfigEntry<float> PrimaryStaggerPowerValue { get; } public ConfigEntry<StaggerApplicationMode> SecondaryStaggerMode { get; } public ConfigEntry<float> SecondaryStaggerPowerValue { get; } public ConfigEntry<HyperArmorMode> PrimaryHyperArmorMode { get; } public ConfigEntry<HyperArmorMode> SecondaryHyperArmorMode { get; } public ConfigEntry<float> PrimaryDamageTakenMultiplier { get; } public ConfigEntry<float> SecondaryDamageTakenMultiplier { get; } public ConfigEntry<float> PrimaryStaggerTakenMultiplier { get; } public ConfigEntry<float> SecondaryStaggerTakenMultiplier { get; } public ConfigEntry<float> PrimaryKnockbackTakenMultiplier { get; } public ConfigEntry<float> SecondaryKnockbackTakenMultiplier { get; } public ConfigEntry<bool> EnableHitStopOnHit { get; } public ConfigEntry<bool> PrimaryCounterDamageEnabled { get; } public ConfigEntry<float> PrimaryCounterDamageMultiplier { get; } public ConfigEntry<bool> SecondaryCounterDamageEnabled { get; } public ConfigEntry<float> SecondaryCounterDamageMultiplier { get; } public ConfigEntry<float> PrimaryDamageRateMultiplier { get; } public ConfigEntry<float> SecondaryDamageRateMultiplier { get; } public ConfigEntry<float> PrimaryStaminaRateMultiplier { get; } public ConfigEntry<float> SecondaryStaminaRateMultiplier { get; } public ConfigEntry<float> PrimaryAttackMovementSpeedMultiplier { get; } public ConfigEntry<float> SecondaryAttackMovementSpeedMultiplier { get; } public ConfigEntry<float> PrimaryAttackAnimationSpeedMultiplier { get; } public ConfigEntry<float> SecondaryAttackAnimationSpeedMultiplier { get; } public ConfigEntry<float> PrimaryAttackRotationFactor { get; } public ConfigEntry<float> SecondaryAttackRotationFactor { get; } public ConfigEntry<bool> PrimaryLockRotationAfterAttackTrigger { get; } public ConfigEntry<bool> SecondaryLockRotationAfterAttackTrigger { get; } public ConfigEntry<float> PrimaryAdrenalineMultiplier { get; } public ConfigEntry<float> SecondaryAdrenalineMultiplier { get; } public ConfigEntry<float> PrimaryPvpDamageMultiplier { get; } public ConfigEntry<float> SecondaryPvpDamageMultiplier { get; } public ConfigEntry<float> PrimaryFullAttackDuration { get; } public ConfigEntry<float> SecondaryFullAttackDuration { get; } public ConfigEntry<float> PrimaryActiveHitboxDuration { get; } public ConfigEntry<float> SecondaryActiveHitboxDuration { get; } public CategoryConfig(ConfigEntry<StaggerApplicationMode> primaryStaggerMode, ConfigEntry<float> primaryStaggerPowerValue, ConfigEntry<StaggerApplicationMode> secondaryStaggerMode, ConfigEntry<float> secondaryStaggerPowerValue, ConfigEntry<HyperArmorMode> primaryHyperArmorMode, ConfigEntry<HyperArmorMode> secondaryHyperArmorMode, ConfigEntry<float> primaryDamageTakenMultiplier, ConfigEntry<float> secondaryDamageTakenMultiplier, ConfigEntry<float> primaryStaggerTakenMultiplier, ConfigEntry<float> secondaryStaggerTakenMultiplier, ConfigEntry<float> primaryKnockbackTakenMultiplier, ConfigEntry<float> secondaryKnockbackTakenMultiplier, ConfigEntry<bool> enableHitStopOnHit, ConfigEntry<bool> primaryCounterDamageEnabled, ConfigEntry<float> primaryCounterDamageMultiplier, ConfigEntry<bool> secondaryCounterDamageEnabled, ConfigEntry<float> secondaryCounterDamageMultiplier, ConfigEntry<float> primaryDamageRateMultiplier, ConfigEntry<float> secondaryDamageRateMultiplier, ConfigEntry<float> primaryStaminaRateMultiplier, ConfigEntry<float> secondaryStaminaRateMultiplier, ConfigEntry<float> primaryAttackMovementSpeedMultiplier, ConfigEntry<float> secondaryAttackMovementSpeedMultiplier, ConfigEntry<float> primaryAttackAnimationSpeedMultiplier, ConfigEntry<float> secondaryAttackAnimationSpeedMultiplier, ConfigEntry<float> primaryAttackRotationFactor, ConfigEntry<float> secondaryAttackRotationFactor, ConfigEntry<bool> primaryLockRotationAfterAttackTrigger, ConfigEntry<bool> secondaryLockRotationAfterAttackTrigger, ConfigEntry<float> primaryAdrenalineMultiplier, ConfigEntry<float> secondaryAdrenalineMultiplier, ConfigEntry<float> primaryPvpDamageMultiplier, ConfigEntry<float> secondaryPvpDamageMultiplier, ConfigEntry<float> primaryFullAttackDuration, ConfigEntry<float> secondaryFullAttackDuration, ConfigEntry<float> primaryActiveHitboxDuration, ConfigEntry<float> secondaryActiveHitboxDuration) { PrimaryStaggerMode = primaryStaggerMode; PrimaryStaggerPowerValue = primaryStaggerPowerValue; SecondaryStaggerMode = secondaryStaggerMode; SecondaryStaggerPowerValue = secondaryStaggerPowerValue; PrimaryHyperArmorMode = primaryHyperArmorMode; SecondaryHyperArmorMode = secondaryHyperArmorMode; PrimaryDamageTakenMultiplier = primaryDamageTakenMultiplier; SecondaryDamageTakenMultiplier = secondaryDamageTakenMultiplier; PrimaryStaggerTakenMultiplier = primaryStaggerTakenMultiplier; SecondaryStaggerTakenMultiplier = secondaryStaggerTakenMultiplier; PrimaryKnockbackTakenMultiplier = primaryKnockbackTakenMultiplier; SecondaryKnockbackTakenMultiplier = secondaryKnockbackTakenMultiplier; EnableHitStopOnHit = enableHitStopOnHit; PrimaryCounterDamageEnabled = primaryCounterDamageEnabled; PrimaryCounterDamageMultiplier = primaryCounterDamageMultiplier; SecondaryCounterDamageEnabled = secondaryCounterDamageEnabled; SecondaryCounterDamageMultiplier = secondaryCounterDamageMultiplier; PrimaryDamageRateMultiplier = primaryDamageRateMultiplier; SecondaryDamageRateMultiplier = secondaryDamageRateMultiplier; PrimaryStaminaRateMultiplier = primaryStaminaRateMultiplier; SecondaryStaminaRateMultiplier = secondaryStaminaRateMultiplier; PrimaryAttackMovementSpeedMultiplier = primaryAttackMovementSpeedMultiplier; SecondaryAttackMovementSpeedMultiplier = secondaryAttackMovementSpeedMultiplier; PrimaryAttackAnimationSpeedMultiplier = primaryAttackAnimationSpeedMultiplier; SecondaryAttackAnimationSpeedMultiplier = secondaryAttackAnimationSpeedMultiplier; PrimaryAttackRotationFactor = primaryAttackRotationFactor; SecondaryAttackRotationFactor = secondaryAttackRotationFactor; PrimaryLockRotationAfterAttackTrigger = primaryLockRotationAfterAttackTrigger; SecondaryLockRotationAfterAttackTrigger = secondaryLockRotationAfterAttackTrigger; PrimaryAdrenalineMultiplier = primaryAdrenalineMultiplier; SecondaryAdrenalineMultiplier = secondaryAdrenalineMultiplier; PrimaryPvpDamageMultiplier = primaryPvpDamageMultiplier; SecondaryPvpDamageMultiplier = secondaryPvpDamageMultiplier; PrimaryFullAttackDuration = primaryFullAttackDuration; SecondaryFullAttackDuration = secondaryFullAttackDuration; PrimaryActiveHitboxDuration = primaryActiveHitboxDuration; SecondaryActiveHitboxDuration = secondaryActiveHitboxDuration; } public StaggerApplicationMode GetStaggerMode(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryStaggerMode.Value; } return SecondaryStaggerMode.Value; } public float GetStaggerPowerValue(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryStaggerPowerValue.Value; } return SecondaryStaggerPowerValue.Value; } public HyperArmorMode GetHyperArmorMode(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryHyperArmorMode.Value; } return SecondaryHyperArmorMode.Value; } public float GetDamageTakenMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryDamageTakenMultiplier.Value; } return SecondaryDamageTakenMultiplier.Value; } public float GetStaggerTakenMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryStaggerTakenMultiplier.Value; } return SecondaryStaggerTakenMultiplier.Value; } public float GetKnockbackTakenMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryKnockbackTakenMultiplier.Value; } return SecondaryKnockbackTakenMultiplier.Value; } public bool GetCounterDamageEnabled(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryCounterDamageEnabled.Value; } return SecondaryCounterDamageEnabled.Value; } public float GetCounterDamageMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryCounterDamageMultiplier.Value; } return SecondaryCounterDamageMultiplier.Value; } public float GetDamageRateMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryDamageRateMultiplier.Value; } return SecondaryDamageRateMultiplier.Value; } public float GetStaminaRateMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryStaminaRateMultiplier.Value; } return SecondaryStaminaRateMultiplier.Value; } public float GetAttackMovementSpeedMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryAttackMovementSpeedMultiplier.Value; } return SecondaryAttackMovementSpeedMultiplier.Value; } public float GetAttackAnimationSpeedMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryAttackAnimationSpeedMultiplier.Value; } return SecondaryAttackAnimationSpeedMultiplier.Value; } public float GetAttackRotationFactor(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryAttackRotationFactor.Value; } return SecondaryAttackRotationFactor.Value; } public bool GetLockRotationAfterAttackTrigger(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryLockRotationAfterAttackTrigger.Value; } return SecondaryLockRotationAfterAttackTrigger.Value; } public float GetAdrenalineMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryAdrenalineMultiplier.Value; } return SecondaryAdrenalineMultiplier.Value; } public float GetPvpDamageMultiplier(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryPvpDamageMultiplier.Value; } return SecondaryPvpDamageMultiplier.Value; } public float GetFullAttackDuration(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryFullAttackDuration.Value; } return SecondaryFullAttackDuration.Value; } public float GetActiveHitboxDuration(AttackRole role) { if (role != AttackRole.Secondary) { return PrimaryActiveHitboxDuration.Value; } return SecondaryActiveHitboxDuration.Value; } } private readonly struct SuppressedFieldValue { public object Target { get; } public FieldInfo Field { get; } public object? OriginalValue { get; } public SuppressedFieldValue(object target, FieldInfo field, object? originalValue) { Target = target; Field = field; OriginalValue = originalValue; } } private readonly struct CounterVulnerableState { public object? AttackInstance { get; } public CounterVulnerableState(object? attackInstance) { AttackInstance = attackInstance; } } private readonly struct AttackRuntimeState { public WeaponCategory Category { get; } public AttackRole Role { get; } public float EndTime { get; } public object? AttackInstance { get; } public AttackRuntimeState(WeaponCategory category, AttackRole role, float endTime, object? attackInstance) { Category = category; Role = role; EndTime = endTime; AttackInstance = attackInstance; } } private readonly struct PendingAttackRoleState { public AttackRole Role { get; } public float EndTime { get; } public PendingAttackRoleState(AttackRole role, float endTime) { Role = role; EndTime = endTime; } } private readonly struct HyperArmorRuntimeState { public WeaponCategory Category { get; } public AttackRole Role { get; } public HyperArmorMode Mode { get; } public float EndTime { get; } public object? AttackInstance { get; } public HyperArmorRuntimeState(WeaponCategory category, AttackRole role, HyperArmorMode mode, float endTime, object? attackInstance) { Category = category; Role = role; Mode = mode; EndTime = endTime; AttackInstance = attackInstance; } } private readonly struct AnimatorSpeedValue { private readonly Animator? _animator; private readonly object? _target; private readonly FieldInfo? _field; private readonly float _originalSpeed; private readonly float _multiplier; public AnimatorSpeedValue(Animator animator, float originalSpeed, float multiplier) { _animator = animator; _target = null; _field = null; _originalSpeed = originalSpeed; _multiplier = multiplier; } public AnimatorSpeedValue(object target, FieldInfo field, float originalSpeed, float multiplier) { _animator = null; _target = target; _field = field; _originalSpeed = originalSpeed; _multiplier = multiplier; } public void Restore() { if ((Object)(object)_animator != (Object)null) { _animator.speed = _originalSpeed; } else if (_target != null && _field != null) { _field.SetValue(_target, _originalSpeed); } } public void Reapply() { float num = _originalSpeed * _multiplier; if ((Object)(object)_animator != (Object)null) { _animator.speed = num; } else if (_target != null && _field != null) { _field.SetValue(_target, num); } } } public sealed class DamagePatchState { public Character Victim { get; } public float HealthBefore { get; } public float DamageMultiplier { get; } public float TargetHealthAfterMitigation { get; set; } public DamagePatchState(Character victim, float healthBefore, float damageMultiplier) { Victim = victim; HealthBefore = healthBefore; DamageMultiplier = damageMultiplier; TargetHealthAfterMitigation = -1f; } } [CompilerGenerated] private sealed class <ApplyHyperArmorFinalDamageReductionDelayed>d__141 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public DamagePatchState state; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ApplyHyperArmorFinalDamageReductionDelayed>d__141(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForEndOfFrame(); <>1__state = 1; return true; case 1: { <>1__state = -1; if (state == null || (Object)(object)state.Victim == (Object)null || IsCharacterDead(state.Victim)) { return false; } if (state.TargetHealthAfterMitigation <= 0f) { return false; } float characterHealth = GetCharacterHealth(state.Victim); if (characterHealth >= 0f && characterHealth + 0.0001f < state.TargetHealthAfterMitigation) { SetCharacterHealthDirect(state.Victim, state.TargetHealthAfterMitigation); } return false; } } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <GetAllInstanceFields>d__114 : IEnumerable<FieldInfo>, IEnumerable, IEnumerator<FieldInfo>, IDisposable, IEnumerator { private int <>1__state; private FieldInfo <>2__current; private int <>l__initialThreadId; private Type type; public Type <>3__type; private Type <t>5__2; private FieldInfo[] <>7__wrap2; private int <>7__wrap3; FieldInfo IEnumerator<FieldInfo>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <GetAllInstanceFields>d__114(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { <t>5__2 = null; <>7__wrap2 = null; <>1__state = -2; } private bool MoveNext() { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } <>1__state = -1; <>7__wrap3++; goto IL_0074; } <>1__state = -1; <t>5__2 = type; goto IL_009c; IL_0074: if (<>7__wrap3 < <>7__wrap2.Length) { FieldInfo fieldInfo = <>7__wrap2[<>7__wrap3]; <>2__current = fieldInfo; <>1__state = 1; return true; } <>7__wrap2 = null; <t>5__2 = <t>5__2.BaseType; goto IL_009c; IL_009c: if (<t>5__2 != null) { <>7__wrap2 = <t>5__2.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); <>7__wrap3 = 0; goto IL_0074; } <t>5__2 = null; return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator<FieldInfo> IEnumerable<FieldInfo>.GetEnumerator() { <GetAllInstanceFields>d__114 <GetAllInstanceFields>d__; if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; <GetAllInstanceFields>d__ = this; } else { <GetAllInstanceFields>d__ = new <GetAllInstanceFields>d__114(0); } <GetAllInstanceFields>d__.type = <>3__type; return <GetAllInstanceFields>d__; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<FieldInfo>)this).GetEnumerator(); } } public const string ModGuid = "goo.valheim.gooscombatoverhaul"; public const string ModName = "Goo's Combat Overhaul"; public const string ModVersion = "1.0.0"; private readonly Harmony _harmony = new Harmony("goo.valheim.gooscombatoverhaul"); internal static ConfigEntry<bool> Enabled = null; internal static ConfigEntry<bool> ApplyGoosSettings = null; internal static ConfigEntry<bool> ResetToVanillaSettings = null; internal static ConfigEntry<bool> AffectPlayersOnlyForOutgoingStagger = null; internal static ConfigEntry<bool> DebugLogging = null; internal static ConfigEntry<bool> DebugAttackLifecycle = null; internal static ConfigEntry<bool> DebugCounterDamage = null; internal static ConfigEntry<bool> DebugHitStop = null; internal static ConfigEntry<bool> DebugMovement = null; internal static ConfigEntry<bool> DebugHyperArmorLifecycle = null; internal static ConfigEntry<bool> EnableCounterDamage = null; internal static ConfigEntry<bool> OnlyPlayersCanDealCounterDamage = null; internal static ConfigEntry<bool> EnableCounterDamageDing = null; internal static ConfigEntry<HyperArmorDamageMitigationMode> HyperArmorMitigationMode = null; internal static ConfigEntry<bool> MitigateUnknownAttackerDamageDuringHyperArmor = null; internal static ConfigEntry<bool> DebugHyperArmorDamageMitigation = null; internal static ConfigEntry<bool> EnablePvpDamageModifiers = null; internal static ConfigEntry<float> HitStopDurationMultiplier = null; internal static ConfigEntry<float> HitStopDurationOverrideSeconds = null; internal static ConfigEntry<float> AttackMovementSpeedActiveWindowSeconds = null; internal static ConfigEntry<bool> ActiveHitboxEndsOnDoMeleeAttackPostfix = null; internal static readonly Dictionary<WeaponCategory, CategoryConfig> CategoryConfigs = new Dictionary<WeaponCategory, CategoryConfig>(); private static readonly Dictionary<Character, AttackRuntimeState> AttackStates = new Dictionary<Character, AttackRuntimeState>(); private static readonly Dictionary<object, Character> AttackInstanceOwners = new Dictionary<object, Character>(); private static readonly Dictionary<object, ItemData> AttackInstanceWeapons = new Dictionary<object, ItemData>(); private static readonly Dictionary<object, AttackRole> AttackInstanceRoles = new Dictionary<object, AttackRole>(); private static readonly HashSet<object> BalancedHyperArmorCompletedAttacks = new HashSet<object>(); private static readonly Dictionary<Character, AttackRuntimeState> MeleeHitStopContexts = new Dictionary<Character, AttackRuntimeState>(); private static readonly Dictionary<Character, HyperArmorRuntimeState> HyperArmorStates = new Dictionary<Character, HyperArmorRuntimeState>(); private static readonly Dictionary<Character, PendingAttackRoleState> PendingAttackRoles = new Dictionary<Character, PendingAttackRoleState>(); private static readonly Dictionary<Character, CounterVulnerableState> CounterVulnerableStates = new Dictionary<Character, CounterVulnerableState>(); private static readonly Dictionary<object, List<SuppressedFieldValue>> AttackMovementSpeedContexts = new Dictionary<object, List<SuppressedFieldValue>>(); private static readonly Dictionary<object, float> AttackMovementSpeedEndTimes = new Dictionary<object, float>(); private static readonly Dictionary<object, List<SuppressedFieldValue>> AttackRotationContexts = new Dictionary<object, List<SuppressedFieldValue>>(); private static readonly Dictionary<object, List<AnimatorSpeedValue>> AttackAnimationSpeedContexts = new Dictionary<object, List<AnimatorSpeedValue>>(); private static readonly Dictionary<string, FieldInfo?> FieldCache = new Dictionary<string, FieldInfo>(); private static readonly Dictionary<string, MethodInfo?> MethodCache = new Dictionary<string, MethodInfo>(); private static bool _processingPresetToggle; private static MethodInfo? _hitDataGetAttacker; private static MethodInfo? CharacterInAttackMethod; internal static GooCombatOverhaulPlugin Instance { get; private set; } = null; internal static ManualLogSource Log => ((BaseUnityPlugin)Instance).Logger; private static bool ModActive { get { if (Enabled != null) { return Enabled.Value; } return false; } } private void Awake() { Instance = this; BindConfig(); _hitDataGetAttacker = AccessTools.Method(typeof(HitData), "GetAttacker", (Type[])null, (Type[])null); PatchCoreDamageAndStagger(); PatchAttackLifecycle(); ((BaseUnityPlugin)this).Config.SettingChanged += delegate { AttackStates.Clear(); AttackInstanceOwners.Clear(); AttackInstanceWeapons.Clear(); AttackInstanceRoles.Clear(); BalancedHyperArmorCompletedAttacks.Clear(); MeleeHitStopContexts.Clear(); HyperArmorStates.Clear(); PendingAttackRoles.Clear(); CounterVulnerableStates.Clear(); RestoreAllAttackMovementSpeedTweaks(); RestoreAllAttackRotationTweaks(); RestoreAllAttackAnimationSpeedTweaks(); ProcessGeneralPresetToggles(); }; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Goo's Combat Overhaul 1.0.0 loaded."); } private void OnDestroy() { _harmony.UnpatchSelf(); AttackStates.Clear(); AttackInstanceOwners.Clear(); AttackInstanceWeapons.Clear(); AttackInstanceRoles.Clear(); BalancedHyperArmorCompletedAttacks.Clear(); MeleeHitStopContexts.Clear(); HyperArmorStates.Clear(); PendingAttackRoles.Clear(); CounterVulnerableStates.Clear(); RestoreAllAttackMovementSpeedTweaks(); RestoreAllAttackRotationTweaks(); RestoreAllAttackAnimationSpeedTweaks(); } private void Update() { if (!ModActive) { AttackStates.Clear(); AttackInstanceOwners.Clear(); AttackInstanceWeapons.Clear(); AttackInstanceRoles.Clear(); BalancedHyperArmorCompletedAttacks.Clear(); MeleeHitStopContexts.Clear(); HyperArmorStates.Clear(); PendingAttackRoles.Clear(); CounterVulnerableStates.Clear(); RestoreAllAttackMovementSpeedTweaks(); RestoreAllAttackRotationTweaks(); RestoreAllAttackAnimationSpeedTweaks(); } else { MaintainAttackMovementSpeedTweaks(); } } private void BindConfig() { Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ApplyModOptions", true, "Master switch. If false, disables every gameplay feature, runtime state check, and live modifier this mod offers without unloading the Harmony patches."); ApplyGoosSettings = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ApplyGoosSettings", false, "One-shot preset button. Fresh configs already default to Goo's curated settings; set true to rewrite combat/balance entries back to Goo's preset after manual edits or ResetToVanilla. This automatically returns to false. ApplyModOptions controls whether the mod is active."); ResetToVanillaSettings = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ResetToVanilla", false, "One-shot reset button. Set true to rewrite combat/balance config entries back to vanilla/no-op Valheim behavior, then this value automatically returns to false. Debug/general runtime toggles are preserved."); AffectPlayersOnlyForOutgoingStagger = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "AffectPlayersOnlyForOutgoingStagger", true, "If true, stagger-power calibration only applies to outgoing attacks made by players."); DebugLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "DebugLogging", false, "Legacy master debug switch. If true, enables all extra debug logs. Prefer the categorized toggles in the Debug section for normal testing."); DebugAttackLifecycle = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugAttackLifecycle", false, "Logs accepted attack state, role detection, and lifecycle cleanup events."); DebugCounterDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugCounterDamage", false, "Logs counter-vulnerability and counter-damage accept/reject reasons."); DebugHitStop = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugHitStop", false, "Logs direct Character.FreezeFrame hit-stop suppression/scaling decisions."); DebugMovement = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugMovement", false, "Logs attack movement-speed multiplier application/restoration."); DebugHyperArmorLifecycle = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugHyperArmorLifecycle", false, "Logs hyperarmor start/end lifecycle events. Damage-specific mitigation logs still use DebugHyperArmorDamageMitigation."); EnableCounterDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("Counter Damage", "EnableCounterDamage", true, "Master switch for counter-damage. Category settings decide which weapon types and which attack buttons can use it."); OnlyPlayersCanDealCounterDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("Counter Damage", "OnlyPlayersCanDealCounterDamage", true, "If true, only player attacks can trigger counter-damage. If false, mobs using eligible melee attacks can also trigger it."); EnableCounterDamageDing = ((BaseUnityPlugin)this).Config.Bind<bool>("Counter Damage", "EnableCounterDamageDing", true, "If true, plays a short parry/stagger-style ding when counter-damage is successfully applied."); HyperArmorMitigationMode = ((BaseUnityPlugin)this).Config.Bind<HyperArmorDamageMitigationMode>("Hyperarmor", "HyperArmorDamageMitigationMode", HyperArmorDamageMitigationMode.RawHitData, "How hyperarmor damage reduction is applied. RawHitData is reliable but interacts with armor nonlinearly; FinalHealthRestore tries to correct final HP loss; RawHitDataAndFinalHealthRestore does both for reliability."); MitigateUnknownAttackerDamageDuringHyperArmor = ((BaseUnityPlugin)this).Config.Bind<bool>("Hyperarmor", "MitigateUnknownAttackerDamageDuringHyperArmor", true, "If true, hyperarmor can mitigate incoming damage even when HitData.GetAttacker() fails. This fixes some enemy hits but may also affect non-environmental unknown-source damage."); DebugHyperArmorDamageMitigation = ((BaseUnityPlugin)this).Config.Bind<bool>("Hyperarmor", "DebugHyperArmorDamageMitigation", false, "Logs why hyperarmor damage mitigation does or does not run. Useful when KB/stagger resistance works but damage reduction does not."); EnablePvpDamageModifiers = ((BaseUnityPlugin)this).Config.Bind<bool>("PvP Damage", "EnablePvpDamageModifiers", true, "Master switch for per-category PvP damage multipliers. Defaults reduce bows, crossbows, and magic to 0.5x against players; all other categories remain 1x."); HitStopDurationMultiplier = ((BaseUnityPlugin)this).Config.Bind<float>("Hit-Stop / Hitlag", "HitStopDurationMultiplier", 1f, "Global multiplier for Valheim's hardcoded melee hit FreezeFrame duration on categories where EnableHitStopOnHit=true. 1 keeps vanilla 0.15s; 0 disables it; 0.5 halves it. Categories with EnableHitStopOnHit=false skip FreezeFrame entirely."); HitStopDurationOverrideSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Hit-Stop / Hitlag", "HitStopDurationOverrideSeconds", -1f, "Optional absolute hit-stop duration in seconds for categories where EnableHitStopOnHit=true. -1 uses HitStopDurationMultiplier. 0 disables hit-stop. This only applies to player melee hit FreezeFrame calls during tracked attacks."); AttackMovementSpeedActiveWindowSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Movement", "AttackMovementSpeedActiveWindowSeconds", 0.25f, "Seconds that AttackMovementSpeedMultiplier remains applied after the real melee hitbox/damage-scan event. This prevents movement-speed changes from activating during failed/no-stamina attack attempts or the entire attack animation."); ActiveHitboxEndsOnDoMeleeAttackPostfix = ((BaseUnityPlugin)this).Config.Bind<bool>("Hyperarmor", "ActiveHitboxEndsOnDoMeleeAttackPostfix", false, "If true, ActiveHitboxFrames hyperarmor ends immediately at DoMeleeAttack postfix. False is recommended: DoMeleeAttack is an instant damage scan, so ending at postfix makes the hyperarmor window too tiny to matter."); AddCategory(WeaponCategory.TwoHandedSword, "Two-Handed Swords", 2f, 2f, HyperArmorMode.Balanced, HyperArmorMode.Balanced, enableHitStopOnHit: false, 2.46f, 2.46f, primaryCounter: false, secondaryCounter: true, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 0.7f, 0.7f, 0f, 0f, 0f, 0f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true, 1f, 6f); AddCategory(WeaponCategory.TwoHandedAxe, "Two-Handed Axes", 2f, 1.5f, HyperArmorMode.Balanced, HyperArmorMode.Balanced, enableHitStopOnHit: false, 3.44f, 3.44f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 0.7f, 0.7f, 0f, 0f, 0f, 0f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.Sledge, "Sledges", 3f, 3f, HyperArmorMode.Balanced, HyperArmorMode.Balanced, enableHitStopOnHit: true, 2.23f, 2.23f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 0.7f, 0.7f, 0f, 0f, 0f, 0f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.DualAxes, "Dual Axes", 1f, 2f, HyperArmorMode.Off, HyperArmorMode.Off, enableHitStopOnHit: false, 2.4f, 2.4f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 2f, 2f, 1f, 1.5f, 1f, -1f, primaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.Atgeir, "Atgeirs", 1f, 1f, HyperArmorMode.Off, HyperArmorMode.Balanced, enableHitStopOnHit: true, 2.98f, 2.98f, primaryCounter: true, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 0.7f, 1f, 0f, 1f, 0f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, -1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: false, 1f, 6f); AddCategory(WeaponCategory.OneHandedSword, "One-Handed Swords", 1f, 1f, HyperArmorMode.Off, HyperArmorMode.Balanced, enableHitStopOnHit: false, 2.46f, 2.46f, primaryCounter: false, secondaryCounter: true, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 0f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.OneHandedAxe, "One-Handed Axes", 1f, 3f, HyperArmorMode.Off, HyperArmorMode.Balanced, enableHitStopOnHit: true, 2.74f, 2.74f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 0f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1.25f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.Club, "Clubs and Maces", 1.5f, 1.5f, HyperArmorMode.Off, HyperArmorMode.Balanced, enableHitStopOnHit: true, 2.46f, 2.46f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 0f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.Knife, "Knives", 1f, 1f, HyperArmorMode.Off, HyperArmorMode.Off, enableHitStopOnHit: true, 2.04f, 2.04f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.Spear, "Spears", 1f, 2f, HyperArmorMode.Off, HyperArmorMode.Off, enableHitStopOnHit: true, 0.68f, 0.68f, primaryCounter: true, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.Bow, "Bows", 1f, 1f, HyperArmorMode.Off, HyperArmorMode.Off, enableHitStopOnHit: true, 1.15f, 1.15f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, -1f, -1f, primaryLockRotationAfterAttackTrigger: false, secondaryLockRotationAfterAttackTrigger: false, 1f, 1f, 0.5f, 0.5f); AddCategory(WeaponCategory.Crossbow, "Crossbows", 1f, 1f, HyperArmorMode.Off, HyperArmorMode.Off, enableHitStopOnHit: true, 1.15f, 1.15f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, -1f, -1f, primaryLockRotationAfterAttackTrigger: false, secondaryLockRotationAfterAttackTrigger: false, 1f, 1f, 0.5f, 0.5f); AddCategory(WeaponCategory.Magic, "Magic", 1f, 1f, HyperArmorMode.Off, HyperArmorMode.Off, enableHitStopOnHit: true, 1.15f, 1.15f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, -1f, -1f, primaryLockRotationAfterAttackTrigger: false, secondaryLockRotationAfterAttackTrigger: false, 1f, 1f, 0.5f, 0.5f); AddCategory(WeaponCategory.Unarmed, "Unarmed", 1f, 1f, HyperArmorMode.Off, HyperArmorMode.Off, enableHitStopOnHit: true, 1.15f, 1.15f, primaryCounter: false, secondaryCounter: false, StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode.MultiplyVanilla, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, primaryLockRotationAfterAttackTrigger: true, secondaryLockRotationAfterAttackTrigger: true); AddCategory(WeaponCategory.Other, "Other Weapons", 1f, 1f, HyperArmorMode.Off, HyperArmorMode.Off, enableHitStopOnHit: true, 1.15f, 1.15f); ProcessGeneralPresetToggles(); } private void AddCategory(WeaponCategory category, string section, float primaryStagger, float secondaryStagger, HyperArmorMode primaryHyper, HyperArmorMode secondaryHyper, bool enableHitStopOnHit, float primaryFullSeconds, float secondaryFullSeconds, bool primaryCounter = false, bool secondaryCounter = false, StaggerApplicationMode primaryStaggerMode = StaggerApplicationMode.MultiplyVanilla, StaggerApplicationMode secondaryStaggerMode = StaggerApplicationMode.MultiplyVanilla, float primaryDamageTakenMultiplierDuringHyperArmor = 1f, float secondaryDamageTakenMultiplierDuringHyperArmor = 1f, float primaryStaggerTakenMultiplierDuringHyperArmor = 1f, float secondaryStaggerTakenMultiplierDuringHyperArmor = 1f, float primaryKnockbackTakenMultiplierDuringHyperArmor = 1f, float secondaryKnockbackTakenMultiplierDuringHyperArmor = 1f, float primaryDamageRateMultiplier = 1f, float secondaryDamageRateMultiplier = 1f, float primaryStaminaRateMultiplier = 1f, float secondaryStaminaRateMultiplier = 1f, float primaryAttackMovementSpeedMultiplier = 1f, float secondaryAttackMovementSpeedMultiplier = 1f, float primaryAttackAnimationSpeedMultiplier = 1f, float secondaryAttackAnimationSpeedMultiplier = 1f, float primaryAttackRotationFactor = -1f, float secondaryAttackRotationFactor = -1f, bool primaryLockRotationAfterAttackTrigger = false, bool secondaryLockRotationAfterAttackTrigger = false, float primaryAdrenalineMultiplier = 1f, float secondaryAdrenalineMultiplier = 1f, float primaryPvpDamageMultiplier = 1f, float secondaryPvpDamageMultiplier = 1f) { CategoryConfigs[category] = new CategoryConfig(((BaseUnityPlugin)this).Config.Bind<StaggerApplicationMode>(section, "PrimaryStaggerMode", primaryStaggerMode, "How PrimaryStaggerPowerValue is applied. MultiplyVanilla multiplies Valheim's current stagger multiplier. SetFinalMultiplier replaces the final HitData stagger multiplier with this value."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryStaggerPowerValue", primaryStagger, "Light/primary attack stagger calibration. In MultiplyVanilla mode, 2 means 2x vanilla. In SetFinalMultiplier mode, 4 means final stagger multiplier becomes 4."), ((BaseUnityPlugin)this).Config.Bind<StaggerApplicationMode>(section, "SecondaryStaggerMode", secondaryStaggerMode, "How SecondaryStaggerPowerValue is applied. MultiplyVanilla multiplies Valheim's current stagger multiplier. SetFinalMultiplier replaces the final HitData stagger multiplier with this value."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryStaggerPowerValue", secondaryStagger, "Heavy/secondary attack stagger calibration. For dual axes, default 2x is intended to turn a vanilla 2x finishing hit into 4x final stagger."), ((BaseUnityPlugin)this).Config.Bind<HyperArmorMode>(section, "PrimaryHyperArmorMode", primaryHyper, "Hyperarmor mode for light/primary attacks. Balanced starts when Valheim enters the real attack animation and ends immediately after Valheim's attack-trigger/hitbox event; FullAttackAnimation lasts through recovery; ActiveHitboxFrames starts at the hitbox event; WhileHoldingWeaponType is always on while the weapon type is held."), ((BaseUnityPlugin)this).Config.Bind<HyperArmorMode>(section, "SecondaryHyperArmorMode", secondaryHyper, "Hyperarmor mode for heavy/secondary attacks. Balanced starts when Valheim enters the real attack animation and ends immediately after Valheim's attack-trigger/hitbox event; FullAttackAnimation lasts through recovery; ActiveHitboxFrames starts at the hitbox event; WhileHoldingWeaponType is always on while the weapon type is held."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryDamageTakenMultiplierDuringHyperArmor", primaryDamageTakenMultiplierDuringHyperArmor, "Final HP-loss multiplier while primary-attack hyperarmor is active. 0.70 means 30% less final damage after armor/block/resistance; 1.00 means no damage reduction. Environmental/lava/fall damage is ignored by requiring a real attacker."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryDamageTakenMultiplierDuringHyperArmor", secondaryDamageTakenMultiplierDuringHyperArmor, "Final HP-loss multiplier while secondary-attack hyperarmor is active. 0.70 means 30% less final damage after armor/block/resistance; 1.00 means no damage reduction. Environmental/lava/fall damage is ignored by requiring a real attacker."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryStaggerTakenMultiplierDuringHyperArmor", primaryStaggerTakenMultiplierDuringHyperArmor, "Incoming stagger multiplier during primary-attack hyperarmor. 0 means stagger immunity; 1 means vanilla."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryStaggerTakenMultiplierDuringHyperArmor", secondaryStaggerTakenMultiplierDuringHyperArmor, "Incoming stagger multiplier during secondary-attack hyperarmor. 0 means stagger immunity; 1 means vanilla."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryKnockbackTakenMultiplierDuringHyperArmor", primaryKnockbackTakenMultiplierDuringHyperArmor, "Incoming push/knockback multiplier during primary-attack hyperarmor. 0 means knockback immunity; 1 means vanilla."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryKnockbackTakenMultiplierDuringHyperArmor", secondaryKnockbackTakenMultiplierDuringHyperArmor, "Incoming push/knockback multiplier during secondary-attack hyperarmor. 0 means knockback immunity; 1 means vanilla."), ((BaseUnityPlugin)this).Config.Bind<bool>(section, "EnableHitStopOnHit", enableHitStopOnHit, "Valheim calls Character.FreezeFrame(0.15s) after successful melee hits. True keeps/modulates that hit-stop through global HitStop settings; false suppresses it for this weapon category."), ((BaseUnityPlugin)this).Config.Bind<bool>(section, "PrimaryCounterDamageEnabled", primaryCounter, "If true, eligible melee primary attacks can gain counter-damage against targets during Valheim's real attack animation/combat state."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryCounterDamageMultiplier", 1.3f, "Counter-damage multiplier for eligible primary attacks. 1.30 means 30% additional damage."), ((BaseUnityPlugin)this).Config.Bind<bool>(section, "SecondaryCounterDamageEnabled", secondaryCounter, "If true, eligible melee secondary attacks can gain counter-damage against targets during Valheim's real attack animation/combat state."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryCounterDamageMultiplier", 1.3f, "Counter-damage multiplier for eligible secondary attacks. 1.30 means 30% additional damage."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryDamageRateMultiplier", primaryDamageRateMultiplier, "Outgoing primary/light attack damage multiplier. 1 leaves vanilla damage unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryDamageRateMultiplier", secondaryDamageRateMultiplier, "Outgoing secondary/heavy attack damage multiplier. 1 leaves vanilla damage unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryStaminaRateMultiplier", primaryStaminaRateMultiplier, "Primary/light attack stamina-cost multiplier where the Valheim stamina method is available. 1 leaves vanilla stamina unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryStaminaRateMultiplier", secondaryStaminaRateMultiplier, "Secondary/heavy attack stamina-cost multiplier where the Valheim stamina method is available. 1 leaves vanilla stamina unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryAttackMovementSpeedMultiplier", primaryAttackMovementSpeedMultiplier, "Primary/light attack movement-speed multiplier applied only for the short active window after DoMeleeAttack/hitbox startup. 1 leaves movement unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryAttackMovementSpeedMultiplier", secondaryAttackMovementSpeedMultiplier, "Secondary/heavy attack movement-speed multiplier applied only for the short active window after DoMeleeAttack/hitbox startup. 1 leaves movement unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryAttackAnimationSpeedMultiplier", primaryAttackAnimationSpeedMultiplier, "Primary/light actual swing animation-speed multiplier using Animator.speed during the attack. 1 leaves vanilla animation timing unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryAttackAnimationSpeedMultiplier", secondaryAttackAnimationSpeedMultiplier, "Secondary/heavy actual swing animation-speed multiplier using Animator.speed during the attack. 1 leaves vanilla animation timing unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryAttackRotationFactor", primaryAttackRotationFactor, "Primary/light attack rotation factor override for the live Attack.m_speedFactorRotation field. -1 leaves vanilla; 0 prevents turning; 1 allows full 100% turn speed during the attack animation."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryAttackRotationFactor", secondaryAttackRotationFactor, "Secondary/heavy attack rotation factor override for the live Attack.m_speedFactorRotation field. -1 leaves vanilla; 0 prevents turning; 1 allows full 100% turn speed during the attack animation."), ((BaseUnityPlugin)this).Config.Bind<bool>(section, "PrimaryLockRotationAfterAttackTrigger", primaryLockRotationAfterAttackTrigger, "If true, primary/light attack rotation is forced to 0 from Valheim's OnAttackTrigger/hitbox event until the attack recovers/stops."), ((BaseUnityPlugin)this).Config.Bind<bool>(section, "SecondaryLockRotationAfterAttackTrigger", secondaryLockRotationAfterAttackTrigger, "If true, secondary/heavy attack rotation is forced to 0 from Valheim's OnAttackTrigger/hitbox event until the attack recovers/stops."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryAdrenalineMultiplier", primaryAdrenalineMultiplier, "Multiplier over vanilla adrenaline gained by primary/light hits. 1 leaves vanilla adrenaline unchanged; 6 attempts to add 5 extra points assuming vanilla gives 1."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryAdrenalineMultiplier", secondaryAdrenalineMultiplier, "Multiplier over vanilla adrenaline gained by secondary/heavy hits. 1 leaves vanilla adrenaline unchanged; 6 attempts to add 5 extra points assuming vanilla gives 1."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryPvpDamageMultiplier", primaryPvpDamageMultiplier, "PvP primary/light damage multiplier when attacker and victim are both players. 1 leaves PvP unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryPvpDamageMultiplier", secondaryPvpDamageMultiplier, "PvP secondary/heavy damage multiplier when attacker and victim are both players. 1 leaves PvP unchanged."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryFullAttackAnimationDurationSeconds", primaryFullSeconds, "Fallback safety cap for FullAttackAnimation primary hyperarmor/counter windows. Attack.Stop/Abort/done hooks clear the real window earlier."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryFullAttackAnimationDurationSeconds", secondaryFullSeconds, "Fallback safety cap for FullAttackAnimation secondary hyperarmor/counter windows. Attack.Stop/Abort/done hooks clear the real window earlier."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "PrimaryActiveHitboxDurationSeconds", 0.25f, "Fallback maximum for ActiveHitboxFrames primary hyperarmor if the DoMeleeAttack postfix/end hook fails. Normally prefix starts and postfix ends the active window."), ((BaseUnityPlugin)this).Config.Bind<float>(section, "SecondaryActiveHitboxDurationSeconds", 0.25f, "Fallback maximum for ActiveHitboxFrames secondary hyperarmor if the DoMeleeAttack postfix/end hook fails. Normally prefix starts and postfix ends the active window.")); } private void ProcessGeneralPresetToggles() { if (_processingPresetToggle || ApplyGoosSettings == null || ResetToVanillaSettings == null || (!ApplyGoosSettings.Value && !ResetToVanillaSettings.Value)) { return; } _processingPresetToggle = true; try { if (ResetToVanillaSettings.Value) { ApplyVanillaPresetValues(); ResetToVanillaSettings.Value = false; ApplyGoosSettings.Value = false; ((BaseUnityPlugin)this).Config.Save(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Reset Goo's Combat Overhaul combat/balance config entries to vanilla/no-op Valheim values."); } else if (ApplyGoosSettings.Value) { ApplyGooPresetValues(); ApplyGoosSettings.Value = false; ((BaseUnityPlugin)this).Config.Save(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Applied Goo's curated combat-overhaul preset to the config. ApplyGoosSettings was reset to false; edit individual entries freely."); } } finally { _processingPresetToggle = false; } } private static void ApplyVanillaPresetValues() { AffectPlayersOnlyForOutgoingStagger.Value = true; EnableCounterDamage.Value = true; OnlyPlayersCanDealCounterDamage.Value = true; EnableCounterDamageDing.Value = true; HyperArmorMitigationMode.Value = HyperArmorDamageMitigationMode.RawHitData; MitigateUnknownAttackerDamageDuringHyperArmor.Value = true; EnablePvpDamageModifiers.Value = true; HitStopDurationMultiplier.Value = 1f; HitStopDurationOverrideSeconds.Value = -1f; AttackMovementSpeedActiveWindowSeconds.Value = 0.25f; ActiveHitboxEndsOnDoMeleeAttackPostfix.Value = false; foreach (CategoryConfig value in CategoryConfigs.Values) { SetCategoryToVanilla(value); } } private static void SetCategoryToVanilla(CategoryConfig cfg) { cfg.PrimaryStaggerMode.Value = StaggerApplicationMode.MultiplyVanilla; cfg.SecondaryStaggerMode.Value = StaggerApplicationMode.MultiplyVanilla; cfg.PrimaryStaggerPowerValue.Value = 1f; cfg.SecondaryStaggerPowerValue.Value = 1f; cfg.PrimaryHyperArmorMode.Value = HyperArmorMode.Off; cfg.SecondaryHyperArmorMode.Value = HyperArmorMode.Off; cfg.PrimaryDamageTakenMultiplier.Value = 1f; cfg.SecondaryDamageTakenMultiplier.Value = 1f; cfg.PrimaryStaggerTakenMultiplier.Value = 1f; cfg.SecondaryStaggerTakenMultiplier.Value = 1f; cfg.PrimaryKnockbackTakenMultiplier.Value = 1f; cfg.SecondaryKnockbackTakenMultiplier.Value = 1f; cfg.EnableHitStopOnHit.Value = true; cfg.PrimaryCounterDamageEnabled.Value = false; cfg.SecondaryCounterDamageEnabled.Value = false; cfg.PrimaryCounterDamageMultiplier.Value = 1.3f; cfg.SecondaryCounterDamageMultiplier.Value = 1.3f; cfg.PrimaryDamageRateMultiplier.Value = 1f; cfg.SecondaryDamageRateMultiplier.Value = 1f; cfg.PrimaryStaminaRateMultiplier.Value = 1f; cfg.SecondaryStaminaRateMultiplier.Value = 1f; cfg.PrimaryAttackMovementSpeedMultiplier.Value = 1f; cfg.SecondaryAttackMovementSpeedMultiplier.Value = 1f; cfg.PrimaryAttackAnimationSpeedMultiplier.Value = 1f; cfg.SecondaryAttackAnimationSpeedMultiplier.Value = 1f; cfg.PrimaryAttackRotationFactor.Value = -1f; cfg.SecondaryAttackRotationFactor.Value = -1f; cfg.PrimaryLockRotationAfterAttackTrigger.Value = false; cfg.SecondaryLockRotationAfterAttackTrigger.Value = false; cfg.PrimaryAdrenalineMultiplier.Value = 1f; cfg.SecondaryAdrenalineMultiplier.Value = 1f; cfg.PrimaryPvpDamageMultiplier.Value = 1f; cfg.SecondaryPvpDamageMultiplier.Value = 1f; cfg.PrimaryActiveHitboxDuration.Value = 0.25f; cfg.SecondaryActiveHitboxDuration.Value = 0.25f; } private static void ApplyGooPresetValues() { ApplyVanillaPresetValues(); AffectPlayersOnlyForOutgoingStagger.Value = true; EnableCounterDamage.Value = true; OnlyPlayersCanDealCounterDamage.Value = true; EnableCounterDamageDing.Value = true; HyperArmorMitigationMode.Value = HyperArmorDamageMitigationMode.RawHitData; MitigateUnknownAttackerDamageDuringHyperArmor.Value = true; EnablePvpDamageModifiers.Value = true; HitStopDurationMultiplier.Value = 1f; HitStopDurationOverrideSeconds.Value = -1f; AttackMovementSpeedActiveWindowSeconds.Value = 0.25f; ActiveHitboxEndsOnDoMeleeAttackPostfix.Value = false; WeaponCategory[] array = new WeaponCategory[11] { WeaponCategory.TwoHandedSword, WeaponCategory.TwoHandedAxe, WeaponCategory.Sledge, WeaponCategory.DualAxes, WeaponCategory.Atgeir, WeaponCategory.OneHandedSword, WeaponCategory.OneHandedAxe, WeaponCategory.Club, WeaponCategory.Knife, WeaponCategory.Spear, WeaponCategory.Unarmed }; for (int i = 0; i < array.Length; i++) { SetRotationPreset(GetConfig(array[i]), 1f, 1f, primaryLock: true, secondaryLock: true); } SetSecondaryRotationPreset(GetConfig(WeaponCategory.DualAxes), -1f, lockAfterTrigger: false); SetSecondaryRotationPreset(GetConfig(WeaponCategory.Atgeir), -1f, lockAfterTrigger: false); CategoryConfig config = GetConfig(WeaponCategory.TwoHandedSword); SetStagger(config, 2f, 2f); SetHyper(config, HyperArmorMode.Balanced, HyperArmorMode.Balanced); SetHyperArmorProtection(config, 0.7f, 0.7f, 0f, 0f, 0f, 0f); config.EnableHitStopOnHit.Value = false; config.SecondaryCounterDamageEnabled.Value = true; config.SecondaryAdrenalineMultiplier.Value = 6f; SetDurations(config, 2.46f, 2.46f); CategoryConfig config2 = GetConfig(WeaponCategory.TwoHandedAxe); SetStagger(config2, 2f, 1.5f); SetHyper(config2, HyperArmorMode.Balanced, HyperArmorMode.Balanced); SetHyperArmorProtection(config2, 0.7f, 0.7f, 0f, 0f, 0f, 0f); config2.EnableHitStopOnHit.Value = false; SetDurations(config2, 3.44f, 3.44f); CategoryConfig config3 = GetConfig(WeaponCategory.Sledge); SetStagger(config3, 3f, 3f); SetHyper(config3, HyperArmorMode.Balanced, HyperArmorMode.Balanced); SetHyperArmorProtection(config3, 0.7f, 0.7f, 0f, 0f, 0f, 0f); config3.EnableHitStopOnHit.Value = true; SetDurations(config3, 2.23f, 2.23f); CategoryConfig config4 = GetConfig(WeaponCategory.DualAxes); SetStagger(config4, 1f, 2f); SetHyper(config4, HyperArmorMode.Off, HyperArmorMode.Off); config4.EnableHitStopOnHit.Value = false; config4.PrimaryAttackMovementSpeedMultiplier.Value = 2f; config4.SecondaryAttackMovementSpeedMultiplier.Value = 2f; config4.SecondaryAttackAnimationSpeedMultiplier.Value = 1.5f; SetDurations(config4, 2.4f, 2.4f); CategoryConfig config5 = GetConfig(WeaponCategory.Atgeir); SetStagger(config5, 1f, 1f); SetHyper(config5, HyperArmorMode.Off, HyperArmorMode.Balanced); SetHyperArmorProtection(config5, 1f, 0.7f, 1f, 0f, 1f, 0f); config5.EnableHitStopOnHit.Value = true; config5.PrimaryCounterDamageEnabled.Value = true; config5.SecondaryAdrenalineMultiplier.Value = 6f; SetDurations(config5, 2.98f, 2.98f); CategoryConfig config6 = GetConfig(WeaponCategory.OneHandedSword); SetStagger(config6, 1f, 1f); SetHyper(config6, HyperArmorMode.Off, HyperArmorMode.Balanced); SetHyperArmorProtection(config6, 1f, 1f, 1f, 1f, 1f, 0f); config6.EnableHitStopOnHit.Value = false; config6.SecondaryCounterDamageEnabled.Value = true; SetDurations(config6, 2.46f, 2.46f); CategoryConfig config7 = GetConfig(WeaponCategory.OneHandedAxe); SetStagger(config7, 1f, 3f); SetHyper(config7, HyperArmorMode.Off, HyperArmorMode.Balanced); SetHyperArmorProtection(config7, 1f, 1f, 1f, 1f, 1f, 0f); config7.EnableHitStopOnHit.Value = true; config7.SecondaryAttackAnimationSpeedMultiplier.Value = 1.25f; SetDurations(config7, 2.74f, 2.74f); CategoryConfig config8 = GetConfig(WeaponCategory.Club); SetStagger(config8, 1.5f, 1.5f); SetHyper(config8, HyperArmorMode.Off, HyperArmorMode.Balanced); SetHyperArmorProtection(config8, 1f, 1f, 1f, 1f, 1f, 0f); config8.EnableHitStopOnHit.Value = true; SetDurations(config8, 2.46f, 2.46f); CategoryConfig config9 = GetConfig(WeaponCategory.Spear); SetStagger(config9, 1f, 2f); config9.PrimaryCounterDamageEnabled.Value = true; config9.EnableHitStopOnHit.Value = true; SetDurations(config9, 0.68f, 0.68f); CategoryConfig config10 = GetConfig(WeaponCategory.Bow); config10.PrimaryPvpDamageMultiplier.Value = 0.5f; config10.SecondaryPvpDamageMultiplier.Value = 0.5f; CategoryConfig config11 = GetConfig(WeaponCategory.Crossbow); config11.PrimaryPvpDamageMultiplier.Value = 0.5f; config11.SecondaryPvpDamageMultiplier.Value = 0.5f; CategoryConfig config12 = GetConfig(WeaponCategory.Magic); config12.PrimaryPvpDamageMultiplier.Value = 0.5f; config12.SecondaryPvpDamageMultiplier.Value = 0.5f; } private static void SetStagger(CategoryConfig cfg, float primary, float secondary) { cfg.PrimaryStaggerMode.Value = StaggerApplicationMode.MultiplyVanilla; cfg.SecondaryStaggerMode.Value = StaggerApplicationMode.MultiplyVanilla; cfg.PrimaryStaggerPowerValue.Value = primary; cfg.SecondaryStaggerPowerValue.Value = secondary; } private static void SetHyper(CategoryConfig cfg, HyperArmorMode primary, HyperArmorMode secondary) { cfg.PrimaryHyperArmorMode.Value = primary; cfg.SecondaryHyperArmorMode.Value = secondary; } private static void SetHyperArmorProtection(CategoryConfig cfg, float primaryDamage, float secondaryDamage, float primaryStagger, float secondaryStagger, float primaryKnockback, float secondaryKnockback) { cfg.PrimaryDamageTakenMultiplier.Value = primaryDamage; cfg.SecondaryDamageTakenMultiplier.Value = secondaryDamage; cfg.PrimaryStaggerTakenMultiplier.Value = primaryStagger; cfg.SecondaryStaggerTakenMultiplier.Value = secondaryStagger; cfg.PrimaryKnockbackTakenMultiplier.Value = primaryKnockback; cfg.SecondaryKnockbackTakenMultiplier.Value = secondaryKnockback; } private static void SetRotationPreset(CategoryConfig cfg, float primaryFactor, float secondaryFactor, bool primaryLock, bool secondaryLock) { cfg.PrimaryAttackRotationFactor.Value = primaryFactor; cfg.SecondaryAttackRotationFactor.Value = secondaryFactor; cfg.PrimaryLockRotationAfterAttackTrigger.Value = primaryLock; cfg.SecondaryLockRotationAfterAttackTrigger.Value = secondaryLock; } private static void SetSecondaryRotationPreset(CategoryConfig cfg, float factor, bool lockAfterTrigger) { cfg.SecondaryAttackRotationFactor.Value = factor; cfg.SecondaryLockRotationAfterAttackTrigger.Value = lockAfterTrigger; } private static void SetDurations(CategoryConfig cfg, float primary, float secondary) { cfg.PrimaryFullAttackDuration.Value = primary; cfg.SecondaryFullAttackDuration.Value = secondary; } private void PatchCoreDamageAndStagger() { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Expected O, but got Unknown //IL_0060: Expected O, but got Unknown //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Expected O, but got Unknown //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Expected O, but got Unknown _harmony.Patch((MethodBase)AccessTools.Method(typeof(Character), "Damage", new Type[1] { typeof(HitData) }, (Type[])null), new HarmonyMethod(typeof(GooCombatOverhaulPlugin), "CharacterDamagePrefix", (Type[])null), new HarmonyMethod(typeof(GooCombatOverhaulPlugin), "CharacterDamagePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Patched Character.Damage for stagger/damage/PvP/counter/adrenaline calibration and hyperarmor damage mitigation."); MethodInfo methodInfo = AccessTools.Method(typeof(Character), "FreezeFrame", new Type[1] { typeof(float) }, (Type[])null); if (methodInfo != null) { _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(GooCombatOverhaulPlugin), "CharacterFreezeFramePrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Patched Character.FreezeFrame(float) for direct melee hit-stop control."); } else { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not find Character.FreezeFrame(float). Hit-stop control is unavailable on this Valheim build."); } int num = 0; foreach (MethodInfo item in from m in AccessTools.GetDeclaredMethods(typeof(Character)) where m.Name == "Stagger" select m) { ParameterInfo[] parameters = item.GetParameters(); if (parameters.Length != 0 && parameters[0].ParameterType == typeof(Vector3)) { _harmony.Patch((MethodBase)item, new HarmonyMethod(typeof(GooCombatOverhaulPlugin), "CharacterStaggerPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; } } if (num > 0) { ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Patched {num} Character.Stagger overload(s) for hyperarmor stagger immunity."); } else { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not find a Character.Stagger(Vector3...) overload. Hyperarmor stagger-immunity hooks may be unavailable on this Valheim build."); } } private void PatchAttackLifecycle() { Type type = AccessTools.TypeByName("Attack"); if (type == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not find Attack type. Hyperarmor/counter timing patches are unavailable."); return; } PatchAttackStartMethods(type); PatchAttackMethod(type, "Update", "AttackUpdatePostfix", postfix: true); PatchAttackMethodOptional(type, "OnAttackTrigger", "AttackHitboxEventPrefix"); PatchAttackMethodOptional(type, "OnAttackTrigger", "AttackHitboxEventPostfix", postfix: true); PatchAttackMethod(type, "Stop", "AttackStopPostfix", postfix: true); PatchAttackMethod(type, "Abort", "AttackStopPostfix", postfix: true); PatchAttackMethodOptional(type, "OnAttackDone", "AttackStopPostfix", postfix: true); PatchAttackMethodOptional(type, "AttackDone", "AttackStopPostfix", postfix: true); PatchAttackMethodOptional(type, "Done", "AttackStopPostfix", postfix: true); PatchAttackMethod(type, "DoMeleeAttack", "AttackDoMeleeAttackPrefix"); PatchAttackMethod(type, "DoMeleeAttack", "AttackDoMeleeAttackPostfix", postfix: true); PatchHumanoidStartAttack(); PatchAttackStaminaMethods(type); } private void PatchAttackStartMethods(Type attackType) { List<MethodInfo> list = (from m in AccessTools.GetDeclaredMethods(attackType) where m.Name == "Start" && m.ReturnType == typeof(bool) select m).ToList(); if (list.Count == 0) { int num = AccessTools.GetDeclaredMethods(attackType).Count((MethodInfo m) => m.Name == "Start"); ((BaseUnityPlugin)this).Logger.LogWarning((object)$"Could not find a bool-returning Attack.Start overload. Found {num} Attack.Start overload(s), but successful-attack gating is unavailable on this Valheim build."); } else { PatchAttackMethods(list, "AttackStartPostfix", postfix: true); ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Patched {list.Count} successful Attack.Start overload(s) for valid-attack gated attack state, animation speed, and FullAttackAnimation hyperarmor. Balanced hyperarmor and counter vulnerability are gated by Attack.Update/InAttack; movement-speed tweaks are gated by DoMeleeAttack hitbox startup and a short active window."); } } private void PatchAttackMethod(Type attackType, string methodName, string patchName, bool postfix = false) { string methodName2 = methodName; List<MethodInfo> list = (from m in AccessTools.GetDeclaredMethods(attackType) where m.Name == methodName2 select m).ToList(); if (list.Count == 0) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not find Attack." + methodName2 + ". Some timing modes may use fallback behavior only.")); return; } PatchAttackMethods(list, patchName, postfix); ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Patched {list.Count} Attack.{methodName2} overload(s)."); } private void PatchAttackMethodOptional(Type attackType, string methodName, string patchName, bool postfix = false) { string methodName2 = methodName; List<MethodInfo> list = (from m in AccessTools.GetDeclaredMethods(attackType) where m.Name == methodName2 select m).ToList(); if (list.Count != 0) { PatchAttackMethods(list, patchName, postfix); ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Patched optional {list.Count} Attack.{methodName2} overload(s)."); } } private void PatchAttackMethods(List<MethodInfo> methods, string patchName, bool postfix) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown foreach (MethodInfo method in methods) { HarmonyMethod val = new HarmonyMethod(typeof(GooCombatOverhaulPlugin), patchName, (Type[])null); if (postfix) { _harmony.Patch((MethodBase)method, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { _harmony.Patch((MethodBase)method, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } } private void PatchHumanoidStartAttack() { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Expected O, but got Unknown //IL_008f: Expected O, but got Unknown Type type = AccessTools.TypeByName("Humanoid"); if (type == null) { return; } List<MethodInfo> list = (from m in AccessTools.GetDeclaredMethods(type) where m.Name == "StartAttack" && m.ReturnType == typeof(bool) select m).ToList(); foreach (MethodInfo item in list) { _harmony.Patch((MethodBase)item, new HarmonyMethod(typeof(GooCombatOverhaulPlugin), "HumanoidStartAttackPrefix", (Type[])null), new HarmonyMethod(typeof(GooCombatOverhaulPlugin), "HumanoidStartAttackPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } if (list.Count > 0) { ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Patched {list.Count} bool-returning Humanoid.StartAttack overload(s) for exact primary/secondary attack-role detection and failed-start cleanup."); } } private void PatchAttackStaminaMethods(Type attackType) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected O, but got Unknown int num = 0; foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(attackType)) { if (SafeLower(declaredMethod.Name).Contains("stamina") && !(declaredMethod.ReturnType != typeof(float))) { _harmony.Patch((MethodBase)declaredMethod, (HarmonyMethod)null, new HarmonyMethod(typeof(GooCombatOverhaulPlugin), "AttackStaminaCostPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; } } if (num > 0) { ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Patched {num} Attack stamina-return method(s) for stamina-rate multipliers."); } else { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not find an Attack stamina-return method. StaminaRateMultiplier config may be unavailable on this Valheim build."); } } public static void HumanoidStartAttackPrefix(object __instance, object[] __args) { if (!ModActive) { return; } Character val = (Character)((__instance is Character) ? __instance : null); if (val == null) { return; } AttackRole role = AttackRole.Primary; bool flag = default(bool); foreach (object obj in __args) { int num; if (obj is bool) { flag = (bool)obj; num = 1; } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0) { role = AttackRole.Secondary; break; } } PendingAttackRoles[val] = new PendingAttackRoleState(role, Time.time + 0.75f); } public static void HumanoidStartAttackPostfix(object __instance, object[] __args, bool __result) { if (ModActive) { Character val = (Character)((__instance is Character) ? __instance : null); if (val != null && !__result) { PendingAttackRoles.Remove(val); } } } public static void AttackStaminaCostPostfix(object __instance, object[] __args, ref float __result) { if (ModActive && !Mathf.Approximately(__result, 0f) && TryGetAttackContext(__instance, __args, out Character character, out ItemData weapon) && weapon != null) { WeaponCategory category = ClassifyWeapon(weapon); AttackRole role = DetermineAttackRole(character, __instance, weapon); float num = Mathf.Clamp(GetConfig(category).GetStaminaRateMultiplier(role), 0f, 10f); if (!Mathf.Approximately(num, 1f)) { __result *= num; } } } public static void AttackStartPostfix(object __instance, object[] __args, bool __result) { if (!ModActive || !__result || !TryGetAttackContext(__instance, __args, out Character character, out ItemData weapon)) { return; } WeaponCategory category = ((weapon != null) ? ClassifyWeapon(weapon) : WeaponCategory.Unarmed); AttackRole role = DetermineAttackRole(character, __instance, weapon); SetAttackState(character, category, role, __instance); ClearBalancedHyperArmorCompleted(__instance); if (character is Player && weapon != null) { ApplyAttackAnimationSpeedIfConfigured(__instance, character, weapon, category, role); ApplyAttackRotationOverrideIfConfigured(__instance, weapon, category, role); } if (character is Player && weapon != null) { CategoryConfig config = GetConfig(category); if (config.GetHyperArmorMode(role) == HyperArmorMode.FullAttackAnimation) { float num = Mathf.Max(0.01f, config.GetFullAttackDuration(role)); SetHyperArmor(character, category, role, HyperArmorMode.FullAttackAnimation, Time.time + num, __instance); } } } public static void AttackUpdatePostfix(object __instance, object[] __args) { if (!ModActive || !TryGetAttackContext(__instance, __args, out Character character, out ItemData weapon)) { return; } if (!CharacterReportsInAttack(character)) { EndCounterVulnerableForAttack(character, __instance); if (character is Player) { EndHyperArmorForAttack(character, __instance, HyperArmorMode.Balanced); } return; } WeaponCategory category = ((weapon != null) ? ClassifyWeapon(weapon) : WeaponCategory.Unarmed); AttackRole role = DetermineAttackRole(character, __instance, weapon); SetAttackStateIfChanged(character, category, role, __instance); MarkCounterVulnerableDuringAttackAnimation(character, category, role, __instance); if (character is Player && weapon != null) { CategoryConfig config = GetConfig(category); if (config.GetHyperArmorMode(role) == HyperArmorMode.Balanced && !IsBalancedHyperArmorCompleted(__instance)) { float num = Mathf.Max(0.05f, config.GetFullAttackDuration(role)); SetHyperArmorIfChanged(character, category, role, HyperArmorMode.Balanced, Time.time + num, __instance); } } } public static void AttackDoMeleeAttackPrefix(object __instance, object[] __args) { if (!ModActive || !TryGetAttackContext(__instance, __args, out Character character, out ItemData weapon)) { return; } WeaponCategory category = ((weapon != null) ? ClassifyWeapon(weapon) : WeaponCategory.Unarmed); AttackRole role = DetermineAttackRole(character, __instance, weapon); SetAttackState(character, category, role, __instance); if (character is Player && weapon != null) { BeginMeleeHitStopContext(character, category, role, __instance); ApplyAttackMovementSpeedIfConfigured(__instance, weapon, category, role); } if (character is Player && weapon != null) { CategoryConfig config = GetConfig(category); if (config.GetHyperArmorMode(role) == HyperArmorMode.ActiveHitboxFrames) { float num = Mathf.Max(0.01f, config.GetActiveHitboxDuration(role)); SetHyperArmor(character, category, role, HyperArmorMode.ActiveHitboxFrames, Time.time + num, __instance); } } } public static void AttackHitboxEventPrefix(object __instance, object[] __args) { if (ModActive && TryGetAttackContext(__instance, __args, out Character character, out ItemData weapon) && character is Player && weapon != null) { WeaponCategory category = ClassifyWeapon(weapon); AttackRole role = DetermineAttackRole(character, __instance, weapon); ApplyAttackRotationLockAfterTriggerIfConfigured(__instance, weapon, category, role); } } public static void AttackHitboxEventPostfix(object __instance, object[] __args) { EndBalancedHyperArmorAfterAttackEvent(__instance, __args, "OnAttackTrigger"); } public static void AttackDoMeleeAttackPostfix(object __instance, object[] __args) { EndBalancedHyperArmorAfterAttackEvent(__instance, __args, "DoMeleeAttack"); EndMeleeHitStopContext(__instance, __args); } private static void EndBalancedHyperArmorAfterAttackEvent(object __instance, object[] __args, string source) { if (ModActive && TryGetAttackContext(__instance, __args, out Character character, out ItemData _) && character is Player) { MarkBalancedHyperArmorCompleted(__instance); EndHyperArmorForAttack(character, __instance, HyperArmorMode.Balanced); if (source == "DoMeleeAttack" && ActiveHitboxEndsOnDoMeleeAttackPostfix.Value) { EndHyperArmorForAttack(character, __instance, HyperArmorMode.ActiveHitboxFrames); } } } public static void AttackStopPostfix(object __instance, object[] __args) { if (ModActive && TryGetAttackContext(__instance, __args, out Character character, out ItemData _)) { EndCounterVulnerableForAttack(character, __instance); EndAttackStateForAttack(character, __instance); EndMeleeHitStopContext(__instance, __args); AttackInstanceOwners.Remove(__instance); AttackInstanceWeapons.Remove(__instance); AttackInstanceRoles.Remove(__instance); ClearBalancedHyperArmorCompleted(__instance); RestoreAttackMovementSpeedTweaks(__instance); RestoreAttackRotationTweaks(__instance); RestoreAttackAnimationSpeedTweaks(__instance); if (character is Player) { EndHyperArmorForAttack(character, __instance, null); } } } public static void CharacterDamagePrefix(Character __instance, HitData hit, ref DamagePatchState __state) { __state = null; if (ModActive && hit != null && !((Object)(object)__instance == (Object)null)) { Character attacker = GetAttacker(hit); ApplyOutgoingDamageAndPvpCalibration(__instance, attacker, hit); ApplyCounterDamageIfEligible(__instance, attacker, hit); ApplyOutgoingStaggerCalibration(attacker, hit); ApplyAdrenalineMultiplierIfEligible(attacker, hit); __state = ApplyHyperArmorIfActive(__instance, attacker, hit); } } public static void CharacterDamagePostfix(Character __instance, DamagePatchState __state) { if ((Object)(object)__instance != (Object)null && __state != null) { ApplyHyperArmorFinalDamageReduction(__state); } } public static bool CharacterStaggerPrefix(Character __instance, ref Vector3 __0) { if (!ModActive || (Object)(object)__instance == (Object)null) { return true; } if (__instance is Player && TryGetActiveHyperArmor(__instance, out var category, out var role)) { CategoryConfig config = GetConfig(category); if (Mathf.Max(0f, config.GetStaggerTakenMultiplier(role)) <= 0.0001f) { if (DebugLogging.Value) { Log.LogInfo((object)("Blocked stagger during hyperarmor for " + SafeName(__instance) + ".")); } return false; } } return true; } private static void BeginMeleeHitStopContext(Character character, WeaponCategory category, AttackRole role, object? attackInstance) { if (!((Object)(object)character == (Object)null)) { MeleeHitStopContexts[character] = new AttackRuntimeState(category, role, Time.time + 0.5f, attackInstance); } } private static void EndMeleeHitStopContext(object? attackInstance, object[]? args) { Character character = null; Character value; if (attackInstance != null && args != null && TryGetAttackContext(attackInstance, args, out character, out ItemData _)) { if ((Object)(object)character != (Object)null) { MeleeHitStopContexts.Remove(character); } } else if (attackInstance != null && AttackInstanceOwners.TryGetValue(attackInstance, out value)) { MeleeHitStopContexts.Remove(value); } } private static bool TryGetMeleeHitStopContext(Character character, out AttackRuntimeState state) { state = default(AttackRuntimeState); if ((Object)(object)character == (Object)null) { return false; } if (!MeleeHitStopContexts.TryGetValue(character, out state)) { return false; } if (Time.time > state.EndTime || IsCharacterDead(character)) { MeleeHitStopContexts.Remove(character); return false; } return true; } public static bool CharacterFreezeFramePrefix(Character __instance, ref float __0) { if (ModActive) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null) { if (__0 <= 0f || __0 > 0.5f) { return true; } if (!TryGetMeleeHitStopContext((Character)(object)val, out var state)) { return true; } if (!GetConfig(state.Category).EnableHitStopOnHit.Value) { if (Debug(DebugHitStop)) { Log.LogInfo((object)$"Suppressed melee FreezeFrame({__0.ToString(CultureInfo.InvariantCulture)}s) for {SafeName((Character)(object)val)} using {state.Category} {state.Role}."); } return false; } float value = HitStopDurationOverrideSeconds.Value; if (value >= 0f) { __0 = Mathf.Max(0f, value); } else { __0 *= Mathf.Clamp(HitStopDurationMultiplier.Value, 0f, 5f); } if (__0 <= 0.0001f) { if (Debug(DebugHitStop)) { Log.LogInfo((object)$"Suppressed melee FreezeFrame through global hit-stop duration settings for {SafeName((Character)(object)val)} using {state.Category} {state.Role}."); } return false; } if (Debug(DebugHitStop)) { Log.LogInfo((object)$"Allowed melee FreezeFrame duration {__0.ToString(CultureInfo.InvariantCulture)}s for {SafeName((Character)(object)val)} using {state.Category} {state.Role}."); } return true; } } return true; } private static bool IsBalancedHyperArmorCompleted(object? attackInstance) { if (attackInstance != null) { return BalancedHyperArmorCompletedAttacks.Contains(attackInstance); } return false; } private static void MarkBalancedHyperArmorCompleted(object? attackInstance) { if (attackInstance != null) { BalancedHyperArmorCompletedAttacks.Add(attackInstance); if (Debug(DebugHyperArmorLifecycle)) { Log.LogInfo((object)"Balanced hyperarmor marked complete for current attack; later recovery-frame Attack.Update calls will not restart it."); } } } private static void ClearBalancedHyperArmorCompleted(object? attackInstance) { if (attackInstance != null) { BalancedHyperArmorCompletedAttacks.Remove(attackInstance); } } private static void SetAttackStateIfChanged(Character character, WeaponCategory category, AttackRole role, object? attackInstance) { if (!((Object)(object)character == (Object)null) && (!AttackStates.TryGetValue(character, out var value) || value.Category != category || value.Role != role || value.AttackInstance != attackInstance || !(Time.time <= value.EndTime))) { SetAttackState(character, category, role, attackInstance); } } private static void SetAttackState(Character character, WeaponCategory category, AttackRole role, object? attackInstance) { if ((Object)(object)character == (Object)null) { return; } float num = Mathf.Max(0.01f, GetConfig(category).GetFullAttackDuration(role)); AttackStates[character] = new AttackRuntimeState(category, role, Time.time + num, attackInstance); if (attackInstance != null) { AttackInstanceOwners[attackInstance] = character; AttackInstanceRoles[attackInstance] = role; ItemData currentWeapon = GetCurrentWeapon(character); if (currentWeapon != null) { AttackInstanceWeapons[attackInstance] = currentWeapon; } } if (Debug(DebugAttackLifecycle)) { Log.LogInfo((object)$"Attack state: {SafeName(character)} {category} {role}; fallback end {(Time.time + num).ToString(CultureInfo.InvariantCulture)}."); } } private static void EndAttackStateForAttack(Character character, object? attackInstance) { if (!((Object)(object)character == (Object)null) && AttackStates.TryGetValue(character, out var value) && (value.AttackInstance == null || attackInstance == null || value.AttackInstance == attackInstance)) { AttackStates.Remove(character); } } private static AttackRole GetCurrentAttackRole(Character attacker, WeaponCategory category) { if ((Object)(object)attacker == (Object)null) { return AttackRole.Primary; } if (!AttackStates.TryGetValue(attacker, out var value)) { return AttackRole.Primary; } if (Time.time > value.EndTime || IsCharacterDead(attacker)) { AttackStates.Remove(attacker); return AttackRole.Primary; } if (value.Category != category) { return AttackRole.Primary; } return value.Role; } private static bool AttackObjectsLookSimilar(object attackInstance, object candidate) { if (attackInstance == candidate) { return true; } if (attackInstance == null || candidate == null) { return false; } string[] obj = new string[8] { "m_attackAnimation", "m_attackChainLevels", "m_attackStamina", "m_damageMultiplier", "m_staggerMultiplier", "m_attackType", "m_attackRandomAnimations", "m_attackProjectile" }; int num = 0; int num2 = 0; string[] array = obj; foreach (string fieldName in array) { object fieldValue = GetFieldValue<object>(attackInstance, fieldName); object fieldValue2 = GetFieldValue<object>(candidate, fieldName); if (fieldValue != null && fieldValue2 != null) { num++; if (object.Equals(fieldValue, fieldValue2) || string.Equals(fieldValue.ToString(), fieldValue2.ToString(), StringComparison.OrdinalIgnoreCase)) { num2++; } } } if (num >= 2) { return num2 >= 2; } return false; } private static bool IsInAttackState(Character character) { if ((Object)(object)character == (Object)null || !AttackStates.TryGetValue(character, out var value)) { return false; } if (Time.time > value.EndTime || IsCharacterDead(character)) { AttackStates.Remove(character); return false; } return true; } private static AttackRole DetermineAttackRole(Character? character, object? attackInstance, ItemData? weapon) { if (attackInstance != null && AttackInstanceRoles.TryGetValue(attackInstance, out var value)) { return value; } if ((Object)(object)character != (Object)null && AttackStates.TryGetValue(character, out var value2) && (value2.AttackInstance == null || attackInstance == null || value2.AttackInstance == attackInstance)) { return value2.Role; } if ((Object)(object)character != (Object)null && PendingAttackRoles.TryGetValue(character, out var value3)) { if (Time.time <= value3.EndTime) { return value3.Role; } PendingAttackRoles.Remove(character); } if ((Object)(object)character != (Object)null) { object fieldValueRaw = GetFieldValueRaw(character, "m_currentAttack"); if (GetFieldValueRaw(character, "m_currentAttackIsSecondary") is bool flag && (attackInstance == null || fieldValueRaw == null || fieldValueRaw == attackInstance)) { if (!flag) { return AttackRole.Primary; } return AttackRole.Secondary; } } if (attackInstance != null && weapon?.m_shared != null) { object fieldValue = GetFieldValue<object>(weapon.m_shared, "m_attack"); object fieldValue2 = GetFieldValue<object>(weapon.m_shared, "m_secondaryAttack"); if (fieldValue2 != null && AttackObjectsLookSimilar(attackInstance, fieldValue2)) { return AttackRole.Secondary; } if (fieldValue != null && AttackObjectsLookSimilar(attackInstance, fieldValue)) { return AttackRole.Primary; } } if (attackInstance != null) { FieldInfo[] fields = attackInstance.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); bool flag2 = default(bool); foreach (FieldInfo fieldInfo in fields) { string text = SafeLower(fieldInfo.Name); object value4; try { value4 = fieldInfo.GetValue(attackInstance); } catch { continue; } int num; if (value4 is bool) { flag2 = (bool)value4; num = 1; } else { num = 0; } if (((uint)num & (flag2 ? 1u : 0u)) != 0 && (text.Contains("secondary") || text.Contains("alternate") || text.Contains("alt") || text.Contains("special") || text.Contains("heavy"))) { return AttackRole.Secondary; } string text2 = SafeLower(value4?.ToString()); if ((text.Contains("secondary") || text.Contains("alternate") || text.Contains("special") || text.Contains("heavy")) && text2 != "false" && text2 != "0" && !string.IsNullOrEmpty(text2)) { return AttackRole.Secondary; } if (text2.Contains("secondary") || text2.Contains("alternate") || text2.Contains("altattack") || text2.Contains("special") || text2.Contains("heavy")) { return AttackRole.Secondary; } } } return AttackRole.Primary; } private static void MarkCounterVulnerableDuringAttackAnimation(Character character, WeaponCategory category, AttackRole role, object? attackInstance) { if (!((Object)(object)character == (Object)null) && (!CounterVulnerableStates.TryGetValue(character, out var value) || value.AttackInstance != attackInstance)) { CounterVulnerableStates[character] = new CounterVulnerableState(attackInstance); if (Debug(DebugCounterDamage)) { Log.LogInfo((object)$"Counter vulnerability began for {SafeName(character)} using {category} {role}; ends when Valheim's real InAttack() state ends."); } } } private static void EndCounterVulnerableForAttack(Character character, object? attackInstance) { if (!((Object)(object)character == (Object)null) && CounterVulnerableStates.TryGetValue(character, out var value) && (value.AttackInstance == null || attackInstance == null || value.AttackInstance == attackInstance)) { CounterVulnerableStates.Remove(character); if (Debug(DebugCounterDamage)) { Log.LogInfo((object)("Counter vulnerability ended for " + SafeName(character) + ".")); } } } private static bool IsCounterVulnerable(Character target) { string rejectReason; return TryGetCounterVulnerable(target, out rejectReason); } private static bool TryGetCounterVulnerable(Character target, out string rejectReason) { rejectReason = string.Empty; if ((Object)(object)target == (Object)null) { rejectReason = "victim is null"; return false; } if (!CounterVulnerableStates.TryGetValue(target, out var value)) { rejectReason = "victim is not in a tracked attack animation"; return false; } if (!AttackStates.TryGetValue(target, out var value2)) { CounterVulnerableStates.Remove(target); rejectReason = "victim has counter state but no tracked Attack runtime state"; return false; } if (value.AttackInstance != null && value2.AttackInstance != null && value.AttackInstance != value2.AttackInstance) { CounterVulnerableStates.Remove(target); rejectReason = "victim counter state does not match current tracked attack instance"; return false; } if (IsCharacterDead(target)) { CounterVulnerableStates.Remove(target); rejectReason = "victim is dead"; return false; } if (!CharacterReportsInAttack(target)) { CounterVulnerableStates.Remove(target); rejectReason = "victim's real Valheim InAttack() state has ended"; return false; } return true; } private static bool CharacterReportsInAttack(Character target) { if ((Object)(object)target == (Object)null) { return false; } try { MethodInfo methodInfo = GetCachedMethod(((object)target).GetType(), "InAttack"); if ((object)CharacterInAttackMethod == null) { CharacterInAttackMethod = AccessTools.Method(typeof(Character), "InAttack", (Type[])null, (Type[])null); } if ((object)methodInfo == null) { methodInfo = CharacterInAttackMethod; } if (methodInfo == null) { return true; } return !(methodInfo.Invoke(target, null) is bool flag) || flag; } catch { return true; } } private static void ApplyCounterDamageIfEligible(Character victim, Character? attacker, HitData hit) { if (!EnableCounterDamage.Value || (Object)(object)victim == (Object)null || (Object)(object)attacker == (Object)null || (Object)(object)attacker == (Object)(object)victim || hit == null) { return; } bool flag = attacker is Player; if (OnlyPlayersCanDealCounterDamage.Value && !flag) { return; } string rejectReason; bool flag2 = TryGetCounterVulnerable(victim, out rejectReason); bool flag3 = IsInAttackState(victim); if (!flag2) { DebugCounterDamageReject(attacker, victim, $"{rejectReason}; victimInAttackState={flag3}"); return; } ItemData currentWeapon = GetCurrentWeapon(attacker); if (currentWeapon == null) { DebugCounterDamageReject(attacker, victim, "attacker has no current weapon"); return; } WeaponCategory weaponCategory = ClassifyWeapon(currentWeapon); if (!IsMeleeWeaponCategory(weaponCategory)) { DebugCounterDamageReject(attacker, victim, $"attacker weapon category {weaponCategory} is not melee"); return; } AttackRole currentAttackRole = GetCurrentAttackRole(attacker, weaponCategory); CategoryConfig config = GetConfig(weaponCategory); if (!config.GetCounterDamageEnabled(currentAttackRole)) { DebugCounterDamageReject(attacker, victim, $"counter damage disabled for {weaponCategory} {currentAttackRole}; victimCounterVulnerable={flag2}, victimInAttackState={flag3}"); return; } float num = Mathf.Max(0f, config.GetCounterDamageMultiplier(currentAttackRole)); if (Mathf.Approximately(num, 1f)) { DebugCounterDamageReject(attacker, victim, $"counter multiplier for {weaponCategory} {currentAttackRole} is 1"); return; } ((DamageTypes)(ref hit.m_damage)).Modify(num); if (EnableCounterDamageDing.Value) { TryPlayCounterDamageDing(victim, attacker); } if (Debug(DebugCounterDamage)) { Log.LogInfo((object)$"Applied counter-damage x{num.ToString(CultureInfo.InvariantCulture)}: {SafeName(attacker)} -> {SafeName(victim)} with {weaponCategory} {currentAttackRole}."); } } private static void DebugCounterDamageReject(Character attacker, Character victim, string reason) { if (Debug(DebugCounterDamage) && attacker is Player) { Log.LogInfo((object)("Counter-damage rejected: " + reason + ". attacker=" + SafeName(attacker) + ", victim=" + SafeName(victim) + ".")); } } private static void TryPlayCounterDamageDing(Character victim, Character attacker) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) Vector3 counterDingPosition = GetCounterDingPosition(victim, attacker); if (TryPlayCounterDamageDingPrefab(counterDingPosition)) { return; } object obj = FindBestCounterDingEffect(victim) ?? FindBestCounterDingEffect(attacker); if (obj == null) { if (DebugLogging.Value) { Log.LogInfo((object)"Counter-damage ding requested, but no prefab or parry/stagger/hit EffectList field was found."); } } else if (!TryCreateEffectList(obj, counterDingPosition, Quaternion.identity, null)) { if (DebugLogging.Value) { Log.LogInfo((object)("Counter-damage ding requested, but EffectList.Create invocation failed on " + obj.GetType().FullName + ".")); } } else if (DebugLogging.Value) { Log.LogInfo((object)("Counter-damage ding played through reflected EffectList " + obj.GetType().FullName + ".")); } } private static Vector3 GetCounterDingPosition(Character? victim, Character? attacker) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0018: 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_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)victim != (Object)null && (Object)(object)attacker != (Object)null) { return Vector3.Lerp(((Component)victim).transform.position, ((Component)attacker).transform.position, 0.5f) + Vector3.up; } if ((Object)(object)victim != (Object)null) { return ((Component)victim).transform.position + Vector3.up; } if ((Object)(object)attacker != (Object)null) { return ((Component)attacker).transform.position + Vector3.up; } return Vector3.zero; } private static bool TryPlayCounterDamageDingPrefab(Vector3 pos) { //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) ZNetScene instance = ZNetScene.instance; if ((Object)(object)instance == (Object)null) { if (DebugLogging.Value) { Log.LogInfo((object)"Counter-damage ding prefab playback skipped: ZNetScene.instance is null."); } return false; } string[] array = new string[5] { "sfx_perfectblock", "sfx_parry", "sfx_blocked", "sfx_stagger", "sfx_hit" }; foreach (string text in array) { GameObject prefab; try { prefab = instance.GetPrefab(text); } catch { continue; } if ((Object)(object)prefab == (Object)null) { continue; } try { Object.Destroy((Object)(object)Object.Instantiate<GameObject>(prefab, pos, Quaternion.identity), 4f); if (DebugLogging.Value) { Log.LogInfo((object)("Counter-damage ding played using prefab '" + text + "'.")); } return true; } catch (Exception ex) { if (DebugLogging.Value) { Log.LogInfo((object)("Counter-damage ding prefab '" + text + "' failed: " + ex.GetType().Name + ": " + ex.Message)); } } } if (DebugLogging.Value) { Log.LogInfo((object)"Counter-damage ding prefab playback failed: no configured SFX prefab was found."); } return false; } private static object? FindBestCounterDingEffect(Character? character) { if ((Object)(object)character == (Object)null) { return null; } object obj = null; foreach (FieldInfo allInstanceField in GetAllInstanceFields(((object)character).GetType())) { if (allInstanceField.FieldType.Name.IndexOf("EffectList", StringComparison.OrdinalIgnoreCase) < 0) { continue; } string text = SafeLower(allInstanceField.Name); object value; try { value = allInstanceField.GetValue(character); } catch { continue; } if (value != null) { if (text.Contains("perfect") || text.Contains("parry") || text.Contains("stagger")) { return value; } if (obj == null && (text.Contains("block") || text.Contains("hit") || text.Contains("damage"))) { obj = value; } } } return obj; } [IteratorStateMachine(typeof(<GetAllInstanceFields>d__114))] private static IEnumerable<FieldInfo> GetAllInstanceFields(Type type) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <GetAllInstanceFields>d__114(-2) { <>3__type = type }; } private static bool TryCreateEffectList(object effectList, Vector3 pos, Quaternion rot, Transform? parent) { //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) try { MethodInfo methodInfo = null; MethodInfo[] methods = effectList.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo2 in methods) { if (!(methodInfo2.Name != "Create")) { ParameterInfo[] parameters = methodInfo2.GetParameters(); if (parameters.Length >= 2 && parameters[0].ParameterType == typeof(Vector3) && parameters[1].ParameterType == typeof(Quaternion)) { methodInfo = methodInfo2; break; } } } if (methodInfo == null) { return false; } ParameterInfo[] parameters2 = methodInfo.GetParameters(); object[] array = new object[parameters2.Length]; for (int j = 0; j < parameters2.Length; j++) { Type parameterType = parameters2[j].ParameterType;