Decompiled source of SpawnKit v0.2.1

SpawnKit/SpawnKit.Core.dll

Decompiled 2 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using Microsoft.CodeAnalysis;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")]
[assembly: AssemblyCompany("SpawnKit.Core")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+d10381aec7116e1ea7ae2d3ff4646fae57640f75")]
[assembly: AssemblyProduct("SpawnKit.Core")]
[assembly: AssemblyTitle("SpawnKit.Core")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class IsReadOnlyAttribute : Attribute
	{
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace SpawnKit.Core
{
	public enum CorpseLootVerdict
	{
		Alive,
		NoLootableComponent,
		ComponentDisabled,
		NoPouch,
		NoPouchInteractionTrigger,
		DropsPresentButInert,
		NoDropsConfigured,
		Lootable
	}
	public readonly struct CorpseLootObservation
	{
		public readonly bool IsDead;

		public readonly bool HasComponent;

		public readonly bool ComponentEnabled;

		public readonly bool HasPouch;

		public readonly bool PouchHasInteractionTrigger;

		public readonly int LootDropEntries;

		public readonly int LootDropsWithDropper;

		public readonly int SkinDropEntries;

		public readonly int SkinDropsWithDropper;

		public readonly bool Lootable;

		public readonly bool Skinable;

		public CorpseLootObservation(bool isDead, bool hasComponent, bool componentEnabled, bool hasPouch, bool pouchHasInteractionTrigger, int lootDropEntries, int lootDropsWithDropper, int skinDropEntries, int skinDropsWithDropper, bool lootable, bool skinable)
		{
			IsDead = isDead;
			HasComponent = hasComponent;
			ComponentEnabled = componentEnabled;
			HasPouch = hasPouch;
			PouchHasInteractionTrigger = pouchHasInteractionTrigger;
			LootDropEntries = lootDropEntries;
			LootDropsWithDropper = lootDropsWithDropper;
			SkinDropEntries = skinDropEntries;
			SkinDropsWithDropper = skinDropsWithDropper;
			Lootable = lootable;
			Skinable = skinable;
		}
	}
	public static class CorpseLootDiagnosis
	{
		public static CorpseLootVerdict Classify(in CorpseLootObservation o)
		{
			if (!o.IsDead)
			{
				return CorpseLootVerdict.Alive;
			}
			if (!o.HasComponent)
			{
				return CorpseLootVerdict.NoLootableComponent;
			}
			if (!o.ComponentEnabled)
			{
				return CorpseLootVerdict.ComponentDisabled;
			}
			if (!o.HasPouch)
			{
				return CorpseLootVerdict.NoPouch;
			}
			if (!o.PouchHasInteractionTrigger)
			{
				return CorpseLootVerdict.NoPouchInteractionTrigger;
			}
			if (o.Lootable || o.Skinable)
			{
				return CorpseLootVerdict.Lootable;
			}
			if (o.LootDropsWithDropper > 0 || o.SkinDropsWithDropper > 0)
			{
				return CorpseLootVerdict.DropsPresentButInert;
			}
			return CorpseLootVerdict.NoDropsConfigured;
		}

		public static bool IsActionableDefect(CorpseLootVerdict v)
		{
			if ((uint)(v - 2) <= 3u)
			{
				return true;
			}
			return false;
		}

		public static string Explain(CorpseLootVerdict v)
		{
			return v switch
			{
				CorpseLootVerdict.Alive => "not dead yet — probe again after death", 
				CorpseLootVerdict.NoLootableComponent => "no LootableOnDeath component — this body never had corpse loot", 
				CorpseLootVerdict.ComponentDisabled => "LootableOnDeath is DISABLED -> OnDeath early-returns (ForceLootableEnabled re-enables it at mint)", 
				CorpseLootVerdict.NoPouch => "Inventory.Pouch is null -> the corpse cannot be made lootable", 
				CorpseLootVerdict.NoPouchInteractionTrigger => "pouch has no interaction trigger -> OnDeath early-returns before MakeLootable", 
				CorpseLootVerdict.DropsPresentButInert => "drop entries carry droppers yet m_lootable/m_skinable are false -> LootableOnDeath.Start did not populate (BUG)", 
				CorpseLootVerdict.NoDropsConfigured => "no loot AND no skin droppers -> empty undetectable pouch; this creature has no corpse loot by design (scripted/boss reward), NOT a SpawnKit regression", 
				CorpseLootVerdict.Lootable => "m_lootable/m_skinable set -> a normal loot/skin prompt is expected", 
				_ => "unknown", 
			};
		}
	}
	public enum CorpsePolicy
	{
		Vanilla,
		NoBody
	}
	public static class CorpseRules
	{
		public static CorpsePolicy EffectivePolicy(CorpsePolicy? requested, CorpsePolicy configured)
		{
			return requested ?? configured;
		}

		public static float EffectiveLinger(float? requested, float configured)
		{
			float num = requested ?? configured;
			if (!(num < 0f))
			{
				return num;
			}
			return 0f;
		}
	}
	public enum RemovalReason
	{
		SilentDespawn,
		Killed,
		WatchDied,
		WatchDespawned,
		CancelledPending
	}
	public static class DisengagePolicy
	{
		public static bool NeedsDisengage(RemovalReason reason)
		{
			return reason != RemovalReason.CancelledPending;
		}

		public static bool ByReference(bool exists)
		{
			return exists;
		}

		public static bool IsStaleEntry(bool entryExists, bool entryAlive)
		{
			if (entryExists)
			{
				return !entryAlive;
			}
			return true;
		}
	}
	public static class ExpeditionRow
	{
		public struct Buttons
		{
			public bool ShowPrewarm;

			public bool ShowWarmTrip;

			public bool Interactive;

			public string SpawnLabel;

			public bool SpawnCostsATrip;
		}

		public struct Arm
		{
			public string Key;

			public float At;
		}

		public struct ClickResult
		{
			public bool Fire;

			public Arm State;
		}

		public static Buttons For(bool bodyReady, bool expeditionOnly, bool expeditionsAllowed, bool tripInFlight, bool armed)
		{
			Buttons result = new Buttons
			{
				Interactive = !tripInFlight,
				SpawnLabel = "Spawn"
			};
			if (tripInFlight)
			{
				return result;
			}
			bool flag = expeditionOnly && !bodyReady && expeditionsAllowed;
			result.ShowPrewarm = !bodyReady && !expeditionOnly;
			result.ShowWarmTrip = flag;
			result.SpawnCostsATrip = flag;
			if (flag)
			{
				result.SpawnLabel = (armed ? "Confirm?" : "Spawn (trip)");
			}
			return result;
		}

		public static ClickResult Click(Arm current, string key, float now, float windowSeconds, bool confirmRequired)
		{
			if (!confirmRequired)
			{
				return new ClickResult
				{
					Fire = true,
					State = default(Arm)
				};
			}
			if (IsArmed(current, key, now, windowSeconds))
			{
				return new ClickResult
				{
					Fire = true,
					State = default(Arm)
				};
			}
			return new ClickResult
			{
				Fire = false,
				State = new Arm
				{
					Key = key,
					At = now
				}
			};
		}

		public static bool IsArmed(Arm current, string key, float now, float windowSeconds)
		{
			if (!string.IsNullOrEmpty(current.Key) && string.Equals(current.Key, key, StringComparison.OrdinalIgnoreCase) && now - current.At >= 0f)
			{
				return now - current.At <= windowSeconds;
			}
			return false;
		}
	}
	public enum FailReason
	{
		None,
		NotInitialized,
		Disabled,
		NotMaster,
		NoPlayer,
		NoAIManager,
		CapExceeded,
		UnknownSpecies,
		MintFailed,
		Cancelled
	}
	public enum CursorStrategy
	{
		None,
		GameSeamReleased,
		VanillaMenuOwns
	}
	public static class MenuCursorPolicy
	{
		public static bool EngageSeam(bool menuOpen, bool vanillaMenuFocused)
		{
			if (menuOpen)
			{
				return !vanillaMenuFocused;
			}
			return false;
		}

		public static bool ForceClose(bool menuOpen, bool vanillaMenuFocused)
		{
			return menuOpen && vanillaMenuFocused;
		}

		public static bool CanOpen(bool vanillaMenuFocused)
		{
			return !vanillaMenuFocused;
		}

		public static bool CloseOnEsc(bool menuOpen, bool escPressed)
		{
			return menuOpen && escPressed;
		}

		public static CursorStrategy Resolve(bool menuOpen, bool vanillaMenuFocused)
		{
			if (!menuOpen)
			{
				return CursorStrategy.None;
			}
			if (!vanillaMenuFocused)
			{
				return CursorStrategy.GameSeamReleased;
			}
			return CursorStrategy.VanillaMenuOwns;
		}

		public static CursorStrategy ResolveLog(bool nowOpen, bool vanillaMenuFocused)
		{
			if (vanillaMenuFocused)
			{
				return CursorStrategy.VanillaMenuOwns;
			}
			if (!nowOpen)
			{
				return CursorStrategy.None;
			}
			return CursorStrategy.GameSeamReleased;
		}
	}
	public static class RingPlacement
	{
		public const float FarRingFactor = 1.6f;

		public const float StepDegrees = 45f;

		public static IReadOnlyList<(float x, float z)> Candidates(float forwardX, float forwardZ, float distance, int count)
		{
			if (count <= 0)
			{
				return Array.Empty<(float, float)>();
			}
			if (distance <= 0f)
			{
				throw new ArgumentOutOfRangeException("distance");
			}
			float num = (float)Math.Sqrt(forwardX * forwardX + forwardZ * forwardZ);
			float num2;
			float num3;
			if (num < 0.0001f)
			{
				num2 = 0f;
				num3 = 1f;
			}
			else
			{
				num2 = forwardX / num;
				num3 = forwardZ / num;
			}
			List<(float, float)> list = new List<(float, float)>(count);
			int num4 = (int)Math.Round(8.0);
			for (int i = 0; i < count; i++)
			{
				int num5 = i % num4;
				float num6 = ((i < num4) ? distance : (distance * 1.6f));
				double num7 = (double)((num5 == 0) ? 0f : ((num5 == num4 - 1) ? 180f : (45f * (float)((num5 + 1) / 2) * ((num5 % 2 == 1) ? 1f : (-1f))))) * Math.PI / 180.0;
				float num8 = (float)Math.Cos(num7);
				float num9 = (float)Math.Sin(num7);
				float num10 = num2 * num8 + num3 * num9;
				float num11 = (0f - num2) * num9 + num3 * num8;
				list.Add((num10 * num6, num11 * num6));
			}
			return list;
		}
	}
	public static class SpawnCap
	{
		public const int DefaultCacheWarnThreshold = 8;

		public static IReadOnlyList<string> PickEvict(IReadOnlyList<string> lruOrder, int count, int max, ICollection<string>? exclude = null)
		{
			List<string> list = new List<string>();
			if (max <= 0 || count <= max)
			{
				return list;
			}
			int num = count - max;
			for (int i = 0; i < lruOrder.Count; i++)
			{
				if (list.Count >= num)
				{
					break;
				}
				string item = lruOrder[i];
				if (exclude == null || !exclude.Contains(item))
				{
					list.Add(item);
				}
			}
			return list;
		}

		public static bool ShouldWarnCacheSize(int count, int threshold)
		{
			if (threshold > 0 && count > 0)
			{
				return count % threshold == 0;
			}
			return false;
		}

		public static bool IsCacheLarge(int count, int threshold)
		{
			if (threshold > 0)
			{
				return count >= threshold;
			}
			return false;
		}
	}
	public static class SpawnMenuLabels
	{
		public static bool IsDonorMismatch(string key, string donorName)
		{
			if (!string.IsNullOrEmpty(donorName) && !string.IsNullOrEmpty(key))
			{
				return !string.Equals(key.Trim(), donorName.Trim(), StringComparison.OrdinalIgnoreCase);
			}
			return false;
		}

		public static string RowLabel(string key, string resolvedDonorName, bool expeditionOnly)
		{
			string text = key ?? "";
			if (IsDonorMismatch(key, resolvedDonorName))
			{
				text = text + "  → spawns “" + resolvedDonorName.Trim() + "”";
			}
			if (expeditionOnly)
			{
				text += "  (expedition-only — see log)";
			}
			return text;
		}
	}
	public static class SpawnPolicy
	{
		public const float MinDistance = 1f;

		public const float MaxDistance = 50f;

		public static float EffectiveDistance(float? requested, float configured)
		{
			if (!requested.HasValue)
			{
				return configured;
			}
			float value = requested.Value;
			if (value < 1f)
			{
				return 1f;
			}
			if (value > 50f)
			{
				return 50f;
			}
			return value;
		}

		public static string NormalizeOwnerTag(string? tag)
		{
			return tag?.Trim() ?? "";
		}

		public static bool MatchesOwner(string recordTag, string? filter)
		{
			if (filter != null)
			{
				return string.Equals(NormalizeOwnerTag(recordTag), NormalizeOwnerTag(filter), StringComparison.OrdinalIgnoreCase);
			}
			return true;
		}
	}
	public static class SpawnUid
	{
		public const string Prefix = "SK_";

		public const string ItemPrefix = "SKi_";

		public static string Mint(Guid guid)
		{
			return "SK_" + guid.ToString("N");
		}

		public static string MintItem(Guid guid)
		{
			return "SKi_" + guid.ToString("N");
		}

		public static bool IsSpawnUid(string? uid)
		{
			if (uid != null && uid.StartsWith("SK_", StringComparison.Ordinal))
			{
				return uid.Length > "SK_".Length;
			}
			return false;
		}

		public static bool IsSpawnItemUid(string? uid)
		{
			if (uid != null && uid.StartsWith("SKi_", StringComparison.Ordinal))
			{
				return uid.Length > "SKi_".Length;
			}
			return false;
		}
	}
	public sealed class SpawnVerbArgs
	{
		public string Species = "";

		public float? Distance;

		public float? LifetimeSeconds;

		public string? Faction;

		public string? OwnerTag;

		public string? Body;

		public float? CorpseLingerSeconds;

		public bool KeepQuestEvents;

		public readonly List<string> UnknownOptions = new List<string>();

		public static SpawnVerbArgs Parse(string[] tokens)
		{
			SpawnVerbArgs spawnVerbArgs = new SpawnVerbArgs();
			List<string> list = new List<string>();
			for (int i = 1; i < tokens.Length; i++)
			{
				string text = tokens[i];
				if (string.IsNullOrWhiteSpace(text))
				{
					continue;
				}
				if (string.Equals(text, "keepquest", StringComparison.OrdinalIgnoreCase))
				{
					spawnVerbArgs.KeepQuestEvents = true;
					continue;
				}
				int num = text.IndexOf('=');
				if (num <= 0)
				{
					list.Add(text);
					continue;
				}
				string text2 = text.Substring(0, num).Trim().ToLowerInvariant();
				string text3 = text.Substring(num + 1).Trim();
				switch (text2)
				{
				case "dist":
					spawnVerbArgs.Distance = ParseFloat(text3, spawnVerbArgs, text);
					break;
				case "life":
					spawnVerbArgs.LifetimeSeconds = ParseFloat(text3, spawnVerbArgs, text);
					break;
				case "faction":
					spawnVerbArgs.Faction = text3;
					break;
				case "owner":
					spawnVerbArgs.OwnerTag = text3;
					break;
				case "body":
					spawnVerbArgs.Body = text3;
					break;
				case "linger":
					spawnVerbArgs.CorpseLingerSeconds = ParseFloat(text3, spawnVerbArgs, text);
					break;
				default:
					spawnVerbArgs.UnknownOptions.Add(text);
					break;
				}
			}
			spawnVerbArgs.Species = string.Join(" ", list).Trim();
			return spawnVerbArgs;
		}

		private static float? ParseFloat(string val, SpawnVerbArgs result, string token)
		{
			if (float.TryParse(val, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2))
			{
				return result2;
			}
			result.UnknownOptions.Add(token);
			return null;
		}
	}
	public static class SpawnWatch
	{
		public const float DefaultIntervalSeconds = 0.5f;

		public static WatchState Next(WatchState current, bool exists, bool alive)
		{
			if (current != WatchState.Alive)
			{
				return current;
			}
			if (!exists)
			{
				return WatchState.Despawned;
			}
			if (!alive)
			{
				return WatchState.Died;
			}
			return WatchState.Alive;
		}

		public static bool IsTerminal(WatchState state)
		{
			if (state != WatchState.Died && state != WatchState.Despawned)
			{
				return state == WatchState.Failed;
			}
			return true;
		}
	}
	public enum WatchState
	{
		Pending,
		Alive,
		Died,
		Despawned,
		Failed
	}
}

SpawnKit/SpawnKit.dll

Decompiled 2 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using CompanionKit;
using CompanionKit.Core;
using ForgeKit;
using HarmonyLib;
using SpawnKit.Core;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("SpawnKit")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+d10381aec7116e1ea7ae2d3ff4646fae57640f75")]
[assembly: AssemblyProduct("SpawnKit")]
[assembly: AssemblyTitle("SpawnKit")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.0.0")]
[module: UnverifiableCode]
namespace SpawnKit;

public static class BoneProbe
{
	public static void Dump(string nameFilter, float radius = 30f)
	{
		//IL_0074: Unknown result type (might be due to invalid IL or missing references)
		//IL_007f: 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)
		//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
		CharacterManager instance = CharacterManager.Instance;
		Character val = ((instance != null) ? instance.GetFirstLocalCharacter() : null);
		if ((Object)(object)val == (Object)null)
		{
			Plugin.Log.LogWarning((object)"[BONES] no local player — nothing to probe.");
			return;
		}
		DictionaryExt<string, Character> characters = instance.Characters;
		StringBuilder stringBuilder = new StringBuilder();
		int num = 0;
		for (int i = 0; i < characters.Count; i++)
		{
			Character val2 = characters.Values[i];
			if ((Object)(object)val2 == (Object)null || (Object)(object)val2 == (Object)(object)val)
			{
				continue;
			}
			float num2 = Vector3.Distance(((Component)val2).transform.position, ((Component)val).transform.position);
			if (!(num2 > radius) && (string.IsNullOrEmpty(nameFilter) || (val2.Name != null && val2.Name.IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0)))
			{
				string text = "?";
				try
				{
					text = ((object)val2.UID/*cast due to .constrained prefix*/).ToString();
				}
				catch
				{
				}
				bool flag = SpawnUid.IsSpawnUid(text);
				stringBuilder.Append(string.Format("\n  '{0}' uid={1} spawned={2} dist={3:0.0}m", val2.Name, text, flag ? "Y" : "N", num2));
				stringBuilder.Append('\n').Append(SkeletonRig.Census(((Component)val2).gameObject));
				num++;
			}
		}
		Plugin.Log.LogMessage((object)((num == 0) ? string.Format("[BONES] no characters within {0:0}m{1}.", radius, string.IsNullOrEmpty(nameFilter) ? "" : (" matching '" + nameFilter + "'")) : string.Format("[BONES] {0} character(s) within {1:0}m{2}:{3}", num, radius, string.IsNullOrEmpty(nameFilter) ? "" : (" matching '" + nameFilter + "'"), stringBuilder)));
	}
}
[HarmonyPatch(/*Could not decode attribute arguments.*/)]
internal static class CursorControl
{
	internal static bool MenuOpen;

	internal static bool VanillaMenuFocused;

	private static CursorStrategy _engaged;

	private static CharacterUI OwnerUI
	{
		get
		{
			CharacterManager instance = CharacterManager.Instance;
			if (instance == null)
			{
				return null;
			}
			return instance.GetFirstLocalCharacter()?.CharacterUI;
		}
	}

	private static void Postfix(CharacterUI __instance, ref bool __result)
	{
		if (__instance == OwnerUI)
		{
			VanillaMenuFocused = __result;
			if (MenuCursorPolicy.EngageSeam(MenuOpen, __result))
			{
				__result = true;
			}
		}
	}

	internal static void SetMenuOpen(bool open)
	{
		//IL_000c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_0013: Unknown result type (might be due to invalid IL or missing references)
		//IL_001b: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Unknown result type (might be due to invalid IL or missing references)
		//IL_0023: Invalid comparison between Unknown and I4
		//IL_0025: Unknown result type (might be due to invalid IL or missing references)
		//IL_0027: Invalid comparison between Unknown and I4
		MenuOpen = open;
		CursorStrategy val = MenuCursorPolicy.ResolveLog(open, VanillaMenuFocused);
		if (val == _engaged)
		{
			return;
		}
		_engaged = val;
		if ((int)val != 1)
		{
			if ((int)val == 2)
			{
				Plugin.Log.LogMessage((object)"[MENU] cursor: vanilla menu owns cursor (yielding — our window force-closes).");
			}
			else
			{
				Plugin.Log.LogMessage((object)"[MENU] cursor: game-seam restored (menu closed — cursor + camera/movement back under game control).");
			}
		}
		else
		{
			Plugin.Log.LogMessage((object)"[MENU] cursor: game-seam released (IsMenuFocused override engaged — game frees cursor + stops camera/movement).");
		}
	}
}
internal static class EnemySpawner
{
	private static readonly List<SpawnHandle> _spawns = new List<SpawnHandle>();

	private static GameObject _mintHolder;

	internal static int TrackedCount => _spawns.Count;

	private static void ReleaseViewId(int viewId)
	{
		if (viewId < 0)
		{
			return;
		}
		try
		{
			PhotonNetwork.UnAllocateViewID(viewId);
		}
		catch (Exception ex)
		{
			Plugin.Log.LogWarning((object)$"[SPAWN] UnAllocateViewID({viewId}) threw: {ex.Message}");
		}
	}

	private static GameObject MintHolder()
	{
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Expected O, but got Unknown
		if ((Object)(object)_mintHolder == (Object)null)
		{
			_mintHolder = new GameObject("SK_MintHolder");
			_mintHolder.SetActive(false);
		}
		return _mintHolder;
	}

	internal static FailReason Preflight(SpawnHandle handle)
	{
		if (!((Object)(object)Plugin.Instance == (Object)null) && Plugin.Log != null)
		{
			if (!Plugin.Enabled.Value)
			{
				Plugin.Log.LogWarning((object)"[SPAWN] refused: [Spawner] Enabled=false (kill-switch).");
				return (FailReason)2;
			}
			if (PhotonNetwork.isNonMasterClientInRoom)
			{
				Plugin.Log.LogWarning((object)"[SPAWN] refused: only the master client spawns (clients receive, Phase 3).");
				return (FailReason)3;
			}
			CharacterManager instance = CharacterManager.Instance;
			Character val = ((instance != null) ? instance.GetFirstLocalCharacter() : null);
			if ((Object)(object)val == (Object)null)
			{
				Plugin.Log.LogWarning((object)"[SPAWN] refused: no local player character yet.");
				return (FailReason)4;
			}
			if ((Object)(object)AISquadManager.Instance == (Object)null)
			{
				Notify.Player(val, "Cannot spawn here (no AI manager in this scene).");
				return (FailReason)5;
			}
			WatchTick();
			if (_spawns.Count + 1 > Plugin.MaxActiveSpawns.Value)
			{
				Notify.Player(val, $"Spawn refused: would exceed MaxActiveSpawns ({_spawns.Count} active/pending, cap {Plugin.MaxActiveSpawns.Value}).");
				return (FailReason)6;
			}
			_spawns.Add(handle);
			return (FailReason)0;
		}
		return (FailReason)1;
	}

	internal static IEnumerator SpawnRoutine(SpawnHandle handle, SpawnOptions opts)
	{
		GameObject template = null;
		bool acquired = false;
		yield return SpawnTemplates.Acquire(handle.SpeciesKey, delegate(GameObject t)
		{
			template = t;
			acquired = true;
		});
		while (!acquired)
		{
			yield return null;
		}
		if (handle.State != SpawnState.Pending)
		{
			yield break;
		}
		CharacterManager instance = CharacterManager.Instance;
		Character val = ((instance != null) ? instance.GetFirstLocalCharacter() : null);
		if ((Object)(object)template == (Object)null)
		{
			if ((Object)(object)val != (Object)null)
			{
				Notify.Player(val, "No spawnable species matches '" + handle.SpeciesKey + "' (see log / 'spawnlist').");
			}
			Fail(handle, (FailReason)7);
		}
		else if ((Object)(object)val == (Object)null)
		{
			Fail(handle, (FailReason)4);
		}
		else if (!Mint(template, val, opts, handle))
		{
			Fail(handle, (FailReason)8);
		}
		else
		{
			handle.Resolve(SpawnState.Alive, (FailReason)0);
		}
	}

	private static void Fail(SpawnHandle handle, FailReason reason)
	{
		//IL_000e: Unknown result type (might be due to invalid IL or missing references)
		_spawns.Remove(handle);
		handle.Resolve(SpawnState.Failed, reason);
	}

	private static bool Mint(GameObject template, Character player, SpawnOptions opts, SpawnHandle handle)
	{
		//IL_045a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0068: Unknown result type (might be due to invalid IL or missing references)
		//IL_04d5: Unknown result type (might be due to invalid IL or missing references)
		//IL_04e0: Unknown result type (might be due to invalid IL or missing references)
		//IL_04e6: Invalid comparison between Unknown and I4
		//IL_020b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0210: Unknown result type (might be due to invalid IL or missing references)
		//IL_023f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0244: Unknown result type (might be due to invalid IL or missing references)
		//IL_0249: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a4: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a9: Unknown result type (might be due to invalid IL or missing references)
		//IL_027c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0281: Unknown result type (might be due to invalid IL or missing references)
		//IL_02b1: Unknown result type (might be due to invalid IL or missing references)
		//IL_02b6: Unknown result type (might be due to invalid IL or missing references)
		//IL_02b8: Unknown result type (might be due to invalid IL or missing references)
		//IL_02bd: Unknown result type (might be due to invalid IL or missing references)
		//IL_02c2: Unknown result type (might be due to invalid IL or missing references)
		//IL_02f5: Unknown result type (might be due to invalid IL or missing references)
		//IL_02fa: Unknown result type (might be due to invalid IL or missing references)
		//IL_0302: Unknown result type (might be due to invalid IL or missing references)
		//IL_0304: Unknown result type (might be due to invalid IL or missing references)
		//IL_02ea: Unknown result type (might be due to invalid IL or missing references)
		//IL_02ec: Unknown result type (might be due to invalid IL or missing references)
		//IL_02e3: Unknown result type (might be due to invalid IL or missing references)
		GameObject val = Object.Instantiate<GameObject>(template, MintHolder().transform);
		int num = -1;
		try
		{
			((Object)val).name = ((Object)template).name.Replace("SK_Template_", "SK_Spawn_");
			Character component = val.GetComponent<Character>();
			if ((Object)(object)component == (Object)null)
			{
				Plugin.Log.LogError((object)"[SPAWN] template clone has no Character component — destroying.");
				Object.Destroy((Object)(object)val);
				return false;
			}
			string text = SpawnUid.Mint(Guid.NewGuid());
			component.SetUID(UID.op_Implicit(text));
			PhotonView component2 = val.GetComponent<PhotonView>();
			if ((Object)(object)component2 != (Object)null)
			{
				num = (component2.viewID = PhotonNetwork.AllocateViewID());
			}
			int num3 = 0;
			Item[] componentsInChildren = val.GetComponentsInChildren<Item>(true);
			foreach (Item val2 in componentsInChildren)
			{
				if (!((Object)(object)val2 == (Object)null))
				{
					try
					{
						val2.UID = SpawnUid.MintItem(Guid.NewGuid());
						num3++;
					}
					catch (Exception ex)
					{
						Plugin.Log.LogWarning((object)("[SPAWN] re-UID of a child item threw: " + ex.Message));
					}
				}
			}
			Plugin.Log.LogMessage((object)$"[SPAWN] re-UID'd {num3} child item(s) with fresh SKi_ UIDs (SK-A weapon-equip fix).");
			if (opts.StripQuestEvents)
			{
				QuestEventOnDeath[] componentsInChildren2 = val.GetComponentsInChildren<QuestEventOnDeath>(true);
				QuestEventOnDeath[] array = componentsInChildren2;
				foreach (QuestEventOnDeath val3 in array)
				{
					if ((Object)(object)val3 != (Object)null)
					{
						Object.DestroyImmediate((Object)(object)val3);
					}
				}
				if (componentsInChildren2.Length != 0)
				{
					Plugin.Log.LogMessage((object)$"[SPAWN] stripped {componentsInChildren2.Length} QuestEventOnDeath from '{((Object)val).name}' (StripQuestEvents).");
				}
			}
			if (Plugin.ForceLootableEnabled.Value)
			{
				int num4 = 0;
				LootableOnDeath[] componentsInChildren3 = val.GetComponentsInChildren<LootableOnDeath>(true);
				foreach (LootableOnDeath val4 in componentsInChildren3)
				{
					if ((Object)(object)val4 != (Object)null && !((Behaviour)val4).enabled)
					{
						((Behaviour)val4).enabled = true;
						num4++;
					}
				}
				if (num4 > 0)
				{
					Plugin.Log.LogMessage((object)$"[SPAWN] re-enabled {num4} disabled LootableOnDeath on '{((Object)val).name}' (ForceLootableEnabled — corpse-loot backstop).");
				}
			}
			if (opts.Faction.HasValue)
			{
				component.Faction = opts.Faction.Value;
			}
			if (opts.LifetimeSeconds.HasValue)
			{
				component.Lifetime = opts.LifetimeSeconds.Value;
			}
			handle.CorpsePolicy = CorpseRules.EffectivePolicy(opts.Corpse, Plugin.DefaultCorpsePolicy.Value);
			handle.CorpseLingerSeconds = CorpseRules.EffectiveLinger(opts.CorpseLingerSeconds, Plugin.DefaultCorpseLingerSeconds.Value);
			Vector3 val5;
			string note;
			if (opts.Position.HasValue)
			{
				val5 = opts.Position.Value;
				note = "pos=explicit";
			}
			else
			{
				val5 = PickGround(player, SpawnPolicy.EffectiveDistance(opts.Distance, Plugin.SpawnDistance.Value), out note);
			}
			Vector3 val6 = Flat(((Component)player).transform.position - val5);
			Quaternion val7 = (Quaternion)(((??)opts.Rotation) ?? ((((Vector3)(ref val6)).sqrMagnitude > 0.01f) ? Quaternion.LookRotation(val6) : Quaternion.identity));
			val.transform.SetPositionAndRotation(val5, val7);
			val.transform.SetParent((Transform)null, true);
			CharacterAI component3 = val.GetComponent<CharacterAI>();
			try
			{
				if (component3 != null)
				{
					component3.InitStartPos();
				}
			}
			catch
			{
			}
			string arg = "?";
			string arg2 = "?";
			string text2 = "?";
			string text3 = "?";
			string text4 = "?";
			try
			{
				arg = (((Object)(object)component3 != (Object)null && component3.AiStates != null) ? component3.AiStates.Length : 0).ToString();
			}
			catch
			{
			}
			try
			{
				arg2 = (((Object)(object)component3 != (Object)null && (Object)(object)component3.CurrentAiState != (Object)null) ? ((object)component3.CurrentAiState).GetType().Name : "none");
			}
			catch
			{
			}
			try
			{
				text2 = (((Object)(object)component.Stats != (Object)null) ? component.Stats.CurrentHealth.ToString("0.#") : "?");
			}
			catch
			{
			}
			try
			{
				text3 = ((object)Unsafe.As<Factions, Factions>(ref component.Faction)/*cast due to .constrained prefix*/).ToString();
			}
			catch
			{
			}
			try
			{
				text4 = component.Alive.ToString();
			}
			catch
			{
			}
			Plugin.Log.LogMessage((object)($"[SPAWN] '{((Object)val).name}' uid={text} viewID={num} owner='{handle.OwnerTag}' pos={val5:F1} ({note}) | " + string.Format("active={0} alive={1} hp={2} faction={3} lifetime={4} ", val.activeInHierarchy, text4, text2, text3, opts.LifetimeSeconds.HasValue ? opts.LifetimeSeconds.Value.ToString("0.#") : "donor") + string.Format("corpse={0}{1} | ", handle.CorpsePolicy, ((int)handle.CorpsePolicy == 1) ? $"({handle.CorpseLingerSeconds:0.#}s)" : "") + $"aiStates={arg} state={arg2} agent={(Object)(object)val.GetComponent<NavMeshAgent>() != (Object)null} " + $"charAIDisable={(Object)(object)val.GetComponent<CharAIDisable>() != (Object)null}"));
			if (!val.activeInHierarchy)
			{
				Plugin.Log.LogError((object)"[SPAWN] spawn is INACTIVE right after activation — duplicate-UID guard signature (grep output_log.txt for 'has the same UID').");
			}
			handle.Character = component;
			handle.Uid = text;
			handle.ViewId = num;
			return true;
		}
		catch (Exception arg3)
		{
			Plugin.Log.LogError((object)$"[SPAWN] mint threw — destroying pending clone and releasing viewID {num}: {arg3}");
			ReleaseViewId(num);
			if ((Object)(object)val != (Object)null)
			{
				Object.Destroy((Object)(object)val);
			}
			return false;
		}
	}

	private static Vector3 PickGround(Character player, float distance, out string note)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_000b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_0017: 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_001e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0036: Unknown result type (might be due to invalid IL or missing references)
		//IL_0049: Unknown result type (might be due to invalid IL or missing references)
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0067: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ca: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
		//IL_007a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0084: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ec: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f0: Unknown result type (might be due to invalid IL or missing references)
		//IL_00df: 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_00f6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
		Vector3 position = ((Component)player).transform.position;
		Vector3 forward = ((Component)player).transform.forward;
		IReadOnlyList<(float, float)> readOnlyList = RingPlacement.Candidates(forward.x, forward.z, distance, 16);
		Vector3 val = default(Vector3);
		NavMeshHit val2 = default(NavMeshHit);
		for (int i = 0; i < readOnlyList.Count; i++)
		{
			((Vector3)(ref val))..ctor(position.x + readOnlyList[i].Item1, position.y, position.z + readOnlyList[i].Item2);
			if (NavMesh.SamplePosition(val, ref val2, 4f, -1) && Mathf.Abs(((NavMeshHit)(ref val2)).position.y - position.y) <= 2.5f)
			{
				note = $"ring[{i}]";
				return ((NavMeshHit)(ref val2)).position;
			}
		}
		note = "NO navmesh candidate in elevation band — placed ahead unprobed";
		Vector3 val3 = Flat(forward);
		Vector3 val4;
		if (!(((Vector3)(ref val3)).sqrMagnitude > 0.01f))
		{
			val4 = Vector3.forward;
		}
		else
		{
			val3 = Flat(forward);
			val4 = ((Vector3)(ref val3)).normalized;
		}
		return position + val4 * distance;
	}

	private static Vector3 Flat(Vector3 v)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_000b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		return new Vector3(v.x, 0f, v.z);
	}

	internal static void Despawn(SpawnHandle handle, bool kill)
	{
		//IL_004a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0050: Unknown result type (might be due to invalid IL or missing references)
		if (handle == null)
		{
			return;
		}
		if (handle.State == SpawnState.Pending)
		{
			Fail(handle, (FailReason)9);
		}
		else
		{
			if (handle.State != SpawnState.Alive)
			{
				return;
			}
			Character character = handle.Character;
			if (kill && (Object)(object)character != (Object)null && SafeAlive(character))
			{
				character.ReceiveHit((Weapon)null, 999999f, Vector3.forward, character.CenterPosition, 45f, 1f, (Character)null, 0f);
				if (!SafeAlive(character))
				{
					Plugin.Log.LogMessage((object)("[SPAWN] overkilled '" + ((Object)((Component)character).gameObject).name + "' (uid " + handle.Uid + ") — WatchTick will resolve the death (loot + disengage on the Died transition)."));
					return;
				}
				Plugin.Log.LogWarning((object)("[SPAWN] overkill left '" + ((Object)((Component)character).gameObject).name + "' Alive (invincible/resistant?) — falling back to a silent despawn (uid " + handle.Uid + ")."));
			}
			if (_spawns.Remove(handle))
			{
				DisengageSpawn((RemovalReason)0, character);
				if ((Object)(object)character != (Object)null)
				{
					Object.Destroy((Object)(object)((Component)character).gameObject);
				}
				ReleaseViewId(handle.ViewId);
				handle.State = SpawnState.Despawned;
				handle.FireDespawned();
			}
		}
	}

	public static void DespawnAll(string ownerTag, bool kill)
	{
		WatchTick();
		int num = 0;
		SpawnHandle[] array = _spawns.ToArray();
		foreach (SpawnHandle spawnHandle in array)
		{
			if (SpawnPolicy.MatchesOwner(spawnHandle.OwnerTag, ownerTag))
			{
				num++;
				Despawn(spawnHandle, kill);
			}
		}
		Plugin.Log.LogMessage((object)("[SPAWN] despawnall (" + (kill ? "kill" : "silent") + ((ownerTag != null) ? (", owner '" + ownerTag + "'") : "") + $"): {num} processed, {_spawns.Count} tracked remain."));
	}

	internal static IReadOnlyList<SpawnHandle> Snapshot(string ownerTag)
	{
		List<SpawnHandle> list = new List<SpawnHandle>();
		foreach (SpawnHandle spawn in _spawns)
		{
			if (SpawnPolicy.MatchesOwner(spawn.OwnerTag, ownerTag))
			{
				list.Add(spawn);
			}
		}
		return list;
	}

	internal static int LootProbeAll()
	{
		WatchTick();
		int num = 0;
		SpawnHandle[] array = _spawns.ToArray();
		foreach (SpawnHandle spawnHandle in array)
		{
			if (spawnHandle.State == SpawnState.Alive)
			{
				Character character = spawnHandle.Character;
				if (!((Object)(object)character == (Object)null))
				{
					LootProbe.Log(character, "probe '" + spawnHandle.SpeciesKey + "' (owner '" + spawnHandle.OwnerTag + "')");
					num++;
				}
			}
		}
		return num;
	}

	public static string Dump()
	{
		//IL_002b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0030: Unknown result type (might be due to invalid IL or missing references)
		//IL_01b6: Unknown result type (might be due to invalid IL or missing references)
		//IL_01c2: Unknown result type (might be due to invalid IL or missing references)
		//IL_0220: Unknown result type (might be due to invalid IL or missing references)
		WatchTick();
		CharacterManager instance = CharacterManager.Instance;
		Character val = ((instance != null) ? instance.GetFirstLocalCharacter() : null);
		object arg = _spawns.Count;
		Scene activeScene = SceneManager.GetActiveScene();
		StringBuilder stringBuilder = new StringBuilder($"[SPAWN] {arg} tracked spawn(s) in '{((Scene)(ref activeScene)).name}':");
		foreach (SpawnHandle spawn in _spawns)
		{
			if (spawn.State == SpawnState.Pending)
			{
				stringBuilder.Append("\n  '" + spawn.SpeciesKey + "' owner='" + spawn.OwnerTag + "' state=Pending (template acquiring)");
				continue;
			}
			Character character = spawn.Character;
			if ((Object)(object)character == (Object)null)
			{
				continue;
			}
			string text = "?";
			string text2 = "?";
			string text3 = "-";
			try
			{
				text = (((Object)(object)character.Stats != (Object)null) ? $"{character.Stats.CurrentHealth:0.#}/{character.Stats.MaxHealth:0.#}" : "?");
			}
			catch
			{
			}
			CharacterAI component = ((Component)character).GetComponent<CharacterAI>();
			try
			{
				text2 = (((Object)(object)component != (Object)null && (Object)(object)component.CurrentAiState != (Object)null) ? ((object)component.CurrentAiState).GetType().Name : "none");
			}
			catch
			{
			}
			try
			{
				Character val2 = (((Object)(object)component != (Object)null && (Object)(object)component.TargetingSystem != (Object)null) ? component.TargetingSystem.LockedCharacter : null);
				if ((Object)(object)val2 != (Object)null)
				{
					text3 = val2.Name;
				}
			}
			catch
			{
			}
			float num = (((Object)(object)val != (Object)null) ? Vector3.Distance(((Component)val).transform.position, ((Component)character).transform.position) : (-1f));
			stringBuilder.Append($"\n  '{((Object)((Component)character).gameObject).name}' species='{spawn.SpeciesKey}' owner='{spawn.OwnerTag}' lifecycle={spawn.State} " + $"uid={character.UID} viewID={spawn.ViewId} alive={SafeAlive(character)} hp={text} dist={num:0.#} " + $"active={((Component)character).gameObject.activeInHierarchy} aiEnabled={(Object)(object)component != (Object)null && ((Behaviour)component).enabled} state={text2} target={text3}");
		}
		return stringBuilder.ToString();
	}

	public static void WatchTick()
	{
		//IL_0051: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Expected I4, but got Unknown
		for (int num = _spawns.Count - 1; num >= 0; num--)
		{
			SpawnHandle spawnHandle = _spawns[num];
			if (spawnHandle.State == SpawnState.Alive)
			{
				bool flag = (Object)(object)spawnHandle.Character != (Object)null;
				bool flag2 = flag && SafeAlive(spawnHandle.Character);
				SpawnState spawnState = (SpawnState)SpawnWatch.Next((WatchState)spawnHandle.State, flag, flag2);
				if (spawnState != spawnHandle.State)
				{
					GameObject corpse = ((spawnState == SpawnState.Died) ? ((Component)spawnHandle.Character).gameObject : null);
					if (spawnState == SpawnState.Died)
					{
						LootProbe.Log(spawnHandle.Character, "death '" + spawnHandle.SpeciesKey + "'");
					}
					DisengageSpawn((RemovalReason)((spawnState == SpawnState.Died) ? 2 : 3), (spawnState == SpawnState.Died) ? spawnHandle.Character : null);
					_spawns.RemoveAt(num);
					ReleaseViewId(spawnHandle.ViewId);
					spawnHandle.State = spawnState;
					Plugin.Log.LogMessage((object)$"[WATCH] '{spawnHandle.SpeciesKey}' (owner '{spawnHandle.OwnerTag}', uid {spawnHandle.Uid}) -> {spawnState}.");
					if (spawnState == SpawnState.Died)
					{
						spawnHandle.FireDied();
					}
					else
					{
						spawnHandle.FireDespawned();
					}
					if (spawnState == SpawnState.Died)
					{
						ScheduleCorpseRemoval(spawnHandle, corpse);
					}
				}
			}
		}
	}

	private static void DisengageSpawn(RemovalReason reason, Character spawn)
	{
		//IL_0000: 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)
		if (!DisengagePolicy.NeedsDisengage(reason))
		{
			return;
		}
		CharacterManager instance = CharacterManager.Instance;
		if ((Object)(object)instance == (Object)null)
		{
			return;
		}
		bool flag = DisengagePolicy.ByReference((Object)(object)spawn != (Object)null);
		int num = 0;
		List<Character> list = instance.Characters?.Values;
		if (list != null)
		{
			Character[] array = list.ToArray();
			foreach (Character val in array)
			{
				if ((Object)(object)val == (Object)null || val == spawn)
				{
					continue;
				}
				List<Character> list2 = null;
				try
				{
					list2 = val.EngagedCharacters;
				}
				catch
				{
				}
				if (list2 == null || list2.Count == 0)
				{
					continue;
				}
				bool flag2 = false;
				for (int num2 = list2.Count - 1; num2 >= 0; num2--)
				{
					Character val2 = list2[num2];
					if (flag)
					{
						if (val2 == spawn)
						{
							try
							{
								val.RemoveCombatEngagement(spawn);
							}
							catch
							{
							}
							flag2 = true;
						}
					}
					else if (DisengagePolicy.IsStaleEntry((Object)(object)val2 != (Object)null, SafeAlive(val2)))
					{
						list2.RemoveAt(num2);
						flag2 = true;
					}
				}
				if (!flag2)
				{
					continue;
				}
				num++;
				if (!flag && list2.Count == 0 && (Object)(object)Global.CombatManager != (Object)null)
				{
					try
					{
						Global.CombatManager.RemoveCombatCharacter(val);
					}
					catch
					{
					}
				}
			}
		}
		if (flag && (Object)(object)spawn != (Object)null)
		{
			try
			{
				spawn.ResetCombat();
			}
			catch
			{
			}
		}
		if (num > 0)
		{
			Plugin.Log.LogMessage((object)$"[SPAWN] disengaged {num} character(s) on {reason} (uid was tracked).");
		}
	}

	private static void ScheduleCorpseRemoval(SpawnHandle handle, GameObject corpse)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_0007: Invalid comparison between Unknown and I4
		if ((int)handle.CorpsePolicy == 1 && !((Object)(object)corpse == (Object)null) && !((Object)(object)Plugin.Instance == (Object)null))
		{
			((MonoBehaviour)Plugin.Instance).StartCoroutine(RemoveCorpse(corpse, handle.SpeciesKey, handle.OwnerTag, handle.CorpseLingerSeconds));
		}
	}

	private static IEnumerator RemoveCorpse(GameObject corpse, string species, string owner, float linger)
	{
		if (linger > 0f)
		{
			yield return (object)new WaitForSeconds(linger);
		}
		else
		{
			yield return null;
		}
		if (!((Object)(object)corpse == (Object)null))
		{
			Object.Destroy((Object)(object)corpse);
			Plugin.Log.LogMessage((object)$"[SPAWN] corpse removed (NoBody): species='{species}' owner='{owner}' linger={linger:0.#}s.");
		}
	}

	private static bool SafeAlive(Character c)
	{
		try
		{
			return c.Alive;
		}
		catch
		{
			return false;
		}
	}
}
internal static class ExpeditionRun
{
	public static bool InProgress { get; private set; }

	public static string CurrentSpecies { get; private set; }

	public static bool ForSpecies(string speciesKey, Action<bool, string> onDone, bool force = false)
	{
		string key = (speciesKey ?? "").Trim();
		bool fired = false;
		Action<bool, string> done = delegate(bool ok, string why)
		{
			if (fired)
			{
				Plugin.Log.LogWarning((object)("[EXPEDITION] double completion for '" + key + "' suppressed (" + why + ")."));
				return;
			}
			fired = true;
			try
			{
				onDone?.Invoke(ok, why);
			}
			catch (Exception arg2)
			{
				Plugin.Log.LogError((object)$"[EXPEDITION] onDone callback for '{key}' threw: {arg2}");
			}
		};
		if (!Plugin.EnableExpeditions.Value)
		{
			done.Invoke(false, "expeditions are disabled ([Expedition] EnableExpeditions = false)");
			return false;
		}
		if (key.Length == 0)
		{
			done.Invoke(false, "no species given");
			return false;
		}
		if (InProgress || ExpeditionHarvest.InProgress)
		{
			done.Invoke(false, "an expedition is already running");
			return false;
		}
		if (Spawner.CanMintNow(key))
		{
			done.Invoke(true, "already resident — no trip needed");
			return false;
		}
		string text = default(string);
		List<string> list = default(List<string>);
		if (!SpeciesTable.TryResolveKey<List<string>>(DonorHarvest.DonorScenes, key, ref text, ref list, (string)null) || list == null || list.Count == 0)
		{
			done.Invoke(false, "'" + key + "' is not in the donor table — 'spawnlist' shows the spawnable species");
			return false;
		}
		if (!force && !Spawner.IsExpeditionOnly(key))
		{
			done.Invoke(false, "'" + key + "' has an ADDITIVE donor — 'spawnprewarm " + key + "' harvests it with no loading screens (pass 'force' to take the trip anyway)");
			return false;
		}
		List<string> list2 = default(List<string>);
		string text2 = default(string);
		if (!DonorHarvest.TryGetExpeditionScenes(key, ref list2, ref text2) || list2 == null || list2.Count == 0)
		{
			done.Invoke(false, "'" + key + "' has no expedition donor scene (see 'spawnlist' / DonorScenes.txt)");
			return false;
		}
		string scene = list2[0];
		InProgress = true;
		CurrentSpecies = key;
		try
		{
			if (!ExpeditionOrchestrator.BeginTrip(scene, (Action<TripResult>)delegate(TripResult trip)
			{
				//IL_000c: Unknown result type (might be due to invalid IL or missing references)
				//IL_0045: Unknown result type (might be due to invalid IL or missing references)
				//IL_007f: Unknown result type (might be due to invalid IL or missing references)
				//IL_009b: Unknown result type (might be due to invalid IL or missing references)
				//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
				InProgress = false;
				CurrentSpecies = null;
				if (!trip.EndedHome)
				{
					Plugin.Log.LogError((object)("[EXPEDITION] SpawnKit: the trip to '" + scene + "' did NOT bring the party home — refusing to spawn on top of it. See the teardown line above; 'goto <scene>' recovers."));
					done.Invoke(false, "the expedition did not bring you home — see the log");
				}
				else if (!trip.PayloadRan)
				{
					done.Invoke(false, "the expedition to '" + scene + "' was aborted before it could harvest");
				}
				else
				{
					Plugin.Log.LogMessage((object)$"[EXPEDITION] SpawnKit: home from '{scene}' — {trip.Built} new body template(s) cached.");
					done.Invoke(true, (trip.Built > 0) ? $"harvested {trip.Built} new body template(s) from {scene}" : (scene + " harvested (0 new — the cache already covered it)"));
				}
			}))
			{
				InProgress = false;
				CurrentSpecies = null;
				done.Invoke(false, "the expedition would not start — see the refusal in the log");
				return false;
			}
			if (!fired)
			{
				Plugin.Log.LogMessage((object)("[EXPEDITION] SpawnKit: '" + text2 + "' → donor region '" + scene + "'" + ((list2.Count > 1) ? $" (+{list2.Count - 1} more candidate(s))" : "") + " — two loading screens, there and back. One trip caches EVERY species that region donates."));
			}
			return true;
		}
		catch (Exception arg)
		{
			InProgress = false;
			CurrentSpecies = null;
			Plugin.Log.LogError((object)$"[EXPEDITION] SpawnKit: launching the trip to '{scene}' threw: {arg}");
			done.Invoke(false, "the expedition threw before it could start — see the log");
			return false;
		}
	}
}
internal static class LootProbe
{
	internal static CorpseLootObservation Observe(Character ch)
	{
		//IL_0171: Unknown result type (might be due to invalid IL or missing references)
		bool flag = true;
		try
		{
			flag = !ch.Alive;
		}
		catch
		{
		}
		LootableOnDeath val = null;
		try
		{
			val = ((Component)ch).GetComponent<LootableOnDeath>();
		}
		catch
		{
		}
		bool flag2 = (Object)(object)val != (Object)null;
		bool flag3 = false;
		if (flag2)
		{
			try
			{
				flag3 = ((Behaviour)val).enabled && ((Component)val).gameObject.activeInHierarchy;
			}
			catch
			{
			}
		}
		int num = 0;
		int num2 = 0;
		int num3 = 0;
		int num4 = 0;
		bool flag4 = false;
		bool flag5 = false;
		if (flag2)
		{
			try
			{
				DropInstance[] lootDrops = val.LootDrops;
				if (lootDrops != null)
				{
					num = lootDrops.Length;
					DropInstance[] array = lootDrops;
					foreach (DropInstance val2 in array)
					{
						if (val2 != null && (Object)(object)val2.Dropper != (Object)null)
						{
							num2++;
						}
					}
				}
			}
			catch
			{
			}
			try
			{
				DropInstance[] skinDrops = val.SkinDrops;
				if (skinDrops != null)
				{
					num3 = skinDrops.Length;
					DropInstance[] array2 = skinDrops;
					foreach (DropInstance val3 in array2)
					{
						if (val3 != null && (Object)(object)val3.Dropper != (Object)null)
						{
							num4++;
						}
					}
				}
			}
			catch
			{
			}
			try
			{
				flag4 = val.m_lootable;
			}
			catch
			{
			}
			try
			{
				flag5 = val.m_skinable;
			}
			catch
			{
			}
		}
		ItemContainer val4 = null;
		try
		{
			val4 = (((Object)(object)ch.Inventory != (Object)null) ? ch.Inventory.Pouch : null);
		}
		catch
		{
		}
		bool flag6 = (Object)(object)val4 != (Object)null;
		bool flag7 = false;
		if (flag6)
		{
			try
			{
				flag7 = ((Item)val4).HasInteractionTrigger;
			}
			catch
			{
			}
		}
		return new CorpseLootObservation(flag, flag2, flag3, flag6, flag7, num, num2, num3, num4, flag4, flag5);
	}

	internal static void Log(Character ch, string context)
	{
		//IL_0111: Unknown result type (might be due to invalid IL or missing references)
		//IL_011f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0128: 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_002e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0033: 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_0161: Unknown result type (might be due to invalid IL or missing references)
		//IL_0181: Unknown result type (might be due to invalid IL or missing references)
		//IL_018f: Unknown result type (might be due to invalid IL or missing references)
		//IL_019d: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ab: Unknown result type (might be due to invalid IL or missing references)
		//IL_01c4: Unknown result type (might be due to invalid IL or missing references)
		//IL_01cf: Unknown result type (might be due to invalid IL or missing references)
		//IL_01e7: Unknown result type (might be due to invalid IL or missing references)
		//IL_01f5: Unknown result type (might be due to invalid IL or missing references)
		//IL_0060: 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)
		if (Plugin.Log == null)
		{
			return;
		}
		if ((Object)(object)ch == (Object)null)
		{
			Plugin.Log.LogMessage((object)("[LOOT] " + context + ": character is null (already destroyed)."));
			return;
		}
		CorpseLootObservation val;
		try
		{
			val = Observe(ch);
		}
		catch (Exception ex)
		{
			Plugin.Log.LogWarning((object)("[LOOT] " + context + ": probe threw: " + ex.Message));
			return;
		}
		CorpseLootVerdict val2 = CorpseLootDiagnosis.Classify(ref val);
		string text = "?";
		int num = -1;
		bool flag = false;
		bool flag2 = false;
		bool flag3 = false;
		try
		{
			text = ((Object)((Component)ch).gameObject).name;
		}
		catch
		{
		}
		try
		{
			flag3 = ch.HasEventOnDeath;
		}
		catch
		{
		}
		try
		{
			ItemContainer val3 = (((Object)(object)ch.Inventory != (Object)null) ? ch.Inventory.Pouch : null);
			if ((Object)(object)val3 != (Object)null)
			{
				try
				{
					num = val3.ItemCount;
				}
				catch
				{
				}
				try
				{
					flag = ((Item)val3).IsForceInteractible;
				}
				catch
				{
				}
				try
				{
					flag2 = val3.VisibleIfEmpty;
				}
				catch
				{
				}
			}
		}
		catch
		{
		}
		Plugin.Log.LogMessage((object)($"[LOOT] {context}: '{text}' dead={val.IsDead} verdict={val2} — {CorpseLootDiagnosis.Explain(val2)}" + (CorpseLootDiagnosis.IsActionableDefect(val2) ? " [ACTIONABLE]" : "") + $" | LootableOnDeath={(val.HasComponent ? 1 : 0)} enabled={val.ComponentEnabled}" + $" | lootDrops={val.LootDropsWithDropper}/{val.LootDropEntries} skinDrops={val.SkinDropsWithDropper}/{val.SkinDropEntries}" + $" m_lootable={val.Lootable} m_skinable={val.Skinable}" + $" | pouch={(val.HasPouch ? 1 : 0)} trigger={val.PouchHasInteractionTrigger} items={num}" + $" forceInteractible={flag} visibleIfEmpty={flag2} hasEventOnDeath={flag3}"));
	}
}
internal static class ParticleProbe
{
	public static void Dump(bool listAll)
	{
		//IL_003d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0042: Unknown result type (might be due to invalid IL or missing references)
		//IL_005a: Unknown result type (might be due to invalid IL or missing references)
		//IL_005f: 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)
		//IL_0064: Invalid comparison between Unknown and I4
		//IL_0066: Unknown result type (might be due to invalid IL or missing references)
		//IL_006a: Invalid comparison between Unknown and I4
		//IL_006c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0070: Invalid comparison between Unknown and I4
		//IL_007e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0080: Unknown result type (might be due to invalid IL or missing references)
		//IL_0154: Unknown result type (might be due to invalid IL or missing references)
		ParticleSystem[] array = Resources.FindObjectsOfTypeAll<ParticleSystem>();
		List<string> list = new List<string>();
		List<string> list2 = new List<string>();
		List<string> list3 = new List<string>();
		int num = 0;
		ParticleSystem[] array2 = array;
		foreach (ParticleSystem val in array2)
		{
			if ((Object)(object)val == (Object)null)
			{
				continue;
			}
			ShapeModule shape;
			try
			{
				shape = val.shape;
			}
			catch
			{
				continue;
			}
			if (!((ShapeModule)(ref shape)).enabled)
			{
				continue;
			}
			ParticleSystemShapeType shapeType = ((ShapeModule)(ref shape)).shapeType;
			if ((int)shapeType == 6 || (int)shapeType == 13 || (int)shapeType == 14)
			{
				string source;
				bool baked;
				Mesh val2 = MeshOf(shape, shapeType, out source, out baked);
				float num2 = MeshArea(val2);
				string arg = (((Object)(object)val2 == (Object)null) ? "NULL" : ((Object)val2).name);
				int num3 = ((!((Object)(object)val2 == (Object)null)) ? val2.vertexCount : 0);
				bool isPlaying = val.isPlaying;
				if (isPlaying)
				{
					num++;
				}
				bool flag = (Object)(object)val2 == (Object)null || num3 == 0 || (num2 >= 0f && num2 <= 1E-07f);
				bool flag2 = !flag && isPlaying && num2 < 0f;
				if (baked)
				{
					Object.Destroy((Object)(object)val2);
				}
				string item = "  " + Path(((Component)val).transform) + "\n" + $"      shape={shapeType} mesh={arg} ({source}) " + $"verts={num3} " + "area=" + ((num2 < 0f) ? "UNREADABLE" : num2.ToString("F6")) + " " + $"playing={isPlaying} activeInHierarchy={((Component)val).gameObject.activeInHierarchy} " + "emissionEnabled=" + EmissionOn(val) + " emissionRate=" + Rate(val);
				if (flag)
				{
					list.Add(item);
				}
				else if (flag2)
				{
					list2.Add(item);
				}
				if (listAll)
				{
					list3.Add(item);
				}
			}
		}
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append($"[PSDUMP] {array.Length} ParticleSystem(s) in memory; {num} of the mesh-shaped ones are PLAYING.\n");
		if (list.Count == 0)
		{
			stringBuilder.Append("[PSDUMP] no PROVABLY zero-area mesh emitter found.\n");
			if (list2.Count == 0)
			{
				stringBuilder.Append("[PSDUMP] (If output_log.txt IS still spamming, the emitter's mesh is being invalidated at RUNTIME —\n         re-run psdump WHILE the spam is flowing, and check the DDOL scene / a scene mid-unload.)\n");
			}
		}
		else
		{
			stringBuilder.Append($"[PSDUMP] {list.Count} ZERO-AREA MESH EMITTER(S) — these are the spam. A PLAYING one logs every frame:\n");
			foreach (string item2 in list)
			{
				stringBuilder.Append(item2).Append('\n');
			}
		}
		if (list2.Count > 0)
		{
			stringBuilder.Append($"[PSDUMP] {list2.Count} SUSPECT(S) — playing, mesh-shaped, but the mesh is not readable so its area\n" + "         cannot be verified from script. Do NOT read this as clean:\n");
			foreach (string item3 in list2)
			{
				stringBuilder.Append(item3).Append('\n');
			}
		}
		if (listAll && list3.Count > 0)
		{
			stringBuilder.Append($"[PSDUMP] all {list3.Count} mesh-shaped system(s):\n");
			foreach (string item4 in list3)
			{
				stringBuilder.Append(item4).Append('\n');
			}
		}
		Plugin.Log.LogMessage((object)stringBuilder.ToString());
	}

	private unsafe static Mesh MeshOf(ShapeModule shape, ParticleSystemShapeType t, out string source, out bool baked)
	{
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_0014: Invalid comparison between Unknown and I4
		//IL_0016: Unknown result type (might be due to invalid IL or missing references)
		//IL_0019: Invalid comparison between Unknown and I4
		//IL_001b: Unknown result type (might be due to invalid IL or missing references)
		//IL_001e: Invalid comparison between Unknown and I4
		//IL_0101: Unknown result type (might be due to invalid IL or missing references)
		//IL_0108: Expected O, but got Unknown
		source = ((object)(*(ParticleSystemShapeType*)(&t))/*cast due to .constrained prefix*/).ToString();
		baked = false;
		if ((int)t != 6)
		{
			if ((int)t != 13)
			{
				if ((int)t == 14)
				{
					SkinnedMeshRenderer skinnedMeshRenderer = ((ShapeModule)(ref shape)).skinnedMeshRenderer;
					if ((Object)(object)skinnedMeshRenderer == (Object)null)
					{
						source = "SkinnedMeshRenderer=NULL (shape was never bound to a renderer)";
						return null;
					}
					int num = ((skinnedMeshRenderer.bones != null) ? skinnedMeshRenderer.bones.Length : 0);
					int num2 = 0;
					if (skinnedMeshRenderer.bones != null)
					{
						Transform[] bones = skinnedMeshRenderer.bones;
						foreach (Transform val in bones)
						{
							if ((Object)(object)val == (Object)null)
							{
								num2++;
							}
						}
					}
					source = $"SkinnedMeshRenderer '{((Object)skinnedMeshRenderer).name}' bones={num} dead={num2}";
					try
					{
						Mesh val2 = new Mesh();
						skinnedMeshRenderer.BakeMesh(val2);
						source += " [baked]";
						baked = true;
						return val2;
					}
					catch
					{
						return skinnedMeshRenderer.sharedMesh;
					}
				}
				return null;
			}
			MeshRenderer meshRenderer = ((ShapeModule)(ref shape)).meshRenderer;
			if ((Object)(object)meshRenderer == (Object)null)
			{
				source = "MeshRenderer=NULL";
				return null;
			}
			source = "MeshRenderer '" + ((Object)meshRenderer).name + "'";
			MeshFilter component = ((Component)meshRenderer).GetComponent<MeshFilter>();
			if (!((Object)(object)component == (Object)null))
			{
				return component.sharedMesh;
			}
			return null;
		}
		return ((ShapeModule)(ref shape)).mesh;
	}

	private static float MeshArea(Mesh mesh)
	{
		//IL_0057: 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)
		//IL_0066: Unknown result type (might be due to invalid IL or missing references)
		//IL_0072: 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_0081: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: Unknown result type (might be due to invalid IL or missing references)
		//IL_008b: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)mesh == (Object)null || mesh.vertexCount == 0)
		{
			return 0f;
		}
		Vector3[] vertices;
		int[] triangles;
		try
		{
			if (!mesh.isReadable)
			{
				return -1f;
			}
			vertices = mesh.vertices;
			triangles = mesh.triangles;
		}
		catch
		{
			return -1f;
		}
		float num = 0f;
		for (int i = 0; i + 2 < triangles.Length; i += 3)
		{
			float num2 = num;
			Vector3 val = Vector3.Cross(vertices[triangles[i + 1]] - vertices[triangles[i]], vertices[triangles[i + 2]] - vertices[triangles[i]]);
			num = num2 + ((Vector3)(ref val)).magnitude * 0.5f;
		}
		return num;
	}

	private static string Rate(ParticleSystem ps)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_0009: Unknown result type (might be due to invalid IL or missing references)
		//IL_000e: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			EmissionModule emission = ps.emission;
			MinMaxCurve rateOverTime = ((EmissionModule)(ref emission)).rateOverTime;
			return ((MinMaxCurve)(ref rateOverTime)).constant.ToString("F1");
		}
		catch
		{
			return "?";
		}
	}

	private static string EmissionOn(ParticleSystem ps)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			EmissionModule emission = ps.emission;
			return ((EmissionModule)(ref emission)).enabled.ToString();
		}
		catch
		{
			return "?";
		}
	}

	private static string Path(Transform t)
	{
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		//IL_0048: Unknown result type (might be due to invalid IL or missing references)
		//IL_005f: 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)
		StringBuilder stringBuilder = new StringBuilder(((Object)t).name);
		Transform parent = t.parent;
		while ((Object)(object)parent != (Object)null)
		{
			stringBuilder.Insert(0, ((Object)parent).name + "/");
			parent = parent.parent;
		}
		Scene scene = ((Component)t).gameObject.scene;
		object obj;
		if (!((Scene)(ref scene)).IsValid())
		{
			obj = "DDOL/none";
		}
		else
		{
			scene = ((Component)t).gameObject.scene;
			obj = ((Scene)(ref scene)).name;
		}
		string arg = (string)obj;
		return $"[{arg}] {stringBuilder}";
	}
}
[BepInPlugin("cobalt.spawnkit", "SpawnKit", "0.2.1")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
[BepInDependency(/*Could not decode attribute arguments.*/)]
public class Plugin : BaseUnityPlugin
{
	public const string GUID = "cobalt.spawnkit";

	public const string NAME = "SpawnKit";

	public const string VERSION = "0.2.1";

	internal static ManualLogSource Log;

	internal static Plugin Instance;

	public static ConfigEntry<bool> Enabled;

	public static ConfigEntry<int> MaxActiveSpawns;

	public static ConfigEntry<float> SpawnDistance;

	public static ConfigEntry<int> MaxCachedTemplates;

	public static ConfigEntry<CorpsePolicy> DefaultCorpsePolicy;

	public static ConfigEntry<float> DefaultCorpseLingerSeconds;

	public static ConfigEntry<bool> ForceLootableEnabled;

	public static ConfigEntry<bool> EnableMenu;

	public static ConfigEntry<KeyboardShortcut> MenuKey;

	public static ConfigEntry<bool> EnableExpeditions;

	public static ConfigEntry<bool> ConfirmMenuExpedition;

	private CommandRegistry _commands;

	private CommandChannel _channel;

	private float _nextWatch;

	internal void Awake()
	{
		//IL_0134: Unknown result type (might be due to invalid IL or missing references)
		//IL_01a2: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ac: Expected O, but got Unknown
		//IL_01ca: Unknown result type (might be due to invalid IL or missing references)
		//IL_01d4: Expected O, but got Unknown
		//IL_01e5: Unknown result type (might be due to invalid IL or missing references)
		//IL_022c: Unknown result type (might be due to invalid IL or missing references)
		Log = ((BaseUnityPlugin)this).Logger;
		Instance = this;
		Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Spawner", "Enabled", true, "Kill-switch: false refuses new spawns (API + verbs). despawnall/spawndump keep working.");
		MaxActiveSpawns = ((BaseUnityPlugin)this).Config.Bind<int>("Spawner", "MaxActiveSpawns", 8, "Refuse spawns that would push the live count (incl. pending) past this cap. Global across ALL consumers.");
		SpawnDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Spawner", "SpawnDistance", 5f, "Preferred ground distance from the player for the placement ring probe (meters). Per-spawn override: SpawnOptions.Distance.");
		MaxCachedTemplates = ((BaseUnityPlugin)this).Config.Bind<int>("Spawner", "MaxCachedTemplates", 12, "Evict the least-recently-used species template past this count (its next spawn re-harvests the donor scene). Each cached template is a full creature clone pinning its meshes/textures/audio, so an unbounded cache grows memory for the whole session. 0 = unlimited (warn-nudge only).");
		DefaultCorpsePolicy = ((BaseUnityPlugin)this).Config.Bind<CorpsePolicy>("Spawner", "DefaultCorpsePolicy", (CorpsePolicy)0, "Corpse fate after a real death when SpawnOptions.Corpse is null. Vanilla = the engine's own semantic (no corpse GC exists — corpse + loot persist until scene unload). NoBody = destroy the corpse (AND its loot bag with it) DefaultCorpseLingerSeconds after the death resolves. Stamped per-spawn at mint — an edit applies to NEW spawns only, never already-live ones.");
		DefaultCorpseLingerSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Spawner", "DefaultCorpseLingerSeconds", 0f, "Seconds a NoBody corpse lingers after death before it is destroyed (death-anim time). 0 = the tick after the death resolves; negative clamps to 0. Per-spawn override: SpawnOptions.CorpseLingerSeconds / spawnex linger=N.");
		ForceLootableEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Spawner", "ForceLootableEnabled", true, "Corpse-loot backstop (V-TLOOT): re-enable any DISABLED LootableOnDeath on a spawn at mint so its corpse is lootable. A disabled component makes OnDeath early-return (no prompt, no error). Quest-safe — LootableOnDeath is self-contained; nothing else in the game reads it, and QuestEventOnDeath (the quest-strip target) is a separate receiver. Does NOT invent loot: a creature with no drops configured (a scripted/boss reward) still shows no prompt — see the [LOOT] verdict line at each spawn death.");
		EnableMenu = ((BaseUnityPlugin)this).Config.Bind<bool>("Menu", "EnableMenu", true, "The in-game spawn menu (the MenuKey toggle + 'spawnmenu' verb). On by default — it is the mod's primary interface; the *_cmd.txt channel is the fallback. false = the menu never opens and its CharacterUI.IsMenuFocused Harmony patch is never applied.");
		MenuKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Menu", "MenuKey", new KeyboardShortcut((KeyCode)292, Array.Empty<KeyCode>()), "Toggles the in-game spawn menu (needs EnableMenu=true). F6-F10/F12 are taken by other kit mods; Backslash is the classic debug-menu rebind.");
		Keybinds.Claim("SpawnKit", "toggle the spawn menu", MenuKey);
		EnableExpeditions = ((BaseUnityPlugin)this).Config.Bind<bool>("Expedition", "EnableExpeditions", true, "Let SpawnKit FETCH a region-only species (one whose every donor is an oversized region scene the additive harvest refuses) by running a real expedition: the whole party rides two vanilla loading screens to the donor region and back, and the game SAVES on each leg. One trip caches every species that region donates. false = those rows behave as before (they can only spawn if something else — Beastwhispering's 'expedition' verb or its boot auto-warm — already cached the body). Host/offline only.");
		ConfirmMenuExpedition = ((BaseUnityPlugin)this).Config.Bind<bool>("Expedition", "ConfirmMenuExpedition", true, "In the spawn menu, require a SECOND click to launch an expedition (the button flips to 'Confirm?' for 5s). A single stray click on a 166-row scrolling list should not teleport the party and write a save. false = one click goes immediately.");
		_commands = new CommandRegistry(Log);
		RegisterVerbs();
		_channel = new CommandChannel("SpawnKit_cmd.txt", Log, _commands, 0.5f, true, true);
		if (EnableMenu.Value)
		{
			new Harmony("cobalt.spawnkit").PatchAll(typeof(CursorControl));
			((Component)this).gameObject.AddComponent<SpawnMenu>();
		}
		Log.LogMessage((object)("SpawnKit 0.2.1 loaded — 'help' in BepInEx/config/SpawnKit_cmd.txt lists the verbs" + (EnableMenu.Value ? $"; spawn menu on {MenuKey.Value}." : ".")));
	}

	internal void Update()
	{
		_channel.Tick();
		if (Time.unscaledTime >= _nextWatch)
		{
			_nextWatch = Time.unscaledTime + 0.5f;
			EnemySpawner.WatchTick();
		}
	}

	private void RegisterVerbs()
	{
		_commands.Register("spawn", "spawn <species> [count] — spawn <species> as live hostile(s) beside the player (thin wrapper over Spawner.Spawn).", (Action<string[]>)delegate(string[] args)
		{
			ParseSpeciesAndCount(args, out var species, out var count);
			if (string.IsNullOrEmpty(species))
			{
				Log.LogWarning((object)"[SPAWN] usage: spawn <species> [count]");
			}
			else
			{
				for (int i = 0; i < count; i++)
				{
					Spawner.Spawn(species, new SpawnOptions
					{
						OwnerTag = "spawnkit.verb"
					}, delegate(SpawnHandle h)
					{
						//IL_0025: Unknown result type (might be due to invalid IL or missing references)
						if (h.State != SpawnState.Alive)
						{
							Log.LogWarning((object)$"[SPAWN] verb spawn '{h.SpeciesKey}' -> {h.State} ({h.FailReason}).");
						}
					});
				}
			}
		});
		_commands.Register("despawnall", "despawnall [kill] — remove every tracked spawn (all owners); 'kill' routes through the real damage pipeline (death anim + loot).", (Action<string[]>)delegate(string[] args)
		{
			bool kill = args.Length > 1 && string.Equals(args[1], "kill", StringComparison.OrdinalIgnoreCase);
			Spawner.DespawnAll(null, kill);
		});
		_commands.Register("spawndump", "Per-spawn live state: uid/alive/hp/dist/AI state/target — plus the template cache.", (Action<string[]>)delegate
		{
			Log.LogMessage((object)EnemySpawner.Dump());
			Log.LogMessage((object)SpawnTemplates.Dump());
		});
		_commands.Register("spawnclearcache", "Destroy all cached species templates (next spawn re-harvests).", (Action<string[]>)delegate
		{
			int num = SpawnTemplates.Clear();
			Log.LogMessage((object)$"[TEMPLATE] cleared {num} cached template(s).");
		});
		_commands.Register("spawnprep", "spawnprep <scene> — ONE donor load builds templates for every table species listing that scene (light-probe cycle budget amortizer).", (Action<string[]>)delegate(string[] args)
		{
			string text = string.Join(" ", args, 1, Math.Max(0, args.Length - 1)).Trim();
			if (text.Length == 0)
			{
				Log.LogWarning((object)"[PREP] usage: spawnprep <sceneName>");
			}
			else
			{
				((MonoBehaviour)this).StartCoroutine(SpawnTemplates.AcquireScene(text, delegate
				{
				}));
			}
		});
		_commands.Register("spawnex", "spawnex <species> [dist=N] [life=N] [faction=Name] [owner=tag] [body=vanilla|none] [linger=N] [keepquest] — one spawn with per-spawn SpawnOptions (the facade's full surface, verb-shaped).", (Action<string[]>)delegate(string[] args)
		{
			//IL_00d4: Unknown result type (might be due to invalid IL or missing references)
			SpawnVerbArgs val = SpawnVerbArgs.Parse(args);
			foreach (string unknownOption in val.UnknownOptions)
			{
				Log.LogWarning((object)("[SPAWN] spawnex: unrecognized option '" + unknownOption + "' (knowns: dist= life= faction= owner= body= linger= keepquest)."));
			}
			if (val.Species.Length == 0)
			{
				Log.LogWarning((object)"[SPAWN] usage: spawnex <species> [dist=N] [life=N] [faction=Name] [owner=tag] [body=vanilla|none] [linger=N] [keepquest]");
			}
			else
			{
				SpawnOptions spawnOptions = new SpawnOptions
				{
					Distance = val.Distance,
					LifetimeSeconds = val.LifetimeSeconds,
					CorpseLingerSeconds = val.CorpseLingerSeconds,
					OwnerTag = (val.OwnerTag ?? "spawnkit.verb"),
					StripQuestEvents = !val.KeepQuestEvents
				};
				if (val.Faction != null)
				{
					if (!Enum.TryParse<Factions>(val.Faction, ignoreCase: true, out Factions result))
					{
						Log.LogWarning((object)("[SPAWN] spawnex: unknown faction '" + val.Faction + "' — valid: " + string.Join(", ", Enum.GetNames(typeof(Factions))) + "."));
						return;
					}
					spawnOptions.Faction = result;
				}
				if (val.Body != null)
				{
					if (string.Equals(val.Body, "vanilla", StringComparison.OrdinalIgnoreCase))
					{
						spawnOptions.Corpse = (CorpsePolicy)0;
					}
					else
					{
						if (!string.Equals(val.Body, "none", StringComparison.OrdinalIgnoreCase))
						{
							Log.LogWarning((object)("[SPAWN] spawnex: unknown body policy '" + val.Body + "' — valid: vanilla, none."));
							return;
						}
						spawnOptions.Corpse = (CorpsePolicy)1;
					}
				}
				Spawner.Spawn(val.Species, spawnOptions, delegate(SpawnHandle h)
				{
					//IL_0025: Unknown result type (might be due to invalid IL or missing references)
					if (h.State != SpawnState.Alive)
					{
						Log.LogWarning((object)$"[SPAWN] spawnex '{h.SpeciesKey}' -> {h.State} ({h.FailReason}).");
					}
				});
			}
		});
		_commands.Register("despawnowner", "despawnowner <tag> [kill] — remove only the spawns tagged with <tag> (Spawner.DespawnAll scoped; V40).", (Action<string[]>)delegate(string[] args)
		{
			bool flag = args.Length > 2 && string.Equals(args[^1], "kill", StringComparison.OrdinalIgnoreCase);
			int num = (flag ? (args.Length - 1) : args.Length);
			string text = string.Join(" ", args, 1, Math.Max(0, num - 1)).Trim();
			if (text.Length == 0)
			{
				Log.LogWarning((object)"[SPAWN] usage: despawnowner <tag> [kill]");
			}
			else
			{
				Spawner.DespawnAll(text, flag);
			}
		});
		_commands.Register("spawnprewarm", "spawnprewarm <species> — pay the donor harvest now so the first spawn is instant (Spawner.Prewarm).", (Action<string[]>)delegate(string[] args)
		{
			string species = string.Join(" ", args, 1, Math.Max(0, args.Length - 1)).Trim();
			if (species.Length == 0)
			{
				Log.LogWarning((object)"[TEMPLATE] usage: spawnprewarm <species>");
			}
			else
			{
				Spawner.Prewarm(species, delegate(bool ok)
				{
					Log.LogMessage((object)("[TEMPLATE] prewarm '" + species + "': " + (ok ? "resident" : "FAILED (see log)") + "."));
				});
			}
		});
		_commands.Register("expeditionreset", "Force a stuck expedition guard open (the fix for 'the spawn menu stopped opening'). Does NOT teleport anyone.", (Action<string[]>)delegate
		{
			Log.LogMessage((object)ExpeditionHarvest.ForceReset());
		});
		_commands.Register("spawnexpedition", "spawnexpedition <species> [spawn] [force] — FETCH a region-only species: a real round trip through the vanilla loader (party teleported to the donor region and back, a save on each leg), batch-caching every species that region donates. 'spawn' also spawns it on return; 'force' takes the trip even for a species an additive harvest could fetch with no loading screens (forensics only). The headless twin of the menu's trip buttons.", (Action<string[]>)delegate(string[] args)
		{
			if (args.Length <= 1)
			{
				Log.LogWarning((object)"[EXPEDITION] usage: spawnexpedition <species> [spawn] [force]");
			}
			else
			{
				int num = args.Length;
				bool alsoSpawn = false;
				bool force = false;
				while (num > 1)
				{
					if (string.Equals(args[num - 1], "spawn", StringComparison.OrdinalIgnoreCase))
					{
						alsoSpawn = true;
						num--;
					}
					else
					{
						if (!string.Equals(args[num - 1], "force", StringComparison.OrdinalIgnoreCase))
						{
							break;
						}
						force = true;
						num--;
					}
				}
				string species = string.Join(" ", args, 1, Math.Max(0, num - 1)).Trim();
				if (species.Length == 0)
				{
					Log.LogWarning((object)"[EXPEDITION] usage: spawnexpedition <species> [spawn] [force]");
				}
				else
				{
					Spawner.Expedition(species, delegate(bool ok, string why)
					{
						if (!ok)
						{
							Log.LogWarning((object)("[EXPEDITION] '" + species + "': " + why + "."));
						}
						else
						{
							Log.LogMessage((object)("[EXPEDITION] '" + species + "': " + why + "."));
							if (alsoSpawn)
							{
								Spawner.Spawn(species, new SpawnOptions
								{
									OwnerTag = "spawnkit.verb"
								}, delegate(SpawnHandle h)
								{
									//IL_002f: Unknown result type (might be due to invalid IL or missing references)
									Log.LogMessage((object)($"[EXPEDITION] post-trip spawn of '{species}': {h.State}" + ((h.State == SpawnState.Alive) ? "." : $" ({h.FailReason}).")));
								});
							}
						}
					}, force);
				}
			}
		});
		_commands.Register("spawnmenu", "Toggle the in-game spawn menu (works regardless of the MenuKey; needs [Menu] EnableMenu=true).", (Action<string[]>)delegate
		{
			SpawnMenu component = ((Component)this).GetComponent<SpawnMenu>();
			if ((Object)(object)component == (Object)null)
			{
				Log.LogWarning((object)"[MENU] EnableMenu=false — flip it in the .cfg and relaunch (menu component is boot-time).");
			}
			else
			{
				component.Toggle();
			}
		});
		_commands.Register("lootprobe", "Corpse-loot diagnosis (V-TLOOT): a [LOOT] verdict line per tracked spawn — LootableOnDeath state, loot/skin dropper counts, pouch trigger. Probe a live spawn before killing, or a corpse after.", (Action<string[]>)delegate
		{
			if (EnemySpawner.LootProbeAll() == 0)
			{
				Log.LogMessage((object)"[LOOT] no tracked spawns to probe (spawn one first; the probe also fires automatically at each spawn death).");
			}
		});
		_commands.Register("spawnlist", "List the merged donor-scene table (the spawnable species keys).", (Action<string[]>)delegate
		{
			PrintSpawnList();
		});
		_commands.Register("scenedump", "List every scene in build settings (donor-scene name discovery).", (Action<string[]>)delegate
		{
			DonorHarvest.DumpBuildScenes();
		});
		_commands.Register("skydump", "RenderSettings/camera/sky snapshot (Bug 22 black screen).", (Action<string[]>)delegate
		{
			SkySnapshot.Log("manual skydump");
		});
		_commands.Register("ragdolldump", "ragdolldump [name] — death-pose machinery census for characters within 30m (gumby-corpse discriminator, testplan V44): deathAnim/ragdollRoot/ragdollRB/joints/managers/mgrJoint per character. A humanoid with mgrJoint=0 will gumby on death; joints>0 on a corpse = the ragdoll assembled.", (Action<string[]>)delegate(string[] args)
		{
			string nameFilter = ((args != null && args.Length > 1) ? string.Join(" ", args, 1, args.Length - 1) : null);
			RagdollProbe.Dump(nameFilter);
		});
		_commands.Register("bonedump", "bonedump [name] — SkinnedMeshRenderer bone census for characters within 30m (the F2 vertical-line discriminator): internal/dead/external/null bones + rootBone per SMR. dead>0 on a spawn = bones destroyed with the donor scene; external>0 = bound outside the hierarchy.", (Action<string[]>)delegate(string[] args)
		{
			string nameFilter = ((args != null && args.Length > 1) ? string.Join(" ", args, 1, args.Length - 1) : null);
			BoneProbe.Dump(nameFilter);
		});
		_commands.Register("psdump", "psdump [all] — NAME the source of the 'Particle System is trying to spawn on a mesh with zero surface area' storm (Bug 27): every mesh-shaped ParticleSystem whose mesh is null/empty/degenerate, with its full hierarchy path, scene, playing state and where the mesh came from. Unity's own message names nothing, which is why that bug has survived two wrong diagnoses. 'all' also lists the healthy mesh-shaped systems. Run it WHILE the spam is flowing.", (Action<string[]>)delegate(string[] args)
		{
			ParticleProbe.Dump(args.Length > 1 && string.Equals(args[1], "all", StringComparison.OrdinalIgnoreCase));
		});
		_commands.Register("selftest", "Run the SpawnKit self-test ([SELFTEST] PASS/FAIL ... DONE).", (Action<string[]>)delegate
		{
			SelfTest();
		});
	}

	private static void ParseSpeciesAndCount(string[] args, out string species, out int count)
	{
		count = 1;
		int num = 1;
		int num2 = args.Length;
		if (num2 - num > 1 && int.TryParse(args[num2 - 1], out var result) && result > 0)
		{
			count = Math.Min(result, 99);
			num2--;
		}
		species = string.Join(" ", args, num, Math.Max(0, num2 - num)).Trim();
	}

	private static void PrintSpawnList()
	{
		Dictionary<string, List<string>> donorScenes = DonorHarvest.DonorScenes;
		StringBuilder stringBuilder = new StringBuilder($"[SPAWNLIST] {donorScenes.Count} species in the donor table:");
		foreach (KeyValuePair<string, List<string>> item in donorScenes)
		{
			stringBuilder.Append("\n  " + item.Key + " = " + string.Join(", ", item.Value));
		}
		Log.LogMessage((object)stringBuilder.ToString());
	}

	private void SelfTest()
	{
		//IL_0005: Unknown result type (might be due to invalid IL or missing references)
		//IL_000b: Expected O, but got Unknown
		//IL_00d4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00da: Invalid comparison between Unknown and I4
		//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f0: Invalid comparison between Unknown and I4
		//IL_0143: Unknown result type (might be due to invalid IL or missing references)
		//IL_0149: Invalid comparison between Unknown and I4
		//IL_01c4: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ca: Invalid comparison between Unknown and I4
		//IL_0212: Unknown result type (might be due to invalid IL or missing references)
		//IL_0218: Invalid comparison between Unknown and I4
		SelfTestHarness val = new SelfTestHarness(Log);
		val.Begin("SpawnKit");
		val.Check("uid mint prefixed+recognized", SpawnUid.IsSpawnUid(SpawnUid.Mint(Guid.NewGuid())));
		IReadOnlyList<(float, float)> readOnlyList = RingPlacement.Candidates(0f, 1f, 4f, 16);
		val.Check("ring plan yields 16 candidates", readOnlyList.Count == 16);
		val.Check("ring first candidate straight ahead", Math.Abs(readOnlyList[0].Item1) < 0.001f && Math.Abs(readOnlyList[0].Item2 - 4f) < 0.001f);
		val.Check("donor table non-empty", DonorHarvest.DonorScenes.Count > 0);
		val.Check("kill-switch config bound", Enabled != null);
		val.Check("watch: alive+gone -> Despawned", (int)SpawnWatch.Next((WatchState)1, false, false) == 3);
		val.Check("watch: alive+dead -> Died", (int)SpawnWatch.Next((WatchState)1, true, false) == 2);
		val.Check("policy: distance request clamped", SpawnPolicy.EffectiveDistance((float?)500f, 5f) == 50f);
		val.Check("policy: null owner filter matches all", SpawnPolicy.MatchesOwner("anything", (string)null));
		val.Check("policy: corpse defaults from config", (int)CorpseRules.EffectivePolicy((CorpsePolicy?)null, (CorpsePolicy)1) == 1);
		val.Check("policy: negative corpse linger clamped", CorpseRules.EffectiveLinger((float?)(-5f), 0f) == 0f);
		val.Check("facade: species list non-empty", Spawner.SpeciesKeys().Count > 0);
		CharacterManager instance = CharacterManager.Instance;
		if ((Object)(object)((instance != null) ? instance.GetFirstLocalCharacter() : null) == (Object)null)
		{
			SpawnHandle spawnHandle = Spawner.Spawn("Hyena");
			val.Check("facade: no-player spawn -> Failed/NoPlayer", spawnHandle.State == SpawnState.Failed && (int)spawnHandle.FailReason == 4);
		}
		SpawnHandle spawnHandle2 = Spawner.Spawn("", null, delegate
		{
			throw new InvalidOperationException("selftest probe");
		});
		val.Check("facade: empty species -> sync Failed/UnknownSpecies + throwing callback isolated (see [SPAWN] error above)", spawnHandle2.State == SpawnState.Failed && (int)spawnHandle2.FailReason == 7);
		val.Check("CharacterManager present", (Object)(object)CharacterManager.Instance != (Object)null);
		CharacterManager instance2 = CharacterManager.Instance;
		val.Check("local player present", (Object)(object)((instance2 != null) ? instance2.GetFirstLocalCharacter() : null) != (Object)null);
		val.Check("AISquadManager present (gameplay scene)", (Object)(object)AISquadManager.Instance != (Object)null);
		val.Done();
	}
}
public static class RagdollProbe
{
	public static void Dump(string nameFilter, float radius = 30f)
	{
		//IL_006e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0079: Unknown result type (might be due to invalid IL or missing references)
		CharacterManager instance = CharacterManager.Instance;
		Character val = ((instance != null) ? instance.GetFirstLocalCharacter() : null);
		if ((Object)(object)val == (Object)null)
		{
			Plugin.Log.LogWarning((object)"[RAGDOLL] no local player — nothing to probe.");
			return;
		}
		DictionaryExt<string, Character> characters = instance.Characters;
		StringBuilder stringBuilder = new StringBuilder();
		int num = 0;
		for (int i = 0; i < characters.Count; i++)
		{
			Character val2 = characters.Values[i];
			if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2 == (Object)(object)val))
			{
				float num2 = Vector3.Distance(((Component)val2).transform.position, ((Component)val).transform.position);
				if (!(num2 > radius) && (string.IsNullOrEmpty(nameFilter) || (val2.Name != null && val2.Name.IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0)))
				{
					stringBuilder.Append('\n').Append(Line(val2, num2));
					num++;
				}
			}
		}
		Plugin.Log.LogMessage((object)((num == 0) ? string.Format("[RAGDOLL] no characters within {0:0}m{1}.", radius, string.IsNullOrEmpty(nameFilter) ? "" : (" matching '" + nameFilter + "'")) : string.Format("[RAGDOLL] {0} character(s) within {1:0}m{2}:{3}", num, radius, string.IsNullOrEmpty(nameFilter) ? "" : (" matching '" + nameFilter + "'"), stringBuilder)));
	}

	private static string Line(Character ch, float dist)
	{
		//IL_0010: Unknown result type (might be due to invalid IL or missing references)
		//IL_0015: Unknown result type (might be due to invalid IL or missing references)
		string text = "?";
		bool flag = false;
		bool flag2 = false;
		bool flag3 = false;
		Transform val = null;
		try
		{
			text = ((object)ch.UID/*cast due to .constrained prefix*/).ToString();
		}
		catch
		{
		}
		try
		{
			flag = ch.Alive;
		}
		catch
		{
		}
		try
		{
			flag2 = ch.UseDeathAnim;
		}
		catch
		{
		}
		try
		{
			flag3 = ch.RagdollActive;
		}
		catch
		{
		}
		try
		{
			val = ch.RagdollRoot;
		}
		catch
		{
		}
		bool flag4 = SpawnUid.IsSpawnUid(text);
		int num = 0;
		int num2 = 0;
		int num3 = 0;
		int num4 = 0;
		try
		{
			Rigidbody[] componentsInChildren = ((Component)ch).GetComponentsInChildren<Rigidbody>(true);
			foreach (Rigidbody val2 in componentsInChildren)
			{
				if ((Object)(object)val2 != (Object)null && ((Component)val2).CompareTag("Ragdoll"))
				{
					num++;
				}
			}
			num2 = ((Component)ch).GetComponentsInChildren<CharacterJoint>(true).Length;
			CharacterJointManager[] componentsInChildren2 = ((Component)ch).GetComponentsInChildren<CharacterJointManager>(true);
			foreach (CharacterJointManager val3 in componentsInChildren2)
			{
				if (!((Object)(object)val3 == (Object)null))
				{
					num3++;
					if (val3.m_hasJoint)
					{
						num4++;
					}
				}
			}
		}
		catch (Exception ex)
		{
			return "  '" + ch.Name + "' uid=" + text + " — component census threw: " + ex.GetType().Name + ": " + ex.Message;
		}
		return string.Format("  '{0}' uid={1} spawned={2} dist={3:0.0}m alive={4} | ", ch.Name, text, flag4 ? "Y" : "N", dist, flag) + "deathAnim=" + (flag2 ? "Y" : "N") + " ragdollRoot=" + (((Object)(object)val != (Object)null) ? "Y" : "N") + " " + $"ragdollRB={num} joints={num2} managers={num3} mgrJoint={num4} ragdollActive={flag3}";
	}
}
public static class Spawner
{
	public static bool IsExpeditionRunning
	{
		get
		{
			if (!ExpeditionRun.InProgress)
			{
				return ExpeditionHarvest.InProgress;
			}
			return true;
		}
	}

