Decompiled source of YAPYAP Expanded v1.0.1

plugins/YAPYAPExpanded/YAPYAPExpanded.dll

Decompiled 6 days ago
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