Decompiled source of SummonChoise v1.0.0

BepInEx/plugins/SkeletonChoice/SkeletonChoice.dll

Decompiled a week ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("SkeletonChoice")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+e84df9328a7b5b18d693b0690420394635269727")]
[assembly: AssemblyProduct("SkeletonChoice")]
[assembly: AssemblyTitle("SkeletonChoice")]
[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 SkeletonChoice
{
	public enum SummonMode
	{
		Random,
		Warrior,
		Archer,
		ShieldWarrior
	}
	internal enum EquipmentKind
	{
		Other,
		Bow,
		Sword,
		Shield,
		OtherWeapon
	}
	internal sealed class EquipmentItem
	{
		internal readonly EquipmentKind Kind;

		internal readonly int Slot;

		internal EquipmentItem(EquipmentKind kind, int slot = 0)
		{
			Kind = kind;
			Slot = slot;
		}
	}
	internal sealed class ChanceItem
	{
		internal EquipmentItem Item;

		internal float Chance;
	}
	internal sealed class EquipmentTable
	{
		internal EquipmentItem[] Defaults = new EquipmentItem[0];

		internal EquipmentItem[] Shields = new EquipmentItem[0];

		internal EquipmentItem[] Weapons = new EquipmentItem[0];

		internal EquipmentItem[] Armor = new EquipmentItem[0];

		internal EquipmentItem[][] Sets = new EquipmentItem[0][];

		internal ChanceItem[] Items = new ChanceItem[0];
	}
	internal interface ISeedRandom
	{
		void Initialize(int seed);

		int Index(int count);

		float Value();
	}
	internal struct EquipmentResult
	{
		internal int Bows;

		internal int Swords;

		internal int Shields;

		internal int OtherWeapons;

		internal void Add(EquipmentItem item)
		{
			if (item != null)
			{
				switch (item.Kind)
				{
				case EquipmentKind.Bow:
					Bows++;
					break;
				case EquipmentKind.Sword:
					Swords++;
					break;
				case EquipmentKind.Shield:
					Shields++;
					break;
				case EquipmentKind.OtherWeapon:
					OtherWeapons++;
					break;
				}
			}
		}

		internal bool Matches(SummonMode mode)
		{
			switch (mode)
			{
			case SummonMode.Random:
				return true;
			case SummonMode.Archer:
				if (Bows > 0 && Swords == 0 && OtherWeapons == 0)
				{
					return Shields == 0;
				}
				return false;
			case SummonMode.Warrior:
				if (Swords > 0 && Bows == 0 && OtherWeapons == 0)
				{
					return Shields == 0;
				}
				return false;
			case SummonMode.ShieldWarrior:
				if (Swords > 0 && Bows == 0 && OtherWeapons == 0)
				{
					return Shields > 0;
				}
				return false;
			default:
				return false;
			}
		}
	}
	internal static class SelectionPolicy
	{
		internal static EquipmentResult Evaluate(EquipmentTable table, ISeedRandom random)
		{
			EquipmentResult result = default(EquipmentResult);
			EquipmentItem[] defaults = table.Defaults;
			foreach (EquipmentItem item in defaults)
			{
				result.Add(item);
			}
			AddRandom(table.Shields, random, ref result);
			AddRandom(table.Weapons, random, ref result);
			AddRandom(table.Armor, random, ref result);
			if (table.Sets.Length != 0)
			{
				defaults = table.Sets[random.Index(table.Sets.Length)];
				foreach (EquipmentItem item2 in defaults)
				{
					result.Add(item2);
				}
			}
			HashSet<int> hashSet = new HashSet<int>();
			ChanceItem[] items = table.Items;
			foreach (ChanceItem chanceItem in items)
			{
				if (chanceItem.Item != null && random.Value() > chanceItem.Chance && hashSet.Add(chanceItem.Item.Slot))
				{
					result.Add(chanceItem.Item);
				}
			}
			return result;
		}

		private static void AddRandom(EquipmentItem[] items, ISeedRandom random, ref EquipmentResult result)
		{
			if (items.Length != 0)
			{
				result.Add(items[random.Index(items.Length)]);
			}
		}

		internal static bool TryChoose(EquipmentTable table, SummonMode mode, int originalSeed, ISeedRandom random, out int selectedSeed, int attempts = 4096)
		{
			selectedSeed = originalSeed;
			if (mode == SummonMode.Random)
			{
				return true;
			}
			for (int i = 0; i < attempts; i++)
			{
				int num = originalSeed + i * -1640531527;
				if (num != 0)
				{
					random.Initialize(num);
					if (Evaluate(table, random).Matches(mode))
					{
						selectedSeed = num;
						return true;
					}
				}
			}
			return false;
		}
	}
	public enum SpiritMode
	{
		Missing,
		Random,
		Bear,
		Moose,
		Wolf,
		Boar
	}
	internal static class SpiritSelectionPolicy
	{
		internal const int Vanilla = -2;

		internal const int Unavailable = -1;

		internal static int Choose(SpiritMode mode, IReadOnlyList<SpiritMode> candidates, IReadOnlyDictionary<SpiritMode, int> occupied, int capacity)
		{
			if (mode == SpiritMode.Random)
			{
				return -2;
			}
			int num = -1;
			int num2 = capacity;
			for (int i = 0; i < candidates.Count; i++)
			{
				SpiritMode spiritMode = candidates[i];
				if (spiritMode < SpiritMode.Bear || spiritMode > SpiritMode.Boar)
				{
					continue;
				}
				if (mode != SpiritMode.Missing)
				{
					if (spiritMode == mode)
					{
						return i;
					}
					continue;
				}
				int value;
				int num3 = (occupied.TryGetValue(spiritMode, out value) ? value : 0);
				if (num3 < num2 || (num >= 0 && num3 == num2 && spiritMode < candidates[num]))
				{
					num = i;
					num2 = num3;
				}
			}
			return num;
		}

		internal static void Add(IDictionary<SpiritMode, int> occupied, SpiritMode species)
		{
			occupied[species] = ((!occupied.TryGetValue(species, out var value)) ? 1 : (value + 1));
		}
	}
	internal sealed class SummonSnapshot
	{
		internal readonly string Id;

		internal readonly long Summoner;

		internal readonly bool Exists;

		internal readonly float Health;

		internal readonly bool Loaded;

		internal readonly bool Unsummoning;

		internal readonly bool HasFollowTarget;

		internal readonly float FollowDistance;

		internal readonly float UnsummonDistance;

		internal SummonSnapshot(string id, long summoner, bool exists, float health, bool loaded = true, bool unsummoning = false, bool hasFollowTarget = false, float followDistance = 0f, float unsummonDistance = 0f)
		{
			Id = id;
			Summoner = summoner;
			Exists = exists;
			Health = health;
			Loaded = loaded;
			Unsummoning = unsummoning;
			HasFollowTarget = hasFollowTarget;
			FollowDistance = followDistance;
			UnsummonDistance = unsummonDistance;
		}
	}
	internal static class SummonCountPolicy
	{
		internal static int Count(long playerId, IEnumerable<SummonSnapshot> summons)
		{
			if (playerId == 0L)
			{
				return 0;
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			int num = 0;
			foreach (SummonSnapshot summon in summons)
			{
				if (IsActive(playerId, summon) && hashSet.Add(summon.Id))
				{
					num++;
				}
			}
			return num;
		}

		internal static bool IsActive(long playerId, SummonSnapshot summon)
		{
			if (playerId != 0L && summon != null && summon.Exists && summon.Loaded && !summon.Unsummoning && summon.Summoner == playerId && summon.Health > 0f && !string.IsNullOrEmpty(summon.Id))
			{
				if (summon.HasFollowTarget && summon.UnsummonDistance > 0f)
				{
					return !(summon.FollowDistance > summon.UnsummonDistance);
				}
				return true;
			}
			return false;
		}

		internal static bool Visible(bool enabled, bool showBuff, bool localPlayerAlive, int count)
		{
			if (enabled && showBuff && localPlayerAlive)
			{
				return count > 0;
			}
			return false;
		}
	}
	[BepInPlugin("Slikfoul.SkeletonChoice", "Summon Choise", "1.0.0")]
	[BepInIncompatibility("local.valheim.skeletonselector")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "Slikfoul.SkeletonChoice";

		public const string PluginName = "Summon Choise";

		public const string PluginVersion = "1.0.0";

		internal static Plugin Instance;

		private Harmony harmony;

		private ConfigEntry<bool> modEnabled;

		private ConfigEntry<SummonMode> mode;

		private ConfigEntry<bool> debug;

		private ConfigEntry<bool> showSummonBuff;

		private ConfigEntry<SpiritMode> spirit;

		private ConfigEntry<bool> showSpiritBuff;

		private ConfigEntry<int> spiritFollowDistance;

		internal ManualLogSource Log => ((BaseUnityPlugin)this).Logger;

		internal bool ModEnabled => modEnabled.Value;

		internal SummonMode SelectedMode
		{
			get
			{
				if (!Enum.IsDefined(typeof(SummonMode), mode.Value))
				{
					return SummonMode.Random;
				}
				return mode.Value;
			}
		}

		internal bool DebugLogging => debug.Value;

		internal bool ShowSummonBuff => showSummonBuff.Value;

		internal SpiritMode SelectedSpirit
		{
			get
			{
				if (!Enum.IsDefined(typeof(SpiritMode), spirit.Value))
				{
					return SpiritMode.Missing;
				}
				return spirit.Value;
			}
		}

		internal bool ShowSpiritBuff => showSpiritBuff.Value;

		internal int SpiritFollowDistanceMeters => spiritFollowDistance.Value;

		private void Awake()
		{
			//IL_00ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c4: Expected O, but got Unknown
			//IL_014d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0157: Expected O, but got Unknown
			Instance = this;
			((BaseUnityPlugin)this).Config.SaveOnConfigSet = false;
			modEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - General", "Enabled", true, "Enable summon selection, spirit follow distance, and status indicators.");
			mode = ((BaseUnityPlugin)this).Config.Bind<SummonMode>("1 - General", "Skeleton type", SummonMode.Random, "Random: vanilla selection. Warrior: sword without a shield. Archer: bow without a shield. ShieldWarrior: sword and shield. Applies to future summons and persists across restarts.");
			showSummonBuff = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Display", "Show summon buff", true, "Show a status icon with your active loaded skeleton count. Excludes skeletons that are dead, unloading, unsummoning, or beyond their vanilla follow-distance limit.");
			spirit = ((BaseUnityPlugin)this).Config.Bind<SpiritMode>("1 - General", "Spirit Caller type", SpiritMode.Missing, "Missing: fill spirit slots in rounds (Bear, Moose, Wolf, Boar), up to the staff's limit per species. Random: vanilla selection. Select a species to summon only that spirit.");
			spiritFollowDistance = ((BaseUnityPlugin)this).Config.Bind<int>("1 - General", "Spirit follow distance", 6, new ConfigDescription("Distance in meters at which your Spirit Caller spirits stop following you outside combat. 3 uses the game's usual distance.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(3, 10), Array.Empty<object>()));
			showSpiritBuff = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Display", "Show spirit buff", true, "Show a separate status icon with your active Spirit Caller summon count.");
			debug = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Diagnostics", "Log summons", false, "Log equipment selection for every summon.");
			RemoveLegacySetting("2 - Interface", "Menu key");
			RemoveLegacySetting("2 - Interface", "Show hint");
			((BaseUnityPlugin)this).Config.SaveOnConfigSet = true;
			((BaseUnityPlugin)this).Config.Save();
			try
			{
				SummonHooks.VerifyHooks();
				harmony = new Harmony("Slikfoul.SkeletonChoice");
				harmony.PatchAll(typeof(Plugin).Assembly);
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Summon Choise 1.0.0 loaded. Dead Raiser and Spirit Caller hooks verified.");
			}
			catch (Exception ex)
			{
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
				modEnabled.Value = false;
				((BaseUnityPlugin)this).Logger.LogError((object)("Summon Choise disabled: summoning hooks are incompatible. " + ex));
			}
		}

		private void RemoveLegacySetting(string section, string key)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Expected O, but got Unknown
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Expected O, but got Unknown
			ConfigDefinition val = new ConfigDefinition(section, key);
			((BaseUnityPlugin)this).Config.Bind<string>(val, "", new ConfigDescription("", (AcceptableValueBase)null, Array.Empty<object>()));
			((BaseUnityPlugin)this).Config.Remove(val);
		}

		private void Update()
		{
			SummonIndicator.Update();
		}

		private void OnDestroy()
		{
			SummonIndicator.Clear();
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			SummonHooks.Clear();
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
		}
	}
	internal sealed class SpiritCast
	{
		internal GameObject[] Prefabs;

		internal SpiritMode Species;

		internal SpawnAbility Ability;

		internal Player Caster;

		internal float Expires;
	}
	internal static class SpiritCaller
	{
		[HarmonyPatch(typeof(Humanoid), "StartAttack")]
		private static class GuardFullRoster
		{
			private static bool Prefix(Humanoid __instance, bool secondaryAttack, ref bool __result)
			{
				Plugin instance = Plugin.Instance;
				Player localPlayer = Player.m_localPlayer;
				if (secondaryAttack || (Object)(object)instance == (Object)null || !instance.ModEnabled || (Object)(object)localPlayer == (Object)null || (Object)(object)__instance != (Object)(object)localPlayer)
				{
					return true;
				}
				SpiritMode selectedSpirit = instance.SelectedSpirit;
				if (selectedSpirit == SpiritMode.Random)
				{
					return true;
				}
				ItemData currentWeapon = ((Humanoid)localPlayer).GetCurrentWeapon();
				if (!IsStaff(currentWeapon))
				{
					return true;
				}
				GameObject attackProjectile = currentWeapon.m_shared.m_attack.m_attackProjectile;
				SpawnAbility val = (((Object)(object)attackProjectile != (Object)null) ? attackProjectile.GetComponent<SpawnAbility>() : null);
				if ((Object)(object)val == (Object)null)
				{
					return true;
				}
				SpiritMode[] array = Candidates(val.m_spawnPrefab);
				if (!Array.Exists(array, (SpiritMode x) => x >= SpiritMode.Bear && x <= SpiritMode.Boar))
				{
					return true;
				}
				if (SpiritSelectionPolicy.Choose(selectedSpirit, array, (selectedSpirit == SpiritMode.Missing) ? Occupied(localPlayer) : null, Capacity(val, currentWeapon)) != -1)
				{
					return true;
				}
				Notify(localPlayer, selectedSpirit);
				__result = false;
				return false;
			}
		}

		internal const string Staff = "StaffSpiritCaller";

		private static readonly Dictionary<int, SpiritMode> types = new Dictionary<int, SpiritMode>
		{
			{
				StringExtensionMethods.GetStableHashCode("Bjorn_spiritcaller"),
				SpiritMode.Bear
			},
			{
				StringExtensionMethods.GetStableHashCode("Moose_spiritcaller"),
				SpiritMode.Moose
			},
			{
				StringExtensionMethods.GetStableHashCode("Wolf_spiritcaller"),
				SpiritMode.Wolf
			},
			{
				StringExtensionMethods.GetStableHashCode("Boar_spiritcaller"),
				SpiritMode.Boar
			}
		};

		private static readonly List<SpiritCast> pending = new List<SpiritCast>();

		internal static bool IsStaff(ItemData item)
		{
			if (item != null && (Object)(object)item.m_dropPrefab != (Object)null)
			{
				return ((Object)item.m_dropPrefab).name == "StaffSpiritCaller";
			}
			return false;
		}

		internal static bool TryGetSpecies(int prefabHash, out SpiritMode species)
		{
			return types.TryGetValue(prefabHash, out species);
		}

		private static SpiritMode[] Candidates(GameObject[] prefabs)
		{
			if (prefabs == null)
			{
				return new SpiritMode[0];
			}
			SpiritMode[] array = new SpiritMode[prefabs.Length];
			for (int i = 0; i < prefabs.Length; i++)
			{
				if ((Object)(object)prefabs[i] != (Object)null)
				{
					TryGetSpecies(StringExtensionMethods.GetStableHashCode(((Object)prefabs[i]).name), out array[i]);
				}
			}
			return array;
		}

		private static Dictionary<SpiritMode, int> Occupied(Player caster)
		{
			Dictionary<SpiritMode, int> dictionary = new Dictionary<SpiritMode, int>();
			SummonIndicator.CollectSpirits(caster, dictionary);
			for (int num = pending.Count - 1; num >= 0; num--)
			{
				SpiritCast spiritCast = pending[num];
				if ((Object)(object)spiritCast.Ability == (Object)null || (Object)(object)spiritCast.Caster != (Object)(object)caster || Time.time > spiritCast.Expires)
				{
					pending.RemoveAt(num);
				}
				else
				{
					SpiritSelectionPolicy.Add(dictionary, spiritCast.Species);
				}
			}
			return dictionary;
		}

		private static int Capacity(SpawnAbility ability, ItemData weapon)
		{
			if (!ability.m_setMaxInstancesFromWeaponLevel)
			{
				return 1;
			}
			return Mathf.Max(1, weapon.m_quality);
		}

		internal static bool Prepare(SpawnAbility ability, Player caster, ItemData weapon, SpiritMode mode, out SpiritCast cast)
		{
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			cast = null;
			SpiritMode[] array = Candidates(ability.m_spawnPrefab);
			if (!Array.Exists(array, (SpiritMode x) => x >= SpiritMode.Bear && x <= SpiritMode.Boar))
			{
				return true;
			}
			Dictionary<SpiritMode, int> dictionary = ((mode == SpiritMode.Missing) ? Occupied(caster) : null);
			int capacity = Capacity(ability, weapon);
			int num = SpiritSelectionPolicy.Choose(mode, array, dictionary, capacity);
			if (num == -1)
			{
				Notify(caster, mode);
				return false;
			}
			cast = new SpiritCast
			{
				Ability = ability,
				Caster = caster
			};
			if (num == -2)
			{
				return true;
			}
			if (dictionary != null && ability.m_maxSpawned > 0)
			{
				SpiritMode[] array2 = (SpiritMode[])array.Clone();
				for (int num2 = 0; num2 < array2.Length; num2++)
				{
					if (array2[num2] >= SpiritMode.Bear && SpawnSystem.GetNrOfInstances(ability.m_spawnPrefab[num2], ((Component)caster).transform.position, 0f, false, false) >= ability.m_maxSpawned)
					{
						array2[num2] = SpiritMode.Missing;
					}
				}
				int num3 = SpiritSelectionPolicy.Choose(mode, array2, dictionary, capacity);
				if (num3 >= 0)
				{
					num = num3;
				}
			}
			cast.Prefabs = (GameObject[])(object)new GameObject[1] { ability.m_spawnPrefab[num] };
			cast.Species = array[num];
			cast.Expires = Time.time + Mathf.Max(0f, ability.m_initialSpawnDelay) + Mathf.Max(0f, ability.m_preSpawnDelay) + 15f;
			pending.Add(cast);
			return true;
		}

		private static void Notify(Player caster, SpiritMode mode)
		{
			((Character)caster).Message((MessageType)2, (mode == SpiritMode.Missing) ? "All spirit slots for this staff are filled or reserved." : "The selected spirit is not available for this staff.", 0, (Sprite)null, false);
		}

		internal static void Release(SpiritCast cast)
		{
			if (cast != null)
			{
				pending.Remove(cast);
			}
		}

		internal static void Clear()
		{
			pending.Clear();
		}
	}
	[HarmonyPatch(typeof(BaseAI), "Follow")]
	internal static class SpiritFollowDistance
	{
		private const float VanillaDistance = 3f;

		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Expected O, but got Unknown
			//IL_00af: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b9: Expected O, but got Unknown
			//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Expected O, but got Unknown
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			int num = list.FindIndex(IsStopDistance);
			if (num < 0 || num != list.FindLastIndex(IsStopDistance))
			{
				Plugin.Instance.Log.LogWarning((object)"Spirit follow distance unavailable: the game's follow method has changed.");
				return list;
			}
			CodeInstruction val = list[num];
			CodeInstruction val2 = new CodeInstruction(OpCodes.Ldarg_0, (object)null);
			val2.labels.AddRange(val.labels);
			val2.blocks.AddRange(val.blocks);
			list[num] = val2;
			list.Insert(num + 1, new CodeInstruction(OpCodes.Ldarg_1, (object)null));
			list.Insert(num + 2, new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(SpiritFollowDistance), "GetStopDistance", (Type[])null, (Type[])null)));
			return list;
		}

		private static bool IsStopDistance(CodeInstruction instruction)
		{
			if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float num)
			{
				return num == 3f;
			}
			return false;
		}

		private static float GetStopDistance(BaseAI ai, GameObject followTarget)
		{
			Plugin instance = Plugin.Instance;
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)instance == (Object)null || !instance.ModEnabled || (Object)(object)localPlayer == (Object)null || (Object)(object)followTarget != (Object)(object)((Component)localPlayer).gameObject || !(ai is MonsterAI))
			{
				return 3f;
			}
			int spiritFollowDistanceMeters = instance.SpiritFollowDistanceMeters;
			if ((float)spiritFollowDistanceMeters <= 3f)
			{
				return 3f;
			}
			ZNetView component = ((Component)ai).GetComponent<ZNetView>();
			if ((Object)(object)component == (Object)null || !component.IsValid() || !component.IsOwner())
			{
				return 3f;
			}
			ZDO zDO = component.GetZDO();
			if (zDO == null || !SpiritCaller.TryGetSpecies(zDO.GetPrefab(), out var _))
			{
				return 3f;
			}
			long playerID = localPlayer.GetPlayerID();
			if (playerID == 0L || zDO.GetLong(SummonIndicator.SummonerKey, 0L) != playerID)
			{
				return 3f;
			}
			return spiritFollowDistanceMeters;
		}
	}
	internal sealed class CastRequest
	{
		internal readonly SummonMode Mode;

		internal readonly Player Caster;

		internal SpiritCast Spirit;

		internal int SelectedCount;

		internal CastRequest(SummonMode mode, Player caster)
		{
			Mode = mode;
			Caster = caster;
		}
	}
	internal static class SummonHooks
	{
		[HarmonyPatch(typeof(SpawnAbility), "Setup")]
		private static class CaptureCast
		{
			private static bool Prefix(SpawnAbility __instance, Character owner, ItemData item)
			{
				//IL_006f: Unknown result type (might be due to invalid IL or missing references)
				//IL_0082: Expected O, but got Unknown
				//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
				//IL_00e1: Expected O, but got Unknown
				//IL_009d: Unknown result type (might be due to invalid IL or missing references)
				//IL_00a7: Expected O, but got Unknown
				if (requests.TryGetValue(__instance, out var value))
				{
					SpiritCaller.Release(value.Spirit);
				}
				requests.Remove(__instance);
				Plugin instance = Plugin.Instance;
				if ((Object)(object)instance == (Object)null || !instance.ModEnabled || (Object)(object)owner == (Object)null || (Object)(object)owner != (Object)(object)Player.m_localPlayer)
				{
					return true;
				}
				ItemData val = item ?? ((Humanoid)Player.m_localPlayer).GetCurrentWeapon();
				if (SpiritCaller.IsStaff(val))
				{
					if (!SpiritCaller.Prepare(__instance, (Player)owner, val, instance.SelectedSpirit, out var cast))
					{
						Object.Destroy((Object)(object)((Component)__instance).gameObject);
						return false;
					}
					if (cast != null)
					{
						requests.Add(__instance, new CastRequest(SummonMode.Random, (Player)owner)
						{
							Spirit = cast
						});
					}
					return true;
				}
				if (!IsDeadRaiser(val) || !SpawnsFriendlySkeleton(__instance))
				{
					return true;
				}
				SummonMode selectedMode = instance.SelectedMode;
				requests.Add(__instance, new CastRequest(selectedMode, (Player)owner));
				if (instance.DebugLogging)
				{
					instance.Log.LogInfo((object)("Captured Dead Raiser cast: " + selectedMode.ToString() + "; weapon supplied=" + (item != null)));
				}
				return true;
			}
		}

		private struct SpawnStepState
		{
			internal CastRequest Previous;

			internal SpawnAbility Ability;

			internal GameObject[] OriginalPrefabs;
		}

		[HarmonyPatch]
		private static class SpawnStepScope
		{
			private static MethodBase TargetMethod()
			{
				return SpawnStep;
			}

			private static void Prefix(object __instance, out SpawnStepState __state)
			{
				__state = new SpawnStepState
				{
					Previous = current
				};
				current = null;
				object? value = abilityField.GetValue(__instance);
				SpawnAbility val = (SpawnAbility)((value is SpawnAbility) ? value : null);
				if (!((Object)(object)val == (Object)null) && requests.TryGetValue(val, out var value2))
				{
					current = value2;
					if (value2.Spirit != null && value2.Spirit.Prefabs != null)
					{
						__state.Ability = val;
						__state.OriginalPrefabs = val.m_spawnPrefab;
						val.m_spawnPrefab = value2.Spirit.Prefabs;
					}
				}
			}

			private static void Finalizer(Exception __exception, SpawnStepState __state)
			{
				if (__exception != null && current != null)
				{
					SpiritCaller.Release(current.Spirit);
				}
				if ((Object)(object)__state.Ability != (Object)null)
				{
					__state.Ability.m_spawnPrefab = __state.OriginalPrefabs;
				}
				current = __state.Previous;
			}

			private static void Postfix(bool __result)
			{
				CastRequest current = SummonHooks.current;
				if (!__result && current != null && current.Spirit != null)
				{
					SpiritCaller.Release(current.Spirit);
				}
				if (!__result && current != null && current.Mode != SummonMode.Random && current.SelectedCount == 0 && Plugin.Instance.DebugLogging)
				{
					Plugin.Instance.Log.LogWarning((object)"Dead Raiser cast finished without selecting equipment (the summon may have been blocked by its limit or spawn point).");
				}
			}
		}

		[HarmonyPatch(typeof(Character), "Awake")]
		private static class TrackSpiritSpawn
		{
			[HarmonyPriority(0)]
			private static void Postfix(Character __instance)
			{
				CastRequest current = SummonHooks.current;
				if (current == null || current.Spirit == null || (Object)(object)__instance == (Object)null || __instance.IsPlayer())
				{
					return;
				}
				ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
				if ((Object)(object)component == (Object)null || !component.IsValid() || !component.IsOwner())
				{
					return;
				}
				ZDO zDO = component.GetZDO();
				if (SpiritCaller.TryGetSpecies(zDO.GetPrefab(), out var species) && (current.Spirit.Prefabs == null || current.Spirit.Species == species))
				{
					SummonIndicator.MarkSummon(zDO, current.Caster);
					SpiritCaller.Release(current.Spirit);
					Plugin instance = Plugin.Instance;
					if (instance.DebugLogging || reportedSelections++ < 12)
					{
						instance.Log.LogInfo((object)("Summoned Spirit Caller spirit: " + species.ToString() + "."));
					}
				}
			}
		}

		[HarmonyPatch(typeof(Humanoid), "Awake")]
		private static class SelectEquipmentSeed
		{
			[HarmonyPriority(0)]
			private static void Postfix(Humanoid __instance)
			{
				//IL_019e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0077: 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)
				CastRequest current = SummonHooks.current;
				if (current == null || current.Spirit != null || (Object)(object)__instance == (Object)null || ((Character)__instance).IsPlayer() || !((Object)((Component)__instance).gameObject).name.StartsWith("Skeleton_Friendly", StringComparison.Ordinal))
				{
					return;
				}
				ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
				if ((Object)(object)component == (Object)null || !component.IsValid() || !component.IsOwner())
				{
					return;
				}
				SummonIndicator.MarkSummon(component.GetZDO(), current.Caster);
				if (current.Mode == SummonMode.Random)
				{
					return;
				}
				State state = Random.state;
				try
				{
					if (!SelectionPolicy.TryChoose(ReadTable(__instance), originalSeed: (int)seedField.GetValue(__instance), mode: current.Mode, random: new UnitySeedRandom(), selectedSeed: out var selectedSeed))
					{
						Warn(current, "The summoned skeleton has no matching vanilla equipment combination.");
						return;
					}
					component.GetZDO().Set(ZDOVars.s_seed, selectedSeed, false);
					seedField.SetValue(__instance, selectedSeed);
					current.SelectedCount++;
					if (Plugin.Instance.DebugLogging || reportedSelections++ < 12)
					{
						Plugin.Instance.Log.LogInfo((object)("Selected " + current.Mode.ToString() + " for " + ((Object)((Component)__instance).gameObject).name + "; synchronized vanilla equipment seed."));
					}
				}
				catch (Exception ex)
				{
					Plugin.Instance.Log.LogError((object)("Equipment selection failed; vanilla summon retained. " + ex));
					Warn(current, "Could not select equipment; vanilla summon retained.");
				}
				finally
				{
					Random.state = state;
				}
			}
		}

		private sealed class UnitySeedRandom : ISeedRandom
		{
			public void Initialize(int seed)
			{
				Random.InitState(seed);
			}

			public int Index(int count)
			{
				return Random.Range(0, count);
			}

			public float Value()
			{
				return Random.value;
			}
		}

		private static ConditionalWeakTable<SpawnAbility, CastRequest> requests = new ConditionalWeakTable<SpawnAbility, CastRequest>();

		[ThreadStatic]
		private static CastRequest current;

		private static readonly FieldInfo seedField = AccessTools.Field(typeof(Humanoid), "m_seed");

		private static FieldInfo abilityField;

		private static int reportedSelections;

		internal static MethodInfo SpawnStep { get; private set; }

		internal static void VerifyHooks()
		{
			SpawnStep = AccessTools.EnumeratorMoveNext((MethodBase)AccessTools.Method(typeof(SpawnAbility), "Spawn", (Type[])null, (Type[])null));
			if (SpawnStep == null || seedField == null)
			{
				throw new MissingMethodException("Valheim summoning hooks could not be found.");
			}
			abilityField = AccessTools.Field(SpawnStep.DeclaringType, "<>4__this");
			if (abilityField == null)
			{
				throw new MissingFieldException("Spawn coroutine does not expose its owner.");
			}
		}

		internal static void Clear()
		{
			current = null;
			requests = new ConditionalWeakTable<SpawnAbility, CastRequest>();
			reportedSelections = 0;
			SpiritCaller.Clear();
		}

		internal static bool IsDeadRaiser(ItemData item)
		{
			if (item != null && (Object)(object)item.m_dropPrefab != (Object)null)
			{
				return ((Object)item.m_dropPrefab).name == "StaffSkeleton";
			}
			return false;
		}

		private static bool SpawnsFriendlySkeleton(SpawnAbility ability)
		{
			if (ability.m_spawnPrefab == null)
			{
				return false;
			}
			GameObject[] spawnPrefab = ability.m_spawnPrefab;
			foreach (GameObject val in spawnPrefab)
			{
				if ((Object)(object)val != (Object)null && ((Object)val).name.StartsWith("Skeleton_Friendly", StringComparison.Ordinal))
				{
					return true;
				}
			}
			return false;
		}

		private static void Warn(CastRequest request, string detail)
		{
			Plugin instance = Plugin.Instance;
			if (!((Object)(object)instance == (Object)null))
			{
				instance.Log.LogWarning((object)detail);
				if ((Object)(object)request.Caster != (Object)null)
				{
					((Character)request.Caster).Message((MessageType)2, "Skeleton type could not be selected. See the log.", 0, (Sprite)null, false);
				}
			}
		}

		private static EquipmentTable ReadTable(Humanoid humanoid)
		{
			EquipmentTable equipmentTable = new EquipmentTable
			{
				Defaults = ReadItems(humanoid.m_defaultItems),
				Shields = ReadItems(humanoid.m_randomShield),
				Weapons = ReadItems(humanoid.m_randomWeapon),
				Armor = ReadItems(humanoid.m_randomArmor)
			};
			ItemSet[] array = (ItemSet[])(((object)humanoid.m_randomSets) ?? ((object)new ItemSet[0]));
			equipmentTable.Sets = new EquipmentItem[array.Length][];
			for (int i = 0; i < array.Length; i++)
			{
				equipmentTable.Sets[i] = ReadItems(array[i].m_items);
			}
			RandomItem[] array2 = (RandomItem[])(((object)humanoid.m_randomItems) ?? ((object)new RandomItem[0]));
			equipmentTable.Items = new ChanceItem[array2.Length];
			for (int j = 0; j < array2.Length; j++)
			{
				equipmentTable.Items[j] = new ChanceItem
				{
					Item = ReadItem(array2[j].m_prefab),
					Chance = array2[j].m_chance
				};
			}
			return equipmentTable;
		}

		private static EquipmentItem[] ReadItems(GameObject[] prefabs)
		{
			if (prefabs == null)
			{
				return new EquipmentItem[0];
			}
			EquipmentItem[] array = new EquipmentItem[prefabs.Length];
			for (int i = 0; i < array.Length; i++)
			{
				array[i] = ReadItem(prefabs[i]);
			}
			return array;
		}

		private static EquipmentItem ReadItem(GameObject prefab)
		{
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Invalid comparison between Unknown and I4
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Invalid comparison between Unknown and I4
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Expected I4, but got Unknown
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Invalid comparison between Unknown and I4
			if ((Object)(object)prefab == (Object)null)
			{
				return null;
			}
			ItemDrop component = prefab.GetComponent<ItemDrop>();
			if ((Object)(object)component == (Object)null)
			{
				throw new InvalidOperationException("Equipment prefab has no ItemDrop: " + ((Object)prefab).name);
			}
			ItemData itemData = component.m_itemData;
			EquipmentKind kind = EquipmentKind.Other;
			if ((int)itemData.m_shared.m_itemType == 5)
			{
				kind = EquipmentKind.Shield;
			}
			else if ((int)itemData.m_shared.m_itemType == 4)
			{
				kind = EquipmentKind.Bow;
			}
			else if (itemData.IsWeapon())
			{
				kind = (((int)itemData.m_shared.m_skillType == 1) ? EquipmentKind.Sword : EquipmentKind.OtherWeapon);
			}
			return new EquipmentItem(kind, (int)itemData.m_shared.m_itemType);
		}
	}
	public sealed class SummonedSkeletonsEffect : StatusEffect
	{
		internal int Count;

		internal string Caption;

		public override string GetIconText()
		{
			return Count.ToString(CultureInfo.InvariantCulture);
		}

		public override string GetTooltipString()
		{
			return Caption + ": " + ((StatusEffect)this).GetIconText();
		}
	}
	internal static class SummonIndicator
	{
		[HarmonyPatch(typeof(Tameable), "RPC_UnSummon")]
		private static class TrackUnsummon
		{
			private static void Prefix(Tameable __instance)
			{
				//IL_004f: Unknown result type (might be due to invalid IL or missing references)
				Player localPlayer = Player.m_localPlayer;
				if ((Object)(object)localPlayer == (Object)null)
				{
					return;
				}
				ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
				if (!((Object)(object)component == (Object)null) && component.IsValid())
				{
					ZDO zDO = component.GetZDO();
					if (zDO != null && zDO.GetLong(SummonerKey, 0L) == localPlayer.GetPlayerID())
					{
						unsummoning.Add(zDO.m_uid);
						nextScan = 0f;
					}
				}
			}
		}

		[HarmonyPatch(typeof(SEMan), "GetHUDStatusEffects")]
		private static class AddIndicator
		{
			private static void Postfix(SEMan __instance, List<StatusEffect> effects)
			{
				Plugin instance = Plugin.Instance;
				Player localPlayer = Player.m_localPlayer;
				if ((Object)(object)instance == (Object)null || (Object)(object)localPlayer == (Object)null || (Object)(object)localPlayer != (Object)(object)player || world != ZDOMan.instance || __instance != ((Character)localPlayer).GetSEMan())
				{
					return;
				}
				bool localPlayerAlive = !((Character)localPlayer).IsDead();
				if (SummonCountPolicy.Visible(instance.ModEnabled, instance.ShowSummonBuff, localPlayerAlive, count))
				{
					SummonedSkeletonsEffect effect = GetEffect(ref SummonIndicator.effect, "Skeletons", "StaffSkeleton", count);
					if ((Object)(object)((StatusEffect)effect).m_icon != (Object)null && !effects.Contains((StatusEffect)(object)effect))
					{
						effects.Add((StatusEffect)(object)effect);
					}
				}
				if (SummonCountPolicy.Visible(instance.ModEnabled, instance.ShowSpiritBuff, localPlayerAlive, spiritCount))
				{
					SummonedSkeletonsEffect effect2 = GetEffect(ref spiritEffect, "Spirits", "StaffSpiritCaller", spiritCount);
					if ((Object)(object)((StatusEffect)effect2).m_icon != (Object)null && !effects.Contains((StatusEffect)(object)effect2))
					{
						effects.Add((StatusEffect)(object)effect2);
					}
				}
			}
		}

		internal static readonly int SummonerKey = StringExtensionMethods.GetStableHashCode("Slikfoul.SkeletonChoice.Summoner");

		private static readonly List<SummonSnapshot> snapshots = new List<SummonSnapshot>();

		private static readonly List<SummonSnapshot> spiritSnapshots = new List<SummonSnapshot>();

		private static readonly HashSet<ZDOID> unsummoning = new HashSet<ZDOID>();

		private static readonly HashSet<ZDOID> loadedIds = new HashSet<ZDOID>();

		private static SummonedSkeletonsEffect effect;

		private static SummonedSkeletonsEffect spiritEffect;

		private static Player player;

		private static ZDOMan world;

		private static float nextScan;

		private static int count;

		private static int spiritCount;

		internal static void MarkSummon(ZDO zdo, Player caster)
		{
			if (zdo != null && !((Object)(object)caster == (Object)null))
			{
				long playerID = caster.GetPlayerID();
				if (playerID != 0L)
				{
					zdo.Set(SummonerKey, playerID);
					nextScan = 0f;
				}
			}
		}

		internal static void Update()
		{
			//IL_012c: Unknown result type (might be due to invalid IL or missing references)
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer != (Object)(object)player || world != ZDOMan.instance)
			{
				count = 0;
				spiritCount = 0;
				snapshots.Clear();
				spiritSnapshots.Clear();
				unsummoning.Clear();
				loadedIds.Clear();
				nextScan = 0f;
				player = localPlayer;
				world = ZDOMan.instance;
			}
			Plugin instance = Plugin.Instance;
			if ((Object)(object)instance == (Object)null || !instance.ModEnabled || (!instance.ShowSummonBuff && !instance.ShowSpiritBuff) || (Object)(object)localPlayer == (Object)null || ((Character)localPlayer).IsDead() || world == null)
			{
				count = 0;
				spiritCount = 0;
				nextScan = 0f;
			}
			else
			{
				if (Time.unscaledTime < nextScan)
				{
					return;
				}
				nextScan = Time.unscaledTime + 0.5f;
				long playerID = localPlayer.GetPlayerID();
				snapshots.Clear();
				spiritSnapshots.Clear();
				loadedIds.Clear();
				foreach (Character allCharacter in Character.GetAllCharacters())
				{
					if (TryRead(allCharacter, playerID, out var zdo, out var snapshot))
					{
						loadedIds.Add(zdo.m_uid);
						if (SpiritCaller.TryGetSpecies(zdo.GetPrefab(), out var _))
						{
							spiritSnapshots.Add(snapshot);
						}
						else
						{
							snapshots.Add(snapshot);
						}
					}
				}
				unsummoning.IntersectWith(loadedIds);
				count = SummonCountPolicy.Count(playerID, snapshots);
				spiritCount = SummonCountPolicy.Count(playerID, spiritSnapshots);
			}
		}

		internal static void CollectSpirits(Player caster, IDictionary<SpiritMode, int> occupied)
		{
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			long playerID = caster.GetPlayerID();
			HashSet<ZDOID> hashSet = new HashSet<ZDOID>();
			foreach (Character allCharacter in Character.GetAllCharacters())
			{
				if (TryRead(allCharacter, playerID, out var zdo, out var snapshot) && SpiritCaller.TryGetSpecies(zdo.GetPrefab(), out var species) && SummonCountPolicy.IsActive(playerID, snapshot) && hashSet.Add(zdo.m_uid))
				{
					SpiritSelectionPolicy.Add(occupied, species);
				}
			}
		}

		private static bool TryRead(Character character, long playerId, out ZDO zdo, out SummonSnapshot snapshot)
		{
			//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			zdo = null;
			snapshot = null;
			if (playerId == 0L || (Object)(object)character == (Object)null || character.IsPlayer() || !((Component)character).gameObject.activeInHierarchy)
			{
				return false;
			}
			ZNetView component = ((Component)character).GetComponent<ZNetView>();
			if ((Object)(object)component == (Object)null || !component.IsValid())
			{
				return false;
			}
			zdo = component.GetZDO();
			if (zdo == null || !zdo.IsValid() || zdo.GetLong(SummonerKey, 0L) != playerId)
			{
				return false;
			}
			Tameable component2 = ((Component)character).GetComponent<Tameable>();
			MonsterAI component3 = ((Component)character).GetComponent<MonsterAI>();
			GameObject val = (((Object)(object)component3 != (Object)null) ? component3.GetFollowTarget() : null);
			bool flag = (Object)(object)val != (Object)null;
			float followDistance = (flag ? Vector3.Distance(val.transform.position, ((Component)character).transform.position) : 0f);
			snapshot = new SummonSnapshot(((object)Unsafe.As<ZDOID, ZDOID>(ref zdo.m_uid)/*cast due to .constrained prefix*/).ToString(), playerId, exists: true, character.IsDead() ? 0f : character.GetHealth(), loaded: true, unsummoning.Contains(zdo.m_uid), flag, followDistance, ((Object)(object)component2 != (Object)null) ? component2.m_unsummonDistance : 0f);
			return true;
		}

		private static SummonedSkeletonsEffect GetEffect(ref SummonedSkeletonsEffect status, string title, string staffName, int amount)
		{
			if ((Object)(object)status == (Object)null)
			{
				status = ScriptableObject.CreateInstance<SummonedSkeletonsEffect>();
				((Object)status).name = "SkeletonChoice_Summoned" + title;
				((StatusEffect)status).m_name = title;
				status.Caption = "Your summoned " + title.ToLowerInvariant();
				((StatusEffect)status).m_ttl = 0f;
				((StatusEffect)status).m_isNew = false;
			}
			if ((Object)(object)((StatusEffect)status).m_icon == (Object)null && (Object)(object)ObjectDB.instance != (Object)null)
			{
				GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(staffName);
				ItemDrop val = (((Object)(object)itemPrefab != (Object)null) ? itemPrefab.GetComponent<ItemDrop>() : null);
				if ((Object)(object)val != (Object)null && val.m_itemData.m_shared.m_icons.Length != 0)
				{
					((StatusEffect)status).m_icon = val.m_itemData.m_shared.m_icons[0];
				}
			}
			status.Count = amount;
			return status;
		}

		internal static void Clear()
		{
			if ((Object)(object)effect != (Object)null)
			{
				Object.Destroy((Object)(object)effect);
			}
			if ((Object)(object)spiritEffect != (Object)null)
			{
				Object.Destroy((Object)(object)spiritEffect);
			}
			effect = null;
			spiritEffect = null;
			player = null;
			world = null;
			nextScan = 0f;
			count = 0;
			spiritCount = 0;
			snapshots.Clear();
			spiritSnapshots.Clear();
			unsummoning.Clear();
			loadedIds.Clear();
		}
	}
}