	public static SpawnHandle Spawn(string speciesKey, SpawnOptions options = null, Action<SpawnHandle> onReady = null)
	{
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0054: 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_005a: Unknown result type (might be due to invalid IL or missing references)
		options = options ?? new SpawnOptions();
		SpawnHandle spawnHandle = new SpawnHandle(speciesKey?.Trim() ?? "", options.OwnerTag);
		if (onReady != null)
		{
			spawnHandle.OnReady += onReady;
		}
		if (spawnHandle.SpeciesKey.Length == 0)
		{
			spawnHandle.Resolve(SpawnState.Failed, (FailReason)7);
			return spawnHandle;
		}
		FailReason val = EnemySpawner.Preflight(spawnHandle);
		if ((int)val != 0)
		{
			spawnHandle.Resolve(SpawnState.Failed, val);
			return spawnHandle;
		}
		((MonoBehaviour)Plugin.Instance).StartCoroutine(EnemySpawner.SpawnRoutine(spawnHandle, options));
		return spawnHandle;
	}

	public static void Despawn(SpawnHandle handle, bool kill = false)
	{
		EnemySpawner.Despawn(handle, kill);
	}

	public static void DespawnAll(string ownerTag = null, bool kill = false)
	{
		EnemySpawner.DespawnAll(ownerTag, kill);
	}

