Decompiled source of YAPYAP Expanded v1.0.1
plugins/YAPYAPExpanded/YAPYAPExpanded.dll
Decompiled 6 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Mirror; using RootMotion.Dynamics; using TMPro; using Unity.Cinemachine; using UnityEngine; using UnityEngine.AI; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Utilities; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.SceneManagement; using UnityEngine.UI; using Utilities; using YAPYAP; using YAPYAP.Npc.Frog; using YAPYAP.Npc.Shopkeeper; using YAPYAP.Npc.UsEnemy; using YAPYAP.Utilities; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("YAPYAP Expanded")] [assembly: AssemblyDescription("YAPYAP Expanded 1.0.0 - Pre-release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0-pre-release")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace YAPYAPExpanded { internal sealed class AnchorSystem : MonoBehaviour { private static readonly Dictionary<uint, ReleaseEvent> Anchors = new Dictionary<uint, ReleaseEvent>(); private static readonly Dictionary<uint, GameObject> Visuals = new Dictionary<uint, GameObject>(); internal static void Install(Harmony h) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected O, but got Unknown h.Patch((MethodBase)AccessTools.Method(typeof(PawnHurtbox), "SvSetHealth", (Type[])null, (Type[])null), new HarmonyMethod(typeof(AnchorSystem), "PreventHealthDeath", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); h.Patch((MethodBase)AccessTools.Method(typeof(PawnHurtbox), "SvDoKill", (Type[])null, (Type[])null), new HarmonyMethod(typeof(AnchorSystem), "PreventKill", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } internal static bool Active(uint id) { if (Anchors.TryGetValue(id, out var value)) { return NetworkTime.time < value.Until; } return false; } internal static double Remaining(uint id) { if (!Anchors.TryGetValue(id, out var value)) { return 0.0; } return Math.Max(0.0, value.Until - NetworkTime.time); } internal static bool Create(Pawn pawn, float duration) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: 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_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) if (Active(((NetworkBehaviour)pawn).netId) || pawn.IsPolymorphed || LocalGravitySystem.IsInverted(pawn) || !pawn.Blackboard.IsGrounded) { return false; } Vector3 val = LocalGravitySystem.Up(pawn); Vector3 position = pawn.Rigidbody.position; RaycastHit[] array = (from h in Physics.RaycastAll(position + val * 0.25f, -val, 2f, -1, (QueryTriggerInteraction)1) orderby ((RaycastHit)(ref h)).distance where (Object)(object)((Component)((RaycastHit)(ref h)).collider).GetComponentInParent<Pawn>() != (Object)(object)pawn select h).ToArray(); if (array.Length == 0 || (Object)(object)((Component)((RaycastHit)(ref array[0])).collider).GetComponentInParent<Pawn>() != (Object)null || (Object)(object)((Component)((RaycastHit)(ref array[0])).collider).GetComponentInParent<NpcBehaviour>() != (Object)null) { return false; } ReleaseEvent releaseEvent = new ReleaseEvent { Kind = 3, Owner = ((NetworkBehaviour)pawn).netId, Position = position, Rotation = pawn.Rigidbody.rotation, Until = NetworkTime.time + (double)duration }; Anchors[((NetworkBehaviour)pawn).netId] = releaseEvent; ReleaseNetwork.Send(releaseEvent); return true; } private static bool PreventHealthDeath(PawnHurtbox __instance, ref int __0) { if (NetworkServer.active && __0 <= 0 && Recall(((Component)__instance).GetComponent<Pawn>(), rescue: true)) { __0 = 1; } return true; } private static bool PreventKill(PawnHurtbox __instance) { if (NetworkServer.active) { return !Recall(((Component)__instance).GetComponent<Pawn>(), rescue: true); } return true; } internal static bool Recall(Pawn pawn, bool rescue) { //IL_0042: 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_0065: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)pawn == (Object)null || !Active(((NetworkBehaviour)pawn).netId)) { return false; } ReleaseEvent releaseEvent = Anchors[((NetworkBehaviour)pawn).netId]; End(((NetworkBehaviour)pawn).netId); TransformationSystem.EndPlayer(pawn); LocalGravitySystem.ReleasePawn(pawn); pawn.SvTeleport(releaseEvent.Position, false, false); pawn.Rigidbody.rotation = releaseEvent.Rotation; pawn.Rigidbody.linearVelocity = Vector3.zero; if (rescue) { pawn.Hurtbox.SvSetHealth(1); } ReleaseNetwork.Send(new ReleaseEvent { Kind = 4, B = 2, Target = ((NetworkBehaviour)pawn).netId, Position = releaseEvent.Position, Rotation = releaseEvent.Rotation, Velocity = Vector3.zero }); return true; } private static void End(uint id) { Anchors.Remove(id); ReleaseNetwork.Send(new ReleaseEvent { Kind = 3, Owner = id, B = 1 }); } internal static void Snapshot() { foreach (ReleaseEvent value in Anchors.Values) { ReleaseNetwork.Send(value); } } internal static void Receive(ReleaseEvent m) { //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) if (m.B == 1) { Anchors.Remove(m.Owner); if (Visuals.TryGetValue(m.Owner, out var value) && (Object)(object)value != (Object)null) { Object.Destroy((Object)(object)value); } Visuals.Remove(m.Owner); } else { Anchors[m.Owner] = m; if (!Visuals.ContainsKey(m.Owner)) { Visuals[m.Owner] = MagicGeometry.Ring("RealityAnchor", m.Position, Quaternion.LookRotation(m.Rotation * Vector3.up), new Color(0.5f, 0.3f, 1f), 0.5f, 32); } } } private void Update() { KeyValuePair<uint, ReleaseEvent>[] array = Anchors.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair<uint, ReleaseEvent> keyValuePair = array[i]; if (NetworkTime.time >= keyValuePair.Value.Until || (NetworkServer.active && (!NetworkServer.spawned.TryGetValue(keyValuePair.Key, out var value) || (Object)(object)((Component)value).GetComponent<Pawn>() == (Object)null || ((Component)value).GetComponent<Pawn>().IsExtracted))) { if (NetworkServer.active) { End(keyValuePair.Key); continue; } Receive(new ReleaseEvent { Owner = keyValuePair.Key, B = 1 }); } } if (!NetworkServer.active && !NetworkClient.active) { Clear(); } } internal static void Clear() { if (NetworkServer.active) { uint[] array = Anchors.Keys.ToArray(); for (int i = 0; i < array.Length; i++) { End(array[i]); } } Anchors.Clear(); foreach (GameObject value in Visuals.Values) { if ((Object)(object)value != (Object)null) { Object.Destroy((Object)(object)value); } } Visuals.Clear(); } private void OnDisable() { Clear(); } } internal static class AssetLoader { internal static AssetBundle Bundle { get; private set; } internal static GameObject ChaosVisual { get; private set; } internal static GameObject FrostVisual { get; private set; } internal static string Directory => Path.GetDirectoryName(typeof(Plugin).Assembly.Location); internal static GameObject RealityVisual => RealityVisualAssets.Template; internal static void Load() { string text = Path.Combine(Directory, "yapyapexpanded"); if (!File.Exists(text)) { Plugin.Log.LogError((object)("AssetBundle not found: " + text)); return; } Bundle = AssetBundle.LoadFromFile(text); if ((Object)(object)Bundle == (Object)null) { Plugin.Log.LogError((object)"AssetBundle.LoadFromFile returned null"); return; } Plugin.Log.LogInfo((object)"AssetBundle loaded"); string[] allAssetNames = Bundle.GetAllAssetNames(); string[] array = allAssetNames; foreach (string text2 in array) { Plugin.Log.LogInfo((object)("Asset: " + text2)); } ChaosVisual = LoadPrefabByEnding(allAssetNames, "/haos.prefab"); FrostVisual = LoadPrefabByEnding(allAssetNames, "/frost.prefab"); Plugin.Log.LogInfo((object)("Chaos visual: " + (((Object)(object)ChaosVisual != (Object)null) ? ((Object)ChaosVisual).name : "NOT FOUND"))); Plugin.Log.LogInfo((object)("Frost visual: " + (((Object)(object)FrostVisual != (Object)null) ? ((Object)FrostVisual).name : "NOT FOUND"))); } private static GameObject LoadPrefabByEnding(string[] names, string ending) { foreach (string text in names) { if (text.EndsWith(ending, StringComparison.OrdinalIgnoreCase)) { return Bundle.LoadAsset<GameObject>(text); } } return null; } } internal static class BalanceConfig { private sealed class FiniteDoubleRange : AcceptableValueRange<double> { private readonly double fallback; internal FiniteDoubleRange(double min, double max, double defaultValue) : base(min, max) { fallback = defaultValue; } public override object Clamp(object value) { double d = (double)value; if (!double.IsNaN(d) && !double.IsInfinity(d)) { return base.Clamp(value); } return fallback; } public override bool IsValid(object value) { double d = (double)value; if (!double.IsNaN(d) && !double.IsInfinity(d)) { return base.IsValid(value); } return false; } } private static readonly string[] NightWeightNames = new string[5] { "Fog", "UnstableMagic", "HeavyGravity", "FishApocalypse", "Hunt" }; internal static void Install(ConfigFile config) { //IL_0123: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Expected O, but got Unknown //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Expected O, but got Unknown //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Expected O, but got Unknown bool saveOnConfigSet = config.SaveOnConfigSet; config.SaveOnConfigSet = false; try { foreach (BalanceParameter item in Balance.Ordered) { string text = item.Description + " Host-authoritative. Restart the game after editing. Invalid finite values are clamped; NaN and infinity use the default."; if (item.Integer) { AcceptableValueBase val = (AcceptableValueBase)((item.Minimum == item.Maximum) ? ((object)new AcceptableValueList<int>(new int[1] { (int)item.Default })) : ((object)new AcceptableValueRange<int>((int)item.Minimum, (int)item.Maximum))); ConfigEntry<int> val2 = config.Bind<int>(item.Section, item.Key, (int)item.Default, new ConfigDescription(text, val, Array.Empty<object>())); item.Local = item.Sanitize(val2.Value); val2.Value = (int)item.Local; } else { AcceptableValueBase val3 = (AcceptableValueBase)(object)((item.Minimum == item.Maximum) ? ((FiniteDoubleRange)(object)new AcceptableValueList<double>(new double[1] { item.Default })) : new FiniteDoubleRange(item.Minimum, item.Maximum, item.Default)); ConfigEntry<double> val4 = config.Bind<double>(item.Section, item.Key, item.Default, new ConfigDescription(text, val3, Array.Empty<object>())); item.Local = item.Sanitize(val4.Value); val4.Value = item.Local; } } MigrateShieldPrices(config); MigratePortalDuration(config); MigrateRealityRange(config); MigrateTraitWeights(config); MigrateNightEvents(config); MigrateOrbitCollection(config); Balance.UseLocal(); foreach (BalanceParameter item2 in Balance.Ordered) { if (item2.Local != item2.Value) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("Adjusted incompatible balance setting: " + item2.Id)); } } item2.Local = item2.Value; config[new ConfigDefinition(item2.Section, item2.Key)].BoxedValue = (item2.Integer ? ((object)(int)item2.Value) : ((object)item2.Value)); } config.Save(); } finally { config.SaveOnConfigSet = saveOnConfigSet; } } private static void MigrateShieldPrices(ConfigFile config) { ConfigEntry<int> val = config.Bind<int>("Configuration", "ShieldPriceMigration", 0, "Internal one-time migration marker. Old standard shield prices 300/500/800 become 200/300/400; other prices are retained. Do not edit this marker."); if (val.Value >= 1) { return; } ConfigEntryBase obj = config["UpgradeShield", "Level1Price"]; ConfigEntryBase val2 = config["UpgradeShield", "Level2Price"]; ConfigEntryBase val3 = config["UpgradeShield", "Level3Price"]; if ((int)obj.BoxedValue == 300 && (int)val2.BoxedValue == 500 && (int)val3.BoxedValue == 800) { int[] array = new int[3] { 200, 300, 400 }; int i; for (i = 0; i < 3; i++) { config["UpgradeShield", "Level" + (i + 1) + "Price"].BoxedValue = array[i]; Balance.Ordered.Find((BalanceParameter x) => x.Section == "UpgradeShield" && x.Key == "Level" + (i + 1) + "Price").Local = array[i]; } ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"Migrated old default shield prices to 200/300/400. Custom prices were preserved."); } } val.Value = 1; } private static void MigratePortalDuration(ConfigFile config) { ConfigEntry<int> val = config.Bind<int>("Configuration", "PortalDurationMigration", 0, "Internal one-time migration marker. The old standard portal lifetime of 60 seconds becomes 30 seconds; other lifetimes are retained. After migration you may set any valid lifetime, including 60. Do not edit this marker."); if (val.Value >= 1) { return; } ConfigEntryBase val2 = config["Portal", "Duration"]; if ((double)val2.BoxedValue == 60.0) { val2.BoxedValue = 30.0; Balance.Ordered.Find((BalanceParameter x) => x.Section == "Portal" && x.Key == "Duration").Local = 30.0; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)"Migrated old default portal duration from 60 to 30 seconds. Other custom lifetimes were preserved."); } } val.Value = 1; } private static void MigrateRealityRange(ConfigFile config) { ConfigEntry<int> val = config.Bind<int>("Configuration", "RealityRangeMigration", 0, "Internal one-time migration marker: old standard Portal/Randomize ranges of 20 metres become 30. Other ranges and all later edits are retained. Do not edit this marker."); if (val.Value >= 1) { return; } string[] array = new string[2] { "Portal", "Randomize" }; foreach (string section in array) { if ((double)config[section, "CastRange"].BoxedValue == 20.0) { config[section, "CastRange"].BoxedValue = 30.0; Balance.Ordered.Find((BalanceParameter x) => x.Section == section && x.Key == "CastRange").Local = 30.0; } } val.Value = 1; } private static void MigrateTraitWeights(ConfigFile config) { ConfigEntry<int> val = config.Bind<int>("Configuration", "TraitWeightMigration", 0, "Internal migration revision. Revision 2 restores old standard speed/range weights 0.35 to 1 for balanced equal-frequency selection. Other weights and edits after revision 2 are retained. Existing wand properties are not rerolled. Do not edit this marker."); if (val.Value >= 2) { return; } string[] array = new string[4] { "LightWeight", "HeavyWeight", "LongRangeWeight", "ShortRangeWeight" }; foreach (string key in array) { if ((double)config["Traits", key].BoxedValue == 0.35) { config["Traits", key].BoxedValue = 1.0; Balance.Ordered.Find((BalanceParameter x) => x.Section == "Traits" && x.Key == key).Local = 1.0; } } val.Value = 2; } private static void MigrateNightEvents(ConfigFile config) { ConfigEntry<int> val = config.Bind<int>("Configuration", "NightEventsMigration", 0, "Internal one-time migration: old standard chance 0.25 becomes 0.6; old all-one night weights reduce HeavyGravity to 0.5. Nonstandard settings and edits after migration are retained. Do not edit this marker."); if (val.Value >= 1) { return; } if ((double)config["NightEvents", "Chance"].BoxedValue == 0.25) { Set("Chance", 0.6); } bool flag = true; string[] nightWeightNames = NightWeightNames; foreach (string text in nightWeightNames) { if ((double)config["NightEvents", text + "Weight"].BoxedValue != 1.0) { flag = false; } } if (flag) { Set("HeavyGravityWeight", 0.5); } val.Value = 1; void Set(string key, double value) { config["NightEvents", key].BoxedValue = value; Balance.Ordered.Find((BalanceParameter x) => x.Section == "NightEvents" && x.Key == key).Local = value; } } private static void MigrateOrbitCollection(ConfigFile config) { ConfigEntry<int> val = config.Bind<int>("Configuration", "OrbitCollectionMigration", 0, "Internal one-time migration: old standard Orbit MaximumObjects 4 becomes 8 and Height 1.15 becomes 0.65 metres. Custom values and later edits are retained. Do not edit this marker."); if (val.Value < 1) { if ((int)config["Orbit", "MaximumObjects"].BoxedValue == 4) { Set("MaximumObjects", 8.0); } if ((double)config["Orbit", "Height"].BoxedValue == 1.15) { Set("Height", 0.65); } val.Value = 1; } void Set(string key, double value) { BalanceParameter balanceParameter = Balance.Ordered.Find((BalanceParameter x) => x.Section == "Orbit" && x.Key == key); config["Orbit", key].BoxedValue = (balanceParameter.Integer ? ((object)(int)value) : ((object)value)); balanceParameter.Local = value; } } } internal static class BalanceDefaults { internal static void Register() { ReleaseBalance.Register(); Balance.Add("Shop", "FrostPrice", 160.0, 0.0, 100000.0, integer: true, "Initial Frost staff price in gold coins, used to populate its ItemPrices section once. Edit that section for the exact current purchase/sale prices."); Balance.Add("Shop", "ChaosPrice", 180.0, 0.0, 100000.0, integer: true, "Initial Chaos staff price in gold coins, used to populate its ItemPrices section once. Edit that section for the exact current purchase/sale prices."); Balance.Add("Shop", "VanillaPriceMultiplier", 4.0, 0.1, 100.0, integer: false, "Legacy formula used only before an individual ItemPrices section exists. That section contains exact coin prices and takes precedence. Valuables and free starter items are excluded from this initial formula."); Balance.Add("Shop", "VanillaMinimumPrice", 40.0, 1.0, 100000.0, integer: true, "Initial minimum for paid vanilla wands without an ItemPrices section. Individual exact coin prices take precedence."); Balance.Add("Shop", "VanillaMaximumPrice", 200.0, 1.0, 100000.0, integer: true, "Initial maximum for paid vanilla wands without an ItemPrices section. Raised to the initial minimum if necessary. Individual exact coin prices take precedence."); Balance.Add("Shop", "FrostStockWeight", 1.0, 0.0, 20.0, integer: false, "Relative weight in the native shop wand pool. Zero disables new stock of this wand."); Balance.Add("Shop", "ChaosStockWeight", 1.0, 0.0, 20.0, integer: false, "Relative weight in the native shop wand pool. Zero disables new stock of this wand."); Balance.Add("UpgradeHealth", "Level1Price", 40.0, 0.0, 100000.0, integer: true, "Cost in team gold to buy level 1. Existing purchases are not refunded when this changes."); Balance.Add("UpgradeHealth", "Level2Price", 60.0, 0.0, 100000.0, integer: true, "Cost in team gold to buy level 2. Existing purchases are not refunded when this changes."); Balance.Add("UpgradeHealth", "Level3Price", 80.0, 0.0, 100000.0, integer: true, "Cost in team gold to buy level 3. Existing purchases are not refunded when this changes."); Balance.Add("UpgradeHealth", "Level4Price", 110.0, 0.0, 100000.0, integer: true, "Cost in team gold to buy level 4. Existing purchases are not refunded when this changes."); Balance.Add("UpgradeHealth", "Level5Price", 150.0, 0.0, 100000.0, integer: true, "Cost in team gold to buy level 5. Existing purchases are not refunded when this changes."); Balance.Add("UpgradeSpeed", "Level1Price", 120.0, 0.0, 100000.0, integer: true, "Cost in team gold to buy level 1. Existing purchases are not refunded when this changes."); Balance.Add("UpgradeInventory", "Level1Price", 120.0, 0.0, 100000.0, integer: true, "Cost in team gold to buy level 1. Existing purchases are not refunded when this changes."); Balance.Add("UpgradeInventory", "Level2Price", 180.0, 0.0, 100000.0, integer: true, "Cost in team gold to buy level 2. Existing purchases are not refunded when this changes."); Balance.Add("UpgradeHealth", "HpPerLevel", 10.0, 1.0, 100.0, integer: true, "Maximum HP added by each new purchase. Previously saved maximum HP is preserved."); Balance.Add("UpgradeSpeed", "BonusPerLevel", 0.05, 0.0, 0.5, integer: false, "Additive movement speed fraction per level: 0.05 means +5%."); Balance.Add("UpgradeInventory", "SlotsPerLevel", 1.0, 1.0, 2.0, integer: true, "Additional inventory slots per level. Base slots and maximum upgrade levels remain unchanged."); Balance.Add("WandLoot", "AcceptanceAtPrice40", 1.0, 0.0, 1.0, integer: false, "Probability to keep a free wand selected by native loot at this shop price. Interpolated between price points. Chances cannot increase with price; shop stock is unaffected."); Balance.Add("WandLoot", "AcceptanceAtPrice80", 0.6, 0.0, 1.0, integer: false, "Probability to keep a free wand selected by native loot at this shop price. Interpolated between price points. Chances cannot increase with price; shop stock is unaffected."); Balance.Add("WandLoot", "AcceptanceAtPrice120", 0.25, 0.0, 1.0, integer: false, "Probability to keep a free wand selected by native loot at this shop price. Interpolated between price points. Chances cannot increase with price; shop stock is unaffected."); Balance.Add("WandLoot", "AcceptanceAtPrice160", 0.08, 0.0, 1.0, integer: false, "Probability to keep a free wand selected by native loot at this shop price. Interpolated between price points. Chances cannot increase with price; shop stock is unaffected."); Balance.Add("WandLoot", "AcceptanceAtPrice200", 0.025, 0.0, 1.0, integer: false, "Probability to keep a free wand selected by native loot at this shop price. Interpolated between price points. Chances cannot increase with price; shop stock is unaffected."); Balance.Add("Vacuum", "Charges", 2.0, 1.0, 12.0, integer: true, "Maximum stored charges. Casting consumes one charge."); Balance.Add("Vacuum", "ChargeRegenSeconds", 16.0, 0.25, 600.0, integer: false, "Seconds needed to regenerate one charge."); Balance.Add("Vacuum", "CastRange", 8.0, 0.5, 50.0, integer: false, "Maximum cast targeting distance in metres. Vacuum also enforces this distance from the caster to every affected body."); Balance.Add("Vacuum", "Duration", 5.0, 0.1, 120.0, integer: false, "Effect lifetime in seconds."); Balance.Add("Vacuum", "Radius", 3.5, 0.2, 12.0, integer: false, "Effect radius in metres. Orbit uses the orbit radius."); Balance.Add("Vacuum", "CastCooldownSeconds", 0.0, 0.0, 600.0, integer: false, "Optional pause between casts in seconds. Zero preserves rapid casting while charges remain."); Balance.Add("FishRain", "Charges", 2.0, 1.0, 12.0, integer: true, "Maximum stored charges. Casting consumes one charge."); Balance.Add("FishRain", "ChargeRegenSeconds", 18.0, 0.25, 600.0, integer: false, "Seconds needed to regenerate one charge."); Balance.Add("FishRain", "CastRange", 16.0, 0.5, 50.0, integer: false, "Maximum cast targeting distance in metres. Vacuum also enforces this distance from the caster to every affected body."); Balance.Add("FishRain", "Duration", 4.0, 0.1, 120.0, integer: false, "Effect lifetime in seconds."); Balance.Add("FishRain", "Radius", 2.5, 0.2, 12.0, integer: false, "Effect radius in metres. Orbit uses the orbit radius."); Balance.Add("FishRain", "CastCooldownSeconds", 0.0, 0.0, 600.0, integer: false, "Optional pause between casts in seconds. Zero preserves rapid casting while charges remain."); Balance.Add("Orbit", "Charges", 2.0, 1.0, 12.0, integer: true, "Maximum stored charges. Casting consumes one charge."); Balance.Add("Orbit", "ChargeRegenSeconds", 20.0, 0.25, 600.0, integer: false, "Seconds needed to regenerate one charge."); Balance.Add("Orbit", "Duration", 6.0, 0.1, 120.0, integer: false, "Effect lifetime in seconds."); Balance.Add("Orbit", "Radius", 2.2, 0.2, 12.0, integer: false, "Effect radius in metres. Orbit uses the orbit radius."); Balance.Add("Orbit", "CastCooldownSeconds", 0.0, 0.0, 600.0, integer: false, "Optional pause between casts in seconds. Zero preserves rapid casting while charges remain."); Balance.Add("Vacuum", "EdgeAcceleration", 14.0, 0.0, 100.0, integer: false, "Pull acceleration near the outer edge in metres per second squared."); Balance.Add("Vacuum", "CenterAccelerationBonus", 46.0, 0.0, 200.0, integer: false, "Extra pull acceleration at the centre, with quadratic distance falloff."); Balance.Add("Vacuum", "VelocityDamping", 2.2, 0.0, 10.0, integer: false, "Velocity damping coefficient while a body is inside the sphere."); Balance.Add("Vacuum", "GravityCompensation", 0.85, 0.0, 1.0, integer: false, "Fraction of normal gravity counteracted inside the sphere."); Balance.Add("Vacuum", "MaximumSpeed", 14.0, 1.0, 40.0, integer: false, "Maximum affected body speed in metres per second."); Balance.Add("Vacuum", "DamageInterval", 0.5, 0.1, 10.0, integer: false, "Seconds between prop damage pulses; each prop is damaged at most once per pulse."); Balance.Add("Vacuum", "PropDamage", 6.0, 0.0, 200.0, integer: true, "Damage to each eligible destructible prop per pulse."); Balance.Add("Physics", "MaximumBodyMass", 45.0, 1.0, 200.0, integer: false, "Maximum rigidbody mass eligible for added vanilla-wand effects."); Balance.Add("Physics", "ImpulseSpeedLimit", 18.0, 1.0, 40.0, integer: false, "Maximum speed after a managed prop impulse, in metres per second."); Balance.Add("Physics", "DamagePropImpulse", 5.0, 0.0, 20.0, integer: false, "Impulse magnitude passed to native prop destruction when dealing damage."); Balance.Add("Physics", "MaximumTemporaryFish", 160.0, 1.0, 256.0, integer: true, "Global cap on temporary fish created by Fish Rain. Excess drops are skipped without accumulating a backlog."); Balance.Add("Physics", "MaximumExtraZones", 16.0, 1.0, 32.0, integer: true, "Global cap on simultaneous extra-spell zones. The oldest zone is ended at this limit."); Balance.Add("FishRain", "DropInterval", 0.4, 0.15, 5.0, integer: false, "Seconds between successive drops. A stalled frame never emits a backlog at once."); Balance.Add("FishRain", "MaximumDrops", 10.0, 1.0, 40.0, integer: true, "Maximum total drops from one cast; duration and drop interval may reduce this number."); Balance.Add("FishRain", "SpawnHeight", 4.0, 1.0, 10.0, integer: false, "Desired height above the target area in metres; lowered to fit beneath the ceiling."); Balance.Add("FishRain", "FallSpeed", 1.5, 0.1, 20.0, integer: false, "Initial downward speed in metres per second."); Balance.Add("FishRain", "FishLifetime", 12.0, 1.0, 60.0, integer: false, "Seconds until each spawned physical fish is destroyed."); Balance.Add("FishRain", "FishScale", 0.4, 0.1, 1.0, integer: false, "Scale of the native physical fish prefab."); Balance.Add("Orbit", "MaximumObjects", 8.0, 1.0, 8.0, integer: true, "Maximum number of unique eligible props in one orbit. Free slots collect nearby props throughout the effect."); Balance.Add("Orbit", "SelectionRadius", 4.0, 0.5, 12.0, integer: false, "Radius in metres used to select the nearest free props."); Balance.Add("Orbit", "ReleaseDistance", 8.0, 1.0, 25.0, integer: false, "Distance from the caster at which an orbiting prop is released; at least the orbit radius."); Balance.Add("Orbit", "AngularSpeed", 1.6, 0.1, 5.0, integer: false, "Orbit angular speed in radians per second."); Balance.Add("Orbit", "Height", 0.65, 0.3, 3.0, integer: false, "Orbit height above the caster origin in metres."); Balance.Add("Orbit", "PositionSpring", 18.0, 1.0, 50.0, integer: false, "Position error acceleration multiplier."); Balance.Add("Orbit", "VelocityDamping", 7.0, 0.1, 20.0, integer: false, "Velocity error damping multiplier."); Balance.Add("Orbit", "MaximumAcceleration", 32.0, 1.0, 100.0, integer: false, "Acceleration cap in metres per second squared."); Balance.Add("Spawning", "InitialActiveEnemies", 6.0, 1.0, 32.0, integer: true, "Minimum active NPC limit before quota growth."); Balance.Add("Spawning", "MaximumActiveEnemies", 16.0, 1.0, 64.0, integer: true, "Maximum quota-based NPC limit; a larger native limit is preserved."); Balance.Add("Spawning", "UnlockedTier", 10.0, 1.0, 10.0, integer: true, "Minimum unlocked NPC tier offered to the native director."); Balance.Add("Spawning", "PowerPerActiveEnemy", 4.0, 1.0, 20.0, integer: true, "Power budget per unit of active NPC capacity."); Balance.Add("Spawning", "BasePower", 10.0, 0.0, 100.0, integer: true, "Flat additional NPC spawning power budget."); Balance.Add("Spawning", "JesterLimit", 2.0, 0.0, 8.0, integer: true, "Per-type active cap for Jesters when native spawning allows them."); Balance.Add("Spawning", "InitialSpawnInterval", 20.0, 1.0, 180.0, integer: false, "Starting interval in seconds between spawn attempts."); Balance.Add("Spawning", "MinimumSpawnInterval", 8.0, 1.0, 180.0, integer: false, "Minimum spawn attempt interval after quota growth."); Balance.Add("Spawning", "IntervalReductionPerQuota", 2.0, 0.0, 30.0, integer: false, "Seconds removed per completed quota after the first one."); Balance.Add("Spawning", "DespawnSeconds", 300.0, 30.0, 1800.0, integer: false, "NPC despawn time passed to the native spawner."); Balance.Add("Spawning", "MinimumDistance", 10.0, 5.0, 100.0, integer: false, "Minimum native spawn distance in metres."); Balance.Add("Scoring", "QuotaMultiplierBase", 1.135, 1.0, 2.0, integer: false, "Score growth base: multiplier is base^(completed quotas + 1). Does not multiply shop gold or quest rewards."); Balance.Add("EnemyHealth", "Level1Multiplier", 1.0, 0.1, 10.0, integer: false, "NPC health multiplier on progression level 1."); Balance.Add("EnemyHealth", "Level2Multiplier", 1.15, 0.1, 10.0, integer: false, "NPC health multiplier on progression level 2."); Balance.Add("EnemyHealth", "Level3Multiplier", 1.3, 0.1, 10.0, integer: false, "NPC health multiplier on progression level 3."); Balance.Add("EnemyHealth", "Level4Multiplier", 1.5, 0.1, 10.0, integer: false, "NPC health multiplier on progression level 4."); Balance.Add("EnemyHealth", "Level5Multiplier", 1.75, 0.1, 10.0, integer: false, "NPC health multiplier on progression level 5."); Balance.Add("EnemyHealth", "AdditionalLevelGrowth", 0.175, 0.0, 2.0, integer: false, "Health multiplier added per level after level 5."); Balance.Add("EnemyHealth", "MaximumMultiplier", 3.0, 0.1, 20.0, integer: false, "Maximum multiplier after level 5; at least Level5Multiplier."); } } internal static class BalanceDescriptions { internal static string Custom(SpellDefinition d) { string key = "spell." + d.Id.ToString().ToLowerInvariant() + ".description"; switch (d.Id) { case SpellId.FrostBind: return Loc.Format(key, 10, Balance.I("FrostBind", "FireThawDamage")); case SpellId.IceRink: case SpellId.Iceberg: return Loc.Format(key, d.Radius, d.Duration); case SpellId.Multiply: return Loc.Format(key, d.Cooldown); case SpellId.ScoreLuck: case SpellId.LuckyEscape: return Loc.Format(key, d.Duration, d.Cooldown); case SpellId.ReverseGravity: return Loc.Format(key, d.Radius, d.Duration); case SpellId.Randomize: return Loc.Format(key, d.Duration, Balance.F("Randomize", "NpcPropDuration")); case SpellId.Portal: return Loc.Format(key, d.Duration); case SpellId.Anchor: return Loc.Format(key, d.Duration, d.Cooldown); default: return Loc.Text(key); } } internal static string Extra(ExtraDefinition d) { return d.Id switch { ExtraId.Vacuum => Loc.Format(d.Description, d.Range, d.Radius), ExtraId.FishRain => Loc.Format(d.Description, Balance.I("FishRain", "MaximumDrops"), Balance.F("FishRain", "FishLifetime")), ExtraId.Orbit => Loc.Format(d.Description, Balance.I("Orbit", "MaximumObjects")), _ => Loc.Text(d.Description), }; } } internal sealed class BalanceParameter { internal readonly string Section; internal readonly string Key; internal readonly string Description; internal readonly double Default; internal readonly double Minimum; internal readonly double Maximum; internal readonly bool Integer; internal double Local; internal double Value; internal string Id => Section + "/" + Key; internal BalanceParameter(string section, string key, double value, double min, double max, bool integer, string description) { Section = section; Key = key; Default = (Local = (Value = value)); Minimum = min; Maximum = max; Integer = integer; Description = description; } internal bool Valid(double value) { if (!double.IsNaN(value) && !double.IsInfinity(value) && value >= Minimum && value <= Maximum) { if (Integer) { return value == Math.Truncate(value); } return true; } return false; } internal double Sanitize(double value) { if (double.IsNaN(value) || double.IsInfinity(value)) { return Default; } value = Math.Max(Minimum, Math.Min(Maximum, value)); if (!Integer) { return value; } return Math.Round(value, MidpointRounding.AwayFromZero); } } internal static class Balance { private static readonly Dictionary<string, BalanceParameter> Entries; internal static readonly List<BalanceParameter> Ordered; internal static bool Remote { get; private set; } internal static string Schema { get; private set; } internal static event Action Changed; static Balance() { Entries = new Dictionary<string, BalanceParameter>(StringComparer.Ordinal); Ordered = new List<BalanceParameter>(); BalanceDefaults.Register(); Spec2Balance.Register(); using SHA256 sHA = SHA256.Create(); string text = string.Join("\n", Ordered.Select((BalanceParameter x) => x.Id + "|" + x.Integer + "|" + x.Minimum.ToString("R", CultureInfo.InvariantCulture) + "|" + x.Maximum.ToString("R", CultureInfo.InvariantCulture))); Schema = BitConverter.ToString(sHA.ComputeHash(Encoding.UTF8.GetBytes("Expanded-Balance-2/fullspec.30/item-prices-v1\n" + text))).Replace("-", ""); } internal static void Add(string section, string key, double value, double min, double max, bool integer, string description) { BalanceParameter balanceParameter = new BalanceParameter(section, key, value, min, max, integer, description); if (!balanceParameter.Valid(value) || Entries.ContainsKey(balanceParameter.Id)) { throw new InvalidOperationException("Invalid balance definition: " + balanceParameter.Id); } Entries.Add(balanceParameter.Id, balanceParameter); Ordered.Add(balanceParameter); } internal static int I(string section, string key) { return (int)Entries[section + "/" + key].Value; } internal static double D(string section, string key) { return Entries[section + "/" + key].Value; } internal static float F(string section, string key) { return (float)Entries[section + "/" + key].Value; } internal static float Optional(string section, string key) { if (!Entries.TryGetValue(section + "/" + key, out var value)) { return 0f; } return (float)value.Value; } internal static int OptionalInt(string section, string key) { return (int)Optional(section, key); } internal static double[] Snapshot() { return Ordered.Select((BalanceParameter x) => x.Value).ToArray(); } internal static void UseLocal() { double[] array = Ordered.Select((BalanceParameter x) => x.Sanitize(x.Local)).ToArray(); Normalize(array); for (int num = 0; num < array.Length; num++) { Ordered[num].Value = array[num]; } Remote = false; Balance.Changed?.Invoke(); } internal static bool ApplyRemote(string schema, double[] values) { if (schema != Schema || values == null || values.Length != Ordered.Count) { return false; } for (int i = 0; i < values.Length; i++) { if (!Ordered[i].Valid(values[i])) { return false; } } double[] obj = (double[])values.Clone(); Normalize(obj); if (!((ReadOnlySpan<double>)obj).SequenceEqual((ReadOnlySpan<double>)values)) { return false; } for (int j = 0; j < values.Length; j++) { Ordered[j].Value = values[j]; } Remote = true; Balance.Changed?.Invoke(); return true; } internal static void Normalize(double[] values) { AtLeast("Shop", "VanillaMaximumPrice", "VanillaMinimumPrice"); AtLeast("Spawning", "MaximumActiveEnemies", "InitialActiveEnemies"); AtLeast("Spawning", "InitialSpawnInterval", "MinimumSpawnInterval"); AtLeast("EnemyHealth", "MaximumMultiplier", "Level5Multiplier"); for (int i = 1; i < 5; i++) { int num = Index("WandLoot", "AcceptanceAtPrice" + i * 40); int num2 = Index("WandLoot", "AcceptanceAtPrice" + (i + 1) * 40); values[num2] = Math.Min(values[num2], values[num]); } AtLeast("Orbit", "ReleaseDistance", "Radius"); AtLeast("UpgradeShield", "Level2Capacity", "Level1Capacity"); AtLeast("UpgradeShield", "Level3Capacity", "Level2Capacity"); double num3 = 0.0; for (int j = 1; j <= 6; j++) { num3 += values[Index("Multiply", "Face" + j + "Weight")]; } if (num3 <= 0.0) { for (int k = 1; k <= 6; k++) { values[Index("Multiply", "Face" + k + "Weight")] = 1.0; } } void AtLeast(string section, string key, string other) { int num4 = Index(section, key); values[num4] = Math.Max(values[num4], values[Index(section, other)]); } static int Index(string section, string key) { return Ordered.IndexOf(Entries[section + "/" + key]); } } } internal struct BalanceHello : NetworkMessage { internal string Schema; } internal struct BalanceSnapshot : NetworkMessage { internal string Schema; internal string Digest; internal double[] Values; internal ItemPriceRecord[] Items; } internal struct BalanceAck : NetworkMessage { internal string Digest; } [DefaultExecutionOrder(-32000)] internal sealed class BalanceSync : MonoBehaviour { private sealed class Peer { internal float Started; internal float Sent; internal bool Acknowledged; } private static readonly Dictionary<NetworkConnectionToClient, Peer> Peers = new Dictionary<NetworkConnectionToClient, Peer>(); private static BalanceSnapshot host; private static bool received; private static bool installed; private static string receivedDigest; private static float clientStarted; private static float clientSent; private static NetworkConnectionToServer client; internal static bool GameplayReady { get { if (NetworkClient.active && !NetworkServer.active) { return received; } return true; } } internal static bool ClientReady(NetworkConnectionToClient connection) { if (connection != null) { if (Peers.TryGetValue(connection, out var value)) { return value.Acknowledged; } return false; } return true; } internal static void InstallCodecs() { Writer<BalanceHello>.write = delegate(NetworkWriter w, BalanceHello m) { NetworkWriterExtensions.WriteString(w, m.Schema); }; Reader<BalanceHello>.read = (NetworkReader r) => new BalanceHello { Schema = NetworkReaderExtensions.ReadString(r) }; Writer<BalanceAck>.write = delegate(NetworkWriter w, BalanceAck m) { NetworkWriterExtensions.WriteString(w, m.Digest); }; Reader<BalanceAck>.read = (NetworkReader r) => new BalanceAck { Digest = NetworkReaderExtensions.ReadString(r) }; Writer<BalanceSnapshot>.write = delegate(NetworkWriter w, BalanceSnapshot m) { NetworkWriterExtensions.WriteString(w, m.Schema); NetworkWriterExtensions.WriteString(w, m.Digest); NetworkWriterExtensions.WriteInt(w, m.Values.Length); double[] values = m.Values; foreach (double num in values) { NetworkWriterExtensions.WriteDouble(w, num); } ItemPriceRecord[] array = m.Items ?? Array.Empty<ItemPriceRecord>(); NetworkWriterExtensions.WriteInt(w, array.Length); ItemPriceRecord[] array2 = array; for (int i = 0; i < array2.Length; i++) { ItemPriceRecord itemPriceRecord = array2[i]; NetworkWriterExtensions.WriteUInt(w, itemPriceRecord.Asset); NetworkWriterExtensions.WriteInt(w, itemPriceRecord.Purchase); NetworkWriterExtensions.WriteInt(w, itemPriceRecord.Sell); } }; Reader<BalanceSnapshot>.read = delegate(NetworkReader r) { BalanceSnapshot result = new BalanceSnapshot { Schema = NetworkReaderExtensions.ReadString(r), Digest = NetworkReaderExtensions.ReadString(r) }; int num = NetworkReaderExtensions.ReadInt(r); if (num < 0 || num > 512) { throw new FormatException("Expanded balance snapshot exceeds the protocol limit."); } result.Values = new double[num]; for (int i = 0; i < num; i++) { result.Values[i] = NetworkReaderExtensions.ReadDouble(r); } num = NetworkReaderExtensions.ReadInt(r); if (num < 0 || num > 4096) { throw new FormatException("Expanded item price table exceeds the protocol limit."); } result.Items = new ItemPriceRecord[num]; for (int j = 0; j < num; j++) { result.Items[j] = new ItemPriceRecord { Asset = NetworkReaderExtensions.ReadUInt(r), Purchase = NetworkReaderExtensions.ReadInt(r), Sell = NetworkReaderExtensions.ReadInt(r) }; } if (!ItemPriceRules.Valid(result.Items)) { throw new FormatException("Invalid Expanded item price table."); } return result; }; } internal static void Install() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown //IL_0076: Expected O, but got Unknown InstallCodecs(); Harmony val = new Harmony("com.yapyapexpanded.mod.balance.v1"); Hook(val, typeof(NetworkServer), "RegisterMessageHandlers", "ServerHandlers"); Hook(val, typeof(NetworkClient), "RegisterMessageHandlers", "ClientHandlers"); Hook(val, typeof(NetworkServer), "Shutdown", "ServerStopped"); Hook(val, typeof(NetworkClient), "Shutdown", "ClientStopped"); Balance.Changed += RefreshRuntime; installed = true; } private static void Hook(Harmony h, Type type, string method, string postfix) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(type, method, (Type[])null, (Type[])null) ?? throw new MissingMethodException(type.FullName, method); h.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(BalanceSync), postfix, (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void ServerHandlers() { Peers.Clear(); ItemPriceSettings.UseLocal(); ShopEconomy.DiscoverPrices(); Balance.UseLocal(); double[] values = Balance.Snapshot(); ItemPriceRecord[] array = ItemPriceSettings.Snapshot(); host = new BalanceSnapshot { Schema = Balance.Schema, Values = values, Items = array, Digest = Digest(values, array) }; ItemPriceSettings.ApplyRemote(array); NetworkServer.RegisterHandler<BalanceHello>((Action<NetworkConnectionToClient, BalanceHello>)Hello, true); NetworkServer.RegisterHandler<BalanceAck>((Action<NetworkConnectionToClient, BalanceAck>)Acknowledge, true); } private static void ClientHandlers() { received = false; receivedDigest = null; client = null; clientStarted = (clientSent = 0f); NetworkClient.RegisterHandler<BalanceSnapshot>((Action<BalanceSnapshot>)Receive, true); } private static void ServerStopped() { Peers.Clear(); host = default(BalanceSnapshot); if (!NetworkClient.active) { ItemPriceSettings.UseLocal(); Balance.UseLocal(); } } private static void ClientStopped() { received = false; receivedDigest = null; client = null; clientStarted = (clientSent = 0f); if (!NetworkServer.active) { ItemPriceSettings.UseLocal(); Balance.UseLocal(); } } internal static string Digest(double[] values) { return Digest(values, Array.Empty<ItemPriceRecord>()); } internal static string Digest(double[] values, ItemPriceRecord[] items) { byte[] array = new byte[values.Length * 8]; for (int i = 0; i < values.Length; i++) { byte[] bytes = BitConverter.GetBytes(values[i]); if (!BitConverter.IsLittleEndian) { Array.Reverse(bytes); } Buffer.BlockCopy(bytes, 0, array, i * 8, 8); } using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream); binaryWriter.Write(values.Length); binaryWriter.Write(array); binaryWriter.Write(items.Length); for (int j = 0; j < items.Length; j++) { ItemPriceRecord itemPriceRecord = items[j]; binaryWriter.Write(itemPriceRecord.Asset); binaryWriter.Write(itemPriceRecord.Purchase); binaryWriter.Write(itemPriceRecord.Sell); } binaryWriter.Flush(); using SHA256 sHA = SHA256.Create(); return Convert.ToBase64String(sHA.ComputeHash(memoryStream.ToArray())); } private static Peer GetPeer(NetworkConnectionToClient connection) { if (!Peers.TryGetValue(connection, out var value)) { value = new Peer { Started = Time.realtimeSinceStartup, Sent = -10f }; Peers.Add(connection, value); } return value; } private static void Hello(NetworkConnectionToClient connection, BalanceHello message) { if (message.Schema != Balance.Schema) { Plugin.Log.LogError((object)"Expanded config protocol mismatch. All players must install the same build."); ((NetworkConnection)connection).Disconnect(); return; } Peer peer = GetPeer(connection); if (!peer.Acknowledged && !(Time.realtimeSinceStartup - peer.Sent < 1f)) { ((NetworkConnection)connection).Send<BalanceSnapshot>(host, 0); peer.Sent = Time.realtimeSinceStartup; } } private static void Acknowledge(NetworkConnectionToClient connection, BalanceAck message) { if (message.Digest != host.Digest) { ((NetworkConnection)connection).Disconnect(); } else { GetPeer(connection).Acknowledged = true; } } private static void Receive(BalanceSnapshot message) { if (NetworkServer.active) { if (message.Digest == host.Digest) { NetworkClient.Send<BalanceAck>(new BalanceAck { Digest = host.Digest }, 0); } return; } if (message.Values == null || !ItemPriceRules.Valid(message.Items) || message.Schema != Balance.Schema || message.Digest != Digest(message.Values, message.Items) || (received && receivedDigest != message.Digest)) { Reject(); return; } if (!received) { try { if (!ValidValues(message.Values)) { Reject(); return; } ItemPriceSettings.ApplyRemote(message.Items); if (!Balance.ApplyRemote(message.Schema, message.Values)) { Reject(); return; } } catch (Exception ex) { Plugin.Log.LogError((object)("Could not apply host balance: " + ex)); Reject(); return; } received = true; receivedDigest = message.Digest; client = NetworkClient.connection; Plugin.Log.LogInfo((object)("Host balance synchronized (" + message.Values.Length + " parameters, " + message.Items.Length + " item price overrides). Local config values unchanged.")); } NetworkClient.Send<BalanceAck>(new BalanceAck { Digest = message.Digest }, 0); } private static void Reject() { Plugin.Log.LogError((object)"Invalid or incompatible host balance. Install the same Expanded build on every player."); NetworkClient.Disconnect(); } private static bool ValidValues(double[] values) { if (values.Length != Balance.Ordered.Count) { return false; } for (int i = 0; i < values.Length; i++) { if (!Balance.Ordered[i].Valid(values[i])) { return false; } } double[] obj = (double[])values.Clone(); Balance.Normalize(obj); return ((ReadOnlySpan<double>)obj).SequenceEqual((ReadOnlySpan<double>)values); } private void Update() { if (!installed) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (NetworkServer.active && host.Values != null) { NetworkConnectionToClient[] array = Peers.Keys.ToArray(); foreach (NetworkConnectionToClient val in array) { if (!NetworkServer.connections.Values.Contains(val)) { Peers.Remove(val); } } array = NetworkServer.connections.Values.ToArray(); foreach (NetworkConnectionToClient val2 in array) { if (val2 == null || !((NetworkConnection)val2).isAuthenticated) { continue; } Peer peer = GetPeer(val2); if (!peer.Acknowledged) { if (realtimeSinceStartup - peer.Started > 30f) { Plugin.Log.LogError((object)"Client did not confirm host balance within 30 seconds; same Expanded build is required."); ((NetworkConnection)val2).Disconnect(); Peers.Remove(val2); } else if (realtimeSinceStartup - peer.Sent >= 2f) { ((NetworkConnection)val2).Send<BalanceSnapshot>(host, 0); peer.Sent = realtimeSinceStartup; } } } } else { if (!NetworkClient.isConnected || NetworkClient.connection == null || !((NetworkConnection)NetworkClient.connection).isAuthenticated) { return; } if (client != NetworkClient.connection) { client = NetworkClient.connection; clientStarted = realtimeSinceStartup; clientSent = -10f; received = false; receivedDigest = null; } if (!received) { if (realtimeSinceStartup - clientStarted > 30f) { Reject(); } else if (realtimeSinceStartup - clientSent >= 2f) { NetworkClient.Send<BalanceHello>(new BalanceHello { Schema = Balance.Schema }, 0); clientSent = realtimeSinceStartup; } } } } private static void RefreshRuntime() { Spell[] array = Resources.FindObjectsOfTypeAll<Spell>(); foreach (Spell val in array) { if ((Object)(object)val == (Object)null || (Object)(object)val.SpellData == (Object)null) { continue; } SpellData spellData = val.SpellData; if (val is ExtraSpell { Definition: var definition }) { spellData.charges = definition.Charges; spellData.cooldown = definition.Cooldown; spellData.chargeRegenInterval = definition.Regen; } else { SpellDefinition spellDefinition = SpellCatalog.Find(((Object)spellData).name); if (spellDefinition == null) { continue; } spellData.charges = spellDefinition.Charges; spellData.cooldown = spellDefinition.Cooldown; spellData.chargeRegenInterval = spellDefinition.Regen; } if (NetworkServer.active && val.CurrentCharges > spellData.charges) { val.SetCurrentCharges(spellData.charges); } } NetworkPuppetProp[] array2 = Resources.FindObjectsOfTypeAll<NetworkPuppetProp>(); foreach (NetworkPuppetProp val2 in array2) { if ((Object)(object)val2 != (Object)null) { ShopEconomy.ApplyPrice(val2); } } } } internal struct UpgradeRequest : NetworkMessage { internal uint Merchant; internal uint Token; internal int Kind; internal int Expected; } internal struct UpgradeState : NetworkMessage { internal uint Pawn; internal uint Token; internal int Hp; internal int Speed; internal int Inventory; internal bool Open; internal string Result; } internal sealed class CharacterUpgrades : MonoBehaviour { private static readonly Dictionary<uint, UpgradeLevels> Clients = new Dictionary<uint, UpgradeLevels>(); private static readonly Dictionary<int, double> LastRequest = new Dictionary<int, double>(); internal static bool Ready; private bool serverRegistered; private bool clientRegistered; private double nextSync; internal static void Install(Harmony harmony) { Ready = false; try { UpgradeInventory.Install(harmony); InstallCodecs(); Ready = true; } catch (Exception ex) { harmony.UnpatchSelf(); Plugin.Log.LogError((object)("Upgrade system unavailable: " + ex)); } } internal static void InstallCodecs() { Writer<UpgradeRequest>.write = delegate(NetworkWriter w, UpgradeRequest m) { NetworkWriterExtensions.WriteUInt(w, m.Merchant); NetworkWriterExtensions.WriteUInt(w, m.Token); NetworkWriterExtensions.WriteInt(w, m.Kind); NetworkWriterExtensions.WriteInt(w, m.Expected); }; Reader<UpgradeRequest>.read = (NetworkReader r) => new UpgradeRequest { Merchant = NetworkReaderExtensions.ReadUInt(r), Token = NetworkReaderExtensions.ReadUInt(r), Kind = NetworkReaderExtensions.ReadInt(r), Expected = NetworkReaderExtensions.ReadInt(r) }; Writer<UpgradeState>.write = delegate(NetworkWriter w, UpgradeState m) { NetworkWriterExtensions.WriteUInt(w, m.Pawn); NetworkWriterExtensions.WriteUInt(w, m.Token); NetworkWriterExtensions.WriteInt(w, m.Hp); NetworkWriterExtensions.WriteInt(w, m.Speed); NetworkWriterExtensions.WriteInt(w, m.Inventory); NetworkWriterExtensions.WriteBool(w, m.Open); NetworkWriterExtensions.WriteString(w, m.Result); }; Reader<UpgradeState>.read = (NetworkReader r) => new UpgradeState { Pawn = NetworkReaderExtensions.ReadUInt(r), Token = NetworkReaderExtensions.ReadUInt(r), Hp = NetworkReaderExtensions.ReadInt(r), Speed = NetworkReaderExtensions.ReadInt(r), Inventory = NetworkReaderExtensions.ReadInt(r), Open = NetworkReaderExtensions.ReadBool(r), Result = NetworkReaderExtensions.ReadString(r) }; } internal static string Key(Pawn pawn, int kind) { return "PLAYER." + pawn.PlayerId + ".YAPYAPEXP.UPGRADE." + kind; } internal static UpgradeLevels Levels(Pawn pawn) { if ((Object)(object)pawn == (Object)null) { return default(UpgradeLevels); } if (NetworkServer.active) { UpgradeLevels result = default(UpgradeLevels); SaveManager val = default(SaveManager); if (!string.IsNullOrEmpty(pawn.PlayerId) && Service.Get<SaveManager>(ref val)) { val.TryGetInt(Key(pawn, 0), ref result.Hp); val.TryGetInt(Key(pawn, 1), ref result.Speed); val.TryGetInt(Key(pawn, 2), ref result.Inventory); result.Hp = UpgradeRules.Clamp(0, result.Hp); result.Speed = UpgradeRules.Clamp(1, result.Speed); result.Inventory = UpgradeRules.Clamp(2, result.Inventory); } return result; } if (!Clients.TryGetValue(((NetworkBehaviour)pawn).netId, out var value)) { return default(UpgradeLevels); } return value; } internal static bool MerchantValid(Pawn pawn, uint merchantId, out ShopkeeperStateMachine merchant) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_013d: 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_0153: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0187: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Unknown result type (might be due to invalid IL or missing references) merchant = null; Dictionary<uint, NetworkIdentity> dictionary = (NetworkServer.active ? NetworkServer.spawned : NetworkClient.spawned); if ((Object)(object)pawn == (Object)null || pawn.IsDead || pawn.IsExtracted || !dictionary.TryGetValue(merchantId, out var value)) { return false; } merchant = ((Component)value).GetComponentInChildren<ShopkeeperStateMachine>(true); if ((Object)(object)merchant == (Object)null || !((Behaviour)merchant).isActiveAndEnabled || (Object)(object)merchant._npcBehaviour == (Object)null || merchant._npcBehaviour.IsKilled) { return false; } object? value2 = AccessTools.Field(typeof(ShopkeeperStateMachine), "_shop").GetValue(merchant); Shop val = (Shop)((value2 is Shop) ? value2 : null); if (!((Object)(object)val == (Object)null) && ((Component)val).gameObject.activeInHierarchy) { Vector3 val2 = ((Component)pawn).transform.position - ((Component)merchant).transform.position; if (!(((Vector3)(ref val2)).sqrMagnitude > 16f)) { object? value3 = AccessTools.Field(typeof(ShopkeeperStateMachine), "_collider").GetValue(merchant); Collider val3 = (Collider)((value3 is Collider) ? value3 : null); ? val4; if (!((Object)(object)val3 != (Object)null)) { val4 = ((Component)merchant).transform.position + Vector3.up; } else { Bounds bounds = val3.bounds; float x = ((Bounds)(ref bounds)).center.x; bounds = val3.bounds; float num = ((Bounds)(ref bounds)).max.y - 0.15f; bounds = val3.bounds; val4 = new Vector3(x, num, ((Bounds)(ref bounds)).center.z); } Vector3 val5 = (Vector3)val4; Vector3 lookOrigin = pawn.LookOrigin; val2 = val5 - pawn.LookOrigin; Vector3 normalized = ((Vector3)(ref val2)).normalized; val2 = val5 - pawn.LookOrigin; foreach (RaycastHit item in from val6 in Physics.RaycastAll(lookOrigin, normalized, ((Vector3)(ref val2)).magnitude, -1, (QueryTriggerInteraction)1) orderby ((RaycastHit)(ref val6)).distance select val6) { RaycastHit current = item; NetworkIdentity componentInParent = ((Component)((RaycastHit)(ref current)).collider).GetComponentInParent<NetworkIdentity>(); if (!((Object)(object)componentInParent == (Object)(object)((NetworkBehaviour)pawn).netIdentity)) { if ((Object)(object)componentInParent == (Object)(object)value) { return true; } if ((Object)(object)((RaycastHit)(ref current)).collider.attachedRigidbody == (Object)null) { return false; } } } return true; } } return false; } private void Update() { if (!Ready) { return; } if (NetworkServer.active && !serverRegistered) { NetworkServer.RegisterHandler<UpgradeRequest>((Action<NetworkConnectionToClient, UpgradeRequest>)Purchase, true); serverRegistered = true; } if (!NetworkServer.active && serverRegistered) { serverRegistered = false; LastRequest.Clear(); nextSync = 0.0; } if (NetworkClient.active && !clientRegistered) { NetworkClient.RegisterHandler<UpgradeState>((Action<UpgradeState>)Receive, true); clientRegistered = true; } if (!NetworkClient.active && clientRegistered) { clientRegistered = false; Clients.Clear(); } if (!NetworkServer.active || !(NetworkTime.time >= nextSync)) { return; } nextSync = NetworkTime.time + 1.0; foreach (NetworkConnectionToClient value in NetworkServer.connections.Values) { Pawn val = (((Object)(object)((value != null) ? ((NetworkConnection)value).identity : null) != (Object)null) ? ((Component)((NetworkConnection)value).identity).GetComponent<Pawn>() : null); if (!((Object)(object)val == (Object)null) && !string.IsNullOrEmpty(val.PlayerId)) { EnsureSlots(val); NetworkServer.SendToAll<UpgradeState>(State(val, 0u, open: false, ""), 0, false); } } } internal static void EnsureSlots(Pawn pawn) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) if (!NetworkServer.active || (Object)(object)((pawn != null) ? pawn.Inventory : null) == (Object)null) { return; } int num = UpgradeRules.Capacity(Levels(pawn).Inventory); SaveManager val = default(SaveManager); if (!string.IsNullOrEmpty(pawn.PlayerId) && Service.Get<SaveManager>(ref val)) { string text = Key(pawn, 2) + ".CAPACITY"; int num2 = default(int); val.TryGetInt(text, ref num2); num = Math.Max(num, Math.Max(3, Math.Min(7, num2))); if (num != num2) { val.SetInt(text, num); } } num += PlayerProgression.ExtraSlots(pawn); while (pawn.Inventory.Items.Count < num) { pawn.Inventory.Items.Add(InventoryItem.Empty); } } private static UpgradeState State(Pawn pawn, uint token, bool open, string result) { UpgradeLevels upgradeLevels = Levels(pawn); return new UpgradeState { Pawn = ((NetworkBehaviour)pawn).netId, Token = token, Hp = upgradeLevels.Hp, Speed = upgradeLevels.Speed, Inventory = upgradeLevels.Inventory, Open = open, Result = result }; } private static void Purchase(NetworkConnectionToClient connection, UpgradeRequest request) { //IL_05cd: Unknown result type (might be due to invalid IL or missing references) //IL_05d2: Unknown result type (might be due to invalid IL or missing references) if (!Ready) { return; } NetworkConnectionToClient obj = connection; if ((Object)(object)((obj != null) ? ((NetworkConnection)obj).identity : null) == (Object)null || !BalanceSync.ClientReady(connection)) { return; } Pawn pawn = ((Component)((NetworkConnection)connection).identity).GetComponent<Pawn>(); if ((Object)(object)pawn == (Object)null) { return; } if (LastRequest.TryGetValue(connection.connectionId, out var value) && NetworkTime.time - value < 0.15) { Reply("error.wait"); return; } LastRequest[connection.connectionId] = NetworkTime.time; if (!MerchantValid(pawn, request.Merchant, out var _)) { Reply("error.merchant"); return; } SaveManager val = default(SaveManager); if (string.IsNullOrEmpty(pawn.PlayerId) || !Service.Get<SaveManager>(ref val) || !(bool)AccessTools.Method(typeof(SaveManager), "IsSlotValid", (Type[])null, (Type[])null).Invoke(val, null)) { Reply("error.save"); return; } if (request.Kind == -1) { Reply("", open: true); return; } if (!UpgradeRules.ValidKind(request.Kind) || (Object)(object)GameManager.Instance == (Object)null) { Reply("error.kind"); return; } int num = Levels(pawn).Get(request.Kind); int gold = GameManager.Instance.Gold; int num2 = UpgradeRules.Price(request.Kind, num); if (!UpgradeRules.CanBuy(request.Kind, num, request.Expected, gold)) { Reply((num2 < 0) ? "error.max" : ((num != request.Expected) ? "error.stale" : "error.gold")); return; } if ((request.Kind == 0 && (Object)(object)pawn.Hurtbox == (Object)null) || (request.Kind == 2 && (Object)(object)pawn.Inventory == (Object)null)) { Reply("error.pawn"); return; } int network_maxHealth = (((Object)(object)pawn.Hurtbox != (Object)null) ? pawn.Hurtbox.MaxHealth : 0); int num3 = (((Object)(object)pawn.Hurtbox != (Object)null) ? pawn.Hurtbox.Health : 0); PawnInventory inventory = pawn.Inventory; int num4 = ((inventory != null) ? inventory.Items.Count : 0); int num5 = default(int); val.TryGetInt(Key(pawn, 2) + ".CAPACITY", ref num5); if (PurchaseVotingSystem.Defer(connection, num2, (new string[3] { "upgrade.health", "upgrade.speed", "upgrade.inventory" })[request.Kind], "upgrade:" + ((NetworkBehaviour)pawn).netId + ":" + request.Kind, delegate { Purchase(connection, request); })) { return; } try { GameManager.Instance.ModifyGold(-num2); val.SetInt(Key(pawn, request.Kind), num + 1); if (request.Kind == 0) { pawn.Hurtbox.SvModifyMaxHp(UpgradeRules.HpPerLevel); pawn.Hurtbox.SvPersistMaxHp(); } if (request.Kind == 2) { EnsureSlots(pawn); } val.SetInt("GAME.GOLD", GameManager.Instance.Gold); val.WriteCurrent(); NetworkServer.SendToAll<UpgradeState>(State(pawn, 0u, open: false, ""), 0, false); Reply("upgrade.success"); Plugin.Log.LogInfo((object)("UPGRADE " + pawn.PlayerId + " kind=" + request.Kind + " level=" + (num + 1) + " price=" + num2)); } catch (Exception ex) { val.SetInt(Key(pawn, request.Kind), num); val.SetInt(Key(pawn, 2) + ".CAPACITY", num5); GameManager.Instance.ModifyGold(num2); val.SetInt("GAME.GOLD", GameManager.Instance.Gold); if (request.Kind == 0 && (Object)(object)pawn.Hurtbox != (Object)null) { pawn.Hurtbox.Network_maxHealth = network_maxHealth; pawn.Hurtbox.SvSetHealth(num3); pawn.Hurtbox.SvPersistMaxHp(); } if (request.Kind == 2 && (Object)(object)pawn.Inventory != (Object)null) { while (pawn.Inventory.Items.Count > num4) { InventoryItem val2 = pawn.Inventory.Items[pawn.Inventory.Items.Count - 1]; if (!((InventoryItem)(ref val2)).IsEmpty) { break; } pawn.Inventory.Items.RemoveAt(pawn.Inventory.Items.Count - 1); } } Reply("error.purchase"); Plugin.Log.LogError((object)("Upgrade purchase rolled back: " + ex)); } void Reply(string result, bool open = false) { ((NetworkConnection)connection).Send<UpgradeState>(State(pawn, request.Token, open, result), 0); } } private static void Receive(UpgradeState state) { Clients[state.Pawn] = new UpgradeLevels { Hp = UpgradeRules.Clamp(0, state.Hp), Speed = UpgradeRules.Clamp(1, state.Speed), Inventory = UpgradeRules.Clamp(2, state.Inventory) }; if ((Object)(object)Pawn.LocalInstance != (Object)null && state.Pawn == ((NetworkBehaviour)Pawn.LocalInstance).netId && !string.IsNullOrEmpty(state.Result)) { ReleaseNetwork.Status = state.Result; } } } internal static class ConfigIdentityMigration { internal const string PluginId = "com.yapyapexpanded.mod"; internal static void Install(ConfigFile config) { string target = config.ConfigFilePath; if (File.Exists(target)) { return; } string directoryName = Path.GetDirectoryName(target); if (!Directory.Exists(directoryName)) { return; } string[] array = (from p in Directory.GetFiles(directoryName, "*.yapyapexpanded.cfg") where !string.Equals(Path.GetFullPath(p), Path.GetFullPath(target), StringComparison.OrdinalIgnoreCase) select p).ToArray(); if (array.Length == 0) { return; } if (array.Length != 1) { throw new IOException("Multiple legacy Expanded config files; migration cannot choose safely."); } string path = array[0]; string value = Regex.Replace(File.ReadAllText(path), "(?m)^## Plugin GUID:[^\\r\\n]*", "## Plugin GUID: com.yapyapexpanded.mod"); string text = target + ".migrating"; if (File.Exists(text)) { throw new IOException("An incomplete config migration already exists."); } using (FileStream stream = new FileStream(text, FileMode.CreateNew, FileAccess.Write)) { using StreamWriter streamWriter = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); streamWriter.Write(value); } File.Move(text, target); config.Reload(); File.Delete(path); } } internal sealed class ContextHints : MonoBehaviour { private string hint = ""; private float next; private GUIStyle style; private bool padHint; private MerchantButtonPrompt modifier; private MerchantButtonPrompt button; private void Update() { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Invalid comparison between Unknown and I4 if (!NetworkClient.active || !NetworkClient.ready || (Object)(object)Pawn.LocalInstance == (Object)null || !ModMenuInput.CanOpen) { hint = ""; } else { if (Time.unscaledTime < next) { return; } next = Time.unscaledTime + 0.2f; if ((Object)(object)ExpandedUI.FindMerchant() == (Object)null) { hint = ""; return; } padHint = (int)InputManager.Instance.InputMode == 1 && GameplayControllerInput.MenuPad != null; hint = Loc.Format("shop.hint", padHint ? "" : "F4"); if (padHint) { modifier = ModMenuInput.ModifierPrompt; button = ModMenuInput.ButtonPrompt; } } } private float Width(MerchantButtonPrompt prompt, float size) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown //IL_001f: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)prompt.Icon != (Object)null)) { return style.CalcSize(new GUIContent(prompt.Text)).x + 8f; } return size; } private void DrawButton(Rect rect, MerchantButtonPrompt prompt) { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)prompt.Icon != (Object)null) { Sprite icon = prompt.Icon; Texture2D texture = icon.texture; try { Rect textureRect = icon.textureRect; GUI.DrawTextureWithTexCoords(rect, (Texture)(object)texture, new Rect(((Rect)(ref textureRect)).x / (float)((Texture)texture).width, ((Rect)(ref textureRect)).y / (float)((Texture)texture).height, ((Rect)(ref textureRect)).width / (float)((Texture)texture).width, ((Rect)(ref textureRect)).height / (float)((Texture)texture).height)); return; } catch (UnityException) { } } GUI.Label(rect, prompt.Text, style); } private void OnGUI() { //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Expected O, but got Unknown //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) if (hint.Length != 0 && ModMenuInput.CanOpen) { if (style == null) { style = new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)4, wordWrap = false }; } float num = Mathf.Clamp((float)Screen.height / 1080f, 0.8f, 1.6f); float num2 = Mathf.Min(520f * num, (float)(Screen.width - 24)); float num3 = 44f * num; style.fontSize = Mathf.RoundToInt(16f * num); Rect val = default(Rect); ((Rect)(ref val))..ctor(((float)Screen.width - num2) / 2f, (float)Screen.height * 0.72f, num2, num3); GUI.Box(val, ""); if (!padHint) { GUI.Label(val, hint, style); return; } float num4 = 30f * num; float num5 = Width(modifier, num4); float num6 = Width(button, num4); float num7 = 24f * num; float num8 = 12f * num; float x = style.CalcSize(new GUIContent(hint)).x; float num9 = ((Rect)(ref val)).x + (num2 - num5 - num6 - num7 - num8 - x) * 0.5f; float num10 = ((Rect)(ref val)).y + (num3 - num4) * 0.5f; DrawButton(new Rect(num9, num10, num5, num4), modifier); num9 += num5; GUI.Label(new Rect(num9, num10, num7, num4), "+", style); num9 += num7; DrawButton(new Rect(num9, num10, num6, num4), button); num9 += num6 + num8; GUI.Label(new Rect(num9, num10, x, num4), hint, style); } } private void OnDisable() { hint = ""; } } internal static class DuplicateControllerPolicy { internal static bool Block(bool localOwner, bool singlePlayer, bool active, bool exactPair, bool preferredAvailable, bool preferredConnected, bool incomingDualSense) { return localOwner && singlePlayer && active && exactPair && preferredAvailable && preferredConnected && incomingDualSense; } } internal static class ControllerDeviceFilter { private static Harmony owner; private static bool enabled; private static bool warned; private static int reports; internal static bool Enabled { get { if (enabled) { return owner != null; } return false; } } internal static MethodInfo Target() { return typeof(PlayerInput).GetMethod("OnUnpairedDeviceUsed", BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[2] { typeof(InputControl), typeof(InputEventPtr) }, null); } internal static void Install(ConfigFile config) { //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Expected O, but got Unknown enabled = config.Bind<bool>("Controller", "PreventDuplicateDualSenseXInputHandover", true, "For exactly one DualSenseGamepadHID plus one XInputControllerWindows, prefer native XInput events from first gamepad selection and keep that source stable. Refresh native UI once when that paired device/context changes; no custom icons or bindings. Keyboard/mouse, other controller configurations and XInput disconnect use native behavior. Restart after changing.").Value; if (!enabled) { return; } MethodInfo methodInfo = Target(); if (methodInfo == null || methodInfo.IsStatic || methodInfo.ReturnType != typeof(void) || methodInfo.ContainsGenericParameters || methodInfo.GetMethodBody() == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"Duplicate controller filter unavailable: native signature differs."); } return; } Harmony val = new Harmony("com.yapyapexpanded.mod.duplicatecontroller"); try { val.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(ControllerDeviceFilter), "Allow", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); owner = val; ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)"Duplicate DualSense/XInput handover filter installed for local native PlayerInput; native icons and actions retained."); } } catch (Exception ex) { val.UnpatchSelf(); ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)("Duplicate controller filter not installed: " + ex.Message)); } } } internal static Gamepad PreferredPad() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (Gamepad.all.Count != 2) { return null; } int num = 0; int num2 = 0; Gamepad val = null; Enumerator<Gamepad> enumerator = Gamepad.all.GetEnumerator(); try { while (enumerator.MoveNext()) { Gamepad current = enumerator.Current; if (((InputControl)current).layout == "DualSenseGamepadHID") { num++; } else if (((InputControl)current).layout == "XInputControllerWindows") { num2++; val = current; } } } finally { ((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose(); } if (num != 1 || num2 != 1 || !((InputDevice)val).added) { return null; } return val; } internal static bool Allow(PlayerInput __instance, InputControl __0) { //IL_0058: 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) if (!enabled) { return true; } try { InputDevice obj = ((__0 != null) ? __0.device : null); Gamepad val = (Gamepad)(object)((obj is Gamepad) ? obj : null); if (val == null || ((InputControl)val).layout != "DualSenseGamepadHID") { return true; } PlayerInput val2 = GameplayControllerInput.NativePlayer(InputManager.Instance); bool flag = (Object)(object)val2 != (Object)null && (Object)(object)__instance == (Object)(object)val2; bool flag2 = PlayerInput.all.Count == 1; bool flag3 = flag && ((Behaviour)val2).isActiveAndEnabled && val2.inputIsActive; if (!flag || !flag2 || !flag3) { return true; } Gamepad val3 = PreferredPad(); if (DuplicateControllerPolicy.Block(flag, flag2, flag3, val3 != null, val3 != null, val3 != null && ((InputDevice)val3).added, incomingDualSense: true)) { if (reports < 8) { reports++; ManualLogSource log = Plugin.Log; if (log != null) { log.LogInfo((object)("[DuplicateControllerFilter] preferred=" + ((InputDevice)val3).deviceId + ":" + ((InputControl)val3).layout + " ignoredHandover=" + ((InputDevice)val).deviceId + ":" + ((InputControl)val).layout)); } } return false; } } catch (Exception ex) { if (!warned) { warned = true; ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("Duplicate controller filter yielded to native input: " + ex.Message)); } } } return true; } internal static void Clear() { Harmony obj = owner; if (obj != null) { obj.UnpatchSelf(); } owner = null; enabled = false; reports = 0; warned = false; } } internal static class ControllerSwitchRules { internal static bool LegacyMenuBinding(string modifier, string button) { if (modifier == "<Gamepad>/leftStickPress" || modifier == "<Gamepad>/select") { return button == "<Gamepad>/rightStickPress"; } return false; } } internal sealed class MerchantChord { private int device; private bool armed; private bool latched; internal bool BlockSubmit => latched; internal bool Poll(int current, bool modifier, bool button) { if (current != device) { Reset(); device = current; } if (current == 0) { return false; } if (!modifier && !button) { armed = true; latched = false; return false; } if (!armed || !modifier || !button) { return false; } armed = false; latched = true; return true; } internal void Reset() { device = 0; armed = (latched = false); } } internal static class CopyPlacement { internal static bool TryPlan<T>(int count, IEnumerable<T> candidates, Func<T, bool> clear, Func<T, T, bool> separated, out List<T> places) { places = new List<T>(); switch (count) { default: return false; case 0: return true; case 1: case 2: case 3: case 4: case 5: foreach (T candidate in candidates) { if (!clear(candidate)) { continue; } bool flag = true; foreach (T place in places) { if (!separated(candidate, place)) { flag = false; break; } } if (flag) { places.Add(candidate); if (places.Count == count) { return true; } } } places.Clear(); return false; } } } internal static class CopyRewards { private static readonly FieldInfo PropPoints = AccessTools.Field(typeof(PropDestroyTriggerEvent), "addScore"); private static readonly FieldInfo NpcPoints = AccessTools.Field(typeof(NpcBehaviour), "npcKillScore"); internal static int Award(int original, bool generated, bool monster) { if (!generated || monster || original <= 0) { return original; } return original / 4; } private static string PathOf(Transform node, Transform root) { List<string> list = new List<string>(); while ((Object)(object)node != (Object)(object)root && (Object)(object)node != (Object)null) { list.Add(((Object)node).name); node = node.parent; } list.Reverse(); return string.Join("/", list); } private static void Match<T>(GameObject copy, GameObject source, Action<T, T> apply) where T : Component { Dictionary<string, T[]> dictionary = (from c in source.GetComponentsInChildren<T>(true) group c by PathOf(((Component)c).transform, source.transform)).ToDictionary((IGrouping<string, T> g) => g.Key, (IGrouping<string, T> g) => g.ToArray()); foreach (IGrouping<string, T> item in from c in copy.GetComponentsInChildren<T>(true) group c by PathOf(((Component)c).transform, copy.transform)) { if (!dictionary.TryGetValue(item.Key, out var value)) { continue; } int num = 0; foreach (T item2 in item) { if (num >= value.Length) { break; } apply(item2, value[num++]); } } } internal static void Inherit(GameObject copy, GameObject source, uint creator) { Match<ValuableObject>(copy, source, (Action<ValuableObject, ValuableObject>)delegate(ValuableObject c, ValuableObject s) { c.SetValue(s.Value); c.SetAndPropagateRecentInteractor(creator); }); Match<PropDestroyTriggerEvent>(copy, source, (Action<PropDestroyTriggerEvent, PropDestroyTriggerEvent>)delegate(PropDestroyTriggerEvent c, PropDestroyTriggerEvent s) { PropPoints.SetValue(c, PropPoints.GetValue(s)); }); Match<NpcBehaviour>(copy, source, (Action<NpcBehaviour, NpcBehaviour>)delegate(NpcBehaviour c, NpcBehaviour s) { NpcPoints.SetValue(c, NpcPoints.GetValue(s)); }); } } internal sealed class MultiplyUseMarker : MonoBehaviour { } internal static class CustomCastDiagnostics { internal static void Install(Harmony harmony) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Expected O, but got Unknown harmony.Patch((MethodBase)AccessTools.Method(typeof(VoiceSpell), "CastChosenSpell", new Type[2] { typeof(int), typeof(bool) }, (Type[])null), new HarmonyMethod(typeof(CustomCastDiagnostics), "Chosen", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void Chosen(VoiceSpell __instance, int __0, bool __1) { if (NetworkServer.active && AccessTools.Field(typeof(VoiceSpell), "allSpells").GetValue(__instance) is List<Spell> list && __0 >= 0 && __0 < list.Count) { Spell val = list[__0]; if (val is ExpandedStaffSpell || val is ExtraSpell) { ManualLogSource log = Plugin.Log; string[] obj = new string[10] { "[CustomCast] selected=", val.VoiceCommandKey, " voice=", __1.ToString(), " canCast=", val.CanCast().ToString(), " charges=", val.NetworkcurrentCharges.ToString(), " caster=", null }; NetworkIdentity casterIdentity = val.CasterIdentity; obj[9] = ((casterIdentity != null) ? casterIdentity.netId : 0u).ToString(); log.LogInfo((object)string.Concat(obj)); } } } } internal static class CustomSpellPatcher { internal static void Install(Harmony harmony) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown CustomSpellSystem.Ready = false; try { SpellVanilla.Install(harmony); harmony.Patch((MethodBase)AccessTools.PropertyGetter(typeof(Spell), "CooldownProgress"), new HarmonyMethod(typeof(CustomSpellPatcher), "Cooldown", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); CustomSpellSystem.Ready = true; Plugin.Log.LogInfo((object)"Standalone staff base lifecycle ready; no donor spell interception."); } catch (Exception ex) { Plugin.Log.LogError((object)("Staff base lifecycle unavailable: " + ex)); } } internal static IEnumerable<MethodInfo> GetPatchCandidates(IEnumerable<Type> types) { HashSet<Type> visited = new HashSet<Type>(); foreach (Type type2 in types) { if (type2 == null || type2.ContainsGenericParameters || !typeof(Spell).IsAssignableFrom(type2)) { continue; } Type type = type2; while (type != null && typeof(Spell).IsAssignableFrom(type)) { if (!type.ContainsGenericParameters && visited.Add(type)) { MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!methodInfo.IsAbstract && !methodInfo.ContainsGenericParameters) { yield return methodInfo; } } } type = type.BaseType; } } } private static bool Cooldown(Spell __instance, ref float __result) { if ((!(__instance is ExpandedStaffSpell) && (!(__instance is VoiceSpell) || !((Object)(object)__instance.SpellData != (Object)null) || !((Object)__instance.SpellData).name.StartsWith("YAPYAPEXP_CONTROLLER_", StringComparison.Ordinal))) || (Object)(object)__instance.SpellData == (Object)null || __instance.SpellData.cooldown > 0f) { return true; } __result = 1f; return false; } } internal static class CustomSpellSystem { private static readonly BindingFlags Fields = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo SpellDataField = typeof(Spell).GetField("spellData", Fields); private static readonly FieldInfo VoiceSpellsField = typeof(VoiceSpell).GetField("spells", Fields); internal static bool Ready; internal static bool CanConfigure(GameObject root) { if ((Object)(object)root != (Object)null) { NetworkPuppetWandProp componentInChildren = root.GetComponentInChildren<NetworkPuppetWandProp>(true); return ((componentInChildren != null) ? componentInChildren.Spell : null) is VoiceSpell; } return false; } internal static bool ConfigureWand(GameObject root, WandKind kind) { //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Expected O, but got Unknown //IL_01f1: Unknown result type (might be due to invalid IL or missing references) //IL_01f6: Unknown result type (might be due to invalid IL or missing references) //IL_01fd: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Unknown result type (might be due to invalid IL or missing references) //IL_021d: Unknown result type (might be due to invalid IL or missing references) //IL_0222: Unknown result type (might be due to invalid IL or missing references) if (!Ready || SpellDataField == null || VoiceSpellsField == null || (Object)(object)root == (Object)null) { return false; } NetworkPuppetWandProp componentInChildren = root.GetComponentInChildren<NetworkPuppetWandProp>(true); Spell obj = ((componentInChildren != null) ? componentInChildren.Spell : null); VoiceSpell val = (VoiceSpell)(object)((obj is VoiceSpell) ? obj : null); if ((Object)(object)val == (Object)null) { return false; } if ((Object)(object)root.GetComponent<ManagedWandState>() != (Object)null) { return false; } ManagedWandState managedWandState = root.AddComponent<ManagedWandState>(); StaffDefinition staffDefinition = StaffDefinition.For(kind); try { Spell[] componentsInChildren = root.GetComponentsInChildren<Spell>(true); foreach (Spell val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)(object)val) { SpellDataField.SetValue(val2, null); Object.DestroyImmediate((Object)(object)val2); } } List<Spell> list = new List<Spell>(); SpellDefinition[] spells = staffDefinition.Spells; foreach (SpellDefinition spellDefinition in spells) { GameObject val3 = new GameObject(spellDefinition.Key); val3.SetActive(false); val3.transform.SetParent(root.transform, false); ExpandedStaffSpell expandedStaffSpell = staffDefinition.AddSpell(val3); expandedStaffSpell.Configure(spellDefinition); managedWandState.Data.Add((Object)(object)((Spell)expandedStaffSpell).SpellData); list.Add((Spell)(object)expandedStaffSpell); val3.SetActive(true); } VoiceSpellsField.SetValue(val, list.ToArray()); if (((Interactable)componentInChildren).triggerableComponents != null) { ((Interactable)componentInChildren).triggerableComponents = ((Interactable)componentInChildren).triggerableComponents.Where((MonoBehaviour c) => (Object)(object)c != (Object)null).ToArray(); } SpellData val4 = ScriptableObject.CreateInstance<SpellData>(); ((Object)val4).name = "YAPYAPEXP_CONTROLLER_" + kind; val4.spellName = Loc.Text(staffDefinition.NameKey); val4.charges = 1; val4.chargePerCast = 0; val4.chargeRegenInterval = 1f; val4.chargesRegenPerInterval = 1; val4.chargeColor = list[0].SpellData.chargeColor; val4.idleColor = Color.white; val4.cooldownColor = new Color(0.35f, 0.35f, 0.35f, 1f); val4.remainingCooldownAlpha = 0.4f; SpellDataField.SetValue(val, val4); managedWandState.Data.Add((Object)(object)val4); (root.GetComponent<CustomWandIdentity>() ?? root.AddComponent<CustomWandIdentity>()).Initialize(kind); NetworkIdentity component = root.GetComponent<NetworkIdentity>(); if (root.GetComponentsInChildren<NetworkBehaviour>(true).Length > 64) { throw new InvalidOperationException("Staff exceeds native Mirror component limit."); } AccessTools.Method(typeof(NetworkIdentity), "InitializeNetworkBehaviours", (Type[])null, (Type[])null).Invoke(component, null); Plugin.Log.LogInfo((object)(kind.ToString() + ": independent item data and " + list.Count + " standalone spells; native layout indexed after construction.")); return true; } catch (Exception ex) { Plugin.Log.LogError((object)("Standalone staff construction rejected: " + kind.ToString() + "\n" + ex)); return false; } } internal static bool TryGetKind(Spell spell, out WandKind kind) { kind = WandKind.Chaos; if ((Object)(object)spell == (Object)null) { return false; } NetworkIdentity componentInParent = ((Component)spell).GetComponentInParent<NetworkIdentity>(); if ((Object)(object)componentInParent == (Object)null) { return false; } if (componentInParent.assetId == 1497712210) { kind = WandKind.Frost; return true; } if (componentInParent.assetId == 1497715276) { kind = WandKind.Reality; return true; } return componentInParent.assetId == 1497711432; } internal static bool IsManagedChild(Spell spell) { if (spell is ExpandedStaffSpell expandedStaffSpell && TryGetKind(spell, out var kind) && expandedStaffSpell.StaffKind == kind && (Object)(object)spell.SpellData != (Object)null) { return ((Object)spell.SpellData).name.StartsWith("YAPYAPEXP_", StringComparison.Ordinal); } return false; } internal static SpellDefinition Definition(Spell spell) { if (!IsManagedChild(spell)) { return null; } SpellDefinition spellDefinition = SpellCatalog.Find(spell.VoiceCommandKey); if (spellDefinition == null || !TryGetKind(spell, out var kind) || kind != spellDefinition.Kind) { return null; } return spellDefinition.ForOwner(spell); } internal static bool IsHeld(Spell spell) { if (!BalanceSync.GameplayReady || (Object)(object)spell == (Object)null || (Object)(object)spell.CasterIdentity == (Object)null) { return false; } if (NetworkServer.active && !BalanceSync.ClientReady(spell.CasterIdentity.connectionToClient)) { return false; } NetworkIdentity componentInParent = ((Component)spell).GetComponentInParent<NetworkIdentity>(); NetworkPuppetWandProp val = (((Object)(object)componentInParent != (Object)null) ? ((Component)componentInParent).GetComponentInChildren<NetworkPuppetWandProp>(true) : null); if ((Object)(object)val != (Object)null && (Object)(object)val.Spell != (Object)null && val.Spell.NetworkisWandActive) { return (Object)(object)val.Spell.CasterIdentity == (Object)(object)spell.CasterIdentity; } return false; } internal static void Activate(Spell spell) { SpellDefinition spellDefinition = Definition(spell); if (spellDefinition != null && spellDefinition.Kind == WandKind.Reality) { RealityWandSystem.Activate(spell, spellDefinition); } else { if (!NetworkServer.active || spellDefinition == null || !IsHeld(spell) || !SpellVanilla.CanCast(spell)) { return; } try { CastStrength.Begin(spell); if (Spec2Runtime.Cast(spell, spellDefinition)) { Plugin.Log.LogInfo((object)("[CustomCast] " + spellDefinition.Key + " applied by caster=" + spell.CasterIdentity.netId)); SpellVanilla.Activate(spell); if (spell.IsCasting) { SpellVanilla.Deactivate(spell); } } } catch (Exception ex) { Plugin.Log.LogError((object)("Spell activation failed: " + spellDefinition.Key + "\n" + ex)); } } } } internal sealed class ManagedWandState : MonoBehaviour { internal readonly List<Object> Data = new List<Object>(); private void OnDestroy() { foreach (Object datum in Data) { if (datum != (Object)null) { Object.Destroy(datum); } } } } internal sealed class CustomWandIdentity : MonoBehaviour { [SerializeField] internal WandKind Kind; private ItemData data; private NetworkPuppetProp prop; private string language; internal void Initialize(WandKind kind) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown Kind = kind; StaffDefinition staffDefinition = StaffDefinition.For(kind); prop = ((Component)this).GetComponent<NetworkPuppetProp>(); if ((Object)(object)prop == (Object)null) { throw new InvalidOperationException("Staff has no native physical prop."); } data = new ItemData(); data.localisationKey = staffDefinition.ItemKey; data.icon = InventoryAppearance.Icon(kind); AccessTools.Field(typeof(NetworkPuppetProp), "itemData").SetValue(prop, data); Refresh(); } internal void Refresh() { if ((Object)(object)prop == (Object)null || data == null) { return; } language = Loc.Code; StaffDefinition staffDefinition = StaffDefinition.For(Kind); prop.DisplayName = Loc.Text(staffDefinition.NameKey); ((Interactable)prop).interactStr = prop.DisplayName; AccessTools.Field(typeof(Interactable), "_interactKey").SetValue(prop, staffDefinition.ItemKey); ExpandedStaffSpell[] componentsInChildren = ((Component)this).GetComponentsInChildren<ExpandedStaffSpell>(true); foreach (ExpandedStaffSpell expandedSt