using System; 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 System.Threading; 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("MeleeBuff")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("MeleeBuff")] [assembly: AssemblyTitle("MeleeBuff")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace MeleeBuff { [BepInPlugin("com.leona.meleebuff", "Melee Buff", "2.2.4")] public sealed class Plugin : BaseUnityPlugin { private sealed class RageBuildDamageFrameState { public int LastFrame; } private sealed class RageIncomingHalfFrameState { public int LastFrame; } [HarmonyPatch(typeof(Character), "Update")] public static class CharacterRageHarmonyPatch { private sealed class RageEdgeState { public bool WasEligible; } private static readonly ConditionalWeakTable EligibleEdge = new ConditionalWeakTable(); [HarmonyPostfix] public static void Postfix(Character __instance) { if (!((Object)(object)__instance == (Object)null)) { bool flag = CharacterUtils.IsBuffEligible(__instance); if (DumpReflectionEnabled != null && DumpReflectionEnabled.Value) { ReflectionDumper.TryDumpOnce(__instance); } if (!EligibleEdge.TryGetValue(__instance, out var value)) { value = new RageEdgeState(); EligibleEdge.Add(__instance, value); } if (flag && !value.WasEligible) { RageBurntHealth.ResetGreyHealth(__instance); } value.WasEligible = flag; float deltaTime = Time.deltaTime; PassiveBuffEffects.Tick(__instance, flag, deltaTime); if (flag) { RageManaDrain.ForceZeroMana(__instance); } } } } [HarmonyPatch] public static class WeaponDamageRageHarmonyPatch { private const int MaxBuildDamageDiagSamples = int.MaxValue; private static int _buildDamageDiagCount; private static MethodBase TargetMethod() { return AccessTools.Method(typeof(WeaponDamage), "BuildDamage", new Type[5] { typeof(Weapon), typeof(Character), typeof(bool), typeof(DamageList).MakeByRefType(), typeof(float).MakeByRefType() }, (Type[])null); } [HarmonyPostfix] public static void Postfix(Weapon _weapon, Character _targetCharacter, bool _isSkillOrShield, ref DamageList _list, ref float _knockback) { bool flag = Interlocked.Increment(ref _buildDamageDiagCount) <= int.MaxValue; if ((Object)(object)_weapon == (Object)null) { if (flag) { ModLog.LogWarning((object)"[MeleeBuff][BuildDamage] _weapon is null"); } return; } Character ownerCharacter = ((EffectSynchronizer)_weapon).OwnerCharacter; bool flag2 = (Object)(object)ownerCharacter != (Object)null && CharacterUtils.IsPlayer(ownerCharacter); bool flag3 = (Object)(object)ownerCharacter != (Object)null && CharacterUtils.HasRageStatus(ownerCharacter); bool flag4 = CharacterUtils.IsBuffEligible(ownerCharacter); int num = -1; float num2 = -1f; if (_list != null) { num2 = _list.TotalDamage; num = ((_list.List != null) ? _list.List.Count : (-1)); } float num3 = num2; float num4 = -1f; float num5 = -1f; string text = null; bool flag5 = false; if (flag4) { List list = _list.List; if (list != null && list.Count > 0 && list[0] != null) { num4 = list[0].Damage; text = ((object)list[0]).GetType().FullName; flag5 = ((object)list[0]).GetType().IsValueType; } int num6 = DamageUtils.AddHundredPercentToDamageList(_list); num3 = _list.TotalDamage; MarkBuildDamageApplied(ownerCharacter); List list2 = _list.List; if (list2 != null && list2.Count > 0 && list2[0] != null) { num5 = list2[0].Damage; } if (flag) { ModLog.LogInfo((object)("[MeleeBuff][BuildDamage][diagRow0] " + $"scaledRows={num6} beforeTotal={num2} afterTotal={num3} " + $"row0Before={num4} row0After={num5} row0Type={text} row0IsValueType={flag5}")); } } else if (flag) { ModLog.LogInfo((object)("[MeleeBuff][BuildDamage] eligible=false => no scaling " + $"listCount={num} totalDamage={num2}")); } if (flag) { ModLog.LogInfo((object)("[MeleeBuff][BuildDamage] " + $"weapon={((Object)_weapon).name} skillOrShield={_isSkillOrShield} " + "targetChar=" + (((object)_targetCharacter)?.GetType().Name ?? "null") + " " + string.Format("attackerNull={0} attackerType={1} ", (Object)(object)ownerCharacter == (Object)null, ((object)ownerCharacter)?.GetType().Name ?? "null") + $"isPlayer={flag2} hasRageStatus={flag3} eligible={flag4} " + $"listNull={_list == null} listCount={num} totalDamage={num2} " + $"afterTotalDamage={num3} row0Before={num4} row0After={num5} row0Type={text}")); } } } [HarmonyPatch] public static class WeaponDamageDamageMultRageHarmonyPatch { private static MethodBase TargetMethod() { return AccessTools.Method(typeof(WeaponDamage), "DamageMult", new Type[2] { typeof(Character), typeof(bool) }, (Type[])null); } [HarmonyPostfix] public static void Postfix(Character _targetCharacter, bool _isSkill, ref float __result) { ModLog.LogInfo((object)("[MeleeBuff][DamageMult][start] targetChar=" + (((object)_targetCharacter)?.GetType().Name ?? "null") + " " + $"isSkill={_isSkill} beforeMult={__result}")); if (!((Object)(object)_targetCharacter == (Object)null) && CharacterUtils.IsBuffEligible(_targetCharacter) && !WasBuildDamageAppliedThisFrame(_targetCharacter)) { float num = __result; __result = num + num; ModLog.LogInfo((object)("[MeleeBuff][DamageMult] " + $"targetCharacter={((object)_targetCharacter).GetType().Name} isSkill={_isSkill} beforeMult={num} afterMult={__result}")); } } } [HarmonyPatch] public static class CharacterDamageReceiveLoggingHarmonyPatch { [HarmonyPatch] public static class ProcessDamageReductionPatch { public static MethodBase TargetMethod() { return TargetMethodProcessDamageReduction(); } [HarmonyPrefix] public static void Prefix(Character __instance, Weapon __0, DamageList __1, bool __2, out float __state) { __state = ((__1 != null) ? __1.TotalDamage : (-1f)); Character val = (((Object)(object)__0 != (Object)null) ? ((EffectSynchronizer)__0).OwnerCharacter : null); bool flag = (Object)(object)val != (Object)null && CharacterUtils.IsBuffEligible(val); bool flag2 = __2; bool flag3 = flag && !flag2 && !WasBuildDamageAppliedThisFrame(val); int num = 0; if (flag3 && __1 != null) { num = DamageUtils.AddHundredPercentToDamageList(__1); __state = ((__1 != null) ? __1.TotalDamage : __state); MarkBuildDamageApplied(val); } bool flag4 = (Object)(object)__instance != (Object)null && CharacterUtils.IsBuffEligible(__instance); ModLog.LogInfo((object)("[MeleeBuff][ProcessDamageReduction][before] " + string.Format("weapon={0} eligibleInstance={1} eligibleAttacker={2} ", ((Object)(object)__0 == (Object)null) ? "null" : ((Object)__0).name, flag4, flag) + $"flag={__2} shouldDoubleBasics={flag3} scaledRows={num} total={__state}")); } [HarmonyPostfix] public static void Postfix(Character __instance, Weapon __0, DamageList __1, bool __2, float __state) { if ((Object)(object)__instance != (Object)null && CharacterUtils.IsBuffEligible(__instance) && !WasIncomingHalfAppliedThisFrame(__instance)) { List list = ((__1 != null) ? __1.List : null); if (list != null) { for (int i = 0; i < list.Count; i++) { DamageType val = list[i]; if (val != null) { val.Damage *= 0.5f; } } } MarkIncomingHalfApplied(__instance); } float num = ((__1 != null) ? __1.TotalDamage : (-1f)); ModLog.LogInfo((object)("[MeleeBuff][ProcessDamageReduction][after] " + string.Format("weapon={0} flag={1} totalBefore={2} totalAfter={3}", ((Object)(object)__0 == (Object)null) ? "null" : ((Object)__0).name, __2, __state, num))); } } [HarmonyPatch] public static class OnReceiveHitPatch { public static MethodBase TargetMethod() { return TargetMethodOnReceiveHit(); } [HarmonyPrefix] public static void Prefix(Weapon __0, float __1, DamageList __2, Vector3 __3, Vector3 __4, float __5, float __6, Character __7, float __8) { bool flag = (Object)(object)__7 != (Object)null && CharacterUtils.IsBuffEligible(__7); float num = ((__2 != null) ? __2.TotalDamage : (-1f)); int num2 = ((((__2 != null) ? __2.List : null) != null) ? __2.List.Count : (-1)); ModLog.LogInfo((object)("[MeleeBuff][OnReceiveHit] " + string.Format("attacker={0} eligible={1} ", ((Object)(object)__7 == (Object)null) ? "null" : ((object)__7).GetType().Name, flag) + string.Format("weapon={0} total={1} listCount={2} f0={3} f1={4} f2={5} f3={6}", ((Object)(object)__0 == (Object)null) ? "null" : ((Object)__0).name, num, num2, __1, __5, __6, __8))); } } private static MethodBase TargetMethodReceiveHitDamageList() { return AccessTools.Method(typeof(Character), "ReceiveHit", new Type[9] { typeof(Object), typeof(DamageList), typeof(Vector3), typeof(Vector3), typeof(float), typeof(float), typeof(Character), typeof(float), typeof(bool) }, (Type[])null); } private static MethodBase TargetMethodProcessDamageReduction() { return AccessTools.Method(typeof(Character), "ProcessDamageReduction", new Type[3] { typeof(Weapon), typeof(DamageList), typeof(bool) }, (Type[])null); } private static MethodBase TargetMethodOnReceiveHit() { return AccessTools.Method(typeof(Character), "OnReceiveHit", new Type[9] { typeof(Weapon), typeof(float), typeof(DamageList), typeof(Vector3), typeof(Vector3), typeof(float), typeof(float), typeof(Character), typeof(float) }, (Type[])null); } public static MethodBase TargetMethod() { return TargetMethodReceiveHitDamageList(); } [HarmonyPrefix] public static void Prefix(Object __0, DamageList __1, Vector3 __2, Vector3 __3, float __4, float __5, Character __6, float __7, bool __8, out float __state) { __state = ((__1 != null) ? __1.TotalDamage : (-1f)); bool flag = (Object)(object)__6 != (Object)null && CharacterUtils.IsBuffEligible(__6); if (__0 != (Object)null) { _ = ((object)__0).GetType().Name.IndexOf("Weapon", StringComparison.OrdinalIgnoreCase) >= 0; } else _ = 0; int num = 0; float num2 = __state; int num3 = ((((__1 != null) ? __1.List : null) != null) ? __1.List.Count : (-1)); float num4 = -1f; if (((__1 != null) ? __1.List : null) != null && __1.List.Count > 0 && __1.List[0] != null) { num4 = __1.List[0].Damage; } ModLog.LogInfo((object)("[MeleeBuff][ReceiveHitPrefix] " + string.Format("attacker={0} eligible={1} ", ((Object)(object)__6 == (Object)null) ? "null" : ((object)__6).GetType().Name, flag) + string.Format("hitInventory={0} sourceType={1} ", __8, (__0 == (Object)null) ? "null" : ((object)__0).GetType().Name) + $"totalBefore={__state} listCount={num3} row0={num4} " + $"rageDoubledRows={num} totalAfterDoubling={num2}")); } [HarmonyPostfix] public static void Postfix(Object __0, DamageList __1, Vector3 __2, Vector3 __3, float __4, float __5, Character __6, float __7, bool __8, DamageList __result, float __state) { float num = ((__result != null) ? __result.TotalDamage : (-1f)); ModLog.LogInfo((object)("[MeleeBuff][ReceiveHitPostfix] " + string.Format("attacker={0} hitInventory={1} ", ((Object)(object)__6 == (Object)null) ? "null" : ((object)__6).GetType().Name, __8) + $"totalBefore={__state} totalAfter={num}")); } } private static class CharacterUtils { private static readonly string[] RageStatusIdentifiers = new string[2] { "Rage", "Rage Amplified" }; public static bool IsCharacterLike(object obj) { if (obj == null) { return false; } string name = obj.GetType().Name; if (name.IndexOf("Character", StringComparison.OrdinalIgnoreCase) < 0) { return name.IndexOf("CharacterStats", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } public static bool IsPlayer(object obj) { if (obj == null) { return false; } if (obj.GetType().Name.IndexOf("Player", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } bool value; return TryGetBoolMember(obj, "IsLocalPlayer", out value) && value; } public static bool IsBuffEligible(object obj) { if (!IsPlayer(obj) || GetMaxMana(obj) >= 50f) { return false; } return HasRageStatus(obj); } public static float GetMaxMana(object obj) { if (obj == null) { return 0f; } object memberValue = ReflectionUtils.GetMemberValue(obj, "Stats"); if (memberValue == null) { return 0f; } if (!ConvertNumeric(ReflectionUtils.GetMemberValue(memberValue, "MaxMana"), out var value)) { return 0f; } return value; } private static bool ConvertNumeric(object raw, out float value) { value = 0f; if (raw == null) { return false; } if (raw is float num) { value = num; return true; } if (raw is double num2) { value = (float)num2; return true; } if (raw is int num3) { value = num3; return true; } if (raw is uint num4) { value = num4; return true; } if (raw is long num5) { value = num5; return true; } if (raw is ulong num6) { value = ((num6 > int.MaxValue) ? float.MaxValue : ((float)num6)); return true; } if (raw is short num7) { value = num7; return true; } if (raw is ushort num8) { value = (int)num8; return true; } if (raw is byte b) { value = (int)b; return true; } if (raw is sbyte b2) { value = b2; return true; } if (raw is decimal num9) { value = (float)num9; return true; } if (raw is string s) { return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value); } return false; } public static bool HasRageStatus(object characterLike) { object statusEffectManager = GetStatusEffectManager(characterLike); if (statusEffectManager == null) { return false; } string[] rageStatusIdentifiers = RageStatusIdentifiers; foreach (string identifier in rageStatusIdentifiers) { if (ManagerKnowsStatus(statusEffectManager, identifier)) { return true; } } return false; } private static object GetStatusEffectManager(object characterLike) { return ReflectionUtils.GetMemberValue(characterLike, "StatusEffectMngr"); } private static bool ManagerKnowsStatus(object manager, string identifier) { object obj = ReflectionUtils.InvokeMethodWithArgs(manager, "HasStatusEffect", identifier); bool flag = default(bool); int num; if (obj is bool) { flag = (bool)obj; num = 1; } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0) { return true; } return false; } private static bool TryGetBoolMember(object obj, string name, out bool value) { value = false; if (ReflectionUtils.GetMemberValue(obj, name) is bool flag) { value = flag; return true; } return false; } } private static class RageManaDrain { public static void ForceZeroMana(object character) { if (character != null) { object memberValue = ReflectionUtils.GetMemberValue(character, "Stats"); if (memberValue != null) { ReflectionUtils.SetMemberValue(memberValue, "m_mana", 0f); ReflectionUtils.SetMemberValue(memberValue, "m_burntMana", 0f); } } } } private static class RageBurntHealth { public static void ResetGreyHealth(Character character) { if (!((Object)(object)character == (Object)null)) { CharacterStats stats = character.Stats; if ((Object)(object)stats != (Object)null) { TryRestoreAllBurnt(stats); } PlayerCharacterStats playerStats = character.PlayerStats; if ((Object)(object)playerStats != (Object)null && playerStats != stats) { TryRestoreAllBurnt((CharacterStats)(object)playerStats); } } } private static void TryRestoreAllBurnt(CharacterStats stats) { float burntHealth = stats.BurntHealth; if (!(burntHealth <= 0.0001f)) { stats.RestoreBurntHealth(burntHealth, true); burntHealth = stats.BurntHealth; if (burntHealth > 0.0001f) { stats.RestoreBurntHealth(burntHealth, false); } } } } private static class DamageUtils { public static int AddHundredPercentToDamageList(DamageList damageList) { if (damageList == null) { return 0; } List list = damageList.List; if (list == null) { return 0; } int num = 0; for (int i = 0; i < list.Count; i++) { DamageType val = list[i]; if (val != null) { val.Damage += val.Damage; num++; } } return num; } } private static class PassiveBuffEffects { private delegate bool TryFindFloatStatMember(object character, out object target, out string memberName, out float baseline); private sealed class FloatStatSnap { public object Target; public string MemberName; public float Baseline; } private const float RegenPerSecondWhileRage = 1f; private const float StaminaRegenMultiplier = 1.25f; private static readonly Dictionary StaminaRegenSnaps = new Dictionary(); public static void Tick(object character, bool eligible, float deltaTime) { if (character == null) { return; } int hashCode = character.GetHashCode(); if (!eligible) { RestoreFloatStat(StaminaRegenSnaps, hashCode); return; } if (deltaTime > 0f && deltaTime < 2f) { ApplySignedHealthPerSecond(character, 1f * deltaTime); } ApplyStaminaRegenBuff(character, hashCode); } private static void RestoreFloatStat(Dictionary dict, int id) { if (dict.TryGetValue(id, out var value)) { if (value.Target != null && !string.IsNullOrEmpty(value.MemberName)) { ReflectionUtils.SetMemberValue(value.Target, value.MemberName, ConvertNumeric(value.Target, value.MemberName, value.Baseline)); } dict.Remove(id); } } private static void ApplySignedHealthPerSecond(object character, float signedAmount) { if (signedAmount == 0f) { return; } if (signedAmount > 0f) { if (!TryModifyHealthViaFields(character, signedAmount)) { TryHealViaMethods(character, signedAmount); } } else { TryModifyHealthViaFields(character, signedAmount); } } private static bool TryHealViaMethods(object character, float amount) { string[] array = new string[6] { "AddHealth", "Heal", "RestoreHealth", "ReceiveHeal", "ChangeHealth", "AddVitality" }; foreach (string methodName in array) { if (ReflectionUtils.TryInvokeMethodWithOneNumericArg(character, methodName, amount)) { return true; } } return false; } private static bool TryModifyHealthViaFields(object character, float delta) { if (delta == 0f) { return false; } object memberValue = ReflectionUtils.GetMemberValue(character, "Stats"); if (memberValue == null) { return false; } if (!TryReadNumeric(memberValue, "CurrentHealth", out var value) || value < 0f) { return false; } if (!TryReadNumeric(memberValue, "MaxHealth", out var value2) || value2 <= 0f) { return false; } float num = value + delta; if (delta > 0f) { if (value >= value2) { return false; } num = Mathf.Min(num, value2); } else { num = Mathf.Max(0f, num); } return ReflectionUtils.SetMemberValue(memberValue, "CurrentHealth", ConvertNumeric(memberValue, "CurrentHealth", num)); } private static object ConvertNumeric(object target, string memberName, float value) { Type type = target.GetType(); PropertyInfo property = type.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { if (property.PropertyType == typeof(int)) { return (int)value; } if (property.PropertyType == typeof(float)) { return value; } if (property.PropertyType == typeof(double)) { return (double)value; } } FieldInfo field = type.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { if (field.FieldType == typeof(int)) { return (int)value; } if (field.FieldType == typeof(float)) { return value; } if (field.FieldType == typeof(double)) { return (double)value; } } return value; } private static bool TryReadNumeric(object target, string memberName, out float value) { value = 0f; object memberValue = ReflectionUtils.GetMemberValue(target, memberName); if (memberValue == null) { return false; } if (memberValue is float num) { value = num; return true; } if (memberValue is int num2) { value = num2; return true; } if (memberValue is double num3) { value = (float)num3; return true; } return float.TryParse(memberValue.ToString(), out value); } private static void ApplyStaminaRegenBuff(object character, int id) { ApplyScaledFloatStat(character, id, StaminaRegenSnaps, TryFindStaminaRegenMember, 1.25f); } private static void ApplyScaledFloatStat(object character, int id, Dictionary dict, TryFindFloatStatMember finder, float multiplier) { if (!dict.TryGetValue(id, out var value) || value.Target == null) { if (!finder(character, out var target, out var memberName, out var baseline)) { return; } value = (dict[id] = new FloatStatSnap { Target = target, MemberName = memberName, Baseline = baseline }); } float value2 = value.Baseline * multiplier; ReflectionUtils.SetMemberValue(value.Target, value.MemberName, ConvertNumeric(value.Target, value.MemberName, value2)); } private static bool TryFindStaminaRegenMember(object character, out object target, out string memberName, out float baseline) { target = null; memberName = null; baseline = 0f; object memberValue = ReflectionUtils.GetMemberValue(character, "Stats"); if (memberValue == null) { return false; } if (!TryReadNumeric(memberValue, "StaminaRegen", out var value) || value < 0f) { return false; } Type type = memberValue.GetType(); PropertyInfo property = type.GetProperty("StaminaRegen", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { if (!property.CanWrite) { return false; } } else { FieldInfo field = type.GetField("StaminaRegen", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field == null || field.IsInitOnly) { return false; } } target = memberValue; memberName = "StaminaRegen"; baseline = value; return true; } } private static class ReflectionDumper { private static readonly string[] StatsCandidates = new string[3] { "Stats", "CharacterStats", "m_stats" }; private static readonly string[] OwnerCandidates = new string[3] { "OwnerCharacter", "Owner", "m_owner" }; private static readonly string[] BoolPlayerCandidates = new string[4] { "IsPlayer", "IsLocalPlayer", "IsPlayerControlled", "OwnerPlayer" }; private static readonly string[] StatusEffectManagerCandidates = new string[4] { "StatusEffectMngr", "StatusEffectManager", "m_statusEffectMngr", "m_statusEffectManager" }; private static readonly string[] MaxManaCandidates = new string[16] { "MaxMana", "MaximumMana", "BaseMaxMana", "ActiveMaxMana", "ManaPoint", "ManaPoints", "ManaCap", "MaxManaPoints", "m_maxMana", "m_maximumMana", "m_baseMaxMana", "m_activeMaxMana", "m_manaPoint", "m_manaPoints", "m_manaCap", "m_maxManaPoints" }; private static readonly string[] RageStatusIdentifiers = new string[2] { "Rage", "Rage Amplified" }; private static readonly string[] CurrentManaCandidates = new string[4] { "CurrentMana", "Mana", "m_mana", "m_currentMana" }; private static readonly string[] BurntManaCandidates = new string[2] { "BurntMana", "m_burntMana" }; private static readonly string[] MoveSpeedCandidates = new string[9] { "MovementSpeed", "MoveSpeed", "m_movementSpeed", "m_moveSpeed", "WalkSpeed", "BaseMoveSpeed", "m_walkSpeed", "Speed", "MoveModifier" }; private static readonly string[] StaminaRegenCandidates = new string[13] { "StaminaRegen", "m_staminaRegen", "StaminaRegeneration", "StaminaRecovery", "StaminaRecoverRate", "PassiveStaminaRegen", "StaminaRegenRate", "BaseStaminaRegen", "StaminaGainPerSecond", "m_staminaRecovery", "StaminaRecharge", "StaminaRegenModifier", "BonusStaminaRegen" }; private static readonly string[] CurHealthCandidates = new string[7] { "CurrentHealth", "Health", "HP", "m_currentHealth", "m_health", "Vitality", "m_vitality" }; private static readonly string[] MaxHealthCandidates = new string[5] { "MaxHealth", "MaximumHealth", "m_maxHealth", "MaxVitality", "m_maxVitality" }; private static readonly string[] HealMethodCandidates = new string[6] { "AddHealth", "Heal", "RestoreHealth", "ReceiveHeal", "ChangeHealth", "AddVitality" }; public static void TryDumpOnce(Character character) { if (!((Object)(object)character == (Object)null) && Interlocked.Exchange(ref _reflectionDumpRan, 1) == 0) { Dump(character); } } private static void Dump(object character) { try { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][begin] characterType=" + character.GetType().FullName)); object obj = null; string text = null; string[] statsCandidates = StatsCandidates; Type memberType; bool isProperty; foreach (string text2 in statsCandidates) { if (TryReadMemberValue(character, text2, out var value, out memberType, out isProperty, out var _) && value != null) { obj = value; text = text2; break; } } ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][statsRoot] chosen=" + (text ?? "null") + " statsRootType=" + ((obj == null) ? "null" : obj.GetType().FullName))); statsCandidates = BoolPlayerCandidates; foreach (string text3 in statsCandidates) { if (TryReadMemberValue(character, text3, out var value2, out var memberType2, out isProperty, out var error2)) { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][boolPlayer] " + text3 + " found memberType=" + (memberType2?.FullName ?? "null") + " value=" + FormatValue(value2) + " err=" + (error2 ?? "none"))); } else { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][boolPlayer] " + text3 + " NOTFOUND")); } } statsCandidates = OwnerCandidates; foreach (string text4 in statsCandidates) { if (TryReadMemberValue(character, text4, out var value3, out var memberType3, out isProperty, out var error3) && value3 != null) { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][owner] " + text4 + " valueType=" + (memberType3?.FullName ?? "null") + " valueTypeActual=" + value3.GetType().FullName + " err=" + (error3 ?? "none"))); } else if (TryHasMember(character, text4, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, out memberType)) { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][owner] " + text4 + " FOUND but value=null err=" + (error3 ?? "none"))); } else { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][owner] " + text4 + " NOTFOUND")); } } object obj2 = null; string text5 = null; Type type = null; statsCandidates = StatusEffectManagerCandidates; foreach (string text6 in statsCandidates) { if (TryReadMemberValue(character, text6, out var value4, out var memberType4, out isProperty, out var error4)) { if (text5 == null) { text5 = text6; type = memberType4; } ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][statusManager] candidate=" + text6 + " memberType=" + (memberType4?.FullName ?? "null") + " value=" + ((value4 == null) ? "null" : value4.GetType().FullName) + " valueFormatted=" + FormatValue(value4) + " err=" + (error4 ?? "none"))); if (obj2 == null && value4 != null) { obj2 = value4; } } else { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][statusManager] candidate=" + text6 + " NOTFOUND")); } } ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][statusManager] chosen=" + (text5 ?? "null") + " managerValueType=" + ((obj2 == null) ? "null" : obj2.GetType().FullName) + " managerDeclaredType=" + ((type == null) ? "null" : type.FullName))); if (obj2 != null) { DumpMethodOverloads(obj2, "HasStatusEffect", null); DumpMethodOverloads(obj2, "HasStatus", null); statsCandidates = RageStatusIdentifiers; foreach (string text7 in statsCandidates) { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][rageStatusId] identifier=\"" + text7 + "\"")); } } else { DumpMethodOverloadsOnType(type, "HasStatusEffect", null); DumpMethodOverloadsOnType(type, "HasStatus", null); } bool flag = false; string text8 = null; object[] array = new object[2] { character, obj }; foreach (object obj3 in array) { if (obj3 == null) { continue; } statsCandidates = MaxManaCandidates; int num = 0; string text9; string error5; float value6; while (num < statsCandidates.Length) { text9 = statsCandidates[num]; if (!TryReadMemberValue(obj3, text9, out var value5, out memberType, out isProperty, out error5) || !TryConvertNumeric(value5, out value6)) { num++; continue; } goto IL_049e; } continue; IL_049e: flag = true; text8 = ((obj3 == character) ? "character" : "statsRoot"); ModLog.LogInfo((object)string.Format("[MeleeBuff][ReflectionDump][maxMana] found root={0} member={1} value={2} err={3}", text8, text9, value6, error5 ?? "none")); break; } if (!flag) { ModLog.LogInfo((object)"[MeleeBuff][ReflectionDump][maxMana] no numeric member found in candidates"); } array = new object[2] { character, obj }; foreach (object obj4 in array) { if (obj4 != null) { string text10 = ((obj4 == character) ? "character" : "statsRoot"); statsCandidates = CurrentManaCandidates; foreach (string memberName in statsCandidates) { DumpNumericProbe(obj4, text10, memberName); } statsCandidates = BurntManaCandidates; foreach (string memberName2 in statsCandidates) { DumpNumericProbe(obj4, text10, memberName2); } DumpCandidateWritableNumeric(obj4, text10 + ".currentMana", CurrentManaCandidates, (float _) => true); DumpCandidateWritableNumeric(obj4, text10 + ".burntMana", BurntManaCandidates, (float _) => true); } } object obj5 = obj ?? character; string text11 = ((obj == null) ? "character" : "statsRoot"); ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][passiveStats] using=" + text11 + " type=" + obj5.GetType().FullName)); DumpCandidateWritableNumeric(obj5, "MoveSpeed", MoveSpeedCandidates, (float v) => v > 0.001f); DumpCandidateWritableNumeric(obj5, "StaminaRegen", StaminaRegenCandidates, (float v) => v >= 0f); if (obj5 != character) { DumpCandidateWritableNumeric(character, "MoveSpeed", MoveSpeedCandidates, (float v) => v > 0.001f); DumpCandidateWritableNumeric(character, "StaminaRegen", StaminaRegenCandidates, (float v) => v >= 0f); } DumpCandidateWritableNumeric(obj5, "healthCur", CurHealthCandidates, (float v) => v >= 0f); statsCandidates = MaxHealthCandidates; foreach (string memberName3 in statsCandidates) { DumpNumericProbe(obj5, "healthStats", memberName3); } DumpOneNumericMethodOverloads(character, HealMethodCandidates); DumpMethodOverloads(character, "SetMana", new Type[2] { typeof(float), typeof(int) }); ModLog.LogInfo((object)"[MeleeBuff][ReflectionDump][end]"); } catch (Exception ex) { ModLog.LogError((object)("[MeleeBuff][ReflectionDump][error] " + ex.GetType().Name + ": " + ex.Message)); } } private static bool TryHasMember(object instance, string memberName, BindingFlags flags, out Type memberType) { memberType = null; if (instance == null || string.IsNullOrWhiteSpace(memberName)) { return false; } Type type = instance.GetType(); PropertyInfo property = type.GetProperty(memberName, flags); if (property != null) { memberType = property.PropertyType; return true; } FieldInfo field = type.GetField(memberName, flags); if (field != null) { memberType = field.FieldType; return true; } return false; } private static void DumpMethodOverloads(object instance, string methodName, Type[] desiredParamTypes) { if (instance == null) { return; } Type type = instance.GetType(); MethodInfo[] array = (from m in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where string.Equals(m.Name, methodName, StringComparison.Ordinal) select m).ToArray(); ModLog.LogInfo((object)$"[MeleeBuff][ReflectionDump][method] {type.FullName}.{methodName} overloads={array.Length}"); if (array.Length == 0) { return; } MethodInfo[] array2 = array; for (int i = 0; i < array2.Length; i++) { ParameterInfo[] parameters = array2[i].GetParameters(); string arg = string.Join(",", parameters.Select((ParameterInfo p) => p.ParameterType.FullName)); bool flag = desiredParamTypes == null || (parameters.Length == 1 && UnityEngineExtensions.Contains(desiredParamTypes, parameters[0].ParameterType)); ModLog.LogInfo((object)$"[MeleeBuff][ReflectionDump][method] overload params=[{arg}] matchesDesired={flag}"); } } private static void DumpMethodOverloadsOnType(Type type, string methodName, Type[] desiredParamTypes) { if (type == null) { return; } MethodInfo[] array = (from m in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where string.Equals(m.Name, methodName, StringComparison.Ordinal) select m).ToArray(); ModLog.LogInfo((object)$"[MeleeBuff][ReflectionDump][methodOnType] {type.FullName}.{methodName} overloads={array.Length}"); if (array.Length == 0) { return; } MethodInfo[] array2 = array; for (int i = 0; i < array2.Length; i++) { ParameterInfo[] parameters = array2[i].GetParameters(); string arg = string.Join(",", parameters.Select((ParameterInfo p) => p.ParameterType.FullName)); bool flag = desiredParamTypes == null || (parameters.Length == 1 && UnityEngineExtensions.Contains(desiredParamTypes, parameters[0].ParameterType)); ModLog.LogInfo((object)$"[MeleeBuff][ReflectionDump][methodOnType] overload params=[{arg}] matchesDesired={flag}"); } } private static void DumpOneNumericMethodOverloads(object instance, string[] methodNames) { Type type = instance?.GetType(); if (type == null) { return; } foreach (string name in methodNames) { MethodInfo[] array = (from m in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where string.Equals(m.Name, name, StringComparison.Ordinal) select m).ToArray(); if (array.Length == 0) { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][healMethod] " + name + " NOTFOUND")); continue; } ModLog.LogInfo((object)$"[MeleeBuff][ReflectionDump][healMethod] {name} overloads={array.Length}"); MethodInfo[] array2 = array; for (int j = 0; j < array2.Length; j++) { ParameterInfo[] parameters = array2[j].GetParameters(); string text = string.Join(",", parameters.Select((ParameterInfo p) => p.ParameterType.FullName)); ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][healMethod] params=[" + text + "]")); } } } private static void DumpCandidateWritableNumeric(object root, string label, string[] candidates, Func accept) { if (root == null) { return; } Type type = root.GetType(); bool flag = false; foreach (string text in candidates) { if (TryReadMemberValue(root, text, out var value, out var memberType, out var isProperty, out var error) && TryConvertNumeric(value, out var value2) && accept(value2)) { flag = true; bool flag2 = false; if (isProperty) { PropertyInfo property = type.GetProperty(text, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); flag2 = property != null && property.CanWrite; } else { FieldInfo field = type.GetField(text, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); flag2 = field != null && !field.IsInitOnly; } ModLog.LogInfo((object)string.Format("[MeleeBuff][ReflectionDump][{0}] found rootType={1} member={2} memberType={3} value={4} canWrite={5} err={6}", label, type.FullName, text, memberType?.FullName ?? "null", value2, flag2, error ?? "none")); } } if (!flag) { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][" + label + "] no writable numeric member met acceptance thresholds on type=" + type.FullName)); } } private static void DumpNumericProbe(object root, string rootLabel, string memberName) { if (root != null) { float value2; if (!TryReadMemberValue(root, memberName, out var value, out var memberType, out var _, out var error)) { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][numericProbe] root=" + rootLabel + " member=" + memberName + " NOTFOUND")); } else if (TryConvertNumeric(value, out value2)) { ModLog.LogInfo((object)string.Format("[MeleeBuff][ReflectionDump][numericProbe] root={0} member={1} value={2} memberType={3} err={4}", rootLabel, memberName, value2, memberType?.FullName ?? "null", error ?? "none")); } else { ModLog.LogInfo((object)("[MeleeBuff][ReflectionDump][numericProbe] root=" + rootLabel + " member=" + memberName + " valueType=" + (value?.GetType().FullName ?? "null") + " memberType=" + (memberType?.FullName ?? "null") + " err=" + (error ?? "none"))); } } } private static bool TryConvertNumeric(object raw, out float value) { value = 0f; if (raw == null) { return false; } if (raw is float num) { value = num; return true; } if (raw is double num2) { value = (float)num2; return true; } if (raw is int num3) { value = num3; return true; } if (raw is uint num4) { value = num4; return true; } if (raw is long num5) { value = num5; return true; } if (raw is ulong num6) { value = ((num6 > int.MaxValue) ? float.MaxValue : ((float)num6)); return true; } if (raw is short num7) { value = num7; return true; } if (raw is ushort num8) { value = (int)num8; return true; } if (raw is byte b) { value = (int)b; return true; } if (raw is sbyte b2) { value = b2; return true; } if (raw is decimal num9) { value = (float)num9; return true; } if (raw is string s) { return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value); } return false; } private static bool TryReadMemberValue(object instance, string memberName, out object value, out Type memberType, out bool isProperty, out string error) { value = null; memberType = null; isProperty = false; error = null; if (instance == null || string.IsNullOrWhiteSpace(memberName)) { return false; } Type type = instance.GetType(); PropertyInfo property = type.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { isProperty = true; memberType = property.PropertyType; if (!property.CanRead) { error = "property !CanRead"; return true; } try { value = property.GetValue(instance, null); } catch (Exception ex) { error = ex.GetType().Name + ":" + ex.Message; } return true; } FieldInfo field = type.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { isProperty = false; memberType = field.FieldType; try { value = field.GetValue(instance); } catch (Exception ex2) { error = ex2.GetType().Name + ":" + ex2.Message; } return true; } return false; } private static string FormatValue(object value) { if (value == null) { return "null"; } try { if (value is float num) { return num.ToString(CultureInfo.InvariantCulture); } if (value is double num2) { return num2.ToString(CultureInfo.InvariantCulture); } string text = value as string; if (text != null) { if (text.Length > 120) { text = text.Substring(0, 120) + "..."; } return "\"" + text + "\""; } _ = value.GetType().IsEnum; return value.ToString(); } catch { return value.GetType().FullName; } } } private static class ReflectionUtils { public static object GetMemberValue(object instance, string memberName) { if (instance == null || string.IsNullOrWhiteSpace(memberName)) { return null; } Type type = instance.GetType(); PropertyInfo property = type.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { if (!property.CanRead) { return null; } if (property.GetIndexParameters().Length != 0) { return null; } return property.GetValue(instance, null); } FieldInfo field = type.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(instance); } return null; } public static bool SetMemberValue(object instance, string memberName, object value) { if (instance == null || string.IsNullOrWhiteSpace(memberName)) { return false; } Type type = instance.GetType(); PropertyInfo property = type.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.CanWrite) { if (property.GetIndexParameters().Length != 0) { return false; } property.SetValue(instance, value, null); return true; } FieldInfo field = type.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { if (field.IsInitOnly) { return false; } field.SetValue(instance, value); return true; } return false; } public static bool TryInvokeMethodWithOneNumericArg(object instance, string methodName, float amount) { if (instance == null || string.IsNullOrWhiteSpace(methodName)) { return false; } MethodInfo[] methods = instance.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, methodName, StringComparison.Ordinal)) { continue; } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 1) { Type parameterType = parameters[0].ParameterType; if (parameterType == typeof(float)) { methodInfo.Invoke(instance, new object[1] { amount }); return true; } if (parameterType == typeof(double)) { methodInfo.Invoke(instance, new object[1] { (double)amount }); return true; } if (parameterType == typeof(int)) { methodInfo.Invoke(instance, new object[1] { (int)amount }); return true; } } } return false; } public static object InvokeMethod(object instance, string methodName) { if (instance == null || string.IsNullOrWhiteSpace(methodName)) { return null; } MethodInfo method = instance.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); if (method == null) { return null; } return method.Invoke(instance, null); } public static object InvokeMethodWithArgs(object instance, string methodName, params object[] args) { if (instance == null || string.IsNullOrWhiteSpace(methodName)) { return null; } MethodInfo[] array = (from m in instance.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where string.Equals(m.Name, methodName, StringComparison.Ordinal) select m).ToArray(); foreach (MethodInfo methodInfo in array) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == ((args != null) ? args.Length : 0) && TryBuildArgumentsForMethod(parameters, args, out var convertedArgs)) { return methodInfo.Invoke(instance, convertedArgs); } } return null; } private static bool TryBuildArgumentsForMethod(ParameterInfo[] parameters, object[] args, out object[] convertedArgs) { convertedArgs = null; if (args == null) { args = Array.Empty(); } object[] array = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { object obj = args[i]; Type parameterType = parameters[i].ParameterType; if (obj == null) { if (parameterType.IsValueType && Nullable.GetUnderlyingType(parameterType) == null) { return false; } array[i] = null; continue; } Type type = obj.GetType(); if (parameterType.IsAssignableFrom(type)) { array[i] = obj; continue; } if (parameterType == typeof(float)) { if (obj is float num) { array[i] = num; continue; } if (obj is double num2) { array[i] = (float)num2; continue; } if (obj is int num3) { array[i] = num3; continue; } if (obj is long num4) { array[i] = num4; continue; } } if (parameterType == typeof(double)) { if (obj is double num5) { array[i] = num5; continue; } if (obj is float num6) { array[i] = (double)num6; continue; } if (obj is int num7) { array[i] = num7; continue; } if (obj is long num8) { array[i] = (double)num8; continue; } } if (parameterType == typeof(int)) { if (obj is int num9) { array[i] = num9; continue; } if (obj is float num10) { array[i] = (int)num10; continue; } if (obj is double num11) { array[i] = (int)num11; continue; } if (obj is long num12) { array[i] = (int)num12; continue; } } return false; } convertedArgs = array; return true; } } public const string PluginGuid = "com.leona.meleebuff"; public const string PluginName = "Melee Buff"; public const string PluginVersion = "2.2.4"; private static ConfigEntry DumpReflectionEnabled; private static int _reflectionDumpRan; private Harmony _harmony; private static readonly ConditionalWeakTable BuildDamageFrameApplied = new ConditionalWeakTable(); private static readonly ConditionalWeakTable IncomingHalfFrameApplied = new ConditionalWeakTable(); internal static ManualLogSource ModLog { get; private set; } private void Awake() { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown ModLog = ((BaseUnityPlugin)this).Logger; DumpReflectionEnabled = ((BaseUnityPlugin)this).Config.Bind("Debug", "DumpReflectionEnabled", false, "Dumps live reflection member/method candidates once during runtime (for removing fallbacks/try blocks)."); LogWeaponDamageBuildDamageTarget(); _harmony = new Harmony("com.leona.meleebuff"); _harmony.PatchAll(typeof(Plugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Melee Buff 2.2.4: Harmony PatchAll. Rage / Rage Amplified: +100% weapon damage, mana clamp, +1 HP/s, burnt (grey) health cleared on Rage start."); } private static void LogWeaponDamageBuildDamageTarget() { Type[] array = new Type[5] { typeof(Weapon), typeof(Character), typeof(bool), typeof(DamageList).MakeByRefType(), typeof(float).MakeByRefType() }; MethodBase methodBase = AccessTools.Method(typeof(WeaponDamage), "BuildDamage", array, (Type[])null); if (methodBase == null) { ModLog.LogError((object)"[MeleeBuff] WeaponDamage.BuildDamage(Weapon, Character, bool, ref DamageList, ref float) NOT FOUND — damage postfix will not apply. Candidates:"); MethodInfo[] methods = typeof(WeaponDamage).GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!(methodInfo.Name != "BuildDamage")) { ParameterInfo[] parameters = methodInfo.GetParameters(); ModLog.LogInfo((object)string.Format("[MeleeBuff] {0} // {1}", methodInfo, string.Join(", ", parameters.Select((ParameterInfo p) => p.ParameterType.Name)))); } } } else { ModLog.LogInfo((object)("[MeleeBuff] BuildDamage Harmony target OK: " + methodBase.DeclaringType?.FullName + "." + methodBase.Name)); } } private static void MarkBuildDamageApplied(Character attacker) { if (!((Object)(object)attacker == (Object)null)) { if (!BuildDamageFrameApplied.TryGetValue(attacker, out var value)) { value = new RageBuildDamageFrameState(); BuildDamageFrameApplied.Add(attacker, value); } value.LastFrame = Time.frameCount; } } private static bool WasBuildDamageAppliedThisFrame(Character attacker) { if ((Object)(object)attacker == (Object)null) { return false; } if (BuildDamageFrameApplied.TryGetValue(attacker, out var value)) { return value.LastFrame == Time.frameCount; } return false; } private static void MarkIncomingHalfApplied(Character receiver) { if (!((Object)(object)receiver == (Object)null)) { if (!IncomingHalfFrameApplied.TryGetValue(receiver, out var value)) { value = new RageIncomingHalfFrameState(); IncomingHalfFrameApplied.Add(receiver, value); } value.LastFrame = Time.frameCount; } } private static bool WasIncomingHalfAppliedThisFrame(Character receiver) { if ((Object)(object)receiver == (Object)null) { return false; } if (IncomingHalfFrameApplied.TryGetValue(receiver, out var value)) { return value.LastFrame == Time.frameCount; } return false; } } }