	public static void Prewarm(string speciesKey, Action<bool> onDone = null)
	{
		if ((Object)(object)Plugin.Instance == (Object)null)
		{
			onDone?.Invoke(obj: false);
			return;
		}
		((MonoBehaviour)Plugin.Instance).StartCoroutine(SpawnTemplates.Acquire(speciesKey?.Trim() ?? "", delegate(GameObject t)
		{
			onDone?.Invoke((Object)(object)t != (Object)null);
		}));
	}

	public static bool IsPrewarmed(string speciesKey)
	{
		return SpawnTemplates.IsCached(speciesKey);
	}

	public static bool IsExpeditionCached(string speciesKey)
	{
		string text = speciesKey?.Trim() ?? "";
		if (text.Length == 0)
		{
			return false;
		}
		try
		{
			BodyTemplate val = default(BodyTemplate);
			return BodyTemplateCache.TryResolveExact(text, ref val) && (Object)(object)val?.Dormant != (Object)null;
		}
		catch (Exception ex)
		{
			Plugin.Log.LogWarning((object)("[TEMPLATE] expedition-cache probe for '" + text + "' threw: " + ex.Message));
			return false;
		}
	}

	public static bool CanMintNow(string speciesKey)
	{
		if (!IsPrewarmed(speciesKey))
		{
			return IsExpeditionCached(speciesKey);
		}
		return true;
	}

