Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of MagicStaffTweakerClientOnly v2.2.5
MagicStaffTweaker.Core.dll
Decompiled 14 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using Microsoft.CodeAnalysis; [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.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("MagicStaffTweaker.Core")] [assembly: AssemblyTitle("MagicStaffTweaker.Core")] [assembly: AssemblyVersion("1.0.0.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.Core { public static class Modifier { public static float Apply(float vanilla, ModifierMode mode, float value) { return mode switch { ModifierMode.Vanilla => vanilla, ModifierMode.Add => vanilla + value, ModifierMode.Multiply => vanilla * value, ModifierMode.Override => value, _ => vanilla, }; } public static int Apply(int vanilla, ModifierMode mode, int value) { return mode switch { ModifierMode.Vanilla => vanilla, ModifierMode.Add => vanilla + value, ModifierMode.Multiply => (int)MathF.Round(vanilla * value), ModifierMode.Override => value, _ => vanilla, }; } } public sealed class StaffDefinition { public string Prefab { get; } public string DisplayName { get; } public StaffMagicType MagicType { get; } public StaffFeatures Features { get; } public StaffDefinition(string prefab, string displayName, StaffMagicType magicType, StaffFeatures features) { Prefab = prefab; DisplayName = displayName; MagicType = magicType; Features = features; } } [Flags] public enum StaffFeatures { None = 0, Projectile = 1, Explosion = 2, Summon = 4, Shield = 8, Area = 0x10, Chain = 0x20, SecondaryAttack = 0x40, BloodMagic = 0x80, ElementalMagic = 0x100 } public enum StaffMagicType { Elemental, Blood } public enum ModifierMode { Vanilla, Add, Multiply, Override } public static class VanillaStaffs { public static readonly IReadOnlyList<StaffDefinition> All = new StaffDefinition[12] { new StaffDefinition("StaffFireball", "Staff of Embers", StaffMagicType.Elemental, StaffFeatures.Projectile | StaffFeatures.Explosion | StaffFeatures.ElementalMagic), new StaffDefinition("StaffIceShards", "Staff of Frost", StaffMagicType.Elemental, StaffFeatures.Projectile | StaffFeatures.ElementalMagic), new StaffDefinition("StaffClusterbomb", "Staff of Fracturing", StaffMagicType.Elemental, StaffFeatures.Projectile | StaffFeatures.Explosion | StaffFeatures.ElementalMagic), new StaffDefinition("StaffGreenRoots", "Staff of the Wild", StaffMagicType.Elemental, StaffFeatures.Projectile | StaffFeatures.Summon | StaffFeatures.Area | StaffFeatures.ElementalMagic), new StaffDefinition("StaffLightning", "Dundr", StaffMagicType.Elemental, StaffFeatures.Projectile | StaffFeatures.Chain | StaffFeatures.ElementalMagic), new StaffDefinition("StaffOrbofAhri", "Echo Spike", StaffMagicType.Blood, StaffFeatures.Projectile | StaffFeatures.Explosion | StaffFeatures.BloodMagic), new StaffDefinition("StaffSpiritCaller", "Spirit Caller", StaffMagicType.Blood, StaffFeatures.Projectile | StaffFeatures.Summon | StaffFeatures.BloodMagic), new StaffDefinition("StaffFrostOrbs", "Northern Vengeance", StaffMagicType.Blood, StaffFeatures.Projectile | StaffFeatures.Explosion | StaffFeatures.BloodMagic), new StaffDefinition("StaffThunderBlood", "Lightning Strike", StaffMagicType.Elemental, StaffFeatures.Area | StaffFeatures.ElementalMagic), new StaffDefinition("StaffShield", "Staff of Protection", StaffMagicType.Blood, StaffFeatures.Shield | StaffFeatures.Area | StaffFeatures.BloodMagic), new StaffDefinition("StaffSkeleton", "Dead Raiser", StaffMagicType.Blood, StaffFeatures.Summon | StaffFeatures.BloodMagic), new StaffDefinition("StaffRedTroll", "Trollstav", StaffMagicType.Blood, StaffFeatures.Summon | StaffFeatures.BloodMagic) }; } }
MagicStaffTweaker.dll
Decompiled 14 hours agousing 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<string> ReadKeys(string text) { HashSet<string> hashSet = new HashSet<string>(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("<DefaultValue>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); public static void Apply(ConfigEntry<float> 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<Player>() : 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<ItemDrop>()?.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<ItemDrop>()?.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<GraphFieldBaseline> GraphFields = new List<GraphFieldBaseline>(); public readonly List<GraphFieldBaseline> SummonGraphFields = new List<GraphFieldBaseline>(); 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<GraphFieldBaseline> CaptureSummonGraphFields(Attack attack) { List<GraphFieldBaseline> list = new List<GraphFieldBaseline>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); Queue<(GameObject, int)> queue = new Queue<(GameObject, int)>(); HashSet<int> hashSet2 = new HashSet<int>(); 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<Component>(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<Component>(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<GraphFieldBaseline> CaptureGraphFields(Attack attack, GameObject staffPrefab) { List<GraphFieldBaseline> list = new List<GraphFieldBaseline>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); Queue<(GameObject, int)> queue = new Queue<(GameObject, int)>(); HashSet<int> hashSet2 = new HashSet<int>(); 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<Component>(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<Component>(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<ModifierMode> Mode { get; } ConfigEntryBase Value { get; } void SubscribeValueChanged(Action handler); } internal sealed class TuningFloat : ISettingGroup { public ConfigEntry<ModifierMode> Mode { get; } public ConfigEntry<float> 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<ModifierMode> mode, ConfigEntry<float> 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<ModifierMode> Mode { get; } public ConfigEntry<int> 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<ModifierMode> mode, ConfigEntry<int> 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<ModifierMode> Mode { get; } public ConfigEntry<bool> Value { get; } ConfigEntryBase ISettingGroup.Value => (ConfigEntryBase)(object)Value; public TuningBool(ConfigEntry<ModifierMode> mode, ConfigEntry<bool> 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<string, HashSet<string>> VisibleSettings = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal) { ["Staff of Embers"] = new HashSet<string>(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Speed", "Explosion / AOE Radius", "Projectile Damage Multiplier" }, ["Staff of Frost"] = new HashSet<string>(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Speed", "Projectile Count", "Status Effect Duration", "Projectile Damage Multiplier" }, ["Staff of Fracturing"] = new HashSet<string>(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Speed", "Projectile Count", "Explosion / AOE Radius", "Projectile Damage Multiplier" }, ["Staff of the Wild"] = new HashSet<string>(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Summon Health", "Summon Damage Multiplier", "Summon Attack Cooldown", "Summon Max Spawns", "Summons Per Cast" }, ["Dundr"] = new HashSet<string>(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Count", "Projectile Speed", "Projectile Accuracy Min", "Projectile Damage Multiplier" }, ["Echo Spike"] = new HashSet<string>(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Projectile Speed", "Projectile Max Bounces", "Projectile Damage Multiplier" }, ["Spirit Caller"] = new HashSet<string>(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<string>(StringComparer.Ordinal) { "Eitr Cost", "Health Cost Percent", "Cooldown", "Ally Application Radius", "Vengeance Reflection Height", "Projectile Damage Multiplier" }, ["Lightning Strike"] = new HashSet<string>(StringComparer.Ordinal) { "Eitr Cost", "Cooldown", "Explosion / AOE Radius", "Projectile Damage Multiplier" }, ["Staff of Protection"] = new HashSet<string>(StringComparer.Ordinal) { "Eitr Cost", "Health Cost Percent", "Cooldown", "Shield Absorption", "Shield Absorption Per Blood Magic Level", "Status Effect Duration" }, ["Dead Raiser"] = new HashSet<string>(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<string>(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<string, float> VanillaEitrCosts = new Dictionary<string, float>(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<string, float> VanillaHealthCosts = new Dictionary<string, float>(StringComparer.Ordinal) { ["Spirit Caller"] = 40f, ["Northern Vengeance"] = 40f, ["Staff of Protection"] = 40f, ["Dead Raiser"] = 40f, ["Trollstav"] = 60f }; public string DisplayName { get; } public ConfigEntry<bool> 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<ConfigEntryBase> GetEntries() { yield return (ConfigEntryBase)(object)Enabled; foreach (ISettingGroup tuning in GetTunings()) { yield return (ConfigEntryBase)(object)tuning.Mode; yield return tuning.Value; } } private IEnumerable<ISettingGroup> 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<string> value)) { return value.Contains(key); } return false; } private static AcceptableValueBase? RangeFor(string key) { if (key == "Health Cost Percent") { return (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0f, 100f); } if (key.Contains("Chance", StringComparison.Ordinal)) { return (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0f, 1f); } if (key.Contains("Multiplier", StringComparison.Ordinal)) { return (AcceptableValueBase?)(object)new AcceptableValueRange<float>(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<GraphFieldBaseline>[] array = new List<GraphFieldBaseline>[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<GraphFieldBaseline> 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<GraphFieldBaseline> 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<T> Bind<T>(ConfigFile config, string section, string key, T defaultValue, string description) { return ConfigFileExtensions.BindConfigInOrder<T>(config, section, key, defaultValue, description, false, true, true, (AcceptableValueBase)null, (Action<ConfigEntryBase>)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<ModifierMode> val2 = config.Bind<ModifierMode>("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<float> value = config.Bind<float>(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<ModifierMode> val = config.Bind<ModifierMode>("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<int>(1, 50); break; case "Summons Per Cast": val2 = (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 20); break; case "Summon Max Spawns": val2 = (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 20); break; case "Projectile Max Bounces": val2 = (AcceptableValueBase)(object)new AcceptableValueRange<int>(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<int> value = config.Bind<int>(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<ModifierMode> mode = config.Bind<ModifierMode>("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<bool> value = config.Bind<bool>(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<string, StaffConfig> _configs = new Dictionary<string, StaffConfig>(); private readonly Dictionary<string, StaffBaseline> _baselines = new Dictionary<string, StaffBaseline>(); private bool _vanillaPrefabsReady; private ConfigEntry<bool>? _logStaffGraph; private ConfigEntry<int>? _uiDefaultsRevision; private Harmony? _harmony; private static Plugin? _instance; private HashSet<string> _savedSettingKeys = new HashSet<string>(StringComparer.Ordinal); private static readonly ConditionalWeakTable<Aoe, LocalAoeFactor> LocalAoeFactors = new ConditionalWeakTable<Aoe, LocalAoeFactor>(); private static readonly ConditionalWeakTable<SharedData, SummonWeaponDamageBaseline> SummonWeaponBaselines = new ConditionalWeakTable<SharedData, SummonWeaponDamageBaseline>(); 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<bool>("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<int>("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<bool>? 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<Component>(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<string>(), }; List<GraphFieldBaseline> 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<Character>() : 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<ItemDrop>(); 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<GraphFieldBaseline> 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<GraphFieldBaseline> 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; } } } }