using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Jotunn.Extensions; using Jotunn.Managers; using MagicStaffTweaker.Core; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("MagicStaffTweaker")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("2.2.5.0")] [assembly: AssemblyInformationalVersion("2.2.5")] [assembly: AssemblyProduct("MagicStaffTweaker")] [assembly: AssemblyTitle("MagicStaffTweaker")] [assembly: AssemblyVersion("2.2.5.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [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] [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; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace MagicStaffTweaker { public static class ConfigPersistence { public static string Key(string section, string key) { return section + "\n" + key; } public static HashSet ReadKeys(string text) { HashSet hashSet = new HashSet(StringComparer.Ordinal); string section = string.Empty; using StringReader stringReader = new StringReader(text); string text2; while ((text2 = stringReader.ReadLine()) != null) { text2 = text2.Trim(); if (text2.Length == 0 || text2.StartsWith("#", StringComparison.Ordinal)) { continue; } if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { section = text2.Substring(1, text2.Length - 2); continue; } int num = text2.IndexOf('='); if (num > 0) { hashSet.Add(Key(section, text2.Substring(0, num).Trim())); } } return hashSet; } } public static class DamageTuning { public static float SpellFactor(ModifierMode mode, float value) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) float num = Modifier.Apply(1f, mode, value); if (!float.IsNaN(num) && !float.IsInfinity(num) && !(num < 0f)) { return num; } return 1f; } public static bool ValidHealth(float value) { if (value > 0f) { return !float.IsInfinity(value); } return false; } public static bool ValidCooldown(float value) { if (value >= 0f) { return !float.IsInfinity(value); } return false; } } public static class HealthDefaults { private static readonly FieldInfo? DefaultField = typeof(ConfigEntryBase).GetField("k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); public static void Apply(ConfigEntry entry, float vanilla) { if (DamageTuning.ValidHealth(vanilla)) { DefaultField?.SetValue(entry, vanilla); if (!DamageTuning.ValidHealth(entry.Value)) { entry.Value = vanilla; } } } } [BepInPlugin("com.magicstafftweaker.client", "Magic Staff Tweaker", "2.2.5")] [BepInDependency("com.jotunn.jotunn", "2.30.0")] public sealed class Plugin : BaseUnityPlugin { [HarmonyPatch(typeof(Attack), "FireProjectileBurst", new Type[] { })] private static class LocalSummonBurstPatch { private static void Prefix(Attack __instance, Humanoid ___m_character, ItemData ___m_weapon, out int? __state) { __state = null; if (TryGetLocalStaffConfig((Character)(object)___m_character, ___m_weapon, out StaffConfig cfg) && cfg.SummonsPerCast.IntendedVisible) { __state = __instance.m_projectiles; __instance.m_projectiles = 1; } } private static void Finalizer(Attack __instance, int? __state) { if (__state.HasValue) { __instance.m_projectiles = __state.Value; } } } [HarmonyPatch(typeof(SpawnAbility), "Setup", new Type[] { typeof(Character), typeof(Vector3), typeof(float), typeof(HitData), typeof(ItemData), typeof(ItemData) })] private static class LocalSummonSetupPatch { private static void Prefix(SpawnAbility __instance, Character owner, ItemData item) { if (TryGetLocalStaffConfig(owner, item, out StaffConfig cfg) && cfg.SummonsPerCast.IntendedVisible) { SummonControls.ApplyToInstance(__instance, cfg.SummonsPerCast.Value.Value, cfg.IgnoreDeadRaiserLevelLimit.Value.Value && ((Object)item.m_dropPrefab).name == "StaffSkeleton"); } } } [HarmonyPatch(typeof(Tameable), "UnsummonMaxInstances", new Type[] { typeof(int) })] private static class LocalDeadRaiserLimitPatch { private static bool Prefix(Character ___m_character, MonsterAI ___m_monsterAI, ZNetView ___m_nview) { Plugin instance = _instance; if ((Object)(object)instance == (Object)null || !instance._configs.TryGetValue("StaffSkeleton", out StaffConfig value) || !value.Enabled.Value || !value.IgnoreDeadRaiserLevelLimit.Value.Value || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)___m_character == (Object)null || !IsDeadRaiserSummon(___m_character) || (Object)(object)___m_nview == (Object)null || !___m_nview.IsValid() || !___m_nview.IsOwner() || (Object)(object)___m_monsterAI == (Object)null) { return true; } GameObject followTarget = ___m_monsterAI.GetFollowTarget(); return (Object)(object)((followTarget != null) ? followTarget.GetComponent() : null) != (Object)(object)Player.m_localPlayer; } } [HarmonyPatch(typeof(Attack), "ModifyDamage", new Type[] { typeof(HitData), typeof(float) })] private static class LocalSpellDamagePatch { private static void Postfix(HitData hitData, Humanoid ___m_character, ItemData ___m_weapon) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (hitData != null && TryGetLocalSpellConfig((Character)(object)___m_character, ___m_weapon, out StaffConfig cfg)) { ((DamageTypes)(ref hitData.m_damage)).Modify(DamageTuning.SpellFactor(cfg.DamageMultiplier.Mode.Value, cfg.DamageMultiplier.Value.Value)); } } } private sealed class LocalAoeFactor { public float Value = 1f; } [HarmonyPatch(typeof(Aoe), "Setup", new Type[] { typeof(Character), typeof(Vector3), typeof(float), typeof(HitData), typeof(ItemData), typeof(ItemData) })] private static class LocalAoeSetupPatch { private static void Postfix(Aoe __instance, Character owner, HitData hitData, ItemData item) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) LocalAoeFactor value = LocalAoeFactors.GetValue(__instance, (Aoe _) => new LocalAoeFactor()); value.Value = 1f; if ((!__instance.m_useAttackSettings || hitData == null) && TryGetLocalSpellConfig(owner, item, out StaffConfig cfg)) { value.Value = DamageTuning.SpellFactor(cfg.DamageMultiplier.Mode.Value, cfg.DamageMultiplier.Value.Value); } } } [HarmonyPatch(typeof(Aoe), "GetDamage", new Type[] { typeof(int) })] private static class LocalAoeDamagePatch { private static void Postfix(Aoe __instance, ref DamageTypes __result) { if (LocalAoeFactors.TryGetValue(__instance, out LocalAoeFactor value)) { ((DamageTypes)(ref __result)).Modify(value.Value); } } } private sealed class SummonWeaponDamageBaseline { public readonly DamageTypes Damages; public readonly DamageTypes DamagesPerLevel; public SummonWeaponDamageBaseline(ItemData item) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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) Damages = item.m_shared.m_damages; DamagesPerLevel = item.m_shared.m_damagesPerLevel; } } [HarmonyPatch(typeof(Character), "Damage")] private static class SummonFinalDamagePatch { private static void Prefix(HitData hit) { Plugin instance = _instance; if ((Object)(object)instance == (Object)null || hit == null) { return; } Character attacker = hit.GetAttacker(); if ((Object)(object)attacker == (Object)null) { return; } string summonStaffPrefab = GetSummonStaffPrefab(attacker); if (summonStaffPrefab == null || !instance._configs.TryGetValue(summonStaffPrefab, out StaffConfig value)) { return; } try { ApplyFinalSummonDamage(hit, value); } catch (Exception arg) { Debug.LogError((object)$"[MagicStaffTweaker] Final summon damage patch failed: {arg}"); } } } [HarmonyPatch(typeof(Humanoid), "EquipItem")] private static class SummonWeaponDamagePatch { private static void Postfix(Humanoid __instance, ItemData item, bool __result) { if (!__result || item == null || (Object)(object)__instance == (Object)null || !IsDeadRaiserCreature(__instance)) { return; } Plugin instance = _instance; if ((Object)(object)instance == (Object)null || !instance._configs.TryGetValue("StaffSkeleton", out StaffConfig value)) { return; } try { ApplySummonWeaponDamage(item, value); } catch (Exception arg) { Debug.LogError((object)$"[MagicStaffTweaker] Summon weapon damage patch failed: {arg}"); } } private static bool IsDeadRaiserCreature(Humanoid humanoid) { string name = ((Object)((Component)humanoid).gameObject).name; if (!name.Contains("Skeleton_Friendly", StringComparison.Ordinal)) { return name.Contains("SkeletonArcher_Friendly", StringComparison.Ordinal); } return true; } } private sealed class StaffBaseline { public float MaxDurability; public float DurabilityPerLevel; public bool UseDurability; public float MovementModifier; public float BlockPower; public float BlockPowerPerLevel; public float DeflectionForce; public float DeflectionForcePerLevel; public float TimedBlockBonus; public float BlockStaminaUseFlatValue; public float BlockStaminaUseModifier; public AttackBaseline PrimaryAttack = new AttackBaseline(); public AttackBaseline SecondaryAttack = new AttackBaseline(); public static StaffBaseline Capture(GameObject prefab) { StaffBaseline staffBaseline = new StaffBaseline(); SharedData val = prefab.GetComponent()?.m_itemData?.m_shared; if (val == null) { return staffBaseline; } staffBaseline.MaxDurability = GetFloat(val, "m_maxDurability"); staffBaseline.DurabilityPerLevel = GetFloat(val, "m_durabilityPerLevel"); staffBaseline.UseDurability = GetBool(val, "m_useDurability"); staffBaseline.MovementModifier = GetFloat(val, "m_movementModifier"); staffBaseline.BlockPower = GetFloat(val, "m_blockPower"); staffBaseline.BlockPowerPerLevel = GetFloat(val, "m_blockPowerPerLevel"); staffBaseline.DeflectionForce = GetFloat(val, "m_deflectionForce"); staffBaseline.DeflectionForcePerLevel = GetFloat(val, "m_deflectionForcePerLevel"); staffBaseline.TimedBlockBonus = GetFloat(val, "m_timedBlockBonus"); staffBaseline.BlockStaminaUseFlatValue = GetFloat(val, "m_blockStaminaUseFlatValue"); staffBaseline.BlockStaminaUseModifier = GetFloat(val, "m_blockStaminaUseModifier"); staffBaseline.PrimaryAttack = AttackBaseline.Capture(val.m_attack, prefab); staffBaseline.SecondaryAttack = AttackBaseline.Capture(val.m_secondaryAttack, prefab); return staffBaseline; } public void Restore(GameObject prefab) { SharedData val = prefab.GetComponent()?.m_itemData?.m_shared; if (val != null) { SetField(val, "m_maxDurability", MaxDurability); SetField(val, "m_durabilityPerLevel", DurabilityPerLevel); SetField(val, "m_useDurability", UseDurability); SetField(val, "m_movementModifier", MovementModifier); SetField(val, "m_blockPower", BlockPower); SetField(val, "m_blockPowerPerLevel", BlockPowerPerLevel); SetField(val, "m_deflectionForce", DeflectionForce); SetField(val, "m_deflectionForcePerLevel", DeflectionForcePerLevel); SetField(val, "m_timedBlockBonus", TimedBlockBonus); SetField(val, "m_blockStaminaUseFlatValue", BlockStaminaUseFlatValue); SetField(val, "m_blockStaminaUseModifier", BlockStaminaUseModifier); PrimaryAttack.Restore(val.m_attack); SecondaryAttack.Restore(val.m_secondaryAttack); } } private static float GetFloat(object target, string name) { object field = GetField(target, name, 0f); if (field is float) { return (float)field; } return 0f; } private static bool GetBool(object target, string name) { object field = GetField(target, name, false); bool flag = default(bool); int num; if (field is bool) { flag = (bool)field; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } private static object? GetField(object target, string name, object fallback) { return target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(target) ?? fallback; } } private sealed class AttackBaseline { public float AttackEitr; public float AttackHealthPercentage; public float AttackCooldown; public float AttackForce; public float AttackForceForward; public float ProjectileAccuracyMin; public float ProjectileVelocity; public int Projectiles; public float AttackDrawTime; public float AttackDrawPercentage; public float AttackStamina; public float AttackStaminaModifier; public float StatusEffectChance; public float AttackHealth; public float HealthReturnHit; public float AttackEitrAdd; public float ChainChance; public float ChainChancePerTarget; public int ChainMaxTargets; public float ChainDelay; public float ChainStartChance; public float ChainStartChanceFalloff; public float ChainStartDelay; public int ChainMinTargets; public float ChainAttackMaxTime; public int AttackChainLevels; public readonly List GraphFields = new List(); public readonly List SummonGraphFields = new List(); public static AttackBaseline Capture(Attack? attack, GameObject staffPrefab = null) { AttackBaseline attackBaseline = new AttackBaseline(); if (attack == null) { return attackBaseline; } attackBaseline.AttackEitr = GetFloat(attack, "m_attackEitr"); attackBaseline.AttackHealthPercentage = GetFloat(attack, "m_attackHealthPercentage"); attackBaseline.AttackCooldown = GetFloat(attack, "m_attackCooldown"); attackBaseline.AttackForce = GetFloat(attack, "m_attackForce"); attackBaseline.AttackForceForward = GetFloat(attack, "m_attackForceForward"); attackBaseline.ProjectileAccuracyMin = GetFloat(attack, "m_projectileAccuracyMin"); attackBaseline.ProjectileVelocity = GetFloat(attack, "m_projectileVel"); attackBaseline.Projectiles = GetInt(attack, "m_projectiles"); attackBaseline.AttackDrawTime = GetFloat(attack, "m_attackDrawTime"); attackBaseline.AttackDrawPercentage = GetFloat(attack, "m_attackDrawPercentage"); attackBaseline.AttackStamina = GetFloat(attack, "m_attackStamina"); attackBaseline.AttackStaminaModifier = GetFloat(attack, "m_attackStaminaModifier"); attackBaseline.StatusEffectChance = GetFloat(attack, "m_attackStatusEffectChance"); attackBaseline.AttackHealth = GetFloat(attack, "m_attackHealth"); attackBaseline.HealthReturnHit = GetFloat(attack, "m_attackHealthReturnHit"); attackBaseline.AttackEitrAdd = GetFloat(attack, "m_attackEitrAdd"); attackBaseline.ChainChance = GetFloat(attack, "m_chainChance"); attackBaseline.ChainChancePerTarget = GetFloat(attack, "m_chainChancePerTarget"); attackBaseline.ChainMaxTargets = GetInt(attack, "m_chainMaxTargets"); attackBaseline.ChainDelay = GetFloat(attack, "m_chainDelay"); attackBaseline.ChainStartChance = GetFloat(attack, "m_chainStartChance"); attackBaseline.ChainStartChanceFalloff = GetFloat(attack, "m_chainStartChanceFalloff"); attackBaseline.ChainStartDelay = GetFloat(attack, "m_chainStartDelay"); attackBaseline.ChainMinTargets = GetInt(attack, "m_chainMinTargets"); attackBaseline.ChainAttackMaxTime = GetFloat(attack, "m_chainAttackMaxTime"); attackBaseline.AttackChainLevels = GetInt(attack, "m_attackChainLevels"); attackBaseline.GraphFields.AddRange(CaptureGraphFields(attack, staffPrefab)); attackBaseline.SummonGraphFields.AddRange(CaptureSummonGraphFields(attack)); return attackBaseline; } public void Restore(Attack? attack) { if (attack != null) { SetField(attack, "m_attackEitr", AttackEitr); SetField(attack, "m_attackHealthPercentage", AttackHealthPercentage); SetField(attack, "m_attackCooldown", AttackCooldown); SetField(attack, "m_attackForce", AttackForce); SetField(attack, "m_attackForceForward", AttackForceForward); SetField(attack, "m_projectileAccuracyMin", ProjectileAccuracyMin); SetField(attack, "m_projectileVel", ProjectileVelocity); SetField(attack, "m_projectiles", Projectiles); SetField(attack, "m_attackDrawTime", AttackDrawTime); SetField(attack, "m_attackDrawPercentage", AttackDrawPercentage); SetField(attack, "m_attackStamina", AttackStamina); SetField(attack, "m_attackStaminaModifier", AttackStaminaModifier); SetField(attack, "m_attackStatusEffectChance", StatusEffectChance); SetField(attack, "m_attackHealth", AttackHealth); SetField(attack, "m_attackHealthReturnHit", HealthReturnHit); SetField(attack, "m_attackEitrAdd", AttackEitrAdd); SetField(attack, "m_chainChance", ChainChance); SetField(attack, "m_chainChancePerTarget", ChainChancePerTarget); SetField(attack, "m_chainMaxTargets", ChainMaxTargets); SetField(attack, "m_chainDelay", ChainDelay); SetField(attack, "m_chainStartChance", ChainStartChance); SetField(attack, "m_chainStartChanceFalloff", ChainStartChanceFalloff); SetField(attack, "m_chainStartDelay", ChainStartDelay); SetField(attack, "m_chainMinTargets", ChainMinTargets); SetField(attack, "m_chainAttackMaxTime", ChainAttackMaxTime); SetField(attack, "m_attackChainLevels", AttackChainLevels); } } private static List CaptureSummonGraphFields(Attack attack) { List list = new List(); HashSet hashSet = new HashSet(StringComparer.Ordinal); Queue<(GameObject, int)> queue = new Queue<(GameObject, int)>(); HashSet hashSet2 = new HashSet(); FieldInfo[] fields = ((object)attack).GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (typeof(GameObject).IsAssignableFrom(fieldInfo.FieldType)) { object? value = fieldInfo.GetValue(attack); GameObject val = (GameObject)((value is GameObject) ? value : null); if (val != null && (Object)(object)val != (Object)null) { queue.Enqueue((val, 0)); } } } while (queue.Count > 0) { var (val2, num) = queue.Dequeue(); if ((Object)(object)val2 == (Object)null || !hashSet2.Add(((Object)val2).GetInstanceID())) { continue; } Component[] componentsInChildren = val2.GetComponentsInChildren(true); foreach (Component val3 in componentsInChildren) { if ((Object)(object)val3 == (Object)null) { continue; } string name = ((object)val3).GetType().Name; if (name != "Character" && name != "Humanoid" && name != "Attack" && name != "SpawnAbility") { continue; } fields = ((object)val3).GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo2 in fields) { if (!IsSummonField(name, fieldInfo2.Name) || !IsNumeric(fieldInfo2.FieldType)) { continue; } object value2 = fieldInfo2.GetValue(val3); if (value2 != null) { string item = ((Object)val3).GetInstanceID() + ":" + fieldInfo2.Name; if (hashSet.Add(item)) { list.Add(new GraphFieldBaseline(val3, fieldInfo2, value2)); } } } } if (num >= 3) { continue; } componentsInChildren = val2.GetComponentsInChildren(true); foreach (Component val4 in componentsInChildren) { if ((Object)(object)val4 == (Object)null) { continue; } fields = ((object)val4).GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo3 in fields) { if (typeof(GameObject).IsAssignableFrom(fieldInfo3.FieldType)) { object? value3 = fieldInfo3.GetValue(val4); GameObject val5 = (GameObject)((value3 is GameObject) ? value3 : null); if (val5 != null && (Object)(object)val5 != (Object)null) { queue.Enqueue((val5, num + 1)); } } else { if (!fieldInfo3.FieldType.IsArray || !typeof(GameObject).IsAssignableFrom(fieldInfo3.FieldType.GetElementType()) || !(fieldInfo3.GetValue(val4) is Array array)) { continue; } foreach (object item2 in array) { GameObject val6 = (GameObject)((item2 is GameObject) ? item2 : null); if (val6 != null && (Object)(object)val6 != (Object)null) { queue.Enqueue((val6, num + 1)); } } } } } } return list; } private static bool IsSummonField(string typeName, string fieldName) { switch (typeName) { case "Character": return fieldName == "m_health"; case "Humanoid": if (!(fieldName == "m_health") && !(fieldName == "m_walkSpeed")) { return fieldName == "m_runSpeed"; } return true; case "Attack": if (!(fieldName == "m_damageMultiplier")) { return fieldName == "m_attackCooldown"; } return true; case "SpawnAbility": return fieldName == "m_maxSpawns"; default: return false; } } private static List CaptureGraphFields(Attack attack, GameObject staffPrefab) { List list = new List(); HashSet hashSet = new HashSet(StringComparer.Ordinal); Queue<(GameObject, int)> queue = new Queue<(GameObject, int)>(); HashSet hashSet2 = new HashSet(); if ((Object)(object)staffPrefab != (Object)null) { queue.Enqueue((staffPrefab, 0)); } FieldInfo[] fields = ((object)attack).GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (typeof(GameObject).IsAssignableFrom(fieldInfo.FieldType)) { object? value = fieldInfo.GetValue(attack); GameObject val = (GameObject)((value is GameObject) ? value : null); if (val != null && (Object)(object)val != (Object)null) { queue.Enqueue((val, 0)); } } } while (queue.Count > 0) { var (val2, num) = queue.Dequeue(); if ((Object)(object)val2 == (Object)null || !hashSet2.Add(((Object)val2).GetInstanceID())) { continue; } Component[] componentsInChildren = val2.GetComponentsInChildren(true); foreach (Component val3 in componentsInChildren) { if ((Object)(object)val3 == (Object)null) { continue; } string name = ((object)val3).GetType().Name; if ((name != "Projectile" && name != "Aoe" && name != "EffectArea" && name != "StatusEffect" && name != "SE_Shield" && name != "ShieldDome") || (name == "Attack" && (object)val3 == attack)) { continue; } fields = ((object)val3).GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo2 in fields) { if (!IsGraphField(name, fieldInfo2.Name) || !IsNumeric(fieldInfo2.FieldType)) { continue; } object value2 = fieldInfo2.GetValue(val3); if (value2 != null) { string item = ((Object)val3).GetInstanceID() + ":" + fieldInfo2.Name; if (hashSet.Add(item)) { list.Add(new GraphFieldBaseline(val3, fieldInfo2, value2)); } } } } if (num >= 2) { continue; } componentsInChildren = val2.GetComponentsInChildren(true); foreach (Component val4 in componentsInChildren) { if ((Object)(object)val4 == (Object)null) { continue; } fields = ((object)val4).GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo3 in fields) { if (typeof(GameObject).IsAssignableFrom(fieldInfo3.FieldType)) { object? value3 = fieldInfo3.GetValue(val4); GameObject val5 = (GameObject)((value3 is GameObject) ? value3 : null); if (val5 != null && (Object)(object)val5 != (Object)null) { queue.Enqueue((val5, num + 1)); } } else { if (!fieldInfo3.FieldType.IsArray || !typeof(GameObject).IsAssignableFrom(fieldInfo3.FieldType.GetElementType()) || !(fieldInfo3.GetValue(val4) is Array array)) { continue; } foreach (object item2 in array) { GameObject val6 = (GameObject)((item2 is GameObject) ? item2 : null); if (val6 != null && (Object)(object)val6 != (Object)null) { queue.Enqueue((val6, num + 1)); } } } } } } return list; } private static bool IsGraphField(string typeName, string fieldName) { switch (typeName) { case "Projectile": switch (fieldName) { default: return fieldName == "m_forceStagger"; case "m_ttl": case "m_ttlMax": case "m_projectileVelocity": case "m_projectileVelocityMax": case "m_maxBounces": case "m_damageMultiplier": case "m_damage": case "m_damagePerHit": case "m_force": return true; } case "Aoe": case "EffectArea": switch (fieldName) { default: return fieldName == "m_damagePerHit"; case "m_radius": case "m_force": case "m_forceStagger": case "m_damage": return true; } case "StatusEffect": if (!(fieldName == "m_duration")) { return fieldName == "m_ttl"; } return true; case "SE_Shield": if (!(fieldName == "m_ttl") && !(fieldName == "m_absorbDamage")) { return fieldName == "m_absorbDamagePerSkillLevel"; } return true; case "ShieldDome": if (!(fieldName == "m_wrapSphereRadius")) { return fieldName == "m_reflectionHeight"; } return true; default: return false; } } private static bool IsNumeric(Type type) { if (!(type == typeof(float)) && !(type == typeof(double)) && !(type == typeof(int))) { return type == typeof(long); } return true; } private static float GetFloat(object target, string name) { object field = GetField(target, name, 0f); if (field is float) { return (float)field; } return 0f; } private static int GetInt(object target, string name) { object field = GetField(target, name, 0); if (field is int) { return (int)field; } return 0; } private static object? GetField(object target, string name, object fallback) { return target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(target) ?? fallback; } } private sealed class GraphFieldBaseline { public readonly Component Target; public readonly FieldInfo Field; public readonly object VanillaValue; public string ComponentTypeName { get { if (!(Target is Character) || !(FieldName == "m_health")) { return ((object)Target).GetType().Name; } return "Character"; } } public string FieldName => Field.Name; public GraphFieldBaseline(Component target, FieldInfo field, object vanillaValue) { Target = target; Field = field; VanillaValue = vanillaValue; } public void Restore() { Field.SetValue(Target, VanillaValue); } } private sealed class StaffConfig { internal interface ISettingGroup { ConfigEntry Mode { get; } ConfigEntryBase Value { get; } void SubscribeValueChanged(Action handler); } internal sealed class TuningFloat : ISettingGroup { public ConfigEntry Mode { get; } public ConfigEntry Value { get; } ConfigEntryBase ISettingGroup.Value => (ConfigEntryBase)(object)Value; public bool Uninitialized { get; set; } public ConfigurationManagerAttributes? Ui { get; } public string UiDescription { get; } public bool IntendedVisible { get; } public TuningFloat(ConfigEntry mode, ConfigEntry value, bool uninitialized = false, ConfigurationManagerAttributes? ui = null, string uiDescription = "", bool intendedVisible = false) { Mode = mode; Value = value; Uninitialized = uninitialized; Ui = ui; UiDescription = uiDescription; IntendedVisible = intendedVisible; } public void SubscribeValueChanged(Action handler) { Value.SettingChanged += delegate { handler(); }; } } internal sealed class TuningInt : ISettingGroup { public ConfigEntry Mode { get; } public ConfigEntry Value { get; } ConfigEntryBase ISettingGroup.Value => (ConfigEntryBase)(object)Value; public bool Uninitialized { get; set; } public ConfigurationManagerAttributes? Ui { get; } public string UiDescription { get; } public bool IntendedVisible { get; } public TuningInt(ConfigEntry mode, ConfigEntry value, bool uninitialized = false, ConfigurationManagerAttributes? ui = null, string uiDescription = "", bool intendedVisible = false) { Mode = mode; Value = value; Uninitialized = uninitialized; Ui = ui; UiDescription = uiDescription; IntendedVisible = intendedVisible; } public void SubscribeValueChanged(Action handler) { Value.SettingChanged += delegate { handler(); }; } } internal sealed class TuningBool : ISettingGroup { public ConfigEntry Mode { get; } public ConfigEntry Value { get; } ConfigEntryBase ISettingGroup.Value => (ConfigEntryBase)(object)Value; public TuningBool(ConfigEntry mode, ConfigEntry value) { Mode = mode; Value = value; } public void SubscribeValueChanged(Action handler) { Value.SettingChanged += delegate { handler(); }; } } internal sealed class ConfigurationManagerAttributes { public bool? Browsable { get; set; } public bool? IsAdvanced { get; set; } public string? Description { get; set; } } private static readonly Dictionary> VisibleSettings = new Dictionary>(StringComparer.Ordinal) { ["Staff of Embers"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Speed", "Explosion / AOE Radius", "Projectile Damage Multiplier" }, ["Staff of Frost"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Speed", "Projectile Count", "Status Effect Duration", "Projectile Damage Multiplier" }, ["Staff of Fracturing"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Speed", "Projectile Count", "Explosion / AOE Radius", "Projectile Damage Multiplier" }, ["Staff of the Wild"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Summon Health", "Summon Damage Multiplier", "Summon Attack Cooldown", "Summon Max Spawns", "Summons Per Cast" }, ["Dundr"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Count", "Projectile Speed", "Projectile Accuracy Min", "Projectile Damage Multiplier" }, ["Echo Spike"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Speed", "Projectile Max Bounces", "Projectile Damage Multiplier" }, ["Spirit Caller"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Health Cost Percent", "Cooldown", "Summon Health", "Summon Damage Multiplier", "Summon Attack Cooldown", "Summon Max Spawns", "Summons Per Cast" }, ["Northern Vengeance"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Health Cost Percent", "Cooldown", "Ally Application Radius", "Vengeance Reflection Height", "Projectile Damage Multiplier" }, ["Lightning Strike"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Explosion / AOE Radius", "Projectile Damage Multiplier" }, ["Staff of Protection"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Health Cost Percent", "Cooldown", "Shield Absorption", "Shield Absorption Per Blood Magic Level", "Status Effect Duration" }, ["Dead Raiser"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Health Cost Percent", "Cooldown", "Summon Max Spawns", "Summon Health", "Summon Melee Damage Multiplier", "Summon Archer Damage Multiplier", "Summon Attack Cooldown", "Summons Per Cast", "Ignore Dead Raiser Level Limit" }, ["Trollstav"] = new HashSet(StringComparer.Ordinal) { "Eitr Cost", "Health Cost Percent", "Cooldown", "Summon Max Spawns", "Summon Health", "Summon Damage Multiplier", "Summon Attack Cooldown", "Summons Per Cast" } }; private static readonly Dictionary VanillaEitrCosts = new Dictionary(StringComparer.Ordinal) { ["Staff of Embers"] = 35f, ["Staff of Frost"] = 5f, ["Staff of Fracturing"] = 30f, ["Staff of the Wild"] = 55f, ["Dundr"] = 25f, ["Echo Spike"] = 25f, ["Spirit Caller"] = 100f, ["Northern Vengeance"] = 100f, ["Lightning Strike"] = 40f, ["Staff of Protection"] = 60f, ["Dead Raiser"] = 100f, ["Trollstav"] = 120f }; private static readonly Dictionary VanillaHealthCosts = new Dictionary(StringComparer.Ordinal) { ["Spirit Caller"] = 40f, ["Northern Vengeance"] = 40f, ["Staff of Protection"] = 40f, ["Dead Raiser"] = 40f, ["Trollstav"] = 60f }; public string DisplayName { get; } public ConfigEntry Enabled { get; } internal TuningFloat Durability { get; } internal TuningFloat DurabilityPerLevel { get; } internal TuningBool UseDurability { get; } internal TuningFloat MovementModifier { get; } internal TuningFloat BlockPower { get; } internal TuningFloat BlockPowerPerLevel { get; } internal TuningFloat DeflectionForce { get; } internal TuningFloat DeflectionForcePerLevel { get; } internal TuningFloat TimedBlockBonus { get; } internal TuningFloat BlockStaminaUseFlatValue { get; } internal TuningFloat BlockStaminaUseModifier { get; } internal TuningFloat EitrCost { get; } internal TuningFloat HealthCostPercent { get; } internal TuningFloat Cooldown { get; } internal TuningFloat AttackForce { get; } internal TuningFloat AttackForceForward { get; } internal TuningFloat ProjectileSpeed { get; } internal TuningInt ProjectileCount { get; } internal TuningFloat DrawTime { get; } internal TuningFloat DrawPercentage { get; } internal TuningFloat AttackStamina { get; } internal TuningFloat AttackStaminaModifier { get; } internal TuningFloat StatusEffectChance { get; } internal TuningFloat HealthCost { get; } internal TuningFloat HealthReturnHit { get; } internal TuningFloat EitrAdd { get; } internal TuningFloat ProjectileAccuracy { get; } internal TuningFloat ProjectileAccuracyMin { get; } internal TuningFloat ProjectileLifetime { get; } internal TuningFloat ProjectileVelocityMax { get; } internal TuningFloat ProjectileVelocity { get; } internal TuningFloat ProjectileVelocityMin { get; } internal TuningInt ProjectileMaxBounces { get; } internal TuningFloat ProjectileSpawnDistance { get; } internal TuningFloat SpawnRadius { get; } internal TuningFloat RaiseRadius { get; } internal TuningFloat SpawnOnHitChance { get; } internal TuningFloat SpawnCount { get; } internal TuningFloat SpawnInterval { get; } internal TuningFloat SpawnDelay { get; } internal TuningFloat SpawnForce { get; } internal TuningFloat SpiritDamageLeft { get; } internal TuningFloat SpiritDamagePerHit { get; } internal TuningFloat ExplosionRadius { get; } internal TuningFloat Knockback { get; } internal TuningFloat StatusEffectDuration { get; } internal TuningFloat ShieldAbsorption { get; } internal TuningFloat ShieldAbsorptionPerBloodMagicLevel { get; } internal TuningFloat VengeanceSphereRadius { get; } internal TuningFloat VengeanceReflectionHeight { get; } internal TuningFloat SummonHealth { get; } internal TuningInt SummonsPerCast { get; } internal TuningBool IgnoreDeadRaiserLevelLimit { get; } internal TuningFloat SummonWalkSpeed { get; } internal TuningFloat SummonRunSpeed { get; } internal TuningFloat SummonMeleeDamageMultiplier { get; } internal TuningFloat SummonArcherDamageMultiplier { get; } internal TuningFloat SummonDamagePerLevelMultiplier { get; } internal TuningFloat SummonDamageMultiplier { get; } internal TuningFloat SummonFinalDamageMultiplier { get; } internal TuningFloat SummonAttackCooldown { get; } internal TuningInt SummonMaxSpawns { get; } internal TuningFloat DamageMultiplier { get; } internal TuningFloat Damage { get; } internal TuningFloat DamagePerHit { get; } internal TuningFloat ChainChance { get; } internal TuningFloat ChainChancePerTarget { get; } internal TuningInt ChainMaxTargets { get; } internal TuningFloat ChainDelay { get; } internal TuningFloat ChainStartChance { get; } internal TuningFloat ChainStartChanceFalloff { get; } internal TuningFloat ChainStartDelay { get; } internal TuningInt ChainMinTargets { get; } internal TuningFloat ChainAttackMaxTime { get; } internal TuningInt AttackChainLevels { get; } public event Action? Changed; public StaffConfig(ConfigFile config, StaffDefinition staff) { //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Invalid comparison between Unknown and I4 DisplayName = staff.DisplayName; string displayName = staff.DisplayName; Enabled = Bind(config, displayName, "Enabled", defaultValue: true, "Enable modifications for this staff."); Durability = TuneFloat(config, displayName, "Durability", 0f, "Value used by the selected modifier mode. Vanilla mode ignores this value."); DurabilityPerLevel = TuneFloat(config, displayName, "Durability Per Level", 0f, "Durability gained per quality level."); UseDurability = TuneBool(config, displayName, "Use Durability", defaultValue: true, "Whether the staff consumes durability."); MovementModifier = TuneFloat(config, displayName, "Movement Modifier", 0f, "Movement modifier applied while equipped."); BlockPower = TuneFloat(config, displayName, "Block Power", 0f, "Base block power while using the staff."); BlockPowerPerLevel = TuneFloat(config, displayName, "Block Power Per Level", 0f, "Block power gained per quality level."); DeflectionForce = TuneFloat(config, displayName, "Deflection Force", 0f, "Base deflection force while blocking."); DeflectionForcePerLevel = TuneFloat(config, displayName, "Deflection Force Per Level", 0f, "Deflection force gained per quality level."); TimedBlockBonus = TuneFloat(config, displayName, "Timed Block Bonus", 0f, "Timed block/parry multiplier."); BlockStaminaUseFlatValue = TuneFloat(config, displayName, "Block Stamina Use Flat Value", 0f, "Flat stamina use while blocking."); BlockStaminaUseModifier = TuneFloat(config, displayName, "Block Stamina Use Modifier", 0f, "Multiplier applied to block stamina use."); EitrCost = TuneFloat(config, displayName, "Eitr Cost", 0f, "Eitr consumed by the attack."); HealthCostPercent = TuneFloat(config, displayName, "Health Cost Percent", 0f, ((int)staff.MagicType == 1) ? "Percentage of current health consumed per cast." : "Percentage of health consumed per cast."); Cooldown = TuneFloat(config, displayName, "Cooldown", 0f, "Attack cooldown."); DrawTime = TuneFloat(config, displayName, "Draw Time", 0f, "Attack charge/draw time."); DrawPercentage = TuneFloat(config, displayName, "Draw Percentage", 0f, "Attack draw percentage."); AttackStamina = TuneFloat(config, displayName, "Attack Stamina", 0f, "Attack stamina cost."); AttackStaminaModifier = TuneFloat(config, displayName, "Attack Stamina Modifier", 0f, "Attack stamina modifier."); HealthCost = TuneFloat(config, displayName, "Health Cost", 0f, "Flat health cost."); HealthReturnHit = TuneFloat(config, displayName, "Health Return Per Hit", 0f, "Health returned on hit."); EitrAdd = TuneFloat(config, displayName, "Eitr Added", 0f, "Flat Eitr added by the attack."); AttackForce = TuneFloat(config, displayName, "Attack Force", 0f, "Attack force / stagger force."); AttackForceForward = TuneFloat(config, displayName, "Attack Force Forward", 0f, "Forward push applied by the cast."); StatusEffectChance = TuneFloat(config, displayName, "Status Effect Chance", 0f, "Chance to apply the attack status effect."); ProjectileSpeed = TuneFloat(config, displayName, "Projectile Speed", 0f, "Primary projectile velocity."); ProjectileCount = TuneInt(config, displayName, "Projectile Count", 0, "Number of projectiles fired."); ProjectileAccuracy = TuneFloat(config, displayName, "Projectile Accuracy", -1f, "Projectile spread/accuracy."); ProjectileAccuracyMin = TuneFloat(config, displayName, "Projectile Accuracy Min", -1f, "Minimum spread used when firing projectiles. Lower values generally produce a tighter spread."); ProjectileLifetime = TuneFloat(config, displayName, "Projectile Lifetime", -1f, "Projectile stay/TTL value."); ProjectileVelocityMax = TuneFloat(config, displayName, "Projectile Velocity Max", -1f, "Maximum projectile velocity."); ProjectileVelocity = TuneFloat(config, displayName, "Projectile Velocity", -1f, "Travel speed of the spell projectile."); ProjectileVelocityMin = TuneFloat(config, displayName, "Projectile Velocity Min", -1f, "Minimum projectile velocity."); ProjectileMaxBounces = TuneInt(config, displayName, "Projectile Max Bounces", -1, "Maximum number of times the projectile can bounce to another target."); ProjectileSpawnDistance = TuneFloat(config, displayName, "Projectile Spawn Distance", -1f, "Projectile spawn distance."); SpawnRadius = TuneFloat(config, displayName, "Spawn Radius", -1f, "Summon/spawn radius."); RaiseRadius = TuneFloat(config, displayName, "Raise Radius", -1f, "Dead Raiser / raise effect radius."); SpawnOnHitChance = TuneFloat(config, displayName, "Spawn On Hit Chance", -1f, "Chance to spawn an entity/effect on hit."); SpawnCount = TuneFloat(config, displayName, "Spawn Count", -1f, "Number of spawned entities/effects."); SpawnInterval = TuneFloat(config, displayName, "Spawn Interval", -1f, "Time between spawns."); SpawnDelay = TuneFloat(config, displayName, "Spawn Delay", -1f, "Delay before spawning."); SpawnForce = TuneFloat(config, displayName, "Spawn Force", -1f, "Force applied to spawned objects."); SpiritDamageLeft = TuneFloat(config, displayName, "Spirit Damage Left", -1f, "Remaining spirit damage pool."); SpiritDamagePerHit = TuneFloat(config, displayName, "Spirit Damage Per Hit", -1f, "Spirit damage consumed/dealt per hit."); ExplosionRadius = TuneFloat(config, displayName, "Explosion / AOE Radius", -1f, "Radius of the spell's area-of-effect or explosion."); Knockback = TuneFloat(config, displayName, "Knockback", -1f, "Knockback or stagger force produced by the spell."); StatusEffectDuration = TuneFloat(config, displayName, "Status Effect Duration", -1f, "Duration of status effects attached to the staff attack."); ShieldAbsorption = TuneFloat(config, displayName, "Shield Absorption", -1f, "Base damage the Staff of Protection barrier can absorb."); ShieldAbsorptionPerBloodMagicLevel = TuneFloat(config, displayName, "Shield Absorption Per Blood Magic Level", -1f, "Additional barrier absorption per Blood Magic level. Vanilla is 5 per level."); VengeanceSphereRadius = TuneFloat(config, displayName, "Ally Application Radius", -1f, "Radius of Northern Vengeance's protective vengeance sphere."); VengeanceReflectionHeight = TuneFloat(config, displayName, "Vengeance Reflection Height", -1f, "Vertical reach used by Northern Vengeance when reflecting incoming attacks."); SummonHealth = TuneFloat(config, displayName, "Summon Health", -1f, "Base health of creatures summoned by this staff."); SummonsPerCast = TuneInt(config, displayName, "Summons Per Cast", 1, "Number of creatures attempted by one local cast. Normal active-summon limits still apply unless bypassed for Dead Raiser."); IgnoreDeadRaiserLevelLimit = TuneBool(config, displayName, "Ignore Dead Raiser Level Limit", defaultValue: false, "Bypass the staff-quality/Blood Magic summon-count limit for your locally owned skeletons. Retains skeleton strength and star scaling. Turning this off restores normal count enforcement."); SummonWalkSpeed = TuneFloat(config, displayName, "Summon Walk Speed", -1f, "Walking speed of creatures summoned by this staff."); SummonRunSpeed = TuneFloat(config, displayName, "Summon Run Speed", -1f, "Running speed of creatures summoned by this staff."); SummonMeleeDamageMultiplier = TuneFloat(config, displayName, "Summon Melee Damage Multiplier", -1f, "Multiplier applied to Dead Raiser melee summon damage."); SummonArcherDamageMultiplier = TuneFloat(config, displayName, "Summon Archer Damage Multiplier", -1f, "Multiplier applied to Dead Raiser archer summon damage."); SummonDamagePerLevelMultiplier = TuneFloat(config, displayName, "Summon Damage Per Level Multiplier", -1f, "Multiplier applied to summon weapon m_damagesPerLevel values. This is weapon-quality scaling, not Blood Magic skill scaling."); SummonDamageMultiplier = TuneFloat(config, displayName, "Summon Attack Damage Multiplier", -1f, "Multiplier applied to the summon's attack damage."); SummonFinalDamageMultiplier = TuneFloat(config, displayName, "Summon Damage Multiplier", -1f, "Multiplier applied to damage dealt by summons created by this staff."); SummonAttackCooldown = TuneFloat(config, displayName, "Summon Attack Cooldown", -1f, "Time between attacks for creatures summoned by this staff."); SummonMaxSpawns = TuneInt(config, displayName, "Summon Max Spawns", -1, "Maximum number of creatures this staff can keep summoned at once. Blood Magic skill scaling remains unchanged."); DamageMultiplier = TuneFloat(config, displayName, "Projectile Damage Multiplier", -1f, "Multiplier applied to this staff's projectile or spell damage."); Damage = TuneFloat(config, displayName, "Direct Damage", -1f, "Direct damage dealt by the spell when this staff uses a flat damage value."); DamagePerHit = TuneFloat(config, displayName, "Damage Per Hit", -1f, "Damage dealt each time the spell hits a target."); ChainChance = TuneFloat(config, displayName, "Chain Chance", -1f, "Chance for a chain attack where the staff Attack exposes it."); ChainChancePerTarget = TuneFloat(config, displayName, "Chain Chance Per Target", -1f, "Per-target chain chance adjustment."); ChainMaxTargets = TuneInt(config, displayName, "Chain Max Targets", -1, "Maximum chain targets."); ChainDelay = TuneFloat(config, displayName, "Chain Delay", -1f, "Delay between chain targets."); ChainStartChance = TuneFloat(config, displayName, "Chain Start Chance", -1f, "Initial chain-start chance."); ChainStartChanceFalloff = TuneFloat(config, displayName, "Chain Start Chance Falloff", -1f, "Chain-start chance falloff."); ChainStartDelay = TuneFloat(config, displayName, "Chain Start Delay", -1f, "Delay before chain begins."); ChainMinTargets = TuneInt(config, displayName, "Chain Min Targets", -1, "Minimum number of targets required by the chain attack when the vanilla field exists."); ChainAttackMaxTime = TuneFloat(config, displayName, "Chain Attack Max Time", -1f, "Maximum time window for the chain attack when the vanilla field exists."); AttackChainLevels = TuneInt(config, displayName, "Attack Chain Levels", -1, "Number of chain levels supported by the attack when the vanilla field exists."); Enabled.SettingChanged += delegate { this.Changed?.Invoke(); }; foreach (ISettingGroup tuning in GetTunings()) { tuning.Mode.SettingChanged += delegate { this.Changed?.Invoke(); }; tuning.SubscribeValueChanged(delegate { this.Changed?.Invoke(); }); } } private IEnumerable GetEntries() { yield return (ConfigEntryBase)(object)Enabled; foreach (ISettingGroup tuning in GetTunings()) { yield return (ConfigEntryBase)(object)tuning.Mode; yield return tuning.Value; } } private IEnumerable GetTunings() { yield return Durability; yield return DurabilityPerLevel; yield return UseDurability; yield return MovementModifier; yield return BlockPower; yield return BlockPowerPerLevel; yield return DeflectionForce; yield return DeflectionForcePerLevel; yield return TimedBlockBonus; yield return BlockStaminaUseFlatValue; yield return BlockStaminaUseModifier; yield return EitrCost; yield return HealthCostPercent; yield return Cooldown; yield return AttackForce; yield return AttackForceForward; yield return ProjectileSpeed; yield return ProjectileCount; yield return DrawTime; yield return DrawPercentage; yield return AttackStamina; yield return AttackStaminaModifier; yield return StatusEffectChance; yield return HealthCost; yield return HealthReturnHit; yield return EitrAdd; yield return ProjectileAccuracy; yield return ProjectileAccuracyMin; yield return ProjectileLifetime; yield return ProjectileVelocityMax; yield return ProjectileVelocity; yield return ProjectileVelocityMin; yield return ProjectileMaxBounces; yield return ProjectileSpawnDistance; yield return SpawnRadius; yield return RaiseRadius; yield return SpawnOnHitChance; yield return SpawnCount; yield return SpawnInterval; yield return SpawnDelay; yield return SpawnForce; yield return SpiritDamageLeft; yield return SpiritDamagePerHit; yield return ExplosionRadius; yield return Knockback; yield return StatusEffectDuration; yield return ShieldAbsorption; yield return ShieldAbsorptionPerBloodMagicLevel; yield return VengeanceSphereRadius; yield return VengeanceReflectionHeight; yield return SummonHealth; yield return SummonWalkSpeed; yield return SummonRunSpeed; yield return SummonsPerCast; yield return IgnoreDeadRaiserLevelLimit; yield return SummonMeleeDamageMultiplier; yield return SummonArcherDamageMultiplier; yield return SummonDamagePerLevelMultiplier; yield return SummonDamageMultiplier; yield return SummonFinalDamageMultiplier; yield return SummonAttackCooldown; yield return SummonMaxSpawns; yield return DamageMultiplier; yield return Damage; yield return DamagePerHit; yield return ChainChance; yield return ChainChancePerTarget; yield return ChainMaxTargets; yield return ChainDelay; yield return ChainStartChance; yield return ChainStartChanceFalloff; yield return ChainStartDelay; yield return ChainMinTargets; yield return ChainAttackMaxTime; yield return AttackChainLevels; } private static bool IsVisible(string section, string key) { if (VisibleSettings.TryGetValue(section, out HashSet value)) { return value.Contains(key); } return false; } private static AcceptableValueBase? RangeFor(string key) { if (key == "Health Cost Percent") { return (AcceptableValueBase?)(object)new AcceptableValueRange(0f, 100f); } if (key.Contains("Chance", StringComparison.Ordinal)) { return (AcceptableValueBase?)(object)new AcceptableValueRange(0f, 1f); } if (key.Contains("Multiplier", StringComparison.Ordinal)) { return (AcceptableValueBase?)(object)new AcceptableValueRange(0f, 5f); } return null; } public void InitializeVanillaValues(StaffBaseline baseline, bool forceReset) { Init(EitrCost, VanillaEitrCosts.TryGetValue(DisplayName, out var value) ? value : baseline.PrimaryAttack.AttackEitr, forceReset); Init(HealthCostPercent, VanillaHealthCosts.TryGetValue(DisplayName, out var value2) ? value2 : (baseline.PrimaryAttack.AttackHealthPercentage * 100f), forceReset); Init(Cooldown, baseline.PrimaryAttack.AttackCooldown, forceReset); Init(ProjectileSpeed, baseline.PrimaryAttack.ProjectileVelocity, forceReset); Init(ProjectileCount, (DisplayName == "Dundr" || DisplayName == "Staff of Fracturing") ? 12 : baseline.PrimaryAttack.Projectiles, forceReset); Init(ProjectileAccuracyMin, baseline.PrimaryAttack.ProjectileAccuracyMin, forceReset); InitGraph(baseline.PrimaryAttack.GraphFields, "Aoe", "m_radius", ExplosionRadius, forceReset); InitGraph(baseline.PrimaryAttack.GraphFields, "EffectArea", "m_radius", ExplosionRadius, forceReset); InitGraph(baseline.PrimaryAttack.GraphFields, "StatusEffect", "m_duration", StatusEffectDuration, forceReset); if (DisplayName == "Staff of Protection") { Init(ShieldAbsorption, 200f, forceReset); Init(ShieldAbsorptionPerBloodMagicLevel, 5f, forceReset); } else { InitGraph(baseline.PrimaryAttack.GraphFields, "SE_Shield", "m_absorbDamage", ShieldAbsorption, forceReset); InitGraph(baseline.PrimaryAttack.GraphFields, "SE_Shield", "m_absorbDamagePerSkillLevel", ShieldAbsorptionPerBloodMagicLevel, forceReset); } InitGraph(baseline.PrimaryAttack.GraphFields, "ShieldDome", "m_wrapSphereRadius", VengeanceSphereRadius, forceReset); InitGraph(baseline.PrimaryAttack.GraphFields, "ShieldDome", "m_reflectionHeight", VengeanceReflectionHeight, forceReset); InitGraph(baseline.PrimaryAttack.GraphFields, "Projectile", "m_maxBounces", ProjectileMaxBounces, forceReset); Init(DamageMultiplier, 1f, forceReset); InitSummonHealth(baseline); InitGraph(baseline.PrimaryAttack.SummonGraphFields, "Attack", "m_attackCooldown", SummonAttackCooldown, forceReset); InitGraph(baseline.PrimaryAttack.SummonGraphFields, "SpawnAbility", "m_maxSpawns", SummonMaxSpawns, forceReset); Init(SummonMeleeDamageMultiplier, 1f, forceReset); Init(SummonArcherDamageMultiplier, 1f, forceReset); Init(SummonFinalDamageMultiplier, 1f, forceReset); if (DisplayName == "Trollstav") { Init(SummonMaxSpawns, 2, forceReset); } } private static void Init(TuningFloat setting, float vanilla, bool force = false) { if (vanilla < 0f) { if (setting.Ui != null) { setting.Ui.Browsable = false; } return; } if (force || setting.Uninitialized) { setting.Value.Value = vanilla; setting.Uninitialized = false; } if (setting.Ui != null) { setting.Ui.Browsable = setting.IntendedVisible; if (setting.IntendedVisible) { setting.Ui.Description = setting.UiDescription + " Vanilla: " + FormatVanilla(vanilla) + "."; } } } private void InitSummonHealth(StaffBaseline baseline) { List[] array = new List[2] { baseline.PrimaryAttack.SummonGraphFields, baseline.SecondaryAttack.SummonGraphFields }; for (int i = 0; i < array.Length; i++) { foreach (GraphFieldBaseline item in array[i]) { if (!(item.ComponentTypeName != "Character") && !(item.FieldName != "m_health")) { float? num = ReadNumeric(item.Field.FieldType, item.VanillaValue); if (num.HasValue && DamageTuning.ValidHealth(num.Value)) { HealthDefaults.Apply(SummonHealth.Value, num.Value); Init(SummonHealth, num.Value); return; } } } } if (SummonHealth.Ui != null) { SummonHealth.Ui.Browsable = false; } } private static void Init(TuningInt setting, int vanilla, bool force = false) { if (vanilla < 0) { if (setting.Ui != null) { setting.Ui.Browsable = false; } return; } if (force || setting.Uninitialized) { setting.Value.Value = vanilla; setting.Mode.Value = (ModifierMode)3; setting.Uninitialized = false; } if (setting.Ui != null) { setting.Ui.Browsable = setting.IntendedVisible; if (setting.IntendedVisible) { setting.Ui.Description = setting.UiDescription + " Vanilla: " + vanilla + "."; } } } private static string FormatVanilla(float value) { if (!(MathF.Abs(value - MathF.Round(value)) < 0.0001f)) { return value.ToString("0.##"); } return ((int)MathF.Round(value)).ToString(); } private static void InitGraph(List fields, string component, string field, TuningFloat setting, bool force = false) { foreach (GraphFieldBaseline field2 in fields) { if (field2.ComponentTypeName == component && field2.FieldName == field) { float? num = ReadNumeric(field2.Field.FieldType, field2.VanillaValue); if (num.HasValue) { Init(setting, num.Value, force); } return; } } if (setting.Ui != null) { setting.Ui.Browsable = false; } } private static void InitGraph(List fields, string component, string field, TuningInt setting, bool force = false) { foreach (GraphFieldBaseline field2 in fields) { if (field2.ComponentTypeName == component && field2.FieldName == field) { float? num = ReadNumeric(field2.Field.FieldType, field2.VanillaValue); if (num.HasValue) { Init(setting, (int)MathF.Round(num.Value), force); } return; } } if (setting.Ui != null) { setting.Ui.Browsable = false; } } private static ConfigEntry Bind(ConfigFile config, string section, string key, T defaultValue, string description) { return ConfigFileExtensions.BindConfigInOrder(config, section, key, defaultValue, description, false, true, true, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); } private static TuningFloat TuneFloat(ConfigFile config, string section, string key, float defaultValue, string description) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Expected O, but got Unknown bool flag = IsVisible(section, key); ConfigurationManagerAttributes configurationManagerAttributes = new ConfigurationManagerAttributes { Browsable = false }; ModifierMode val = (ModifierMode)(key.Contains("Multiplier", StringComparison.Ordinal) ? 2 : 3); ConfigEntry val2 = config.Bind("Internal / " + section, key + " Mode", val, new ConfigDescription("Internal modifier mode.", (AcceptableValueBase)null, new object[1] { configurationManagerAttributes })); val2.Value = val; AcceptableValueBase val3 = RangeFor(key); bool flag2 = _instance?._savedSettingKeys.Contains(ConfigPersistence.Key(section, key)) ?? false; float num = ((val3 == null) ? defaultValue : ((key == "Health Cost Percent") ? Mathf.Clamp(defaultValue, 0f, 100f) : (key.Contains("Multiplier", StringComparison.Ordinal) ? Mathf.Clamp(defaultValue, 0f, 5f) : Mathf.Clamp(defaultValue, 0f, 1f)))); ConfigurationManagerAttributes configurationManagerAttributes2 = (flag ? new ConfigurationManagerAttributes { Browsable = true, Description = description } : configurationManagerAttributes); ConfigDescription val4 = new ConfigDescription(description, val3, new object[1] { configurationManagerAttributes2 }); ConfigEntry value = config.Bind(section, key, num, val4); return new TuningFloat(val2, value, !flag2, configurationManagerAttributes2, description, flag); } private static TuningInt TuneInt(ConfigFile config, string section, string key, int defaultValue, string description) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Expected O, but got Unknown bool flag = IsVisible(section, key); ConfigurationManagerAttributes configurationManagerAttributes = new ConfigurationManagerAttributes { Browsable = false }; ConfigEntry val = config.Bind("Internal / " + section, key + " Mode", (ModifierMode)3, new ConfigDescription("Internal modifier mode.", (AcceptableValueBase)null, new object[1] { configurationManagerAttributes })); val.Value = (ModifierMode)3; AcceptableValueBase val2 = null; switch (key) { case "Projectile Count": val2 = (AcceptableValueBase)(object)new AcceptableValueRange(1, 50); break; case "Summons Per Cast": val2 = (AcceptableValueBase)(object)new AcceptableValueRange(1, 20); break; case "Summon Max Spawns": val2 = (AcceptableValueBase)(object)new AcceptableValueRange(1, 20); break; case "Projectile Max Bounces": val2 = (AcceptableValueBase)(object)new AcceptableValueRange(0, 20); break; } bool flag2 = _instance?._savedSettingKeys.Contains(ConfigPersistence.Key(section, key)) ?? false; int num = key switch { "Projectile Max Bounces" => Math.Max(0, Math.Min(20, defaultValue)), "Summon Max Spawns" => Math.Max(1, Math.Min(20, defaultValue)), "Projectile Count" => Math.Max(1, Math.Min(50, defaultValue)), _ => defaultValue, }; ConfigurationManagerAttributes configurationManagerAttributes2 = (flag ? new ConfigurationManagerAttributes { Browsable = true, Description = description } : configurationManagerAttributes); ConfigDescription val3 = new ConfigDescription(description, val2, new object[1] { configurationManagerAttributes2 }); ConfigEntry value = config.Bind(section, key, num, val3); return new TuningInt(val, value, !flag2, configurationManagerAttributes2, description, flag); } private static TuningBool TuneBool(ConfigFile config, string section, string key, bool defaultValue, string description) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Expected O, but got Unknown ConfigurationManagerAttributes configurationManagerAttributes = new ConfigurationManagerAttributes { Browsable = false }; ConfigEntry mode = config.Bind("Internal / " + section, key + " Mode", (ModifierMode)3, new ConfigDescription("Internal modifier mode.", (AcceptableValueBase)null, new object[1] { configurationManagerAttributes })); ConfigurationManagerAttributes configurationManagerAttributes2 = ((key == "Ignore Dead Raiser Level Limit" && IsVisible(section, key)) ? new ConfigurationManagerAttributes { Browsable = true, Description = description } : configurationManagerAttributes); ConfigEntry value = config.Bind(section, key, defaultValue, new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { configurationManagerAttributes2 })); return new TuningBool(mode, value); } } public const string Guid = "com.magicstafftweaker.client"; public const string Name = "Magic Staff Tweaker"; public const string Version = "2.2.5"; private readonly Dictionary _configs = new Dictionary(); private readonly Dictionary _baselines = new Dictionary(); private bool _vanillaPrefabsReady; private ConfigEntry? _logStaffGraph; private ConfigEntry? _uiDefaultsRevision; private Harmony? _harmony; private static Plugin? _instance; private HashSet _savedSettingKeys = new HashSet(StringComparer.Ordinal); private static readonly ConditionalWeakTable LocalAoeFactors = new ConditionalWeakTable(); private static readonly ConditionalWeakTable SummonWeaponBaselines = new ConditionalWeakTable(); private void Awake() { //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Expected O, but got Unknown //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Expected O, but got Unknown _instance = this; _savedSettingKeys = ConfigPersistence.ReadKeys(File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath) ? File.ReadAllText(((BaseUnityPlugin)this).Config.ConfigFilePath) : string.Empty); _logStaffGraph = ((BaseUnityPlugin)this).Config.Bind("Diagnostics", "Log Staff Attack Graph", false, "Log the relevant component/field graph for each vanilla staff when prefabs become available."); StaffConfig.ConfigurationManagerAttributes configurationManagerAttributes = new StaffConfig.ConfigurationManagerAttributes { Browsable = false }; _uiDefaultsRevision = ((BaseUnityPlugin)this).Config.Bind("Internal", "UI Vanilla Defaults Revision", 0, new ConfigDescription("Internal migration marker for player-facing vanilla defaults.", (AcceptableValueBase)null, new object[1] { configurationManagerAttributes })); foreach (StaffDefinition item in VanillaStaffs.All) { StaffConfig staffConfig = new StaffConfig(((BaseUnityPlugin)this).Config, item); _configs[item.Prefab] = staffConfig; staffConfig.Changed += ApplyAll; } PrefabManager.OnVanillaPrefabsAvailable += OnVanillaPrefabsAvailable; _harmony = new Harmony("com.magicstafftweaker.client"); _harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1} loaded. {2} vanilla staffs registered.", "Magic Staff Tweaker", "2.2.5", VanillaStaffs.All.Count)); } private void OnDestroy() { _instance = null; PrefabManager.OnVanillaPrefabsAvailable -= OnVanillaPrefabsAvailable; Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } foreach (StaffConfig value in _configs.Values) { value.Changed -= ApplyAll; } } private void OnVanillaPrefabsAvailable() { _vanillaPrefabsReady = true; ConfigEntry? logStaffGraph = _logStaffGraph; if (logStaffGraph != null && logStaffGraph.Value) { DumpStaffGraphs(); } CaptureBaselines(); ApplyAll(); } private void DumpStaffGraphs() { foreach (StaffDefinition item in VanillaStaffs.All) { GameObject prefab = PrefabManager.Instance.GetPrefab(item.Prefab); if ((Object)(object)prefab == (Object)null) { continue; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Graph] " + item.DisplayName + " (" + item.Prefab + ")")); Component[] componentsInChildren = prefab.GetComponentsInChildren(true); foreach (Component val in componentsInChildren) { if ((Object)(object)val == (Object)null) { continue; } string name = ((object)val).GetType().Name; if (name != "ItemDrop" && name != "Attack" && name != "Projectile" && name != "Aoe" && name != "EffectArea" && name != "StatusEffect" && name != "SE_Shield") { continue; } FieldInfo[] fields = ((object)val).GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.Name.StartsWith("m_", StringComparison.Ordinal)) { object value = fieldInfo.GetValue(val); if (value == null || value is string || value.GetType().IsPrimitive || value is Enum) { ((BaseUnityPlugin)this).Logger.LogInfo((object)$" {name}.{fieldInfo.Name} = {value}"); } } } } } } private void CaptureBaselines() { if (!_vanillaPrefabsReady) { return; } bool flag = (_uiDefaultsRevision?.Value ?? 0) < 214; foreach (StaffDefinition item in VanillaStaffs.All) { GameObject prefab = PrefabManager.Instance.GetPrefab(item.Prefab); if ((Object)(object)prefab == (Object)null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not find vanilla staff prefab '" + item.Prefab + "'.")); continue; } _baselines[item.Prefab] = StaffBaseline.Capture(prefab); CaptureKnownSummonHealth(item.Prefab, _baselines[item.Prefab]); _configs[item.Prefab].InitializeVanillaValues(_baselines[item.Prefab], forceReset: false); } if (flag && _uiDefaultsRevision != null) { _uiDefaultsRevision.Value = 214; ((BaseUnityPlugin)this).Config.Save(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Initialized missing configuration defaults while preserving saved values."); } } private void ApplyAll() { if (!_vanillaPrefabsReady) { return; } foreach (StaffDefinition item in VanillaStaffs.All) { if (_baselines.TryGetValue(item.Prefab, out StaffBaseline value)) { GameObject prefab = PrefabManager.Instance.GetPrefab(item.Prefab); if (!((Object)(object)prefab == (Object)null)) { ApplyStaff(prefab, value, _configs[item.Prefab]); } } } } private static void CaptureKnownSummonHealth(string staffPrefab, StaffBaseline baseline) { object obj = staffPrefab switch { "StaffSkeleton" => new string[2] { "Skeleton_Friendly", "SkeletonArcher_Friendly" }, "StaffRedTroll" => new string[1] { "Troll_Summoned" }, "StaffGreenRoots" => new string[1] { "staff_greenroots_tentaroot" }, "StaffSpiritCaller" => new string[4] { "Boar_spiritcaller", "Wolf_spiritcaller", "Moose_spiritcaller", "Bjorn_spiritcaller" }, _ => Array.Empty(), }; List summonGraphFields = baseline.PrimaryAttack.SummonGraphFields; string[] array = (string[])obj; foreach (string text in array) { GameObject prefab = PrefabManager.Instance.GetPrefab(text); Character creature = ((prefab != null) ? prefab.GetComponent() : null); if (!((Object)(object)creature == (Object)null)) { FieldInfo fieldInfo = FindField(creature, "m_health"); if (!(fieldInfo == null) && !summonGraphFields.Exists((GraphFieldBaseline f) => (object)f.Target == creature && f.FieldName == "m_health") && !baseline.SecondaryAttack.SummonGraphFields.Exists((GraphFieldBaseline f) => (object)f.Target == creature && f.FieldName == "m_health")) { summonGraphFields.Add(new GraphFieldBaseline((Component)(object)creature, fieldInfo, fieldInfo.GetValue(creature))); } } } } private void ApplyStaff(GameObject prefab, StaffBaseline baseline, StaffConfig cfg) { if (!cfg.Enabled.Value) { baseline.Restore(prefab); return; } ItemDrop component = prefab.GetComponent(); if (component?.m_itemData?.m_shared == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(cfg.DisplayName + ": no ItemDrop/SharedData found.")); return; } SharedData shared = component.m_itemData.m_shared; ApplyAttack(shared.m_attack, baseline.PrimaryAttack, cfg); ApplyAttack(shared.m_secondaryAttack, baseline.SecondaryAttack, cfg); ApplyKnownAttackFields(prefab, shared.m_attack, cfg); ApplyKnownAttackFields(prefab, shared.m_secondaryAttack, cfg); ApplySpecialStaffGraph(prefab, shared.m_attack, baseline.PrimaryAttack, cfg); ApplySpecialStaffGraph(prefab, shared.m_secondaryAttack, baseline.SecondaryAttack, cfg); ApplySummonGraph(baseline.PrimaryAttack, cfg); ApplySummonGraph(baseline.SecondaryAttack, cfg); } private static void ApplyAttack(Attack? attack, AttackBaseline baseline, StaffConfig cfg) { if (attack != null) { SetFloat(attack, "m_attackEitr", baseline.AttackEitr, cfg.EitrCost); SetPercentFloat(attack, "m_attackHealthPercentage", baseline.AttackHealthPercentage, cfg.HealthCostPercent); SetFloat(attack, "m_attackCooldown", baseline.AttackCooldown, cfg.Cooldown); SetFloat(attack, "m_attackForce", baseline.AttackForce, cfg.AttackForce); SetFloat(attack, "m_attackForceForward", baseline.AttackForceForward, cfg.AttackForceForward); SetFloat(attack, "m_projectileVel", baseline.ProjectileVelocity, cfg.ProjectileSpeed); SetFloat(attack, "m_projectileAccuracyMin", baseline.ProjectileAccuracyMin, cfg.ProjectileAccuracyMin); SetInt(attack, "m_projectiles", baseline.Projectiles, cfg.ProjectileCount); SetFloat(attack, "m_attackDrawTime", baseline.AttackDrawTime, cfg.DrawTime); SetFloat(attack, "m_attackDrawPercentage", baseline.AttackDrawPercentage, cfg.DrawPercentage); SetFloat(attack, "m_attackStamina", baseline.AttackStamina, cfg.AttackStamina); SetFloat(attack, "m_attackStaminaModifier", baseline.AttackStaminaModifier, cfg.AttackStaminaModifier); SetFloat(attack, "m_attackStatusEffectChance", baseline.StatusEffectChance, cfg.StatusEffectChance); SetFloat(attack, "m_attackHealth", baseline.AttackHealth, cfg.HealthCost); SetFloat(attack, "m_attackHealthReturnHit", baseline.HealthReturnHit, cfg.HealthReturnHit); SetFloat(attack, "m_attackEitrAdd", baseline.AttackEitrAdd, cfg.EitrAdd); SetFloat(attack, "m_chainChance", baseline.ChainChance, cfg.ChainChance); SetFloat(attack, "m_chainChancePerTarget", baseline.ChainChancePerTarget, cfg.ChainChancePerTarget); SetInt(attack, "m_chainMaxTargets", baseline.ChainMaxTargets, cfg.ChainMaxTargets); SetFloat(attack, "m_chainDelay", baseline.ChainDelay, cfg.ChainDelay); SetFloat(attack, "m_chainStartChance", baseline.ChainStartChance, cfg.ChainStartChance); SetFloat(attack, "m_chainStartChanceFalloff", baseline.ChainStartChanceFalloff, cfg.ChainStartChanceFalloff); SetFloat(attack, "m_chainStartDelay", baseline.ChainStartDelay, cfg.ChainStartDelay); SetInt(attack, "m_chainMinTargets", baseline.ChainMinTargets, cfg.ChainMinTargets); SetFloat(attack, "m_chainAttackMaxTime", baseline.ChainAttackMaxTime, cfg.ChainAttackMaxTime); SetInt(attack, "m_attackChainLevels", baseline.AttackChainLevels, cfg.AttackChainLevels); } } private static void ApplySummonGraph(AttackBaseline baseline, StaffConfig cfg) { ApplyGraphField(baseline.SummonGraphFields, new string[1] { "Character" }, "m_health", cfg.SummonHealth); ApplyGraphField(baseline.SummonGraphFields, new string[1] { "Humanoid" }, "m_walkSpeed", cfg.SummonWalkSpeed); ApplyGraphField(baseline.SummonGraphFields, new string[1] { "Humanoid" }, "m_runSpeed", cfg.SummonRunSpeed); ApplyGraphField(baseline.SummonGraphFields, new string[1] { "Attack" }, "m_damageMultiplier", cfg.SummonDamageMultiplier); ApplyGraphField(baseline.SummonGraphFields, new string[1] { "Attack" }, "m_attackCooldown", cfg.SummonAttackCooldown); ApplyGraphField(baseline.SummonGraphFields, new string[1] { "SpawnAbility" }, "m_maxSpawns", cfg.SummonMaxSpawns); } private static bool TryGetLocalStaffConfig(Character owner, ItemData item, out StaffConfig cfg) { cfg = null; Plugin instance = _instance; if ((Object)(object)instance == (Object)null || (Object)(object)owner == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)owner != (Object)(object)Player.m_localPlayer || (Object)(object)item?.m_dropPrefab == (Object)null) { return false; } if (instance._configs.TryGetValue(((Object)item.m_dropPrefab).name, out cfg)) { return cfg.Enabled.Value; } return false; } private static bool TryGetLocalSpellConfig(Character owner, ItemData item, out StaffConfig cfg) { if (TryGetLocalStaffConfig(owner, item, out cfg)) { return cfg.DamageMultiplier.IntendedVisible; } return false; } private static string? GetSummonStaffPrefab(Character character) { if ((Object)(object)character == (Object)null) { return null; } string name = ((Object)((Component)character).gameObject).name; if (name.Contains("Skeleton_Friendly", StringComparison.Ordinal) || name.Contains("SkeletonArcher_Friendly", StringComparison.Ordinal)) { return "StaffSkeleton"; } if (name.Contains("Troll_Summoned", StringComparison.Ordinal)) { return "StaffRedTroll"; } if (name.Contains("staff_greenroots_tentaroot", StringComparison.Ordinal)) { return "StaffGreenRoots"; } if (name.Contains("Boar_spiritcaller", StringComparison.Ordinal) || name.Contains("Wolf_spiritcaller", StringComparison.Ordinal) || name.Contains("Moose_spiritcaller", StringComparison.Ordinal) || name.Contains("Bjorn_spiritcaller", StringComparison.Ordinal)) { return "StaffSpiritCaller"; } return null; } private static bool IsDeadRaiserSummon(Character character) { return GetSummonStaffPrefab(character) == "StaffSkeleton"; } private static void ApplyFinalSummonDamage(HitData hit, StaffConfig cfg) { //IL_0035: 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) if (hit != null && cfg.Enabled.Value && !((Object)(object)hit.GetAttacker() == (Object)null)) { StaffConfig.TuningFloat summonFinalDamageMultiplier = cfg.SummonFinalDamageMultiplier; if (summonFinalDamageMultiplier.IntendedVisible && (int)summonFinalDamageMultiplier.Mode.Value != 0) { float num = DamageTuning.SpellFactor(summonFinalDamageMultiplier.Mode.Value, summonFinalDamageMultiplier.Value.Value); hit.m_damage.m_damage *= num; hit.m_damage.m_blunt *= num; hit.m_damage.m_slash *= num; hit.m_damage.m_pierce *= num; hit.m_damage.m_chop *= num; hit.m_damage.m_pickaxe *= num; hit.m_damage.m_fire *= num; hit.m_damage.m_frost *= num; hit.m_damage.m_lightning *= num; hit.m_damage.m_poison *= num; hit.m_damage.m_spirit *= num; } } } private static void ApplySummonWeaponDamage(ItemData item, StaffConfig cfg) { //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Invalid comparison between Unknown and I4 //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) if (item?.m_shared != null) { SummonWeaponDamageBaseline value = SummonWeaponBaselines.GetValue(item.m_shared, (SharedData _) => new SummonWeaponDamageBaseline(item)); if (!cfg.Enabled.Value) { item.m_shared.m_damages = value.Damages; item.m_shared.m_damagesPerLevel = value.DamagesPerLevel; return; } bool num = (int)item.m_shared.m_itemType == 4; DamageTypes damages = value.Damages; StaffConfig.TuningFloat setting = (num ? cfg.SummonArcherDamageMultiplier : cfg.SummonMeleeDamageMultiplier); ApplyDamageTypes(ref damages, setting); item.m_shared.m_damages = damages; DamageTypes damages2 = value.DamagesPerLevel; ApplyDamageTypes(ref damages2, cfg.SummonDamagePerLevelMultiplier); item.m_shared.m_damagesPerLevel = damages2; } } private static void ApplyDamageTypes(ref DamageTypes damages, StaffConfig.TuningFloat setting) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Unknown result type (might be due to invalid IL or missing references) if (setting.IntendedVisible) { damages.m_damage = Modifier.Apply(damages.m_damage, setting.Mode.Value, setting.Value.Value); damages.m_blunt = Modifier.Apply(damages.m_blunt, setting.Mode.Value, setting.Value.Value); damages.m_slash = Modifier.Apply(damages.m_slash, setting.Mode.Value, setting.Value.Value); damages.m_pierce = Modifier.Apply(damages.m_pierce, setting.Mode.Value, setting.Value.Value); damages.m_chop = Modifier.Apply(damages.m_chop, setting.Mode.Value, setting.Value.Value); damages.m_pickaxe = Modifier.Apply(damages.m_pickaxe, setting.Mode.Value, setting.Value.Value); damages.m_fire = Modifier.Apply(damages.m_fire, setting.Mode.Value, setting.Value.Value); damages.m_frost = Modifier.Apply(damages.m_frost, setting.Mode.Value, setting.Value.Value); damages.m_lightning = Modifier.Apply(damages.m_lightning, setting.Mode.Value, setting.Value.Value); damages.m_poison = Modifier.Apply(damages.m_poison, setting.Mode.Value, setting.Value.Value); damages.m_spirit = Modifier.Apply(damages.m_spirit, setting.Mode.Value, setting.Value.Value); } } private static void ApplySpecialStaffGraph(GameObject staffPrefab, Attack? attack, AttackBaseline baseline, StaffConfig cfg) { if (attack != null) { ApplyGraphField(baseline.GraphFields, new string[2] { "Aoe", "EffectArea" }, "m_radius", cfg.ExplosionRadius); ApplyGraphField(baseline.GraphFields, new string[3] { "Projectile", "Aoe", "EffectArea" }, "m_force", cfg.Knockback); ApplyGraphField(baseline.GraphFields, new string[3] { "Projectile", "Aoe", "EffectArea" }, "m_forceStagger", cfg.Knockback); ApplyGraphField(baseline.GraphFields, new string[1] { "StatusEffect" }, "m_duration", cfg.StatusEffectDuration); ApplyGraphField(baseline.GraphFields, new string[1] { "StatusEffect" }, "m_ttl", cfg.StatusEffectDuration); ApplyGraphField(baseline.GraphFields, new string[1] { "SE_Shield" }, "m_absorbDamage", cfg.ShieldAbsorption); ApplyGraphField(baseline.GraphFields, new string[1] { "SE_Shield" }, "m_absorbDamagePerSkillLevel", cfg.ShieldAbsorptionPerBloodMagicLevel); ApplyGraphField(baseline.GraphFields, new string[1] { "ShieldDome" }, "m_wrapSphereRadius", cfg.VengeanceSphereRadius); ApplyGraphField(baseline.GraphFields, new string[1] { "ShieldDome" }, "m_reflectionHeight", cfg.VengeanceReflectionHeight); ApplyGraphField(baseline.GraphFields, new string[1] { "Projectile" }, "m_ttl", cfg.ProjectileLifetime); ApplyGraphField(baseline.GraphFields, new string[1] { "Projectile" }, "m_ttlMax", cfg.ProjectileLifetime); ApplyGraphField(baseline.GraphFields, new string[1] { "Projectile" }, "m_projectileVelocity", cfg.ProjectileVelocity); ApplyGraphField(baseline.GraphFields, new string[1] { "Projectile" }, "m_projectileVelocityMax", cfg.ProjectileVelocityMax); ApplyGraphField(baseline.GraphFields, new string[1] { "Projectile" }, "m_maxBounces", cfg.ProjectileMaxBounces); ApplyGraphField(baseline.GraphFields, new string[3] { "Projectile", "Aoe", "EffectArea" }, "m_damage", cfg.Damage); ApplyGraphField(baseline.GraphFields, new string[3] { "Projectile", "Aoe", "EffectArea" }, "m_damagePerHit", cfg.DamagePerHit); } } private static void ApplyGraphField(List fields, string[] componentTypeNames, string fieldName, StaffConfig.TuningFloat entry) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) if (!entry.IntendedVisible) { return; } foreach (GraphFieldBaseline field in fields) { if (Array.IndexOf(componentTypeNames, field.ComponentTypeName) < 0 || !string.Equals(field.FieldName, fieldName, StringComparison.Ordinal)) { continue; } if ((int)entry.Mode.Value == 0) { field.Restore(); continue; } float? num = ReadNumeric(field.Field.FieldType, field.VanillaValue); if (num.HasValue) { float value = Modifier.Apply(num.Value, entry.Mode.Value, entry.Value.Value); object converted; if ((fieldName == "m_health" && !DamageTuning.ValidHealth(value)) || (fieldName == "m_attackCooldown" && !DamageTuning.ValidCooldown(value))) { field.Restore(); } else if (TryConvertNumber(value, field.Field.FieldType, out converted)) { field.Field.SetValue(field.Target, converted); } } } } private static void ApplyGraphField(List fields, string[] componentTypeNames, string fieldName, StaffConfig.TuningInt entry) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) if (!entry.IntendedVisible) { return; } foreach (GraphFieldBaseline field in fields) { if (Array.IndexOf(componentTypeNames, field.ComponentTypeName) < 0 || !string.Equals(field.FieldName, fieldName, StringComparison.Ordinal)) { continue; } if ((int)entry.Mode.Value == 0) { field.Restore(); continue; } float? num = ReadNumeric(field.Field.FieldType, field.VanillaValue); if (num.HasValue && TryConvertNumber(Modifier.Apply((int)MathF.Round(num.Value), entry.Mode.Value, entry.Value.Value), field.Field.FieldType, out object converted)) { field.Field.SetValue(field.Target, converted); } } } private static FieldInfo? FindField(object target, string fieldName) { return target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } private static float? ReadNumeric(Type fieldType, object? value) { if (value == null) { return null; } try { return Convert.ToSingle(value, CultureInfo.InvariantCulture); } catch { return null; } } private static bool TryConvertNumber(float value, Type targetType, out object converted) { try { if (targetType == typeof(float)) { converted = value; return true; } if (targetType == typeof(double)) { converted = (double)value; return true; } if (targetType == typeof(int)) { converted = (int)MathF.Round(value); return true; } if (targetType == typeof(long)) { converted = (long)MathF.Round(value); return true; } converted = Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); return true; } catch { converted = null; return false; } } private static void ApplyKnownAttackFields(GameObject staffPrefab, Attack? attack, StaffConfig cfg) { } private static void SetPercentFloat(object target, string fieldName, float baseline, StaffConfig.TuningFloat entry) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Invalid comparison between Unknown and I4 //IL_0065: Unknown result type (might be due to invalid IL or missing references) if (!entry.IntendedVisible) { return; } FieldInfo fieldInfo = FindField(target, fieldName); if (!(fieldInfo == null)) { if ((int)entry.Mode.Value == 0) { fieldInfo.SetValue(target, baseline); } else if ((int)entry.Mode.Value == 3) { fieldInfo.SetValue(target, entry.Value.Value); } else { fieldInfo.SetValue(target, Modifier.Apply(baseline, entry.Mode.Value, entry.Value.Value)); } } } private static void SetFloat(object target, string fieldName, float baseline, StaffConfig.TuningFloat entry) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) if (entry.IntendedVisible) { float num = Modifier.Apply(baseline, entry.Mode.Value, entry.Value.Value); if ((int)entry.Mode.Value == 0) { num = baseline; } SetField(target, fieldName, num); } } private static void SetInt(object target, string fieldName, int baseline, StaffConfig.TuningInt entry) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) if (entry.IntendedVisible) { int num = Modifier.Apply(baseline, entry.Mode.Value, entry.Value.Value); if ((int)entry.Mode.Value == 0) { num = baseline; } SetField(target, fieldName, num); } } private static void SetBool(object target, string fieldName, bool baseline, StaffConfig.TuningBool entry) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) bool flag = entry.Value.Value; if ((int)entry.Mode.Value == 0) { flag = baseline; } SetField(target, fieldName, flag); } private static void SetField(object target, string fieldName, object value) { FieldInfo field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(field == null) && field.FieldType.IsAssignableFrom(value.GetType())) { field.SetValue(target, value); } } } public static class SummonControls { public static void ApplyToInstance(SpawnAbility spawn, int count, bool ignoreDeadRaiserLimit) { count = Math.Max(1, Math.Min(20, count)); spawn.m_minToSpawn = count; spawn.m_maxToSpawn = count; if (ignoreDeadRaiserLimit) { spawn.m_maxSpawned = 0; } } } }