	public static void Expedition(string speciesKey, Action<bool, string> onDone = null, bool force = false)
	{
		ExpeditionRun.ForSpecies(speciesKey, onDone, force);
	}

	public static string ResolvedDonorName(string speciesKey)
	{
		return SpawnTemplates.ResolvedDonorName(speciesKey);
	}

	public static bool IsExpeditionOnly(string speciesKey)
	{
		Dictionary<string, List<string>> donorScenes = DonorHarvest.DonorScenes;
		string text = default(string);
		List<string> list = default(List<string>);
		if (!SpeciesTable.TryResolveKey<List<string>>(donorScenes, speciesKey?.Trim() ?? "", ref text, ref list, (string)null) || list == null || list.Count == 0)
		{
			return false;
		}
		List<string> list2 = default(List<string>);
		return DonorTable.FilterViable((IEnumerable<string>)list, ref list2).Count == 0;
	}

	public static IReadOnlyList<SpawnHandle> Active(string ownerTag = null)
	{
		return EnemySpawner.Snapshot(ownerTag);
	}

	public static IReadOnlyList<string> SpeciesKeys()
	{
		List<string> list = new List<string>(DonorHarvest.DonorScenes.Keys);
		list.Sort(StringComparer.OrdinalIgnoreCase);
		return list;
	}
}
public enum SpawnState
{
	Pending,
	Alive,
	Died,
	Despawned,
	Failed
}
public sealed class SpawnHandle
{
	private Action<SpawnHandle> _onReady;

	internal int ViewId = -1;

	internal CorpsePolicy CorpsePolicy;

	internal float CorpseLingerSeconds;

	public SpawnState State { get; internal set; }

	public FailReason FailReason { get; internal set; }

	public string SpeciesKey { get; }

	public string OwnerTag { get; }

	public string Uid { get; internal set; }

	public Character Character { get; internal set; }

	public bool IsAlive
	{
		get
		{
			if (State == SpawnState.Alive)
			{
				return (Object)(object)Character != (Object)null;
			}
			return false;
		}
	}

	public event Action<SpawnHandle> OnReady
	{
		add
		{
			if (State != SpawnState.Pending)
			{
				Invoke(value, "OnReady(late)");
			}
			else
			{
				_onReady = (Action<SpawnHandle>)Delegate.Combine(_onReady, value);
			}
		}
		remove
		{
			_onReady = (Action<SpawnHandle>)Delegate.Remove(_onReady, value);
		}
	}

	public event Action<SpawnHandle> OnDied;

	public event Action<SpawnHandle> OnDespawned;

	internal SpawnHandle(string speciesKey, string ownerTag)
	{
		SpeciesKey = speciesKey;
		OwnerTag = SpawnPolicy.NormalizeOwnerTag(ownerTag);
	}

	internal void Resolve(SpawnState state, FailReason reason = (FailReason)0)
	{
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		if (State == SpawnState.Pending)
		{
			State = state;
			FailReason = reason;
			Action<SpawnHandle> onReady = _onReady;
			_onReady = null;
			FireAll(onReady, "OnReady");
		}
	}

	internal void FireDied()
	{
		FireAll(this.OnDied, "OnDied");
		this.OnDied = null;
		this.OnDespawned = null;
	}

	internal void FireDespawned()
	{
		FireAll(this.OnDespawned, "OnDespawned");
		this.OnDied = null;
		this.OnDespawned = null;
	}

	private void FireAll(Action<SpawnHandle> handlers, string label)
	{
		if (handlers != null)
		{
			Delegate[] invocationList = handlers.GetInvocationList();
			foreach (Delegate obj in invocationList)
			{
				Invoke((Action<SpawnHandle>)obj, label);
			}
		}
	}

	private void Invoke(Action<SpawnHandle> handler, string label)
	{
		try
		{
			handler?.Invoke(this);
		}
		catch (Exception ex)
		{
			ManualLogSource log = Plugin.Log;
			if (log != null)
			{
				log.LogError((object)$"[SPAWN] consumer {label} callback threw for '{SpeciesKey}' (owner '{OwnerTag}'): {ex}");
			}
		}
	}
}
internal class SpawnMenu : MonoBehaviour
{
	private const float ArmWindowSeconds = 5f;

	private bool _open;

	private Rect _rect = new Rect(80f, 80f, 420f, 480f);

	private Vector2 _scroll;

	private int _count = 1;

	private string _status = "";

	private IReadOnlyList<string> _species = Array.Empty<string>();

	private Arm _arm;

	private bool _closeRequested;

	private bool _tripInFlight;

	public void Toggle()
	{
		SetOpen(!_open);
	}

	private void SetOpen(bool open)
	{
		if (open != _open)
		{
			_open = open;
			if (_open)
			{
				_species = Spawner.SpeciesKeys();
			}
			CursorControl.SetMenuOpen(_open);
		}
	}

	private void Update()
	{
		//IL_003e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		if (_closeRequested)
		{
			_closeRequested = false;
			SetOpen(open: false);
		}
		_tripInFlight = Spawner.IsExpeditionRunning;
		if (_tripInFlight)
		{
			if (_open)
			{
				SetOpen(open: false);
			}
			return;
		}
		KeyboardShortcut value = Plugin.MenuKey.Value;
		if (((KeyboardShortcut)(ref value)).IsDown() && MenuCursorPolicy.CanOpen(CursorControl.VanillaMenuFocused))
		{
			Toggle();
		}
		if (MenuCursorPolicy.CloseOnEsc(_open, Input.GetKeyDown((KeyCode)27)))
		{
			SetOpen(open: false);
		}
		if (MenuCursorPolicy.ForceClose(_open, CursorControl.VanillaMenuFocused))
		{
			SetOpen(open: false);
		}
	}

	private void OnDisable()
	{
		SetOpen(open: false);
	}

	private void OnGUI()
	{
		//IL_0015: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Unknown result type (might be due to invalid IL or missing references)
		//IL_0030: Unknown result type (might be due to invalid IL or missing references)
		//IL_0049: Expected O, but got Unknown
		//IL_0044: Unknown result type (might be due to invalid IL or missing references)
		//IL_0049: Unknown result type (might be due to invalid IL or missing references)
		if (_open)
		{
			_rect = GUILayout.Window("cobalt.spawnkit".GetHashCode(), _rect, new WindowFunction(DrawWindow), $"SpawnKit — {Plugin.MenuKey.Value} closes", Array.Empty<GUILayoutOption>());
		}
	}

	private void DrawWindow(int id)
	{
		//IL_015f: 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_01a1: Unknown result type (might be due to invalid IL or missing references)
		//IL_01be: Unknown result type (might be due to invalid IL or missing references)
		//IL_01c3: Unknown result type (might be due to invalid IL or missing references)
		//IL_0224: Unknown result type (might be due to invalid IL or missing references)
		//IL_0230: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a5: Unknown result type (might be due to invalid IL or missing references)
		//IL_02d6: Unknown result type (might be due to invalid IL or missing references)
		//IL_02e5: Unknown result type (might be due to invalid IL or missing references)
		//IL_030e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0318: Unknown result type (might be due to invalid IL or missing references)
		//IL_0330: Unknown result type (might be due to invalid IL or missing references)
		//IL_0335: Unknown result type (might be due to invalid IL or missing references)
		//IL_0338: Unknown result type (might be due to invalid IL or missing references)
		//IL_033a: Unknown result type (might be due to invalid IL or missing references)
		//IL_033f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0344: Unknown result type (might be due to invalid IL or missing references)
		int num = Math.Max(1, Plugin.MaxActiveSpawns.Value);
		int count = Spawner.Active().Count;
		GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
		GUILayout.Label($"Active: {count}/{num}", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) });
		GUILayout.FlexibleSpace();
		GUILayout.Label("Count:", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(45f) });
		if (GUILayout.Button("-", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(24f) }))
		{
			_count = Math.Max(1, _count - 1);
		}
		GUILayout.Label(_count.ToString(), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(20f) });
		if (GUILayout.Button("+", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(24f) }))
		{
			_count = Math.Min(num, _count + 1);
		}
		GUILayout.EndHorizontal();
		GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
		if (GUILayout.Button("Despawn All (silent)", Array.Empty<GUILayoutOption>()))
		{
			Spawner.DespawnAll();
		}
		if (GUILayout.Button("Kill All (loot)", Array.Empty<GUILayoutOption>()))
		{
			Spawner.DespawnAll(null, kill: true);
		}
		GUILayout.EndHorizontal();
		bool tripInFlight = _tripInFlight;
		bool value = Plugin.EnableExpeditions.Value;
		float unscaledTime = Time.unscaledTime;
		_scroll = GUILayout.BeginScrollView(_scroll, Array.Empty<GUILayoutOption>());
		foreach (string item in _species)
		{
			bool flag = Spawner.CanMintNow(item);
			bool flag2 = Spawner.IsExpeditionOnly(item);
			bool flag3 = ExpeditionRow.IsArmed(_arm, item, unscaledTime, 5f);
			Buttons val = ExpeditionRow.For(flag, flag2, value, tripInFlight, flag3);
			string text = SpawnMenuLabels.RowLabel(item, Spawner.ResolvedDonorName(item), flag2 && !flag);
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			GUILayout.Label(flag ? "●" : "○", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(18f) });
			GUILayout.Label(text, Array.Empty<GUILayoutOption>());
			GUILayout.FlexibleSpace();
			GUI.enabled = val.Interactive;
			if (val.ShowPrewarm && GUILayout.Button("Prewarm", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(70f) }))
			{
				string k = item;
				_status = "Prewarming " + k + "…";
				Spawner.Prewarm(k, delegate(bool ok)
				{
					_status = (ok ? (k + " template resident.") : ("Prewarm " + k + " FAILED (see log)."));
				});
			}
			if (val.ShowWarmTrip && GUILayout.Button("Warm (trip)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) }))
			{
				BeginTrip(item, 0);
			}
			if (GUILayout.Button(val.SpawnLabel, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(val.SpawnCostsATrip ? 90f : 60f) }))
			{
				string text2 = item;
				if (val.SpawnCostsATrip)
				{
					ClickResult val2 = ExpeditionRow.Click(_arm, text2, unscaledTime, 5f, Plugin.ConfirmMenuExpedition.Value);
					_arm = val2.State;
					if (val2.Fire)
					{
						BeginTrip(text2, _count);
					}
					else
					{
						_status = text2 + " needs an EXPEDITION: two loading screens there and back, and the game saves on each leg. Click Confirm to go (it also caches every other species of that region).";
					}
				}
				else
				{
					_status = (flag ? $"Spawning {_count}x {text2}…" : $"Spawning {_count}x {text2} (cold — donor harvest first)…");
					for (int num2 = 0; num2 < _count; num2++)
					{
						SpawnOne(text2, toast: false);
					}
				}
			}
			GUI.enabled = true;
			GUILayout.EndHorizontal();
		}
		GUILayout.EndScrollView();
		if (_status.Length > 0)
		{
			GUILayout.Label(_status, Array.Empty<GUILayoutOption>());
		}
		GUI.DragWindow();
	}

	private void BeginTrip(string key, int spawnCount)
	{
		//IL_0027: Unknown result type (might be due to invalid IL or missing references)
		int count = Mathf.Max(0, spawnCount);
		_arm = default(Arm);
		_closeRequested = true;
		Notify((count > 0) ? ("Expedition for " + key + " — there and back, then it appears.") : ("Expedition for " + key + " — fetching a body."));
		Spawner.Expedition(key, delegate(bool ok, string why)
		{
			if (!ok)
			{
				_status = key + ": expedition FAILED — " + why + ".";
				Notify("Expedition for " + key + " failed: " + why + ".");
			}
			else if (count == 0)
			{
				_status = key + ": " + why + ".";
				Notify(key + ": body template cached — it spawns instantly now.");
			}
			else
			{
				_status = $"Spawning {count}x {key} (home from the expedition)…";
				for (int i = 0; i < count; i++)
				{
					SpawnOne(key, toast: true);
				}
			}
		});
	}

	private void SpawnOne(string key, bool toast)
	{
		Spawner.Spawn(key, new SpawnOptions
		{
			OwnerTag = "spawnkit.menu"
		}, delegate(SpawnHandle h)
		{
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			bool flag = h.State == SpawnState.Alive;
			string text = (((Object)(object)h.Character != (Object)null && SpawnMenuLabels.IsDonorMismatch(h.SpeciesKey, h.Character.Name)) ? (h.SpeciesKey + " spawned as '" + h.Character.Name + "'.") : (h.SpeciesKey + " spawned."));
			_status = (flag ? text : $"{h.SpeciesKey}: {h.State} ({h.FailReason}).");
			if (toast)
			{
				Notify(flag ? text : $"{h.SpeciesKey} could NOT be spawned ({h.FailReason}) — the expedition brought no body for it. See the log.");
			}
		});
	}

	private static void Notify(string message)
	{
		Plugin.Log.LogMessage((object)("[EXPEDITION] " + message));
		try
		{
			CharacterManager instance = CharacterManager.Instance;
			Notify.Player((instance != null) ? instance.GetFirstLocalCharacter() : null, message);
		}
		catch (Exception ex)
		{
			Plugin.Log.LogWarning((object)("[EXPEDITION] toast failed: " + ex.Message));
		}
	}
}
public sealed class SpawnOptions
{
	public float? Distance;

	public Vector3? Position;

	public Quaternion? Rotation;

	public Factions? Faction;

	public float? LifetimeSeconds;

	public CorpsePolicy? Corpse;

	public float? CorpseLingerSeconds;

	public bool StripQuestEvents = true;

	public string OwnerTag;
}
internal static class SpawnTemplates
{
	private static readonly Dictionary<string, GameObject> _cache = new Dictionary<string, GameObject>(StringComparer.OrdinalIgnoreCase);

	private static readonly Dictionary<string, List<Action<GameObject>>> _pending = new Dictionary<string, List<Action<GameObject>>>(StringComparer.OrdinalIgnoreCase);

	private static readonly List<string> _lru = new List<string>();

	private static readonly Dictionary<string, string> _resolvedDonor = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

	private static GameObject _holder;

	public static string ResolvedDonorName(string speciesKey)
	{
		if (string.IsNullOrEmpty(speciesKey) || !_resolvedDonor.TryGetValue(speciesKey.Trim(), out var value))
		{
			return null;
		}
		return value;
	}

	private static GameObject Holder()
	{
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Expected O, but got Unknown
		if ((Object)(object)_holder == (Object)null)
		{
			_holder = new GameObject("SK_TemplateHolder");
			_holder.SetActive(false);
			Object.DontDestroyOnLoad((Object)(object)_holder);
		}
		return _holder;
	}

	public static IEnumerator AcquireScene(string sceneName, Action<int, int> onDone)
	{
		List<string> list = DonorTable.KeysForScene(DonorHarvest.DonorScenes, sceneName);
		List<string> pending = new List<string>();
		foreach (string item in list)
		{
			if (!_cache.TryGetValue(item, out var value) || (Object)(object)value == (Object)null)
			{
				pending.Add(item);
			}
		}
		if (pending.Count == 0)
		{
			Plugin.Log.LogMessage((object)$"[PREP] done '{sceneName}': nothing to build ({list.Count} species listed, all cached or none).");
			onDone.Invoke(0, 0);
			yield break;
		}
		int built = 0;
		List<string> missing = new List<string>();
		object result = null;
		yield return DonorHarvest.HarvestScene(sceneName, $"batch={pending.Count} species", (Func<Scene, object>)delegate(Scene donor)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			foreach (string item2 in pending)
			{
				Character val = DonorHarvest.FindLiveInScene(donor, item2);
				if ((Object)(object)val == (Object)null)
				{
					missing.Add(item2);
				}
				else
				{
					object obj = BuildTemplate(val, item2);
					GameObject val2 = (GameObject)((obj is GameObject) ? obj : null);
					if ((Object)(object)val2 != (Object)null)
					{
						Insert(item2, val2);
						built++;
					}
					else
					{
						missing.Add(item2);
					}
				}
			}
			return built;
		}, (Action<object>)delegate(object r)
		{
			result = r;
		});
		if (result == null && built == 0 && missing.Count == 0)
		{
			missing.AddRange(pending);
		}
		Plugin.Log.LogMessage((object)($"[PREP] done '{sceneName}': built {built}/{pending.Count}" + ((missing.Count > 0) ? (", missing: " + string.Join(", ", missing)) : "") + "."));
		onDone.Invoke(built, missing.Count);
	}

	public static bool IsCached(string speciesKey)
	{
		if (!string.IsNullOrEmpty(speciesKey) && _cache.TryGetValue(speciesKey, out var value))
		{
			return (Object)(object)value != (Object)null;
		}
		return false;
	}

	public static IEnumerator Acquire(string speciesKey, Action<GameObject> onTemplate)
	{
		if (_cache.TryGetValue(speciesKey, out var value))
		{
			if ((Object)(object)value != (Object)null)
			{
				Touch(speciesKey);
				onTemplate(value);
				yield break;
			}
			RemoveKey(speciesKey);
			Plugin.Log.LogWarning((object)("[TEMPLATE] cached template for '" + speciesKey + "' was destroyed — re-harvesting."));
		}
		if (_pending.TryGetValue(speciesKey, out var value2))
		{
			value2.Add(onTemplate);
			yield break;
		}
		_pending[speciesKey] = new List<Action<GameObject>> { onTemplate };
		GameObject resolved = null;
		try
		{
			GameObject val = AdoptFromExpedition(speciesKey);
			if ((Object)(object)val != (Object)null)
			{
				Insert(speciesKey, val);
				Plugin.Log.LogMessage((object)("[TEMPLATE] adopted '" + speciesKey + "' from the expedition body-template cache — no donor load needed."));
				resolved = val;
				yield break;
			}
			List<string> list = default(List<string>);
			string text = default(string);
			if (!DonorHarvest.TryGetDonorScenes(speciesKey, ref list, ref text))
			{
				Plugin.Log.LogWarning((object)("[TEMPLATE] no DonorScenes entry matches '" + speciesKey + "' (and the expedition cache has no template for it) — 'spawnlist' shows the table; an 'expedition' can supply region-only species."));
				yield break;
			}
			object result = null;
			yield return DonorHarvest.HarvestChain(list, text, (Func<Character, object>)((Character src) => BuildTemplate(src, speciesKey)), (Action<object>)delegate(object r)
			{
				result = r;
			});
			object obj = result;
			GameObject val2 = (GameObject)((obj is GameObject) ? obj : null);
			if ((Object)(object)val2 != (Object)null)
			{
				Insert(speciesKey, val2);
			}
			resolved = val2;
		}
		finally
		{
			Deliver(speciesKey, resolved);
		}
	}

	private static void Deliver(string speciesKey, GameObject template)
	{
		if (!_pending.TryGetValue(speciesKey, out var value))
		{
			return;
		}
		_pending.Remove(speciesKey);
		if (value.Count > 1)
		{
			Plugin.Log.LogMessage((object)$"[TEMPLATE] '{speciesKey}': one harvest served {value.Count} concurrent acquires.");
		}
		foreach (Action<GameObject> item in value)
		{
			try
			{
				item?.Invoke(template);
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[TEMPLATE] acquire callback for '{speciesKey}' threw: {arg}");
			}
		}
	}

	private static void Touch(string key)
	{
		_lru.Remove(key);
		_lru.Add(key);
	}

	private static void RemoveKey(string key, bool destroy = false)
	{
		if (destroy && _cache.TryGetValue(key, out var value) && (Object)(object)value != (Object)null)
		{
			Object.Destroy((Object)(object)value);
		}
		_cache.Remove(key);
		_lru.Remove(key);
	}

	private static void Insert(string key, GameObject template)
	{
		_cache[key] = template;
		Touch(key);
		int num = ((Plugin.MaxCachedTemplates != null) ? Plugin.MaxCachedTemplates.Value : 0);
		if (num > 0)
		{
			foreach (string item in SpawnCap.PickEvict((IReadOnlyList<string>)_lru, _cache.Count, num, (ICollection<string>)_pending.Keys))
			{
				Plugin.Log.LogMessage((object)$"[TEMPLATE] LRU cap ({num}): evicting '{item}' (next spawn of it re-harvests).");
				RemoveKey(item, destroy: true);
			}
			return;
		}
		if (SpawnCap.ShouldWarnCacheSize(_cache.Count, 8))
		{
			Plugin.Log.LogWarning((object)($"[TEMPLATE] cache now holds {_cache.Count} resident creature template(s) — " + "each pins its mesh/texture/audio in memory. 'spawnclearcache' frees them (safe when nothing is mid-spawn)."));
		}
	}

	private static object BuildTemplate(Character src, string speciesKey)
	{
		RecordDonorName(speciesKey, DonorHarvest.IdentityFor(speciesKey, src));
		GameObject val = Object.Instantiate<GameObject>(((Component)src).gameObject, Holder().transform);
		try
		{
			Normalize(val, speciesKey, "donor '" + src.Name + "'");
			string text = RagdollRig.RestoreJoints(((Component)src).gameObject, val, (Action<string>)delegate(string w)
			{
				Plugin.Log.LogWarning((object)("[TEMPLATE] ragdoll ('" + speciesKey + "'): " + w));
			});
			if (text != null)
			{
				Plugin.Log.LogMessage((object)("[TEMPLATE] ragdoll ('" + speciesKey + "'): " + text + "."));
			}
			if (!RigCheck(((Component)src).gameObject, val, speciesKey))
			{
				Object.Destroy((Object)(object)val);
				return null;
			}
			return val;
		}
		catch (Exception arg)
		{
			Plugin.Log.LogError((object)$"[TEMPLATE] build of '{speciesKey}' threw — destroying orphan clone under the holder: {arg}");
			if ((Object)(object)val != (Object)null)
			{
				Object.Destroy((Object)(object)val);
			}
			throw;
		}
	}

	private static bool RigCheck(GameObject donor, GameObject clone, string speciesKey)
	{
		Action<string> action = delegate(string w)
		{
			Plugin.Log.LogWarning((object)("[RIG] ('" + speciesKey + "') " + w));
		};
		Report val = SkeletonRig.Audit(donor, clone, action);
		Plugin.Log.LogMessage((object)("[TEMPLATE] rig ('" + speciesKey + "'): " + val.Summary + "."));
		if (val.Healthy)
		{
			return true;
		}
		SkeletonRig.LogForensics(val, donor, action);
		if (!SkeletonRig.RepairEnabled)
		{
			Plugin.Log.LogWarning((object)("[TEMPLATE] rig ('" + speciesKey + "'): external references present and [Rig] RepairSkinnedBones=false (log-only) — spawns of this template will mis-render."));
			return true;
		}
		string text = SkeletonRig.Repair(donor, clone, val, action);
		Report val2 = SkeletonRig.Audit((GameObject)null, clone, action);
		Plugin.Log.LogMessage((object)("[TEMPLATE] rig repair ('" + speciesKey + "'): " + text + " — post: " + val2.Summary + "."));
		if (val2.Healthy)
		{
			return true;
		}
		Plugin.Log.LogWarning((object)("[TEMPLATE] rig ('" + speciesKey + "'): UNREPAIRABLE after repair pass — rejecting this donor; the harvest chain advances to the next scene."));
		return false;
	}

	private static void RecordDonorName(string speciesKey, string donorName)
	{
		if (!string.IsNullOrEmpty(speciesKey) && !string.IsNullOrEmpty(donorName))
		{
			string text = speciesKey.Trim();
			bool flag = !_resolvedDonor.ContainsKey(text);
			_resolvedDonor[text] = donorName;
			if (flag && SpawnMenuLabels.IsDonorMismatch(text, donorName))
			{
				Plugin.Log.LogWarning((object)("[SPAWN] label: species key '" + text + "' resolves to donor creature '" + donorName + "' (the menu row / spawn verb name is a NAME-MATCH substring, not the exact creature — a spawn of '" + text + "' produces a '" + donorName + "'). The menu row now shows this."));
			}
		}
	}

	private static GameObject AdoptFromExpedition(string speciesKey)
	{
		BodyTemplate val = default(BodyTemplate);
		try
		{
			if (!BodyTemplateCache.TryResolveExact(speciesKey, ref val) || (Object)(object)val.Dormant == (Object)null)
			{
				return null;
			}
		}
		catch (Exception ex)
		{
			Plugin.Log.LogWarning((object)("[TEMPLATE] expedition-cache lookup for '" + speciesKey + "' threw: " + ex.Message));
			return null;
		}
		GameObject val2 = Object.Instantiate<GameObject>(val.Dormant, Holder().transform);
		try
		{
			Normalize(val2, speciesKey, "expedition cache (species '" + val.SpeciesId + "')");
			return val2;
		}
		catch (Exception arg)
		{
			Plugin.Log.LogError((object)$"[TEMPLATE] adoption of '{speciesKey}' from the expedition cache threw — destroying orphan clone: {arg}");
			if ((Object)(object)val2 != (Object)null)
			{
				Object.Destroy((Object)(object)val2);
			}
			return null;
		}
	}

	private static void Normalize(GameObject go, string speciesKey, string sourceLabel)
	{
		//IL_0017: Unknown result type (might be due to invalid IL or mis